File Coverage

blib/lib/DateTime/Format/Text.pm
Criterion Covered Total %
statement 78 94 82.9
branch 53 70 75.7
condition 20 45 44.4
subroutine 8 8 100.0
pod 3 3 100.0
total 162 220 73.6


line stmt bran cond sub pod time code
1             package DateTime::Format::Text;
2              
3 4     4   496802 use strict;
  4         36  
  4         117  
4 4     4   25 use warnings;
  4         9  
  4         97  
5 4     4   4168 use DateTime;
  4         2251432  
  4         217  
6 4     4   36 use Carp;
  4         8  
  4         6690  
7              
8             =head1 NAME
9              
10             DateTime::Format::Text - Find a Date in Text
11              
12             =head1 VERSION
13              
14             Version 0.06
15              
16             =cut
17              
18             our $VERSION = '0.06';
19              
20             our @month_names = (
21             'january',
22             'february',
23             'march',
24             'april',
25             'may',
26             'june',
27             'july',
28             'august',
29             'september',
30             'october',
31             'november',
32             'december'
33             );
34              
35             our @day_names = (
36             'monday',
37             'tuesday',
38             'wednesday',
39             'thursday',
40             'friday',
41             'saturday',
42             'sunday'
43             );
44              
45             our @ordinal_number = ('st', 'nd', 'rd', 'th');
46             our @short_month_names = map { _shorten($_) } @month_names;
47             our @short_day_names = map { _shorten($_) } @day_names;
48              
49             our $d = join('|', @day_names);
50             our $sd = join('|', @short_day_names);
51             our $o = join('|', @ordinal_number);
52             our $m = join('|', @month_names);
53             our $sm = join('|', @short_month_names);
54              
55             sub _shorten {
56 76     76   210 return substr(shift, 0, 3);
57             };
58              
59             =head1 SYNOPSIS
60              
61             Find a date in any text.
62              
63             use DateTime::Format::Text;
64             my $dft = DateTime::Format::Text->new();
65             # ...
66              
67             =head1 SUBROUTINES/METHODS
68              
69             =head2 new
70              
71             Creates a DateTime::Format::Text object.
72             Takes no arguments
73              
74             =cut
75              
76             sub new {
77 20     20 1 855 my $proto = shift;
78 20   100     84 my $class = ref($proto) || $proto;
79              
80 20 100       77 if(!defined($class)) {
    50          
81             # Using DateTime::Format::Text::new(), not DateTime::Format::Text->new()
82             # carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
83             # return;
84              
85             # FIXME: this only works when no arguments are given
86 1         5 return bless { }, __PACKAGE__;
87             } elsif(ref($class)) {
88             # clone the given object
89 0         0 return bless { }, ref($class);
90             }
91 19         83 return bless { }, $class;
92             }
93              
94             =head2 parse_datetime
95              
96             Synonym for parse().
97              
98             =cut
99              
100             sub parse_datetime {
101 44     44 1 43786 my $self = shift;
102              
103 44 100       195 if(!ref($self)) {
    100          
    50          
104 8 100       20 if(scalar(@_)) {
105 7         20 return(__PACKAGE__->new()->parse(@_));
106             }
107 1         7 return(__PACKAGE__->new()->parse($self));
108             } elsif(ref($self) eq 'HASH') {
109 1         5 return(__PACKAGE__->new()->parse($self));
110             } elsif(ref($_[0])) {
111 0         0 Carp::croak('Usage: ', __PACKAGE__, '::parse_datetime(string => $string)');
112             }
113              
114 35         91 return $self->parse(@_);
115             }
116              
117             =head2 parse
118              
119             Returns a L<DateTime> object constructed from a date/time string embedded in
120             arbitrary text.
121              
122             Can be called as a class or object method.
123              
124             When called in an array context, returns an array containing all of the matches.
125              
126             If the given test is an object, it's sent the message as_string() and that is parsed
127              
128             use Class::Simple;
129             my $foo = Class::Simple->new();
130             $foo->as_string('25/12/2022');
131             my $dt = $dft->parse($foo);
132              
133             =cut
134              
135             sub parse {
136 149     149 1 20766 my $self = shift;
137 149         220 my %params;
138              
139 149 100 66     863 if(!ref($self)) {
    100          
    100          
    50          
    100          
140 5 100       16 if(scalar(@_)) {
    50          
    50          
141 4         19 return(__PACKAGE__->new()->parse(@_));
142             } elsif(!defined($self)) {
143             # DateTime::Format::Text->parse()
144 0         0 Carp::croak('Usage: ', __PACKAGE__, '::parse(string => $string)');
145             } elsif($self eq __PACKAGE__) {
146 0         0 Carp::croak('Usage: ', $self, '::parse(string => $string)');
147             }
148 1         6 return(__PACKAGE__->new()->parse($self));
149             } elsif(ref($self) eq 'HASH') {
150 1         6 return(__PACKAGE__->new()->parse($self));
151             } elsif(ref($_[0]) eq 'HASH') {
152 5         16 %params = %{$_[0]};
  5         22  
153             # } elsif(ref($_[0]) && (ref($_[0] !~ /::/))) {
154             } elsif(ref($_[0])) {
155 0         0 Carp::croak('Usage: ', __PACKAGE__, '::parse(string => $string)');
156             } elsif(scalar(@_) && (scalar(@_) % 2 == 0)) {
157 5         22 %params = @_;
158             } else {
159 133         320 $params{'string'} = shift;
160             }
161              
162 143 50       288 if(my $string = $params{'string'}) {
163             # Allow the text to be an object
164 143 100       284 if(ref($string)) {
165 1         3 $string = $string->as_string();
166             }
167              
168 143 100       339 if(wantarray) {
169             # Return an array with all of the dates which match
170 62         94 my @rc;
171              
172             # Ensure that the result includes the dates in the
173             # same order that they are in the string
174 62         692 while($string =~ /(^|\D)([0-9]?[0-9])[\.\-\/ ]+?([0-1]?[0-9])[\.\-\/ ]+?([0-9]{2,4})/g) {
175             # Match dates: 01/01/2012 or 30-12-11 or 1 2 1985
176 17         98 $rc[pos $string] = $self->parse("$2 $3 $4");
177             }
178 62         6397 while($string =~ /($d|$sd)[\s,\-_\/]*?(\d?\d)[,\-\/]*($o)?[\s,\-\/]*($m|$sm)[\s,\-\/]+(\d{4})/ig) {
179             # Match dates: Sunday 1st March 2015; Sunday, 1 March 2015; Sun 1 Mar 2015; Sun-1-March-2015
180 25         161 $rc[pos $string] = $self->parse("$2 $4 $5");
181             }
182 62         14905 while($string =~ /(\d{1,2})\s($m|$sm)\s(\d{4})/ig) {
183 27         1150 $rc[pos $string] = $self->parse("$1 $2 $3");
184             }
185 62         7871 while($string =~ /($m|$sm)[\s,\-_\/]*?(\d?\d)[,\-\/]*($o)?[\s,\-\/]+(\d{4})/ig) {
186 12         363 $rc[pos $string] = $self->parse("$1 $2 $4");
187             }
188 62 100       3544 if(scalar(@rc)) {
189             # Remove empty items and create a well-ordered
190             # array to return
191 61         114 return grep { defined($_) } @rc;
  1302         2406  
192             }
193             }
194              
195             # !wantarray
196 82         196 my $day;
197             my $month;
198 82         0 my $year;
199              
200 82 100       1591 if($string =~ /(^|\D)([0-9]?[0-9])[\.\-\/ ]+?([0-1]?[0-9])[\.\-\/ ]+?([0-9]{2,4})/) {
    50          
    100          
201             # Match dates: 01/01/2012 or 30-12-11 or 1 2 1985
202 17         38 $day = $2;
203 17         44 $month = $3;
204 17         24 $year = $4;
205             } elsif($string =~ /($d|$sd)[\s,\-_\/]*?(\d?\d)[,\-\/]*($o)?[\s,\-\/]*($m|$sm)[\s,\-\/]+(\d{4})/i) {
206             # Match dates: Sunday 1st March 2015; Sunday, 1 March 2015; Sun 1 Mar 2015; Sun-1-March-2015
207 0   0     0 $day //= $2;
208 0   0     0 $month //= $4;
209 0   0     0 $year //= $5;
210             } elsif($string =~ /($m|$sm)[\s,\-_\/]*?(\d?\d)[,\-\/]*($o)?[\s,\-\/]+(\d{4})/i) {
211 12   33     83 $month //= $1;
212 12   33     52 $day //= $2;
213 12   33     36 $year //= $4;
214             # } elsif($string =~ /[^\s,\(](\d{1,2})\s+($m|$sm)[\s,]+(\d{4})/i) {
215             # # 12 September 1856
216             # $day = $1;
217             # $month = $2;
218             # $year = $3;
219             }
220              
221 82 100 66     556 if((!defined($month)) && ($string =~ /($m|$sm)/i)) {
222             # Match month name
223 53         140 $month = $1;
224             }
225              
226 82 100 66     310 if((!defined($year)) && ($string =~ /(\d{4})/)) {
227             # Match Year if not already set
228 53         112 $year = $1;
229             }
230              
231             # We've managed to dig out a month and year, is there anything that looks like a day?
232 82 100 33     332 if(defined($month) && defined($year) && !defined($day)) {
      66        
233             # Match "Sunday 1st"
234 53 50       3681 if($string =~ /($d|$sd)[,\s\-\/]+(\d?\d)[,\-\/]*($o)\s+$year/i) {
    50          
    100          
    50          
    0          
    0          
    0          
235 0         0 $day = $1;
236             } elsif($string =~ /[\s\(](\d{1,2})\s+($m|$sm)/i) {
237 0         0 $day = $1;
238             } elsif($string =~ /^(\d{1,2})\s+($m|$sm)\s/i) {
239 52         167 $day = $1;
240             } elsif($string =~ /($m|$sm)\s+(the\s+)?(\d{1,2})th\s/i) {
241 1         7 $day = $3;
242             } elsif($string =~ /($m|$sm)\s+the\s+(\d{1,2})th\s/) {
243 0         0 $day = $2;
244             } elsif($string =~ /\s1st\s/i) {
245 0         0 $day = 1;
246             } elsif($string =~ /\s2nd\s/i) {
247 0         0 $day = 2;
248             }
249             }
250              
251 82 50 33     449 if($day && $month && $year) {
      33        
252 82 100       286 if($year < 100) {
253 17         26 $year += 2000;
254             }
255 82         158 $month = lc($month);
256 82 100       241 if($month =~ /[a-z]/i) {
257 65         140 foreach my $i(0..11) {
258 228 100 100     697 if(($month eq $month_names[$i]) || ($month eq $short_month_names[$i])) {
259 65         258 return DateTime->new(day => $day, month => $i + 1, year => $year);
260             }
261             }
262 0         0 Carp::croak(__PACKAGE__, ": unknown month $month");
263 0         0 return;
264             } else {
265 17         66 return DateTime->new(day => $day, month => $month, year => $year);
266             }
267             }
268             } else {
269 0           Carp::croak('Usage: ', __PACKAGE__, '::parse(string => $string)');
270             }
271             }
272              
273             =head1 AUTHOR
274              
275             Nigel Horne, C<< <njh at bandsman.co.uk> >>
276              
277             Based on L<https://github.com/etiennetremel/PHP-Find-Date-in-String>.
278             Here's the author information from that:
279              
280             author Etienne Tremel
281             license L<https://creativecommons.org/licenses/by/3.0/> CC by 3.0
282             link L<http://www.etiennetremel.net>
283             version 0.2.0
284              
285             =head1 BUGS
286              
287             =head1 SEE ALSO
288              
289             L<DateTime::Format::Natural>
290              
291             =head1 SUPPORT
292              
293             You can find documentation for this module with the perldoc command.
294              
295             perldoc DateTime::Format::Text
296              
297             You can also look for information at:
298              
299             =over 4
300              
301             =item * RT: CPAN's request tracker
302              
303             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=DateTime-Format-Text>
304              
305             =item * Search CPAN
306              
307             L<http://search.cpan.org/dist/DateTime-Format-Text/>
308              
309             =back
310              
311             =head1 LICENSE AND COPYRIGHT
312              
313             Copyright 2019-2023 Nigel Horne.
314              
315             This program is released under the following licence: GPL2
316              
317             =cut
318              
319             1;