File Coverage

blib/lib/Gedcom.pm
Criterion Covered Total %
statement 275 294 93.5
branch 73 112 65.1
condition 26 53 49.0
subroutine 41 50 82.0
pod 18 26 69.2
total 433 535 80.9


line stmt bran cond sub pod time code
1             # Copyright 1998-2013, Paul Johnson (paul@pjcj.net)
2              
3             # This software is free. It is licensed under the same terms as Perl itself.
4              
5             # The latest version of this software should be available from my homepage:
6             # http://www.pjcj.net
7              
8             # documentation at __END__
9              
10 11     11   73872 use strict;
  11         15  
  11         380  
11              
12             require 5.005;
13              
14             package Gedcom;
15              
16 11     11   39 use Carp;
  11         12  
  11         595  
17 11     11   6492 use Data::Dumper;
  11         82912  
  11         687  
18 11     11   5013 use FileHandle;
  11         95043  
  11         61  
19              
20 11     11   3873 BEGIN { eval "use Text::Soundex" } # We'll use this if it is available
  11     11   5781  
  11         26820  
  11         1173  
21              
22 11     11   58 use vars qw($VERSION $AUTOLOAD %Funcs);
  11         16  
  11         4031  
23              
24             my $Tags;
25             my %Top_tag_order;
26              
27             BEGIN {
28 11     11   25 $VERSION = "1.20";
29              
30 11         2349 $Tags = {
31             ABBR => "Abbreviation",
32             ADDR => "Address",
33             ADOP => "Adoption",
34             ADR1 => "Address1",
35             ADR2 => "Address2",
36             AFN => "Afn",
37             AGE => "Age",
38             AGNC => "Agency",
39             ALIA => "Alias",
40             ANCE => "Ancestors",
41             ANCI => "Ances Interest",
42             ANUL => "Annulment",
43             ASSO => "Associates",
44             AUTH => "Author",
45             BAPL => "Baptism-LDS",
46             BAPM => "Baptism",
47             BARM => "Bar Mitzvah",
48             BASM => "Bas Mitzvah",
49             BIRT => "Birth",
50             BLES => "Blessing",
51             BLOB => "Binary Object",
52             BURI => "Burial",
53             CALN => "Call Number",
54             CAST => "Caste",
55             CAUS => "Cause",
56             CENS => "Census",
57             CHAN => "Change",
58             CHAR => "Character",
59             CHIL => "Child",
60             CHR => "Christening",
61             CHRA => "Adult Christening",
62             CITY => "City",
63             CONC => "Concatenation",
64             CONF => "Confirmation",
65             CONL => "Confirmation L",
66             CONT => "Continued",
67             COPR => "Copyright",
68             CORP => "Corporate",
69             CREM => "Cremation",
70             CTRY => "Country",
71             DATA => "Data",
72             DATE => "Date",
73             DEAT => "Death",
74             DESC => "Descendants",
75             DESI => "Descendant Int",
76             DEST => "Destination",
77             DIV => "Divorce",
78             DIVF => "Divorce Filed",
79             DSCR => "Phy Description",
80             EDUC => "Education",
81             EMIG => "Emigration",
82             ENDL => "Endowment",
83             ENGA => "Engagement",
84             EVEN => "Event",
85             FAM => "Family",
86             FAMC => "Family Child",
87             FAMF => "Family File",
88             FAMS => "Family Spouse",
89             FCOM => "First Communion",
90             FILE => "File",
91             FORM => "Format",
92             GEDC => "Gedcom",
93             GIVN => "Given Name",
94             GRAD => "Graduation",
95             HEAD => "Header",
96             HUSB => "Husband",
97             IDNO => "Ident Number",
98             IMMI => "Immigration",
99             INDI => "Individual",
100             LANG => "Language",
101             LEGA => "Legatee",
102             MARB => "Marriage Bann",
103             MARC => "Marr Contract",
104             MARL => "Marr License",
105             MARR => "Marriage",
106             MARS => "Marr Settlement",
107             MEDI => "Media",
108             NAME => "Name",
109             NATI => "Nationality",
110             NATU => "Naturalization",
111             NCHI => "Children_count",
112             NICK => "Nickname",
113             NMR => "Marriage_count",
114             NOTE => "Note",
115             NPFX => "Name_prefix",
116             NSFX => "Name_suffix",
117             OBJE => "Object",
118             OCCU => "Occupation",
119             ORDI => "Ordinance",
120             ORDN => "Ordination",
121             PAGE => "Page",
122             PEDI => "Pedigree",
123             PHON => "Phone",
124             PLAC => "Place",
125             POST => "Postal_code",
126             PROB => "Probate",
127             PROP => "Property",
128             PUBL => "Publication",
129             QUAY => "Quality Of Data",
130             REFN => "Reference",
131             RELA => "Relationship",
132             RELI => "Religion",
133             REPO => "Repository",
134             RESI => "Residence",
135             RESN => "Restriction",
136             RETI => "Retirement",
137             RFN => "Rec File Number",
138             RIN => "Rec Id Number",
139             ROLE => "Role",
140             SEX => "Sex",
141             SLGC => "Sealing Child",
142             SLGS => "Sealing Spouse",
143             SOUR => "Source",
144             SPFX => "Surn Prefix",
145             SSN => "Soc Sec Number",
146             STAE => "State",
147             STAT => "Status",
148             SUBM => "Submitter",
149             SUBN => "Submission",
150             SURN => "Surname",
151             TEMP => "Temple",
152             TEXT => "Text",
153             TIME => "Time",
154             TITL => "Title",
155             TRLR => "Trailer",
156             TYPE => "Type",
157             VERS => "Version",
158             WIFE => "Wife",
159             WILL => "Will",
160             };
161              
162 11         59 %Top_tag_order = (
163             HEAD => 1,
164             SUBM => 2,
165             INDI => 3,
166             FAM => 4,
167             NOTE => 5,
168             REPO => 6,
169             SOUR => 7,
170             TRLR => 8,
171             );
172              
173 11         73 while (my ($tag, $name) = each (%$Tags)) {
174 1419         5892 $Funcs{$tag} = $Funcs{lc $tag} = $tag;
175 1419 50       2259 if ($name) {
176 1419         1435 $name =~ s/ /_/g;
177 1419         7082 $Funcs{lc $name} = $tag;
178             }
179             }
180             }
181              
182       0     sub DESTROY {}
183              
184             sub AUTOLOAD {
185 10     10   642 my ($self) = @_; # don't change @_ because of the goto
186 10         13 my $func = $AUTOLOAD;
187             # print "autoloading $func\n";
188 10         46 $func =~ s/^.*:://;
189 10         14 my $tag;
190             croak "Undefined subroutine $func called"
191             if $func !~ /^(add|get)_(.*)$/ ||
192             !($tag = $Funcs{lc $2}) ||
193 10 50 33     105 !exists $Top_tag_order{$tag};
      33        
194 11     11   144 no strict "refs";
  11         12  
  11         9890  
195 10 100       29 if ($1 eq "add") {
196             *$func = sub {
197 12     12   12 my $self = shift;
198 12         14 my ($arg, $val) = @_;
199 12         11 my $xref;
200 12 100       18 if (ref $arg) {
201 1         1 $xref = $arg->{xref};
202             } else {
203 11         12 $val = $arg;
204             }
205 12         20 my $record = $self->add_record(tag => $tag, val => $val);
206 12 100 100     31 if (defined $val && $tag eq "NOTE") {
207 2         3 $record->{value} = $val;
208             }
209 12 100       30 $xref = $tag eq "SUBM" ? "SUBM" : substr $tag, 0, 1
    100          
210             unless defined $xref;
211 12 100       39 unless ($tag =~ /^(HEAD|TRLR)$/) {
212 10 50       37 croak "Invalid xref $xref requested in $func"
213             unless $xref =~ /^[^\W\d_]+(\d*)$/;
214 10 100       35 $xref = $self->next_xref($xref) unless length $1;
215 10         11 $record->{xref} = $xref;
216 10         19 $self->{xrefs}{$xref} = $record;
217             }
218             $record
219 6         32 };
  12         31  
220             } else {
221             *$func = sub {
222 4     4   7 my $self = shift;
223 4         6 my ($xref) = @_;
224 4         7 my $nxr = !defined $xref;
225 584 100 33     927 my @a = grep { $_->{tag} eq $tag && ($nxr || $_->{xref} eq $xref) }
226 4         6 @{$self->{record}->_items};
  4         25  
227 4 50       29 wantarray ? @a : $a[0]
228 4         27 };
229             }
230 10         31 goto &$func
231             }
232              
233 11     11   4442 use Gedcom::Grammar 1.20;
  11         146  
  11         351  
