File Coverage

blib/lib/Date/Extract.pm
Criterion Covered Total %
statement 111 111 100.0
branch 26 28 92.8
condition 45 60 75.0
subroutine 22 22 100.0
pod 2 2 100.0
total 206 223 92.3


line stmt bran cond sub pod time code
1             package Date::Extract;
2 7     7   157752 use strict;
  7         42  
  7         279  
3 7     7   39 use warnings;
  7         13  
  7         178  
4 7     7   7137 use DateTime::Format::Natural;
  7         2001914  
  7         596  
5 7     7   94 use List::Util 'reduce';
  7         14  
  7         745  
6 7     7   44 use parent 'Class::Data::Inheritable';
  7         17  
  7         69  
7              
8             our $VERSION = '0.05.01'; # VERSION
9             our $DATE = '2014-06-09'; # DATE
10              
11             __PACKAGE__->mk_classdata($_) for qw/scalar_downgrade handlers regex/;
12              
13             sub _croak {
14 3     3   31 require Carp;
15 3         592 Carp::croak @_;
16             }
17              
18             sub new {
19 43     43 1 68229 my $class = shift;
20 43         283 my %args = (
21             format => 'DateTime',
22             returns => 'first',
23             prefers => 'nearest',
24             time_zone => 'floating',
25             @_,
26             );
27              
28 43 100 100     262 if ($args{format} ne 'DateTime'
      100        
      100        
29             && $args{format} ne 'verbatim'
30             && $args{format} ne 'epoch'
31             && $args{format} ne 'combined') {
32 1         3 _croak "Invalid `format` passed to constructor: expected `DateTime', `verbatim', `epoch', `combined'.";
33             }
34              
35 42 100 100     235 if ($args{returns} ne 'first'
      100        
      100        
      100        
      100        
36             && $args{returns} ne 'last'
37             && $args{returns} ne 'earliest'
38             && $args{returns} ne 'latest'
39             && $args{returns} ne 'all'
40             && $args{returns} ne 'all_cron') {
41 1         4 _croak "Invalid `returns` passed to constructor: expected `first', `last', `earliest', `latest', `all', or `all_cron'.";
42             }
43              
44 41 100 66     167 if ($args{prefers} ne 'nearest'
      100        
45             && $args{prefers} ne 'past'
46             && $args{prefers} ne 'future') {
47 1         3 _croak "Invalid `prefers` passed to constructor: expected `nearest', `past', or `future'.";
48             }
49              
50 40   33     261 my $self = bless \%args, ref($class) || $class;
51              
52 40         119 return $self;
53             }
54              
55             # This method will combine the arguments of parser->new and extract. Modify the
56             # "to" hash directly.
57              
58             sub _combine_args {
59 58     58   91 shift;
60              
61 58         96 my $from = shift;
62 58         87 my $to = shift;
63              
64 58   33     438 $to->{format} ||= $from->{format};
65 58   33     354 $to->{prefers} ||= $from->{prefers};
66 58   66     280 $to->{returns} ||= $from->{returns};
67 58   33     301 $to->{time_zone} ||= $from->{time_zone};
68             }
69              
70             sub extract {
71 58     58 1 80425 my $self = shift;
72 58         127 my $text = shift;
73 58         192 my %args = @_;
74              
75             # using extract as a class method
76 58 100       302 $self = $self->new
77             if !ref($self);
78              
79             # combine the arguments of parser->new and this
80 58         249 $self->_combine_args($self, \%args);
81              
82             # when in scalar context, downgrade
83 58 100       292 $args{returns} = $self->_downgrade($args{returns})
84             unless wantarray;
85              
86             # do the work
87 58         264 my @ret = $self->_extract($text, %args);
88              
89             # munge the output to match the desired return type
90 58         465 return $self->_handle($args{returns}, @ret);
91             }
92              
93             # build the giant regex used for parsing. it has to be a single regex, so that
94             # the order of matches is correct.
95             sub _build_regex {
96 5     5   82 my $self = shift;
97              
98 5         12 my $relative = '(?:today|tomorrow|yesterday)';
99              
100 5         13 my $long_weekday = '(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)';
101 5         10 my $short_weekday = '(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)';
102 5         21 my $weekday = "(?:$long_weekday|$short_weekday)";
103              
104 5         17 my $relative_weekday = "(?:(?:next|previous|last)\\s*$weekday)";
105              
106 5         11 my $long_month = '(?:January|February|March|April|May|June|July|August|September|October|November|December)';
107 5         9 my $short_month = '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)';
108 5         156 my $month = "(?:$long_month|$short_month)";
109              
110             # 1 - 31
111 5         15 my $cardinal_monthday = "(?:[1-9]|[12][0-9]|3[01])";
112 5         14 my $monthday = "(?:$cardinal_monthday(?:st|nd|rd|th)?)";
113              
114 5         16 my $day_month = "(?:$monthday\\s*$month)";
115 5         16 my $month_day = "(?:$month\\s*$monthday)";
116 5         25 my $day_month_year = "(?:(?:$day_month|$month_day)\\s*,?\\s*\\d\\d\\d\\d)";
117              
118 5         6 my $yyyymmdd = "(?:\\d\\d\\d\\d[-/]\\d\\d[-/]\\d\\d)";
119 5         10 my $ddmmyy = "(?:\\d\\d[-/]\\d\\d[-/]\\d\\d)";
120 5         8 my $ddmmyyyy = "(?:\\d\\d[-/]\\d\\d[-/]\\d\\d\\d\\d)";
121              
122 5         20 my $other = $self->_build_more_regex;
123 5 50       23 $other = "|$other"
124             if $other;
125              
126 5         3400 my $regex = qr{
127             \b(
128             $relative # today
129             | $relative_weekday # last Friday
130             | $weekday # Monday
131             | $day_month_year # November 13th, 1986
132             | $day_month # November 13th
133             | $month_day # 13 Nov
134             | $yyyymmdd # 1986/11/13
135             | $ddmmyy # 11-13-86
136             | $ddmmyyyy # 11-13-1986
137             $other # anything from the subclass
138             )\b
139             }ix;
140              
141 5         37 $self->regex($regex);
142             }
143              
144             # this is to be used in subclasses for adding more stuff to the regex
145             # for example, to add support for $foo_bar and $baz_quux, return
146             # "$foo_bar|$baz_quux"
147 5     5   13 sub _build_more_regex { '' }
148              
149             # build the list->scalar downgrade types
150             sub _build_scalar_downgrade {
151 5     5   64 my $self = shift;
152              
153 5         37 $self->scalar_downgrade({
154             all => 'first',
155             all_cron => 'earliest',
156             });
157             }
158              
159             # build the handlers that munge the list of dates to the desired order
160             sub _build_handlers {
161 5     5   113 my $self = shift;
162              
163             $self->handlers({
164             all_cron => sub {
165 1     1   5 sort { DateTime->compare_ignore_floating($a, $b) } @_
  2         53  
166             },
167 1     1   7 all => sub { @_ },
168              
169 2 100   2   35 earliest => sub { reduce { $a < $b ? $a : $b } @_ },
  4         209  
170 1 100   1   14 latest => sub { reduce { $a > $b ? $a : $b } @_ },
  2         92  
171 52     52   408 first => sub { $_[0] },
172 1     1   15 last => sub { $_[-1] },
173 5         100 });
174             }
175              
176             # actually perform the scalar downgrade
177             sub _downgrade {
178 52     52   89 my $self = shift;
179 52         99 my $returns = shift;
180              
181 52   66     210 my $downgrades = $self->scalar_downgrade || $self->_build_scalar_downgrade;
182 52   66     696 return $downgrades->{$returns} || $returns;
183             }
184              
185             sub _handle {
186 58     58   111 my $self = shift;
187 58         115 my $returns = shift;
188              
189 58   66     333 my $handlers = $self->handlers || $self->_build_handlers;
190 58         690 my $handler = $handlers->{$returns};
191 58 50       266 return defined $handler ? $handler->(@_) : @_
192             }
193              
194             sub _extract {
195 58     58   99 my $self = shift;
196 58         91 my $text = shift;
197 58         178 my %args = @_;
198              
199 58         111 my $fmt = $self->{format};
200              
201 58   66     219 my $regex = $self->regex || $self->_build_regex;
202 58         605 my @combined;
203 58         808 while ($text =~ /$regex/g) {
204 84         1401 push @combined, {
205             pos => $-[0],
206             verbatim => $1,
207             };
208             }
209              
210 58 100       207 return (map {$_->{verbatim}} @combined) if $fmt eq 'verbatim';
  3         14  
211              
212 57         110 my %dtfn_args;
213 57 100 66     399 $dtfn_args{prefer_future} = 1
214             if $args{prefers} && $args{prefers} eq 'future';
215 57         142 $dtfn_args{time_zone} = $args{time_zone};
216              
217 57         395 my $parser = DateTime::Format::Natural->new(%dtfn_args);
218 57         441676 for (@combined) {
219 81         390 my $dt = $parser->parse_datetime($_->{verbatim});
220 81 100       473771 if ($parser->success) {
221 78         2864 $dt->set_time_zone($args{time_zone});
222 78         1210 $_->{DateTime} = $dt;
223             }
224             }
225              
226 57 100       433 if ($fmt eq 'epoch') {
    100          
227 1         3 return map { $_->{DateTime}->epoch } @combined;
  3         28  
228             } elsif ($fmt eq 'combined') {
229 1         81 return @combined;
230             } else {
231 55         120 return map {$_->{DateTime}} @combined;
  75         3206  
232             }
233             }
234              
235             1;
236              
237             __END__