File Coverage

blib/lib/Date/RangeParser/EN.pm
Criterion Covered Total %
statement 323 344 93.9
branch 195 230 84.7
condition 31 38 81.5
subroutine 17 17 100.0
pod 2 2 100.0
total 568 631 90.0


line stmt bran cond sub pod time code
1             package Date::RangeParser::EN;
2              
3             our $AUTHORITY = 'cpan:GSG';
4             # ABSTRACT: Parse plain English date/time range strings
5 3     3   406808 use version;
  3         4535  
  3         28  
6             our $VERSION = 'v1.2.1'; # VERSION
7              
8 3     3   345 use strict;
  3         7  
  3         83  
9 3     3   20 use warnings;
  3         4  
  3         228  
10 3     3   1667 use utf8;
  3         934  
  3         19  
11              
12 3     3   1648 use Date::Manip;
  3         364021  
  3         781  
13 3     3   3792 use DateTime;
  3         1877245  
  3         26462  
14              
15             my $dm_backend = $Date::Manip::Backend || '';
16             if ($Date::Manip::VERSION lt '6' or $dm_backend eq 'DM5') {
17             warnings::warnif 'deprecated', "Versions of Date::Manip prior to 6.0.0 and DM5 backend will be deprecated in future releases.";
18             }
19              
20             #pod =head1 NAME
21             #pod
22             #pod Date::RangeParser::EN - Parser for plain English date/time range strings
23             #pod
24             #pod =head1 SYNOPSIS
25             #pod
26             #pod use Date::RangeParser::EN;
27             #pod
28             #pod my $parser = Date::RangeParser::EN->new;
29             #pod my ($begin, $end) = $parser->parse_range("this week");
30             #pod
31             #pod =head1 DESCRIPTION
32             #pod
33             #pod Parses plain-English strings representing date/time ranges
34             #pod
35             #pod =cut
36              
37             my %BOD = (hour => 0, minute => 0, second => 0);
38             my %EOD = (hour => 23, minute => 59, second => 59);
39             my %BOY = (month => 1, day => 1, %BOD);
40             my %EOY = (month => 12, day=> 31, %EOD);
41              
42             my $US_FORMAT_WITH_DASHES = qr/^ (0[1-9]|1[012]) - (0[1-9]|[12][0-9]|3[01]) - ( (?:[12][0-9]) [0-9]{2} ) $/x;
43              
44             my %weekday = (
45             sunday => 0,
46             monday => 1,
47             tuesday => 2,
48             wednesday => 3,
49             thursday => 4,
50             friday => 5,
51             saturday => 6,
52             );
53              
54             my $weekday = qr/(?:mon|tues|wednes|thurs|fri|satur|sun)day/;
55              
56             my %ordinal = (
57             qr/\bfirst\b/ => "1st", qr/\bsecond\b/ => "2nd",
58             qr/\bthird\b/ => "3rd", qr/\bfourth\b/ => "4th",
59             qr/\bfifth\b/ => "5th", qr/\bsixth\b/ => "6th",
60             qr/\bseventh\b/ => "7th", qr/\beighth\b/ => "8th",
61             qr/\bninth\b/ => "9th", qr/\btenth\b/ => "10th",
62             qr/\beleventh\b/ => "11th", qr/\btwelfth\b/ => "12th",
63             qr/\bthirteenth\b/ => "13th", qr/\bfourteenth\b/ => "14th",
64             qr/\bfifteenth\b/ => "15th", qr/\bsixteenth\b/ => "16th",
65             qr/\bseventeenth\b/ => "17th", qr/\beighteenth\b/ => "18th",
66             qr/\bnineteenth\b/ => "19th", qr/\btwentieth\b/ => "20th",
67             qr/\btwenty-?first\b/ => "21st", qr/\btwenty-?second\b/ => "22nd",
68             qr/\btwenty-?third\b/ => "23rd", qr/\btwenty-?fourth\b/ => "24th",
69             qr/\btwenty-?fifth\b/ => "25th", qr/\btwenty-?sixth\b/ => "26th",
70             qr/\btwenty-?seventh\b/ => "27th", qr/\btwenty-?eighth\b/ => "28th",
71             qr/\btwenty-?ninth\b/ => "29th", qr/\bthirtieth\b/ => "30th",
72             qr/\bthirty-?first\b/ => "31st",
73             qr/\bone\b/ => "1", qr/\btwo\b/ => "2",
74             qr/\bthree\b/ => "3", qr/\bfour\b/ => "4",
75             qr/\bfive\b/ => "5", qr/\bsix\b/ => "6",
76             qr/\bseven\b/ => "7", qr/\beight\b/ => "8",
77             qr/\bnine\b/ => "9", qr/\bten\b/ => "10",
78             qr/\beleven\b/ => "11", qr/\btwelve\b/ => "12",
79             qr/\bthirteen\b/ => "13", qr/\bfourteen\b/ => "14",
80             qr/\bfifteen\b/ => "15", qr/\bsixteen\b/ => "16",
81             qr/\bseventeen\b/ => "17", qr/\beighteen\b/ => "18",
82             qr/\bnineteen\b/ => "19", qr/\btwenty\b/ => "20",
83             qr/\btwenty-one\b/ => "21", qr/\btwenty-two\b/ => "22",
84             qr/\btwenty-three\b/ => "23", qr/\btwenty-four\b/ => "24",
85             qr/\btwenty-five\b/ => "25", qr/\btwenty-six\b/ => "26",
86             qr/\btwenty-seven\b/ => "27", qr/\btwenty-eight\b/ => "28",
87             qr/\btwenty-nine\b/ => "29", qr/\bthirty\b/ => "30",
88             qr/\bthirty-one\b/ => "31",
89             );
90              
91             my %month = (
92             qr/jan(?:uary)?/ => 1, qr/feb(?:ruary)?/ => 2,
93             qr/mar(?:ch)?/ => 3, qr/apr(?:il)?/ => 4,
94             qr/may/ => 5, qr/jun(?:e)?/ => 6,
95             qr/jul(?:y)?/ => 7, qr/aug(?:ust)?/ => 8,
96             qr/sep(?:tember)?/ => 9, qr/oct(?:ober)?/ => 10,
97             qr/nov(?:ember)?/ => 11, qr/dec(?:ember)?/ => 12,
98             );
99              
100             my $month_re = qr/\b(?:
101             a(?:pr(?:il)?|ug(?:ust)?) |
102             dec(?:ember)? |
103             feb(?:ruary)? |
104             j(?:an(?:uary)?|u(?:ne?|ly?)) |
105             ma(?:y|r(?:ch)?) |
106             nov(?:ember)? |
107             oct(?:ober)? |
108             sep(?:tember)?
109             )\b/x;
110              
111             #pod =head1 METHODS
112             #pod
113             #pod =head2 new
114             #pod
115             #pod Returns a new instance of Date::RangeParser::EN.
116             #pod
117             #pod Takes an optional hash of parameters:
118             #pod
119             #pod =over 4
120             #pod
121             #pod =item * B<datetime_class>
122             #pod
123             #pod By default, Date::RangeParser::EN returns two L<DateTime> objects representing the beginning and end of the range. If you use a subclass of DateTime (or another module that implements the DateTime API), you may pass the name of this class to use it instead.
124             #pod
125             #pod At the very least, this given class must implement a C<new> method that accepts a hash of arguments, where the following keys will be set:
126             #pod
127             #pod year
128             #pod month
129             #pod day
130             #pod hour
131             #pod minute
132             #pod second
133             #pod
134             #pod This gives you the freedom to set your time zones and such however you need to.
135             #pod
136             #pod =item * B<infinite_past_class>
137             #pod =item * B<infinite_future_class>
138             #pod
139             #pod By default, Date::RangeParser::EN uses DateTime::Infinite::Past and DateTime::Infinite::Future to create open-ended ranges (for example "after today"). If you have extended these classes, you may pass the corresponding names in.
140             #pod
141             #pod The given classes must implement a C<new> method that accepts no arguments.
142             #pod
143             #pod =item * B<now_callback>
144             #pod
145             #pod By default, Date::RangeParser::EN uses DateTime->now to determine the current date/time for calculations. If you need to work with a different time (for instance, if you need to adjust for time zones), you may pass a callback (code reference) which returns a DateTime object.
146             #pod
147             #pod =back
148             #pod
149             #pod =cut
150              
151             sub new
152             {
153 165     165 1 1203236 my ($class, %params) = @_;
154              
155 165         352 my $self = \%params;
156              
157 165         424 bless $self, $class;
158              
159 165         635 return $self;
160             }
161              
162             #pod =head2 parse_range
163             #pod
164             #pod Accepts a string representing a plain-English date range, for instance:
165             #pod
166             #pod =over 4
167             #pod
168             #pod =item * today
169             #pod
170             #pod =item * this week
171             #pod
172             #pod =item * the past 2 months
173             #pod
174             #pod =item * next Tuesday
175             #pod
176             #pod =item * two weeks ago
177             #pod
178             #pod =item * the next 3 hours
179             #pod
180             #pod =item * the 3rd of next month
181             #pod
182             #pod =item * the end of this month
183             #pod
184             #pod =back
185             #pod
186             #pod More formally, this will parse the following kinds of date strings:
187             #pod
188             #pod NUMBER : ordinary number (1)
189             #pod PERIOD : one of: hour, day, week, month, quarter, or year (or the plural of these)
190             #pod WEEKDAY : one of: Monday, Tuesday, Wedensday, Thursday, Friday, Saturday, or Sunday
191             #pod CARDINAL : a cardinal number (21st) or the word for that number (twenty-first) or end
192             #pod MONTH : a month name: January, Feburary, March, April, May, June, July August,
193             #pod September, October, November, or Decmeber or any 3-letter abbreviation
194             #pod YEAR : a 4-digit year (2-digits will not work)
195             #pod TIMES: January 1st, 2000 at 10:00am through January 1st, 2000 at 2:00pm
196             #pod RANGE : any date range that can be parsed by parse_range
197             #pod ELEMENT : any element of a date range that can be parsed by parse_range
198             #pod
199             #pod today : today, midnight to midnight
200             #pod
201             #pod this PERIOD : the current period, start to end
202             #pod this month
203             #pod
204             #pod current PERIOD : the current period, start to end
205             #pod current year
206             #pod
207             #pod this WEEKDAY : the WEEKDAY that is in the current week, midnight to midnight
208             #pod this Monday
209             #pod
210             #pod NUMBER PERIOD ago : past date relative to now until now
211             #pod 3 days ago
212             #pod
213             #pod past NUMBER PERIOD : past date relative to now until now
214             #pod past 2 weeks
215             #pod
216             #pod last NUMBER PERIOD : past date relative to now until now
217             #pod last 6 hours
218             #pod
219             #pod past NUMBER WEEKDAY : the weekday a number of weeks before now until now
220             #pod past 4 Saturdays
221             #pod
222             #pod NUMBER WEEKDAY ago : the weekday a number of weeks before now until now
223             #pod 3 Fridays ago
224             #pod
225             #pod yesterday : yesterday, midnight to midnight
226             #pod
227             #pod last WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
228             #pod last Wednesday
229             #pod
230             #pod previous WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
231             #pod previous Friday
232             #pod
233             #pod past WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
234             #pod past Tuesday
235             #pod
236             #pod this past WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
237             #pod this past Saturday
238             #pod
239             #pod coming WEEKDAY : the WEEKDAY that is in the week after this, midnight to midnight
240             #pod coming Monday
241             #pod
242             #pod this coming WEEKDAY : the WEEKDAY that is in the week after this, midnight to midnight
243             #pod this coming Thursday
244             #pod
245             #pod NUMBER Business days ago : past number of business days relative to now until now
246             #pod
247             #pod NUMBER weekdays ago : past number of weekdays relative to now until now
248             #pod
249             #pod LAST or PAST NUMBER weekdays ago : past number of weekdays relative to now until now
250             #pod
251             #pod NUMBER PERIOD hence : now to a future date relative to now
252             #pod 4 months hence
253             #pod
254             #pod NUMBER PERIOD from now : now to a future date relative to now
255             #pod 6 days from now
256             #pod
257             #pod next NUMBER PERIOD : now to a future date relative to now
258             #pod next 7 years
259             #pod
260             #pod tomorrow : tomorrow, midnight to midnight
261             #pod
262             #pod next NUMBER WEEKDAY : the WEEKDAY that is in a number of weeks after this, midnight to midnight
263             #pod next 4 Sundays
264             #pod
265             #pod CARDINAL of this month : the specified day of the current month, midnight to midnight
266             #pod 14th of this month
267             #pod
268             #pod CARDINAL of last month : the specified day of the previous month, midnight to midnight
269             #pod 31st of last month
270             #pod
271             #pod CARDINAL of next month : the specified day of the month following this, midnight to midnight
272             #pod 3rd of next month
273             #pod
274             #pod CARDINAL of NUMBER months ago : the specified day of a previous month, midnight to midnight
275             #pod 12th of 2 months ago
276             #pod
277             #pod CARDINAL of NUMBER months from now : the specified day of a following month, midnight to midnight
278             #pod 7th of 22 months from now
279             #pod
280             #pod CARDINAL of NUMBER months hence : the specified day of a following month, midnight to midnight
281             #pod 22nd of 6 months hence
282             #pod
283             #pod CARDINAL of TIME : the specific time of day which can be accompanied by a date
284             #pod 10:00am through 12:00pm defaults to today if no date is given
285             #pod
286             #pod MONTH : the named month of the current year, 1st to last day
287             #pod August
288             #pod
289             #pod this MONTH : the named month of the current year, 1st to last day
290             #pod this Sep
291             #pod
292             #pod last MONTH : the named month of the previous year, 1st to last day
293             #pod last January
294             #pod
295             #pod next MONTH : the named month of the next year, 1st to last day
296             #pod next Dec
297             #pod
298             #pod MONTH YEAR : the named month of the named year, 1st to last day
299             #pod June 1969
300             #pod
301             #pod RANGE to RANGE : the very start of the first range to the very end of the second
302             #pod Tuesday to Next Saturday
303             #pod
304             #pod RANGE thru RANGE : the very start of the first range to the very end of the second
305             #pod 2 hours ago thru the next 6 hours
306             #pod
307             #pod RANGE through RANGE : the very start of the first range to the very end of the second
308             #pod August through December
309             #pod
310             #pod RANGE - RANGE : the very start of the first range to the very end of the second
311             #pod 9-1-2012 - 9-30-2012
312             #pod
313             #pod RANGE-RANGE : the very start of the first range to the very end of the second
314             #pod 10/10-10/20 (ranges must not contain hyphens, "-")
315             #pod
316             #pod American style dates : Month / Day / Year
317             #pod 6/15/2000
318             #pod
319             #pod before ELEMENT : all dates before the very start of the date specified in the ELEMENT
320             #pod < ELEMENT
321             #pod before today
322             #pod
323             #pod <= ELEMENT : all dates up to the very end of the date specified in the ELEMENT
324             #pod <= today
325             #pod
326             #pod after ELEMENT : all dates after the very end of the date specified in the ELEMENT
327             #pod > ELEMENT
328             #pod after next Tuesday
329             #pod
330             #pod >= ELEMENT : the date specified in the ELEMENT to the end of forever
331             #pod >= this Friday
332             #pod
333             #pod since ELEMENT : the date specified in the ELEMENT to the end of the current day
334             #pod since last Sunday
335             #pod
336             #pod Anything else is parsed by L<Date::Manip>. If Date::Manip is unable to parse the
337             #pod date given either, then the dates returned will be undefined.
338             #pod
339             #pod Also, when parsing:
340             #pod
341             #pod =over
342             #pod
343             #pod =item *
344             #pod
345             #pod The words "the" and "and" will always be ignored and can appear anywhere.
346             #pod
347             #pod =item *
348             #pod
349             #pod Cardinal numbers may be spelled out as words, i.e. "September first" instead of
350             #pod "September 1st". Similarly, "two weeks ago" and "2 weeks ago" will be treated as the same
351             #pod
352             #pod =item *
353             #pod
354             #pod Any plural or singular period shown above can be used with the opposite.
355             #pod
356             #pod =item *
357             #pod
358             #pod All dates are parsed relative to the parser's notion of now. You can control
359             #pod this by setting the C<now_callback> option on the constructor.
360             #pod
361             #pod =back
362             #pod
363             #pod Returns two L<DateTime> objects, representing the beginning and end of the range.
364             #pod
365             #pod =cut
366              
367             sub parse_range
368             {
369 245     245 1 6739 my ($self, $string, %params) = @_;
370 245         497 my ($beg, $end, $y, $m, $d);
371              
372 245         858 $string = lc $string;
373              
374             # The words "the" and "and" may be used with ridiculous impunity
375 245         1139 $string =~ s/\bthe\b//g;
376 245         581 $string =~ s/\band\b//g;
377              
378 245         990 $string =~ s/^\s+//g;
379 245         989 $string =~ s/\s+$//g;
380 245         1176 $string =~ s/\s+/ /g;
381              
382             # We address the ordinals (let's not get silly, though). If we wanted
383             # to get silly, we'd use Lingua::EN::Words2Nums, which would horribly
384             # complicate the general parsing
385 245         1766 while (my ($str, $num) = each %ordinal)
386             {
387 15190         140832 $string =~ s/$str/$num/g;
388             }
389             # at this point, we may have changed the word 'second' into '2nd'
390             # when we did not mean to. So we swap back. We do this outside of
391             # the while loop above, because this function is recursive, and
392             # the following regex is anchored to the end of the string.
393 245         660 $string =~ s/2nd$/second/;
394              
395             # Handle weekdays as we do business days
396             # We are going to mimic what we do for business days and let it
397             # fall into Date::Manip since we can handle business days there.
398 245 100       791 if ($string =~ /^(?:last|past)\s?(\d+)?\s?weekdays?/) {
399 4   100     55 my $interval = $1 || 1;
400 4 100       28 if ($interval == 1) {
401 2         27 $string =~ s/^(?:last|past)\s?(\d+)?\s?weekdays?/$interval business days ago/;
402             } else {
403 2         71 $string =~ s/^(?:last|past)\s?(\d+)?\s?weekdays?/past $interval business days/;
404             }
405             }
406 245 100       978 if ($string =~ /^(\d+)?\s?weekdays? ago/) {
407 3   50     38 my $interval = $1 || 1;
408 3         51 $string =~ s/^(\d+)?\s?weekdays? ago/$interval business days ago/;
409             }
410              
411             # Sometimes we get a bare US style date, but the user has used dashes.
412             # Let's de-scramble that before moving on.
413 245         1294 $string = $self->_convert_from_us_dashed($string);
414              
415 245 100 100     26641 if ($string =~ /\s(?:to|thru|through|-|–|—)\s/)
    100 66        
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
416             {
417 24         217 my ($first, $second) = split /\s+(?:to|thru|through|-|–|—)\s+/, $string, 2;
418              
419 24         174 ($beg) = $self->parse_range($first);
420 24         322 (undef, $end) = $self->parse_range($second);
421             }
422              
423             # See if this is a range between two other dates separated by -
424             elsif ($string !~ /^\d+-\d+$/ and $string =~ /^[^-]+-[^-]+$/)
425             {
426 3         28 my ($first, $second) = split /\s?-\s?/, $string, 2;
427 3         46 ($beg) = $self->parse_range($first);
428 3         40 (undef, $end) = $self->parse_range($second);
429             }
430              
431             # "This thing" and "current thing"
432             elsif ($string eq "today" || $string =~ /^(?:this|current) day$/)
433             {
434 6         24 $beg = $self->_bod();
435 6         2983 $end = $self->_eod();
436             }
437             elsif ($string =~ /^(?:this|current) hour$/) {
438 1         30 $beg = $self->_now()->set(minute => 0, second => 0);
439 1         1159 $end = $beg->clone->set(minute => 59, second => 59);
440             }
441             elsif ($string =~ /^(?:this|current) minute$/) {
442 1         18 $beg = $self->_now()->set(second => 0);
443 1         1125 $end = $beg->clone->set(second => 59);
444             }
445             elsif ($string =~ /^(?:this|current) second$/) {
446             # Relly this comes from this or current second,
447             # but our ordinals messed this up.
448 2         16 $beg = $self->_now();
449 2         892 $end = $beg->clone;
450             }
451             elsif ($string =~ /^(?:this|current) week$/)
452             {
453 1         7 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
454 1         414 $beg = $self->_bod()->subtract(days => $dow); # Subtract to Sunday
455 1         1009 $end = $self->_eod()->add(days => 6 - $dow); # Add to Saturday
456             }
457             elsif ($string =~ /^(?:this|current) month$/)
458             {
459 1         6 $beg = $self->_bod()->set_day(1);
460 1         1211 $end = $self->_datetime_class()->last_day_of_month(
461             year => $self->_now()->year,
462             month => $self->_now()->month,
463             %EOD);
464             }
465             elsif ($string =~ /^(?:this|current) quarter$/)
466             {
467 1         3 my $zq = int(($self->_now()->month - 1) / 3); # 0..3
468 1         316 $beg = $self->_bod()->set_month($zq * 3 + 1)->set_day(1);
469 1         1038 $end = $self->_datetime_class()->last_day_of_month(
470             year => $self->_now()->year,
471             month => $zq * 3 + 3 ,
472             %EOD);
473             }
474             elsif ($string =~ /^(?:this|current) year$/)
475             {
476 1         6 $beg = $self->_datetime_class()->new(year => $self->_now()->year, %BOY);
477 1         932 $end = $self->_datetime_class()->new(year => $self->_now()->year, %EOY);
478             }
479             elsif ($string =~ /^this ($weekday)$/)
480             {
481 3         10 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
482 3         806 my $adjust = $weekday{$1} - $dow;
483 3 100       9 if ($adjust < 0)
    50          
484             {
485 2         4 $beg = $self->_bod()->subtract(days => abs($adjust));
486             }
487             elsif ($adjust > 0)
488             {
489 1         4 $beg = $self->_bod()->add(days => $adjust);
490             }
491             else
492             {
493 0         0 $beg = $self->_bod();
494             }
495 3         3531 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
496             }
497             # "Last N things" and "Past N things"
498             elsif ($string =~ /^(?:last|past) (\d+)?\s?(hour|minute|second)s?$/)
499             {
500 8         35 my $unit = $self->_clean_units($2);
501 8         18 my $offset = $1;
502              
503             # The "+0" math avoids call-by-reference side effects
504 8         23 $beg = $self->_now();
505 8   100     2543 $beg->subtract($unit => $offset // 1 + 0);
506 8         7963 $end = $self->_now();
507              
508 8 100       2152 if ($unit eq 'hours') {
    100          
509 3         23 $beg->set(minute => 0, second => 0);
510 3         1466 $end->set(minute => 59, second => 59);
511             } elsif ($unit eq 'minutes') {
512 3         11 $beg->set(second => 0);
513 3         1026 $end->set(second => 59);
514             }
515             }
516             elsif ($string =~ /^(?:last|past) (\d+) days?$/)
517             {
518 0         0 $beg = $self->_bod()->subtract(days => $1 - 1);
519 0         0 $end = $self->_eod();
520             }
521             elsif ($string =~ /^(?:last|past) (\d+) weeks?$/)
522             {
523 4         18 my $offset = $self->_now()->day_of_week % 7; # sun offset: 0 ... sat offset: 6
524 4         1728 $beg = $self->_bod()->subtract(days => $offset)->subtract(weeks => $1 - 1); # sunday
525 4         12478 $end = $self->_eod()->add(days => 6 - $offset); #saturday of current week
526             }
527             elsif ($string =~ /^(?:last|past) (\d+) months?$/)
528             {
529 2         10 $beg = $self->_bod()->set_day(1)->subtract(months => $1 - 1);
530 2         5535 $end = $self->_datetime_class()->last_day_of_month(
531             year => $self->_now()->year,
532             month => $self->_now()->month,
533             %EOD);
534             }
535             elsif ($string =~ /^(?:last|past) (\d+) years?$/)
536             {
537 2         106 $beg = $self->_bod()->set_month(1)->set_day(1)->subtract(years => $1 - 1);
538 2         7979 $end = $self->_eod()->set_month(12)->set_day(31);
539             }
540             elsif ($string =~ /^(?:last|past) (\d+) quarters?$/)
541             {
542 1         4 my $zq = int(($self->_now()->month - 1) / 3);
543 1         314 $end = $self->_bod()->set_month($zq * 3 + 1)->set_day(1)
544             ->add(months => 3)->subtract(seconds => 1);
545 1         2599 $beg = $end->clone->set_day(1)
546             ->subtract(months => (3 * $1) - 1)
547             ->subtract(days => 1)->add(seconds => 1);
548             }
549             elsif ($string =~ /^(?:last|past) (\d+) ($weekday)s?$/)
550             {
551 1 50       13 my $c = defined $1 ? $1 : 1;
552 1         8 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
553 1         259 my $adjust = $weekday{$2} - $dow;
554 1 50       9 $adjust -= 7 if $adjust >=0;
555 1         5 $adjust -= 7*($1 - 1);
556 1         9 $end = $self->_eod()->subtract(days => abs($adjust));
557 1         1234 $beg = $end->clone->subtract(days => 7*($c-1)+1)->add(seconds => 1);
558             }
559             # "Last thing" and "Previous thing"
560             elsif ($string =~ /^yesterday$/)
561             {
562 1         6 $beg = $self->_bod()->subtract("days" => 1);
563 1         2169 $end = $beg->clone->set(%EOD);
564             }
565             elsif ($string =~ /^(?:last|previous) week$/)
566             {
567 1         6 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
568 1         446 $beg = $self->_bod()->subtract(days => 7 + $dow); # Subtract to last Sunday
569 1         2117 $end = $self->_eod()->subtract(days => 1 + $dow); # Subtract to Saturday
570             }
571             elsif ($string =~ /^(?:last|previous) month$/)
572             {
573 1         6 $beg = $self->_bod()->set_day(1)->subtract(months => 1);
574 1         2746 $end = $self->_bod()->set_day(1)->subtract(seconds => 1);
575             }
576             elsif ($string =~ /^(?:last|previous) quarter$/)
577             {
578 1         4 my $zq = int(($self->_now()->month - 1) / 3);
579 1         253 $beg = $self->_bod()->set_month($zq * 3 + 1)->set_day(1)->subtract(months => 3);
580 1         1925 $end = $beg->clone->add(months => 3)->subtract(seconds => 1);
581             }
582             elsif ($string =~ /^(?:last|previous) year$/)
583             {
584 0         0 $beg = $self->_bod()->set_month(1)->set_day(1)->subtract(months => 12);
585 0         0 $end = $self->_bod()->set_month(1)->set_day(1)->subtract(seconds => 1);
586             }
587             elsif ($string =~ /^(?:last|previous) ($weekday)$/) {
588 2         9 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
589 2         710 my $adjust = $weekday{$1} - $dow - 7;
590 2         8 $beg = $self->_bod()->subtract(days => abs($adjust));
591 2         3379 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
592             }
593             # "Past weekday" and "This past weekday"
594             elsif ($string =~ /^(?:this )?past ($weekday)$/)
595             {
596 2         8 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
597 2         704 my $adjust = $weekday{$1} - $dow;
598 2 100       11 $adjust -= 7 if $adjust >= 0;
599 2         9 $beg = $self->_bod()->subtract(days => abs($adjust));
600 2         3364 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
601             }
602              
603             # Dates ago
604             elsif ($string =~ /^(\d+) ((?:month|day|week|year|quarter)s?) ago$/)
605             {
606             # "N months|days|weeks|years|quarters ago"
607 9         49 my $ct = $1 + 0;
608 9         36 my $unit = $self->_clean_units($2);
609              
610 9 100       34 if($unit eq 'quarters') {
611 2         33 $unit = 'months';
612 2         14 $ct *= 3;
613             }
614              
615 9         32 $beg = $self->_bod()->subtract($unit => $ct);
616 9         18155 $end = $beg->clone->set(%EOD);
617             }
618              
619             # Strictly time ago
620             elsif ($string =~ /^(\d+) ((?:hour|minute|second)s?) ago$/) {
621 3         28 my $ct = $1 + 0;
622 3         28 my $unit = $self->_clean_units($2);
623              
624 3         23 $beg = $self->_now()->subtract($unit => $ct);
625              
626 3 100       5664 if ($unit eq 'hours') {
    50          
    0          
627 1         17 $beg->set(minute => 0, second => 0);
628 1         714 $end = $beg->clone()->set(minute => 59, second => 59);
629             } elsif($unit eq 'minutes') {
630 2         16 $beg->set(second => 0);
631 2         984 $end = $beg->clone()->set(second => 59);
632             } elsif($unit eq 'seconds') {
633 0         0 $end = $self->_now();
634             }
635             }
636              
637             # N <Day of the week>s ago
638             elsif ($string =~ /^(\d+) ($weekday)s? ago$/) {
639 3         16 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
640 3         1034 my $adjust = $weekday{$2} - $dow;
641 3 50       12 $adjust -= 7 if $adjust >=0;
642 3         14 $adjust -= 7*($1 - 1);
643 3         12 $beg = $self->_bod()->subtract(days => abs($adjust));
644 3         4781 $end = $beg->clone->set(%EOD);
645             }
646              
647             # Hence from now portions
648             elsif ($string =~ /^(\d+) ($weekday)s? (?:hence|from\s+now)$/) {
649             # That's both "next sunday" and "3 sundays from now"
650 2 50       13 my $c = defined $1 ? $1 : 1;
651 2         7 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
652 2         879 my $adjust = $weekday{$2} - $dow; # get to right day of week
653 2 100       39 $adjust += 7 if $adjust <= 0; # add 7 days if its today or in the past
654 2         8 $adjust += 7*($c - 1);
655 2         9 $beg = $self->_bod()->add(days => $adjust);
656 2         3951 $end = $beg->clone->set(%EOD);
657             }
658             elsif ($string =~ /^(\d+)? weeks? (?:hence|from\s+now)$/) {
659 1         12 $beg = $self->_now();
660 1         371 $end = $beg->clone->add(weeks => $1);
661             }
662             # from now pieces
663             elsif ($string =~ /^(\d+)? days? (?:hence|from\s+now)$/) {
664 1         12 $beg = $self->_now();
665 1         452 $end = $beg->clone->add(days => $1);
666             }
667             elsif ($string =~ /^next (\d+)?\s?($weekday)s?$/)
668             {
669 2 50       16 my $c = defined $1 ? $1 : 1;
670 2         7 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
671 2         551 my $adjust = $weekday{$2} - $dow; # get to right day of week
672 2 50       9 $adjust += 7 if $adjust <= 0; # add 7 days if its today or in the past
673 2         10 $beg = $self->_bod()->add(days => $adjust);
674 2         2282 $adjust += 7*($c - 1);
675 2         14 $end = $beg->clone->add(days => 7*$c - 6)->subtract(seconds => 1);
676             }
677             # "Coming weekday" and "This coming weekday"
678             elsif ($string =~ /^(?:this )?coming ($weekday)$/)
679             {
680 1         3 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
681 1         287 my $adjust = $weekday{$1} - $dow;
682 1 50       5 $adjust += 7 if $adjust <= 0;
683 1         3 $beg = $self->_bod()->add(days => $adjust);
684 1         1123 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
685             }
686             # "Next thing" and "Next N things"
687             elsif ($string =~ /^next (\d+)?\s?(second|minute|hour)s?$/)
688             {
689 5 100       36 my $c = defined $1 ? $1 : 1;
690 5         18 my $unit = $self->_clean_units($2);
691              
692 5         15 $beg = $self->_now();
693 5         1417 $end = $beg->clone->add($unit => $c);
694             }
695             elsif ($string =~ /^(?:next (\d+)?\s?days?|tomorrow)$/)
696             {
697 2 100       13 my $c = defined $1 ? $1 : 1;
698 2         9 $beg = $self->_bod()->add(days => 1);
699 2         4203 $end = $beg->clone->add(days => $c)->subtract(seconds => 1)
700             }
701             elsif ($string =~ /^next (\d+)?\s?weeks?$/)
702             {
703 3 100       20 my $c = defined $1 ? $1 : 1;
704 3         12 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
705 3         1316 $beg = $self->_bod()->add(days => 7 - $dow); # Add to Sunday
706 3         5916 $end = $self->_eod()->add(days => 6 + 7*$c - $dow); # Add N Saturdays following
707             }
708             elsif ($string =~ /^next (\d+)?\s?months?$/)
709             {
710 1 50       6 my $c = defined $1 ? $1 : 1;
711 1         5 $beg = $self->_bod()->add(months => 1, end_of_month => 'preserve')->set_day(1);
712 1         2633 my $em = $self->_now()->add(months => $c, end_of_month => 'preserve');
713 1         1759 $end = $self->_datetime_class()->last_day_of_month(year => $em->year, month => $em->month, %EOD);
714             }
715             elsif ($string =~ /^next (\d+)?\s?quarters?$/)
716             {
717 2 100       7 my $c = defined $1 ? $1 : 1;
718 2         7 my $zq = int(($self->_now()->month - 1) / 3);
719 2         546 $beg = $self->_bod()->set_month($zq * 3 + 1)->set_day(1)
720             ->add(months => 3, end_of_month => 'preserve');
721 2         3649 $end = $beg->clone ->add(months => 3 * $c, end_of_month => 'preserve')
722             ->subtract(seconds => 1);
723             }
724             # Add support for N quarters from now
725             elsif ($string =~ /^(\d+)?\s?quarters? (?:hence|from\s+now)$/)
726             {
727 0 0       0 my $c = defined $1 ? $1 : 1;
728 0         0 my $zq = int(($self->_now()->month - 1) / 3);
729 0         0 $beg = $self->_bod()->set_month($zq * 3 + 1)->set_day(1)
730             ->add(months => 3, end_of_month => 'preserve');
731 0         0 $end = $beg->clone ->add(months => 3, end_of_month => 'preserve')
732             ->subtract(seconds => 1);
733             }
734             elsif ($string =~ /^next (\d+)?\s?years?$/)
735             {
736 2 50       14 my $c = defined $1 ? $1 : 1;
737 2         10 $beg = $self->_bod()->set_month(1)->set_day(1)->add(years => 1);
738 2         6542 $end = $self->_eod()->set_month(12)->set_day(31)->add(years => $c);
739             }
740             elsif ($string =~ /^next (\d+)?\s?($weekday)s?$/)
741             {
742             # That's both "next sunday" and "3 sundays from now"
743 0 0       0 my $c = defined $1 ? $1 : 1;
744 0         0 my $dow = $self->_now()->day_of_week % 7; # Monday == 1
745 0         0 my $adjust = $weekday{$2} - $dow; # get to right day of week
746 0 0       0 $adjust += 7 if $adjust <= 0; # add 7 days if its today or in the past
747 0         0 $adjust += 7*($c - 1);
748 0         0 $beg = $self->_bod()->add(days => $adjust);
749 0         0 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
750             }
751             # The something of the month (or this, last, next, or previous...)
752             elsif ($string =~ /^(\d+(?:st|nd|rd|th)?|end) of (this|last|next) month$/)
753             {
754 4         33 $beg = $self->_bod()->set_day(1);
755              
756 4 100       5243 if ($2 eq "last") {
    50          
757 1         18 $beg = $beg->subtract(months => 1);
758             } elsif ($2 eq "next") {
759 0         0 $beg = $beg->add(months => 1);
760             }
761              
762 4 100       2250 if ($1 eq "end") {
763 1         22 $beg = $beg->add(months => 1)->add(days => -1);
764             } else {
765 3         33 my ($d) = $1 =~ /(^\d+)/; # remove st/nd/rd/th
766 3         22 $beg = $beg->set_day($d);
767             }
768              
769 4         4711 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
770             }
771             # Add support for N (Time) from now
772             elsif ($string =~ /^(\d+) seconds? (?:hence|from\s+now)$/) {
773 1         8 $beg = $self->_now();
774 1         253 $end = $beg->clone->add(seconds => $1);
775             }
776             elsif ($string =~ /^(\d+) minutes? (?:hence|from\s+now)$/) {
777 1         6 $beg = $self->_now();
778 1         279 $end = $beg->clone->add(minutes => $1);
779             }
780             elsif ($string =~ /^(\d+) hours? (?:hence|from\s+now)$/) {
781 1         9 $beg = $self->_now();
782 1         249 $end = $beg->clone->add(hours => $1);
783             }
784             elsif ($string =~ /^(\d+)? months? (?:hence|from\s+now)$/) {
785 1         4 $beg = $self->_now();
786 1         258 $end = $beg->clone->add(months => $1);
787             }
788             elsif ($string =~ /^(\d+)? years? (?:hence|from\s+now)$/) {
789 1         6 $beg = $self->_now();
790 1         1019 $end = $beg->clone->add(years => $1);
791             }
792             # The something of N month (ago|from now|hence)
793             elsif ($string =~ /^(\d+(?:st|nd|rd|th)?|end) of (\d+) months? (ago|from now|hence)$/)
794             {
795 6         34 $beg = $self->_bod()->set_day(1);
796              
797 6         7694 my $n = $2; # Avoid call-by-reference side effects in add/subtract
798              
799 6 100 33     43 if ($3 eq "ago") {
    50          
800 3         17 $beg = $beg->subtract(months => $n);
801             } elsif ($3 eq "from now" || $3 eq "hence") {
802 3         15 $beg = $beg->add(months => $n);
803             }
804              
805 6 50       8673 if ($1 eq "end") {
806 0         0 $beg = $beg->add(months => 1)->add(days => -1);
807             } else {
808 6         47 my ($d) = $1 =~ /(^\d+)/; # remove st/nd/rd/th
809 6         30 $beg = $beg->set_day($d);
810             }
811              
812 6         3554 $end = $beg->clone->add(days => 1)->subtract(seconds => 1);
813             }
814             # Handle rewriting things with months in them
815             elsif ($string =~ /^(this|last|next)?\s?($month_re)$/)
816             {
817 7         31 my ($y, $m) = ($1, $2);
818 7 100 100     64 if (defined $y and $y eq 'last') {
    100 100        
819 1         3 $y = $self->_now->year - 1;
820             } elsif (defined $y and $y eq 'next') {
821 1         4 $y = $self->_now->year + 1;
822             } else {
823 5         15 $y = $self->_now->year;
824             }
825 7         2184 while (my ($re, $val) = each %month) {
826 26 100       305 if ($m =~ /$re/) {
827 7         11 $m = $val;
828 7         14 keys %month; # Reset each counter
829 7         20 last;
830             }
831             }
832 7         23 $beg = $self->_bod()->set(year => $y, month => $m, day => 1);
833 7         5852 $end = $self->_datetime_class()->last_day_of_month(year => $y, month => $m, %EOD);
834             }
835              
836             # Match a month with a 4-digit year
837             elsif ($string =~ /^($month_re)\s+(\d{4})$/)
838             {
839 2         22 my ($y, $m) = ($2, $1);
840 2         17 while (my ($re, $val) = each %month) {
841 6 100       132 if ($m =~ /$re/) {
842 2         37 $m = $val;
843 2         16 keys %month; # Reset each counter
844 2         11 last;
845             }
846             }
847 2         31 $beg = $self->_bod()->set(year => $y, month => $m, day => 1);
848 2         2570 $end = $self->_datetime_class()->last_day_of_month(year => $y, month => $m, %EOD);
849             }
850              
851             elsif ($string =~ /^<=/) {
852 2         18 $string =~ s/^<=//;
853 2         20 $beg = $self->_infinite_past_class->new();
854 2         18 (undef, $end) = $self->parse_range($string);
855             }
856             elsif ($string =~ /^(?:before |<)/i) {
857 9         41 $string =~ s/^(?:before |<)//i;
858 9         40 ($end) = $self->parse_range($string);
859              
860 9 50       61 if ( defined $end ) {
861 9         41 $beg = $self->_infinite_past_class->new();
862 9         113 $end = $end->subtract(seconds => 1);
863             }
864             }
865              
866             elsif ($string =~ /^>=/) {
867 2         16 $string =~ s/^>=//;
868 2         17 ($beg) = $self->parse_range($string);
869 2         27 $end = $self->_infinite_future_class->new();
870             }
871             elsif ($string =~ /^(?:after |>)/i) {
872 8         33 $string =~ s/^(?:after |>)//i;
873 8         133 (undef, $beg) = $self->parse_range($string);
874              
875 8 50       52 if ( defined $beg ) {
876 8         82 $beg = $beg->add(seconds => 1);
877 8         7194 $end = $self->_infinite_future_class->new();
878             }
879             }
880              
881             elsif ($string =~ /^since /i) {
882 3         18 $string =~ s/^since //i;
883 3         20 ($beg) = $self->parse_range($string);
884             # Merriam-Webster defines since as "from a definite past time until now",
885             # thus $end is the end of the day today and not infinity.
886 3         40 $end = $self->_now()->clone->set(%EOD);
887             }
888              
889             # If all else fails, see if Date::Manip can figure this out
890             # If some component of the date or time is missing, Date::Manip
891             # will default it, generally to 00.
892             elsif (($beg, my $incomplete) = $self->_parse_date_manip($string))
893             {
894             # We have dropped into date manip because the previous cases have not
895             # been triggered. Generally speaking that means we have to deal with
896             # when there is a time given in addition to a date.
897              
898 90         773 $end = $beg->clone;
899              
900             # If we think that we got a complete datetime object but did not.
901             # Primarily, we need this to help us out with our business day logic.
902 90 100       1605 if (!scalar @$incomplete) {
903             # business days ago is always one day long.
904 72 100       523 if ($string =~ /^(\d+)? (business day)(s?) ago$/) {
905 8         132 $beg->set(%BOD);
906 8         9243 $end = $beg->clone()->set(%EOD);
907             }
908              
909             # past N business days
910             # generally includes today, unless it is being run on a weekend.
911 72 100       7474 if ($string =~ /^(?:last|past) (\d+)? (business day)(s?)$/) {
912              
913 8         117 $beg->set(%BOD);
914 8         6458 my $bdow = $beg->day_of_week % 7; # Monday == 1
915              
916 8         108 $end = $self->_now()->set(%EOD);
917 8         7592 my $edow = $end->day_of_week % 7; # Monday == 1
918              
919             # Back up if today is not a business day.
920             # But keep Date::Manip's starting date
921              
922 8 100       119 if ($edow == 0) {
    100          
    100          
923             # Sunday
924 1         23 $end->subtract(days => 2);
925             } elsif($edow == 6) {
926             # Saturday
927 1         63 $end->subtract(days => 1);
928             }
929              
930             # We generally disagree with Date::Manip's starting date if
931             # today is a weekday, so potentially move the starting date forward.
932             # But, how far to move it depends on what day it is.
933             elsif ($bdow == 5) {
934             # Include today since we are on a weekday.
935             # Date manip goes back one day too far.
936 1         27 $beg->add(days => 3);
937             } else {
938 5         91 $beg->add(days => 1);
939             }
940             }
941              
942             # Dates in the MM/DD/YYYY format will have beginning and ending
943             # time of midnight; however, we want them to be the entire day;
944             # so, we set the end time to the end of the day.
945             #
946             # However, the user can specify midnight, which looks just the
947             # same to us; so, we don't extend the range in those cases.
948             #
949             # TODO: Handle other ways of specifying midnight or fix
950             # Date::Manip so that it doesn't return an empty incomplete array.
951 72 100 100     13448 if ( $beg->hms eq "00:00:00"
      100        
952             && $end->hms eq "00:00:00"
953             && $string !~ /(midnight|00:00:00|12(:00){0,2}AM)/ ) {
954 49         2559 $end->set(%EOD);
955             }
956             }
957              
958             # If Date::Manip had to supply defaults for some parts,
959             # it gave the earliest possible datetime.
960             # For the end of the range, we swap those defaults with
961             # the latest possible.
962 90         44611 for my $component (@$incomplete){
963 39 50       12533 if($component eq 'day'){
964 0         0 $end->add(months => 1)->subtract(days => 1);
965             }
966             else{
967 39         395 $end->set($component => $EOY{$component});
968             }
969             }
970             }
971              
972             else
973             {
974 0         0 return ();
975             }
976              
977 245         151821 return ($beg, $end);
978             }
979              
980             sub _bod {
981 76     76   195 my $self = shift;
982 76         219 my $now = $self->_now();
983 76         28509 return $now->set(%BOD);
984             }
985              
986             sub _eod {
987 20     20   44 my $self = shift;
988 20         56 my $now = $self->_now();
989 20         7396 return $now->set(%EOD);
990             }
991              
992             sub _now {
993 279     279   1768 my $self = shift;
994              
995 279 100       1021 if (my $cb = $self->{now_callback}) {
996 274         1295 return &$cb($self);
997             }
998              
999 5         37 return $self->_datetime_class->now;
1000             }
1001              
1002             sub _datetime_class {
1003 117     117   559 my $self = shift;
1004 117   100     3900 return $self->{datetime_class} || 'DateTime';
1005             }
1006              
1007             sub _infinite_future_class {
1008 10     10   20 my $self = shift;
1009 10   50     196 return $self->{infinite_future_class} || 'DateTime::Infinite::Future';
1010             }
1011              
1012             sub _infinite_past_class {
1013 11     11   21 my $self = shift;
1014 11   50     214 return $self->{infinite_past_class} || 'DateTime::Infinite::Past';
1015             }
1016              
1017             my $abbrevs = [
1018             [month => 'm'],
1019             [day => 'd'],
1020             [hour => 'h'],
1021             [minute => 'mn'],
1022             [second => 's'],
1023             ];
1024              
1025             sub _parse_date_manip
1026             {
1027 90     90   413 my ($self, $val) = @_;
1028              
1029 90         211 my $date;
1030 90         205 my $incomplete = [];
1031              
1032             # wrap in eval as Date::Manip fatally dies on strange input (ie. 010101)
1033 90         243 eval {
1034              
1035             # we need to know what we consider to be "now"
1036 90         396 my $now = $self->_now;
1037              
1038             # If this is all we have or the DM5 interface has been selected by the
1039             # app, use the ol' functional and reset after each parse.
1040 90         36142 my ($y, $m, $d, $H, $M, $S);
1041 90   100     827 my $dm_backend = $Date::Manip::Backend || '';
1042 90 100 66     1116 if ($Date::Manip::VERSION lt '6' or $dm_backend eq 'DM5') {
1043 2         13 my @orig_config = Date::Manip::Date_Init();
1044 2         1146 Date::Manip::Date_Init("ForceDate=" . $now->ymd . "-" . $now->hms);
1045 2         1065 my $date = Date::Manip::ParseDate($val);
1046 2         9648 Date::Manip::Date_Init(@orig_config);
1047              
1048 2         78488 ($y, $m, $d, $H, $M, $S) = Date::Manip::UnixDate($date, "%Y", "%m", "%d", "%H", "%M", "%S");
1049             }
1050              
1051             # When available, use the DM6 OO API to prevent this configuration from
1052             # infecting the global state
1053             else {
1054 88         693 my $dm = Date::Manip::Date->new;
1055 88         1953075 $dm->config("forcedate", $now->ymd . '-' . $now->hms);
1056 88         137461 my $err = $dm->parse($val);
1057              
1058 88 50       1742165 if (!$err){
1059 88         913 ($y, $m, $d, $H, $M, $S) = $dm->value;
1060              
1061 88         3478 for my $section (@$abbrevs){
1062 440 100       6818 push (@$incomplete, $section->[0]) if !$dm->complete($section->[1]);
1063             }
1064             }
1065             }
1066              
1067 90 50       50031 if ( $y )
1068             {
1069 90         1177 $date = $self->_datetime_class->new(
1070             year => $y,
1071             month => $m,
1072             day => $d,
1073             hour => $H,
1074             minute => $M,
1075             second => $S,
1076             );
1077             }
1078             };
1079              
1080             # Our caller expects a false value on failure
1081 90 50       82307 return if !$date;
1082              
1083 90         1639 return ($date, $incomplete);
1084             }
1085              
1086             #pod =head2 _convert_from_us_dashed
1087             #pod
1088             #pod Converts a US date string in the format MM-DD-YYYY into a datetime object.
1089             #pod
1090             #pod =cut
1091              
1092             sub _convert_from_us_dashed {
1093 245     245   655 my ($self, $dashed_date) = @_;
1094              
1095 245 100       1936 if ($dashed_date !~ m/$US_FORMAT_WITH_DASHES/) {
1096 239         741 return $dashed_date;
1097             }
1098              
1099 6         28 my $year = $3;
1100 6 50       27 my $month = 1 == length($1) ? "0$1" : $1;
1101 6 50       35 my $day = 1 == length($2) ? "0$2" : $2;
1102              
1103 6         18 return $self->_datetime_class->new(
1104             year => $year,
1105             month => $month,
1106             day => $day,
1107             )->ymd;
1108             }
1109              
1110             #pod =head2 _clean_units
1111             #pod
1112             #pod Given a unit of measurement such as hours?, minutes?, seconds?, or days?, we will return a string of the form hours, minutes, seconds, or days.
1113             #pod
1114             #pod =cut
1115              
1116             sub _clean_units {
1117 25     25   102 my ($self, $measure) = @_;
1118              
1119 25 100       115 if($measure !~ /s$/) {
1120 13         31 $measure .= 's';
1121             }
1122              
1123 25         73 return $measure;
1124             }
1125              
1126             #pod =head1 TO DO
1127             #pod
1128             #pod There's a lot more that this module could handle. A few items that come to mind:
1129             #pod
1130             #pod =over 4
1131             #pod
1132             #pod =item *
1133             #pod
1134             #pod More testing to make sure certain date configurations are handled, like start of
1135             #pod week.
1136             #pod
1137             #pod =item *
1138             #pod
1139             #pod Handle Unicode in places where such handling makes sense (like hyphen detection)
1140             #pod
1141             #pod =item *
1142             #pod
1143             #pod Allow full words instead of digits ("two weeks ago" vs "2 weeks ago")
1144             #pod
1145             #pod =item *
1146             #pod
1147             #pod Allow "between" for ranges ("between last February and this Friday") in addition
1148             #pod to "to/through" ranges
1149             #pod
1150             #pod =item *
1151             #pod
1152             #pod This module is US English-centric (hence the "EN") and might do some things
1153             #pod wrong for other variants of English and a generic C<Date::RangeParser> interface
1154             #pod could be made to allow for other languages to be parsed this way.
1155             #pod
1156             #pod =item *
1157             #pod
1158             #pod Depends on L<Date::Manip>. This may or may not be a good thing.
1159             #pod
1160             #pod =back
1161             #pod
1162             #pod =head1 DEPENDENCIES
1163             #pod
1164             #pod L<DateTime>, L<Date::Manip>
1165             #pod
1166             #pod =head1 AUTHORS
1167             #pod
1168             #pod This module was authored by Grant Street Group (L<http://grantstreet.com>), who were kind enough to give it back to the Perl community.
1169             #pod
1170             #pod The CPAN distribution is maintained by
1171             #pod Grant Street Group <developers@grantstreet.com>.
1172             #pod
1173             #pod =head1 THANK YOU
1174             #pod
1175             #pod Sterling Hanenkamp, for adding support for explicit date ranges, improved parsing, and improving the documentation.
1176             #pod
1177             #pod Sam Varshavchik, for fixing a bug affecting the "[ordinal] of [last/next] month" syntax.
1178             #pod
1179             #pod Allan Noah and James Hammer, for adding support for times in addition to dates and various bug fixes.
1180             #pod
1181             #pod =head1 COPYRIGHT AND LICENSE
1182             #pod
1183             #pod Copyright (C) 2012-2023 Grant Street Group.
1184             #pod
1185             #pod This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
1186             #pod
1187             #pod =cut
1188              
1189             1;
1190              
1191             __END__
1192              
1193             =pod
1194              
1195             =encoding UTF-8
1196              
1197             =head1 NAME
1198              
1199             Date::RangeParser::EN - Parse plain English date/time range strings
1200              
1201             =head1 VERSION
1202              
1203             version v1.2.1
1204              
1205             =head1 SYNOPSIS
1206              
1207             use Date::RangeParser::EN;
1208              
1209             my $parser = Date::RangeParser::EN->new;
1210             my ($begin, $end) = $parser->parse_range("this week");
1211              
1212             =head1 DESCRIPTION
1213              
1214             Parses plain-English strings representing date/time ranges
1215              
1216             =head1 NAME
1217              
1218             Date::RangeParser::EN - Parser for plain English date/time range strings
1219              
1220             =head1 METHODS
1221              
1222             =head2 new
1223              
1224             Returns a new instance of Date::RangeParser::EN.
1225              
1226             Takes an optional hash of parameters:
1227              
1228             =over 4
1229              
1230             =item * B<datetime_class>
1231              
1232             By default, Date::RangeParser::EN returns two L<DateTime> objects representing the beginning and end of the range. If you use a subclass of DateTime (or another module that implements the DateTime API), you may pass the name of this class to use it instead.
1233              
1234             At the very least, this given class must implement a C<new> method that accepts a hash of arguments, where the following keys will be set:
1235              
1236             year
1237             month
1238             day
1239             hour
1240             minute
1241             second
1242              
1243             This gives you the freedom to set your time zones and such however you need to.
1244              
1245             =item * B<infinite_past_class>
1246             =item * B<infinite_future_class>
1247              
1248             By default, Date::RangeParser::EN uses DateTime::Infinite::Past and DateTime::Infinite::Future to create open-ended ranges (for example "after today"). If you have extended these classes, you may pass the corresponding names in.
1249              
1250             The given classes must implement a C<new> method that accepts no arguments.
1251              
1252             =item * B<now_callback>
1253              
1254             By default, Date::RangeParser::EN uses DateTime->now to determine the current date/time for calculations. If you need to work with a different time (for instance, if you need to adjust for time zones), you may pass a callback (code reference) which returns a DateTime object.
1255              
1256             =back
1257              
1258             =head2 parse_range
1259              
1260             Accepts a string representing a plain-English date range, for instance:
1261              
1262             =over 4
1263              
1264             =item * today
1265              
1266             =item * this week
1267              
1268             =item * the past 2 months
1269              
1270             =item * next Tuesday
1271              
1272             =item * two weeks ago
1273              
1274             =item * the next 3 hours
1275              
1276             =item * the 3rd of next month
1277              
1278             =item * the end of this month
1279              
1280             =back
1281              
1282             More formally, this will parse the following kinds of date strings:
1283              
1284             NUMBER : ordinary number (1)
1285             PERIOD : one of: hour, day, week, month, quarter, or year (or the plural of these)
1286             WEEKDAY : one of: Monday, Tuesday, Wedensday, Thursday, Friday, Saturday, or Sunday
1287             CARDINAL : a cardinal number (21st) or the word for that number (twenty-first) or end
1288             MONTH : a month name: January, Feburary, March, April, May, June, July August,
1289             September, October, November, or Decmeber or any 3-letter abbreviation
1290             YEAR : a 4-digit year (2-digits will not work)
1291             TIMES: January 1st, 2000 at 10:00am through January 1st, 2000 at 2:00pm
1292             RANGE : any date range that can be parsed by parse_range
1293             ELEMENT : any element of a date range that can be parsed by parse_range
1294              
1295             today : today, midnight to midnight
1296              
1297             this PERIOD : the current period, start to end
1298             this month
1299              
1300             current PERIOD : the current period, start to end
1301             current year
1302              
1303             this WEEKDAY : the WEEKDAY that is in the current week, midnight to midnight
1304             this Monday
1305              
1306             NUMBER PERIOD ago : past date relative to now until now
1307             3 days ago
1308              
1309             past NUMBER PERIOD : past date relative to now until now
1310             past 2 weeks
1311              
1312             last NUMBER PERIOD : past date relative to now until now
1313             last 6 hours
1314              
1315             past NUMBER WEEKDAY : the weekday a number of weeks before now until now
1316             past 4 Saturdays
1317              
1318             NUMBER WEEKDAY ago : the weekday a number of weeks before now until now
1319             3 Fridays ago
1320              
1321             yesterday : yesterday, midnight to midnight
1322              
1323             last WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
1324             last Wednesday
1325              
1326             previous WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
1327             previous Friday
1328              
1329             past WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
1330             past Tuesday
1331              
1332             this past WEEKDAY : the WEEKDAY that is in the week prior to this, midnight to midnight
1333             this past Saturday
1334              
1335             coming WEEKDAY : the WEEKDAY that is in the week after this, midnight to midnight
1336             coming Monday
1337              
1338             this coming WEEKDAY : the WEEKDAY that is in the week after this, midnight to midnight
1339             this coming Thursday
1340              
1341             NUMBER Business days ago : past number of business days relative to now until now
1342              
1343             NUMBER weekdays ago : past number of weekdays relative to now until now
1344              
1345             LAST or PAST NUMBER weekdays ago : past number of weekdays relative to now until now
1346              
1347             NUMBER PERIOD hence : now to a future date relative to now
1348             4 months hence
1349              
1350             NUMBER PERIOD from now : now to a future date relative to now
1351             6 days from now
1352              
1353             next NUMBER PERIOD : now to a future date relative to now
1354             next 7 years
1355              
1356             tomorrow : tomorrow, midnight to midnight
1357              
1358             next NUMBER WEEKDAY : the WEEKDAY that is in a number of weeks after this, midnight to midnight
1359             next 4 Sundays
1360              
1361             CARDINAL of this month : the specified day of the current month, midnight to midnight
1362             14th of this month
1363              
1364             CARDINAL of last month : the specified day of the previous month, midnight to midnight
1365             31st of last month
1366              
1367             CARDINAL of next month : the specified day of the month following this, midnight to midnight
1368             3rd of next month
1369              
1370             CARDINAL of NUMBER months ago : the specified day of a previous month, midnight to midnight
1371             12th of 2 months ago
1372              
1373             CARDINAL of NUMBER months from now : the specified day of a following month, midnight to midnight
1374             7th of 22 months from now
1375              
1376             CARDINAL of NUMBER months hence : the specified day of a following month, midnight to midnight
1377             22nd of 6 months hence
1378              
1379             CARDINAL of TIME : the specific time of day which can be accompanied by a date
1380             10:00am through 12:00pm defaults to today if no date is given
1381              
1382             MONTH : the named month of the current year, 1st to last day
1383             August
1384              
1385             this MONTH : the named month of the current year, 1st to last day
1386             this Sep
1387              
1388             last MONTH : the named month of the previous year, 1st to last day
1389             last January
1390              
1391             next MONTH : the named month of the next year, 1st to last day
1392             next Dec
1393              
1394             MONTH YEAR : the named month of the named year, 1st to last day
1395             June 1969
1396              
1397             RANGE to RANGE : the very start of the first range to the very end of the second
1398             Tuesday to Next Saturday
1399              
1400             RANGE thru RANGE : the very start of the first range to the very end of the second
1401             2 hours ago thru the next 6 hours
1402              
1403             RANGE through RANGE : the very start of the first range to the very end of the second
1404             August through December
1405              
1406             RANGE - RANGE : the very start of the first range to the very end of the second
1407             9-1-2012 - 9-30-2012
1408              
1409             RANGE-RANGE : the very start of the first range to the very end of the second
1410             10/10-10/20 (ranges must not contain hyphens, "-")
1411              
1412             American style dates : Month / Day / Year
1413             6/15/2000
1414              
1415             before ELEMENT : all dates before the very start of the date specified in the ELEMENT
1416             < ELEMENT
1417             before today
1418              
1419             <= ELEMENT : all dates up to the very end of the date specified in the ELEMENT
1420             <= today
1421              
1422             after ELEMENT : all dates after the very end of the date specified in the ELEMENT
1423             > ELEMENT
1424             after next Tuesday
1425              
1426             >= ELEMENT : the date specified in the ELEMENT to the end of forever
1427             >= this Friday
1428              
1429             since ELEMENT : the date specified in the ELEMENT to the end of the current day
1430             since last Sunday
1431              
1432             Anything else is parsed by L<Date::Manip>. If Date::Manip is unable to parse the
1433             date given either, then the dates returned will be undefined.
1434              
1435             Also, when parsing:
1436              
1437             =over
1438              
1439             =item *
1440              
1441             The words "the" and "and" will always be ignored and can appear anywhere.
1442              
1443             =item *
1444              
1445             Cardinal numbers may be spelled out as words, i.e. "September first" instead of
1446             "September 1st". Similarly, "two weeks ago" and "2 weeks ago" will be treated as the same
1447              
1448             =item *
1449              
1450             Any plural or singular period shown above can be used with the opposite.
1451              
1452             =item *
1453              
1454             All dates are parsed relative to the parser's notion of now. You can control
1455             this by setting the C<now_callback> option on the constructor.
1456              
1457             =back
1458              
1459             Returns two L<DateTime> objects, representing the beginning and end of the range.
1460              
1461             =head2 _convert_from_us_dashed
1462              
1463             Converts a US date string in the format MM-DD-YYYY into a datetime object.
1464              
1465             =head2 _clean_units
1466              
1467             Given a unit of measurement such as hours?, minutes?, seconds?, or days?, we will return a string of the form hours, minutes, seconds, or days.
1468              
1469             =head1 TO DO
1470              
1471             There's a lot more that this module could handle. A few items that come to mind:
1472              
1473             =over 4
1474              
1475             =item *
1476              
1477             More testing to make sure certain date configurations are handled, like start of
1478             week.
1479              
1480             =item *
1481              
1482             Handle Unicode in places where such handling makes sense (like hyphen detection)
1483              
1484             =item *
1485              
1486             Allow full words instead of digits ("two weeks ago" vs "2 weeks ago")
1487              
1488             =item *
1489              
1490             Allow "between" for ranges ("between last February and this Friday") in addition
1491             to "to/through" ranges
1492              
1493             =item *
1494              
1495             This module is US English-centric (hence the "EN") and might do some things
1496             wrong for other variants of English and a generic C<Date::RangeParser> interface
1497             could be made to allow for other languages to be parsed this way.
1498              
1499             =item *
1500              
1501             Depends on L<Date::Manip>. This may or may not be a good thing.
1502              
1503             =back
1504              
1505             =head1 DEPENDENCIES
1506              
1507             L<DateTime>, L<Date::Manip>
1508              
1509             =head1 AUTHORS
1510              
1511             This module was authored by Grant Street Group (L<http://grantstreet.com>), who were kind enough to give it back to the Perl community.
1512              
1513             The CPAN distribution is maintained by
1514             Grant Street Group <developers@grantstreet.com>.
1515              
1516             =head1 THANK YOU
1517              
1518             Sterling Hanenkamp, for adding support for explicit date ranges, improved parsing, and improving the documentation.
1519              
1520             Sam Varshavchik, for fixing a bug affecting the "[ordinal] of [last/next] month" syntax.
1521              
1522             Allan Noah and James Hammer, for adding support for times in addition to dates and various bug fixes.
1523              
1524             =head1 COPYRIGHT AND LICENSE
1525              
1526             Copyright (C) 2012-2023 Grant Street Group.
1527              
1528             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
1529              
1530             =head1 AUTHORS
1531              
1532             =over 4
1533              
1534             =item *
1535              
1536             Grant Street Group <developers@grantstreet.com>
1537              
1538             =item *
1539              
1540             Michael Aquilina <aquilina@cpan.org>
1541              
1542             =back
1543              
1544             =head1 COPYRIGHT AND LICENSE
1545              
1546             This software is Copyright (c) 2012 - 2024 by Grant Street Group.
1547              
1548             This is free software, licensed under:
1549              
1550             The Artistic License 2.0 (GPL Compatible)
1551              
1552             =cut