File Coverage

blib/lib/DateTime/Format/Genealogy.pm
Criterion Covered Total %
statement 93 99 93.9
branch 58 70 82.8
condition 15 21 71.4
subroutine 10 10 100.0
pod 2 2 100.0
total 178 202 88.1


line stmt bran cond sub pod time code
1             package DateTime::Format::Genealogy;
2              
3             # Author Nigel Horne: njh@bandsman.co.uk
4             # Copyright (C) 2018-2023, Nigel Horne
5              
6             # Usage is subject to licence terms.
7             # The licence terms of this software are as follows:
8             # Personal single user, single computer use: GPL2
9             # All other users (including Commercial, Charity, Educational, Government)
10             # must apply in writing for a licence for use from Nigel Horne at the
11             # above e-mail.
12              
13 4     4   522955 use strict;
  4         31  
  4         99  
14 4     4   18 use warnings;
  4         7  
  4         83  
15             # use diagnostics;
16             # use warnings::unused;
17 4     4   66 use 5.006_001;
  4         14  
18              
19 4     4   1645 use namespace::clean;
  4         53612  
  4         24  
20 4     4   730 use Carp;
  4         7  
  4         182  
21 4     4   1883 use DateTime::Format::Natural;
  4         1975464  
  4         274  
22 4     4   2184 use Genealogy::Gedcom::Date 2.01;
  4         946810  
  4         39  