234 11     11   4154 use Gedcom::Individual 1.20;
  11         155  
  11         373  
235 11     11   3781 use Gedcom::Family 1.20;
  11         127  
  11         299  
236 11     11   3348 use Gedcom::Event 1.20;
  11         140  
  11         2517  
237              
238             sub new {
239 8     8 1 256940 my $proto = shift;
240 8   33     71 my $class = ref($proto) || $proto;
241 8 50       28 @_ = (gedcom_file => @_) if @_ == 1;
242 8         61 my $self = {
243             records => [],
244             tags => $Tags,
245             read_only => 0,
246             types => {},
247             xrefs => {},
248             encoding => "ansel",
249             @_
250             };
251              
252             # TODO - find a way to do this nicely for different grammars
253 8         20 $self->{types}{INDI} = "Individual";
254 8         18 $self->{types}{FAM} = "Family";
255 8         274 $self->{types}{$_} = "Event" for qw(
256             ADOP ANUL BAPM BARM BASM BIRT BLES BURI CAST CENS CENS CHR CHRA CONF
257             CREM DEAT DIV DIVF DSCR EDUC EMIG ENGA EVEN EVEN FCOM GRAD IDNO IMMI
258             MARB MARC MARL MARR MARS NATI NATU NCHI NMR OCCU ORDN PROB PROP RELI
259             RESI RETI SSN WILL
260             );
261 8         17 bless $self, $class;
262              
263             # first read in the grammar
264 8         17 my $grammar;
265 8 100       50 if (defined $self->{grammar_file}) {
266 1         1 my $version;
267 1 50       2 if (defined $self->{grammar_version}) {
268 0         0 $version = $self->{grammar_version};
269             } else {
270 1         6 ($version) = $self->{grammar_file} =~ /(\d+(\.\d+)*)/;
271             }
272 1 50       3 die "version must be a gedcom version number\n" unless $version;
273             return undef unless
274             $grammar = Gedcom::Grammar->new(
275             file => $self->{grammar_file},
276             version => $version,
277             callback => $self->{callback}
278 1 50       11 );
279             } else {
280 7 100       25 $self->{grammar_version} = 5.5 unless defined $self->{grammar_version};
281 7         74 (my $v = $self->{grammar_version}) =~ tr/./_/;
282 7         21 my $g = "Gedcom::Grammar_$v";
283 7     7   4239 eval "use $g $VERSION";
  7         272  
  7         443  
  7         535  
284 7 50       35 die $@ if $@;
285 11     11   55 no strict "refs";
  11         13  
  11         20541  
286 7 50       10 return undef unless $grammar = ${$g . "::grammar"};
  7         52  
287             }
288 8         51 my @c = ($self->{grammar} = $grammar);
289 8         27 while (@c) {
290 48         56 @c = map { $_->{top} = $grammar; @{$_->{items}} } @c;
  3256         2223  
  3256         1706  
  3256         3330  
291             }
292              
293             # now read in or create the gedcom file
294             return undef unless
295             my $r = $self->{record} = Gedcom::Record->new(
296             defined $self->{gedcom_file} ? (file => $self->{gedcom_file}) : (),
297             line => 0,
298             tag => "GEDCOM",
299             grammar => $grammar->structure("GEDCOM"),
300             gedcom => $self,
301             callback => $self->{callback},
302 8 100       78 );
    50          
303              
304 8 100       38 unless (defined $self->{gedcom_file}) {
305              
306             # Add the required elements, unless they are already there.
307              
308 1 50       6 unless ($r->get_record("head")) {
309 1         1 my $me = "Unknown user";
310 1         1 my $login = $me;
311 1 50 0     658 if ($login = getlogin || (getpwuid($<))[0] ||
312             $ENV{USER} || $ENV{LOGIN}) {
313 1         1 my $name;
314 1         2 eval { $name = (getpwnam($login))[6] };
  1         48  
315 1   33     5 $me = $name || $login;
316             }
317 1         20 my $date = localtime;
318              
319 1         1 my ($l0, $l1, $l2, $l3);
320 1         11 $l0 = $self->add_header;
321 1         4 $l1 = $l0->add("SOUR", "Gedcom.pm");
322 1         3 $l1->add("NAME", "Gedcom.pm");
323 1         4 $l1->add("VERS", $VERSION);
324 1         3 $l2 = $l1->add("CORP", "Paul Johnson");
325 1         3 $l2->add("ADDR", "http://www.pjcj.net");
326 1         4 $l2 = $l1->add("DATA");
327 1         3 $l3 = $l2->add(
328             "COPR",
329             'Copyright 1998-2013, Paul Johnson (paul@pjcj.net)'
330             );
331 1         3 $l1 = $l0->add("NOTE", "");
332 1         5 for (split /\n/, <<'EOH')
333             This output was generated by Gedcom.pm.
334             Gedcom.pm is Copyright 1999-2013, Paul Johnson (paul@pjcj.net)
335             Version 1.20 - 17th September 2017
336              
337             Gedcom.pm is free. It is licensed under the same terms as Perl itself.
338              
339             The latest version of Gedcom.pm should be available from my homepage:
340             http://www.pjcj.net
341             EOH
342             {
343 8         14 $l1->add("CONT", $_);
344             };
345 1         4 $l1 = $l0->add("GEDC");
346 1         4 $l1->add("VERS", $self->{grammar}{version});
347 1         2 $l1->add("FORM", "LINEAGE-LINKED");
348 1         2 $l0->add("DATE", $date);
349 1   50     5 $l0->add("CHAR", uc ($self->{encoding} || "ansel"));
350 1         5 my $s = $r->get_record("subm");
351 1 50       3 unless ($s) {
352 1         8 $s = $self->add_submitter;
353 1         4 $s->add("NAME", $me);
354             }
355 1         7 $l0->add("SUBM", $s->xref);
356             }
357              
358 1 50       4 $self->add_trailer unless $r->get_record("trlr");
359             }
360              
361 8         42 $self->collect_xrefs;
362              
363 8         36 $self
364             }
365              
366             sub set_encoding {
367 7     7 1 10 my $self = shift;
368 7         23 ($self->{encoding}) = @_;
369             }
370              
371             sub write {
372 7     7 1 3131 my $self = shift;
373 7 50       31 my $file = shift or die "No filename specified";
374 7         12 my $flush = shift;
375 7 50       75 $self->{fh} = FileHandle->new($file, "w") or die "Can't open $file: $!";
376             binmode $self->{fh}, ":encoding(UTF-8)"
377 7 50 33     1423 if $self->{encoding} eq "utf-8" && $] >= 5.8;
378 7         85 $self->{record}->write($self->{fh}, -1, $flush);
379 7 50       42 $self->{fh}->close or die "Can't close $file: $!";
380             }
381              
382             sub write_xml {
383 0     0 1 0 my $self = shift;
384 0 0       0 my $file = shift or die "No filename specified";
385 0 0       0 $self->{fh} = FileHandle->new($file, "w") or die "Can't open $file: $!";
386             binmode $self->{fh}, ":encoding(UTF-8)"
387 0 0 0     0 if $self->{encoding} eq "utf-8" && $] >= 5.8;
388 0         0 $self->{fh}->print(<<'EOH');
389            
390              
391             \n\n");
404 0         0 $self->{record}->write_xml($self->{fh});
405 0 0       0 $self->{fh}->close or die "Can't close $file: $!";
406             }
407              
408             sub add_record {
409 12     12 1 10 my $self = shift;
410 12         52 $self->{record}->add_record(@_);
411             }
412              
413             sub collect_xrefs {
414 21     21 1 34 my $self = shift;
415 21         34 my ($callback) = @_;
416 21         50 $self->{xrefs} = {};
417 21         559 $self->{record}->collect_xrefs($callback);
418             }
419              
420             sub resolve_xref {
421 29437     29437 1 26191 my $self = shift;;
422 29437         23089 my ($x) = @_;
423 29437         16551 my $xref;
424 29437 50       57313 $xref = $self->{xrefs}{$x =~ /^\@(.+)\@$/ ? $1 : $x} if defined $x;
    100          
425 29437         51177 $xref
426             }
427              
428             sub resolve_xrefs {
429 19     19 1 2338 my $self = shift;
430 19         29 my ($callback) = @_;
431 19         120 $self->{record}->resolve_xrefs($callback);
432             }
433              
434             sub unresolve_xrefs {
435 12     12 1 6104 my $self = shift;
436 12         23 my ($callback) = @_;
437 12         68 $self->{record}->unresolve_xrefs($callback);
438             }
439              
440             sub validate {
441 61     61 1 3916 my $self = shift;
442 61         107 my ($callback) = @_;
443 61         123 $self->{validate_callback} = $callback;
444 61         281 my $ok = $self->{record}->validate_syntax;
445 61         82 for my $item (@{$self->{record}->_items}) {
  61         186  
446 8770 50       13690 $ok = 0 unless $item->validate_semantics;
447             }
448             $ok
449 61         795 }
450              
451             sub normalise_dates {
452 6     6 1 2706 my $self = shift;
453 6         34 $self->{record}->normalise_dates(@_);
454             }
455              
456             sub renumber {
457 13     13 1 968 my $self = shift;
458 13         57 my (%args) = @_;
459 13         50 $self->resolve_xrefs;
460              
461             # initially, renumber any records passed in
462 13         23 for my $xref (@{$args{xrefs}}) {
  13         56  
463             $self->{xrefs}{$xref}->renumber(\%args, 1)
464 6 50       102 if exists $self->{xrefs}{$xref};
465             }
466              
467             # now, renumber any records left over
468 13         21 $_->renumber(\%args, 1) for @{$self->{record}->_items};
  13         47  
469              
470             # actually change the xref
471 13         21 for my $record (@{$self->{record}->_items}) {
  13         53  
472 1762         1978 $record->{xref} = delete $record->{new_xref};
473             delete $record->{recursed}
474 1762         1403 }
475              
476             # and update the xrefs
477 13         78 $self->collect_xrefs;
478              
479 13         174 %args
480             }
481              
482             sub sort_sub {
483             # subroutine to sort on tag order first, and then on xref
484              
485             my $t = sub {
486 6840     6840   4787 my ($r) = @_;
487 6840 50       8439 return -2 unless defined $r->{tag};
488 6840 50       13625 exists $Top_tag_order{$r->{tag}} ? $Top_tag_order{$r->{tag}} : -1
489 7     7 1 46 };
490              
491             my $x = sub {
492 6206     6206   4407 my ($r) = @_;
493 6206 50       7518 return -2 unless defined $r->{xref};
494 6206         7828 $r->{xref} =~ /(\d+)/;
495 6206 50       15051 defined $1 ? $1 : -1
496 7         28 };
497              
498             sub {
499 3420 100   3420   4031 $t->($a) <=> $t->($b)
500             ||
501             $x->($a) <=> $x->($b)
502             }
503 7         45 }
504              
505             sub order {
506 7     7 1 4949 my $self = shift;
507 7   33     53 my $sort_sub = shift || sort_sub; # use default sort unless one passed in
508 7         14 @{$self->{record}{items}} = sort $sort_sub @{$self->{record}->_items}
  7         292  
  7         37  
509             }
510              
511             sub items {
512 130     130 0 187 my $self = shift;
513 130         143 @{$self->{record}->_items}
  130         447  
514             }
515              
516 0     0 0 0 sub heads { grep $_->tag eq "HEAD", shift->items }
517 0     0 0 0 sub submitters { grep $_->tag eq "SUBM", shift->items }
518 94     94 1 9657 sub individuals { grep ref eq "Gedcom::Individual", shift->items }
519 36     36 1 4199 sub families { grep ref eq "Gedcom::Family", shift->items }
520 0     0 0 0 sub notes { grep $_->tag eq "NOTE", shift->items }
521 0     0 0 0 sub repositories { grep $_->tag eq "REPO", shift->items }
522 0     0 0 0 sub sources { grep $_->tag eq "SOUR", shift->items }
523 0     0 0 0 sub trailers { grep $_->tag eq "TRLR", shift->items }
524              
525             sub get_individual {
526 68     68 1 11625 my $self = shift;
527 68         151 my $name = "@_";
528 68         82 my $all = wantarray;
529 68         60 my @i;
530              
531 68   66     186 my $i = $self->resolve_xref($name) || $self->resolve_xref(uc $name);
532 68 100       138 if ($i) {
533 12 50       45 return $i unless $all;
534 0         0 push @i, $i;
535             }
536              
537             # search for the name in the specified order
538             my $ordered = sub {
539 423     423   1785 my ($n, @ind) = @_;
540 423 50       403 map { $_->[1] } grep { $_ && $_->[0] =~ $n } @ind
  146         280  
  37611         98370  
541 56         274 };
542              
543             # search for the name in any order
544             my $unordered = sub {
545 198     198   847 my ($names, $t, @ind) = @_;
546 222         436 map { $_->[1] } grep {
547 198         225 my $i = $_->[0];
  18414         12130  
548 18414         10620 my $r = 1;
549 18414         13046 for my $n (@$names) {
550             # remove matches as they are found
551             # we don't want to match the same name twice
552 18858 100       38505 last unless $r = $i =~ s/$n->[$t]//;
553             }
554             $r
555 18414         16905 }
556             @ind;
557 56         181 };
558              
559             # look for various matches in decreasing order of exactitude
560 56         124 my @individuals = $self->individuals;
561              
562             # Store the name with the individual to avoid continually recalculating it.
563             # This is a bit like a Schwartzian transform, with a grep instead of a sort.
564             my @ind =
565 56 50       300 map [do { my $n = $_->tag_value("NAME"); defined $n ? $n : "" } => $_],
  5208         6872  
  5208         9341  
566             @individuals;
567              
568 56         794 for my $n (map { qr/^$_$/, qr/\b$_\b/, $_ } map { $_, qr/$_/i } qr/\Q$name/)
  112         1117  
  56         246  
569             {
570 327         593 push @i, $ordered->($n, @ind);
571 327 100 100     737 return $i[0] if !$all && @i;
572             }
573              
574             # create an array with one element per name
575             # each element is an array of REs in decreasing order of exactitude
576 54         319 my @names = map [ map { qr/\b$_\b/, $_ } map { qr/$_/, qr/$_/i } "\Q$_" ],
  216         1238  
  108         1027  
577             split / /, $name;
578 54         70 for my $t (0 .. $#{$names[0]}) {
  54         173  
579 198         384 push @i, $unordered->(\@names, $t, @ind);
580 198 100 66     572 return $i[0] if !$all && @i;
581             }
582              
583             # check soundex
584 48 100       95 my @sdx = map { my $s = $_->soundex; $s ? [ $s => $_ ] : () } @individuals;
  4464         6814  
  4464         8309  
585              
586 48         176 my $soundex = soundex($name);
587 48   33     120 for my $n ( map { qr/$_/ } $name, ($soundex || ()) ) {
  96         872  
588 96         194 push @i, $ordered->($n, @sdx);
589 96 50 33     243 return $i[0] if !$all && @i;
590             }
591              
592 48 50       154 return undef unless $all;
593              
594 48         53 my @s;
595             my %s;
596 48         84 for (@i) {
597 360 100       526 unless (exists $s{$_->{xref}}) {
598 114         102 push @s, $_;
599 114         203 $s{$_->{xref}}++;
600             }
601             }
602              
603             @s
604 48         1966 }
605              
606             sub next_xref {
607 45     45 1 73 my $self = shift;
608 45         63 my ($type) = @_;
609 45         606 my $re = qr/^$type(\d+)$/;
610 45         58 my $last = 0;
611 45         47 for my $c (@{$self->{record}->_items}) {
  45         134  
612 5318 100 100     22540 $last = $1 if defined $c->{xref} and $c->{xref} =~ /$re/ and $1 > $last;
      100        
613             }
614 45         272 $type . ++$last
615             }
616              
617             sub top_tag {
618 0     0 0   my $self = shift;
619 0           my ($tag) = @_;
620 0           $Top_tag_order{$tag}
621             }
622              
623             1;
624              
625             __END__