23              
24             our %months = (
25             'January' => 'Jan',
26             'February' => 'Feb',
27             'March' => 'Mar',
28             'April' => 'Apr',
29             # 'May' => 'May',
30             'June' => 'Jun',
31             'July' => 'Jul',
32             'August' => 'Aug',
33             'September' => 'Sep',
34             'Sept' => 'Sep',
35             'Sept.' => 'Sep',
36             'October' => 'Oct',
37             'November' => 'Nov',
38             'December' => 'Dec'
39             );
40              
41             =head1 NAME
42              
43             DateTime::Format::Genealogy - Create a DateTime object from a Genealogy Date
44              
45             =head1 VERSION
46              
47             Version 0.05
48              
49             =cut
50              
51             our $VERSION = '0.05';
52              
53             =head1 SYNOPSIS
54              
55             use DateTime::Format::Genealogy;
56             my $dtg = DateTime::Format::Genealogy->new();
57             # ...
58              
59             =head1 SUBROUTINES/METHODS
60              
61             =head2 new
62              
63             Creates a DateTime::Format::Genealogy object.
64              
65             =cut
66              
67             sub new {
68 15     15 1 3468 my($proto, %args) = @_;
69 15   66     111 my $class = ref($proto) || $proto;
70              
71 15 100       77 if(!defined($class)) {
    50          
72             # FIXME: this only works when no arguments are given
73 1         3 $class = __PACKAGE__;
74             } elsif(ref($class)) {
75             # clone the given object
76 0         0 return bless { %{$class}, %args }, ref($class);
  0         0  
77             }
78 15         84 return bless {}, $class;
79             }
80              
81             =head2 parse_datetime($string)
82              
83             Given a date,
84             runs it through L<Genealogy::Gedcom::Date> to create a L<DateTime> object.
85             If a date range is given, return a two element array in array context, or undef in scalar context
86              
87             Returns undef if the date can't be parsed,
88             is before AD100,
89             is just a year or if it is an approximate date starting with "c", "ca" or "abt".
90             Can be called as a class or object method.
91              
92             my $dt = DateTime::Format::Genealogy('25 Dec 2022');
93             $dt = $dtg->(date => '25 Dec 2022');
94              
95             date: the date to be parsed
96             quiet: set to fail silently if there is an error with the date
97             strict: more strictly enforce the Gedcom standard, for example don't allow long month names
98              
99             =cut
100              
101             sub parse_datetime {
102 51     51 1 430844 my $self = shift;
103 51         104 my %params;
104              
105 51 100 100     381 if(!ref($self)) {
    100          
    100          
    100          
    100          
106 10 100       51 if(scalar(@_)) {
107 7         42 return(__PACKAGE__->new()->parse_datetime(@_));
108             }
109 3         21 return(__PACKAGE__->new()->parse_datetime($self));
110             } elsif(ref($self) eq 'HASH') {
111 1         6 return(__PACKAGE__->new()->parse_datetime($self));
112             } elsif(ref($_[0]) eq 'HASH') {
113 12         26 %params = %{$_[0]};
  12         51  
114             } elsif(ref($_[0])) {
115 1         4 Carp::croak('Usage: ', __PACKAGE__, '::parse_datetime(date => $date)');
116             } elsif(scalar(@_) && (scalar(@_) % 2 == 0)) {
117 8         29 %params = @_;
118             } else {
119 19         60 $params{'date'} = shift;
120             }
121 39         102 my $quiet = $params{'quiet'};
122              
123 39 100       112 if(my $date = $params{'date'}) {
124             # TODO: Needs much more sanity checking
125 35 100 100     301 if(($date =~ /^bef\s/i) || ($date =~ /^aft\s/i) || ($date =~ /^abt\s/i)) {
      100        
126 5 100       20 Carp::carp("$date is invalid, need an exact date to create a DateTime")
127             unless($quiet);
128 5         1090 return;
129             }
130 30 100       104 if($date =~ /^31 Nov/) {
131 1         5 Carp::carp("$date is invalid, there are only 30 days in November");
132 1         295 return;
133             }
134 29         90 my $dfn = $self->{'dfn'};
135 29 100       91 if(!defined($dfn)) {
136 13         81 $self->{'dfn'} = $dfn = DateTime::Format::Natural->new();
137             }
138 29 100       75195 if($date =~ /^\s*(.+\d\d)\s*\-\s*(.+\d\d)\s*$/) {
139 1         8 Carp::carp("Changing date '$date' to 'bet $1 and $2'");
140 1         283 $date = "bet $1 and $2";
141             }
142 29 100       123 if($date =~ /^bet (.+) and (.+)/i) {
143 3 100       13 if(wantarray) {
144 1         9 return $self->parse_datetime($1), $self->parse_datetime($2);
145             }
146 2         15 return;
147             }
148              
149 26 100       137 if($date !~ /^\d{3,4}$/) {
150 25         63 my $strict = $params{'strict'};
151 25 100       157 if($strict) {
    100          
152 2 50       8 if($date !~ /^(\d{1,2})\s+([A-Z]{3})\s+(\d{3,4})$/i) {
153 2 50       12 Carp::carp("Unparseable date $date - often because the month name isn't 3 letters") unless($quiet);
154 2         553 return;
155             }
156             } elsif($date =~ /^(\d{1,2})\s+([A-Z]{4,}+)\.?\s+(\d{3,4})$/i) {
157 4 100       31 if(my $abbrev = $months{ucfirst(lc($2))}) {
158 3         16 $date = "$1 $abbrev $3";
159             } else {
160 1 50       9 Carp::carp("Unparseable date $date - often because the month name isn't 3 letters") unless($quiet);
161 1         482 return;
162             }
163             }
164 22 100 66     144 if(($date =~ /^\d/) && (my $d = $self->_date_parser_cached($date))) {
165             # D:T:Natural doesn't seem to work before AD100
166 18 100       101 return if($date =~ /\s\d{1,2}$/);
167 17         119 return $dfn->parse_datetime($d->{'canonical'});
168             }
169 4 50 33     39 if(($date !~ /^(Abt|ca?)/i) && ($date =~ /^[\w\s,]+$/)) {
170             # ACOM exports full month names and non-standard format dates e.g. U.S. format MMM, DD YYYY
171             # TODO: allow that when not in strict mode
172 4 50       19 if(my $rc = $dfn->parse_datetime($date)) {
173 4 100       16753 if($dfn->success()) {
174 1         48 return $rc;
175             }
176 3 50       65 Carp::carp($dfn->error()) unless($quiet);
177             } else {
178 0 0       0 Carp::carp("Can't parse date '$date'") unless($quiet);
179             }
180 3         1040 return;
181             }
182             } else {
183 1         7 return; # undef
184             }
185             } else {
186 4         10 Carp::croak('Usage: ', __PACKAGE__, '::parse_datetime(date => $date)');
187             }
188             }
189              
190             # Parse Gedcom format dates
191             # Genealogy::Gedcom::Date is expensive, so cache results
192             sub _date_parser_cached
193             {
194 18     18   51 my $self = shift;
195 18         40 my $date = shift;
196              
197 18 50       52 if(!defined($date)) {
198 0         0 Carp::croak('Usage: _date_parser_cached(date => $date)');
199             }
200              
201 18 100       67 if($self->{'all_dates'}{$date}) {
202 2         10 return $self->{'all_dates'}{$date};
203             }
204 16         31 my $date_parser = $self->{'date_parser'};
205 16 100       55 if(!defined($date_parser)) {
206 12         280 $date_parser = $self->{'date_parser'} = Genealogy::Gedcom::Date->new();
207             }
208              
209 16         2639480 my $d;
210 16         44 eval {
211 16         115 $d = $date_parser->parse(date => $date);
212             };
213 16 50       381165 if(my $error = $date_parser->error()) {
214 0         0 Carp::carp("$date: '$error'");
215 0         0 return;
216             }
217 16 50 33     268 if($d && (ref($d) eq 'ARRAY')) {
218 16         48 $d = @{$d}[0];
  16         52  
219 16         67 $self->{'all_dates'}{$date} = $d;
220             }
221 16         107 return $d;
222             }
223              
224             1;
225              
226             =head1 AUTHOR
227              
228             Nigel Horne, C<< <njh at bandsman.co.uk> >>
229              
230             =head1 BUGS
231              
232             I can't get L<DateTime::Format::Natural> to work on dates before AD100,
233             so this module rejects dates that old.
234              
235             =head1 SEE ALSO
236              
237             L<Genealogy::Gedcom::Date> and
238             L<DateTime>
239              
240             =head1 SUPPORT
241              
242             You can find documentation for this module with the perldoc command.
243              
244             perldoc DateTime::Format::Gedcom
245              
246             You can also look for information at:
247              
248             =over 4
249              
250             =item * RT: CPAN's request tracker
251              
252             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=DateTime-Format-Gedcom>
253              
254             =item * CPAN Ratings
255              
256             L<http://cpanratings.perl.org/d/DateTime-Format-Gedcom>
257              
258             =back
259              
260             =head1 LICENSE AND COPYRIGHT
261              
262             Copyright 2018-2023 Nigel Horne.
263              
264             This program is released under the following licence: GPL2
265              
266             =cut
267              
268             1;