File Coverage

blib/lib/DateTimeX/Easy.pm
Criterion Covered Total %
statement 14 16 87.5
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 20 22 90.9


line stmt bran cond sub pod time code
1             package DateTimeX::Easy;
2             BEGIN {
3 3     3   481113 $DateTimeX::Easy::VERSION = '0.089';
4             }
5             # ABSTRACT: Parse a date/time string using the best method available
6              
7 3     3   32 use warnings;
  3         7  
  3         91  
8 3     3   18 use strict;
  3         5  
  3         106  
9              
10 3     3   21 use constant DEBUG => 0;
  3         6  
  3         191  
11              
12              
13 3     3   15 use base qw/Exporter/;
  3         5  
  3         672  
14             our @EXPORT_OK = qw/datetime parse parse_datetime parse_date new_datetime new_date date/;
15              
16 3     3   7335 use DateTime;
  0            
  0            
17             use DateTime::Format::Natural;
18             use DateTime::Format::Flexible;
19             # use DateTime::Format::DateParse; # Unfortunately, not as useful to use because of that default "local" time zone business.
20             use DateTimeX::Easy::DateParse; # Using this instead, hrm.
21             use Scalar::Util qw/blessed/;
22             use Carp;
23              
24             my $have_ICal;
25             eval {
26             require DateTime::Format::ICal;
27             $have_ICal = 1;
28             };
29              
30             my $have_DateManip;
31             eval {
32             require DateTime::Format::DateManip;
33             $have_DateManip = 1;
34             };
35             my $natural_parser = DateTime::Format::Natural->new;
36              
37             my %_truncate_range = qw/
38             month year
39             day month
40             hour day
41             minute hour
42             second minute
43             nanosecond second
44             /;
45             my %_delta_range = (
46             month => [qw/years months/],
47             day => [qw/months days/],
48             hour => [qw/days hours/],
49             minute => [qw/hours minutes/],
50             second => [qw/minutes seconds/],
51             );
52             my %_first_or_last = qw/
53             first first
54             last last
55             begin first
56             beginning first
57             start first
58             end last
59             ending last
60             /;
61              
62             my @_parser_order = qw/
63             Flexible
64             DateParse
65             Natural
66             /;
67             unshift @_parser_order, qw/ICal/ if $have_ICal;
68             push @_parser_order, qw/DateManip/ if $have_DateManip;
69             my %_parser_source = (
70             ICal => sub {
71             return DateTime::Format::ICal->parse_datetime(shift);
72             },
73              
74             DateParse => sub {
75             return DateTimeX::Easy::DateParse->parse_datetime(shift);
76             },
77            
78             Natural => sub {
79             local $SIG{__WARN__} = sub {}; # Make sure ::Natural/Date::Calc stay quiet... don't really like this, oh well...
80             my $dt = $natural_parser->parse_datetime(shift);
81             return unless $natural_parser->success;
82             return $dt;
83             },
84              
85             Flexible => sub {
86             my $parse = shift;
87             my $time_zone;
88             # First, try to extract out any timezone information
89             {
90             ##################################################
91             # 2008-09-16 13:23:57 Eastern Daylight (?:Time)? #
92             ##################################################
93             if ($parse =~ s/\s+(?:(Eastern|Central|Mountain|Pacific)\s+(?:Daylight|Standard)(?:\s+Time)?).*$//) {
94             $time_zone = "US/$1";
95             }
96             ##################################
97             # 2008-09-16 13:23:57 US/Eastern #
98             ##################################
99             elsif ($parse =~ s/\s+([A-Za-z][A-Za-z0-9\/\._]*)\s*$//) { # Look for a timezone-like string at the end of $parse
100             $time_zone = $1;
101             $parse = "$parse $time_zone" and undef $time_zone if $time_zone && $time_zone =~ m/^[ap]\.?m\.?$/i; # Put back AM/PM if we accidentally slurped it out
102             }
103             #########################################################
104             # 2008-09-16 13:23:57 Eastern Daylight Time (GMT+05:00) #
105             #########################################################
106             elsif ($parse =~ s/(?:\s+[A-Z]\w+)*\s+\(?(?:GMT|UTC)?([-+]\d{2}:\d{2})\)?\s*$//) {
107             $time_zone = $1;
108             }
109             # Flexible can't seem to parse (GMT+0:500)
110             # elsif ($parse =~ s/(?:\s+[A-Z]\w+)*(\s+\(GMT[-+]\d{2}:\d{2}\)\s*)$//) {
111             # $parse = "$parse $1";
112             # }
113             #############################
114             # 2008-09-16 13:23:57 +0500 #
115             #############################
116             elsif ($parse =~ s/\s+([-+]\d{3,})\s*$//) {
117             $time_zone = $1;
118             }
119             }
120             return unless my $dt = DateTime::Format::Flexible->build($parse);
121             if ($time_zone) {
122             $dt->set_time_zone("floating");
123             $dt->set_time_zone($time_zone);
124             }
125             return $dt;
126             },
127              
128             DateManip => sub {
129             return DateTime::Format::DateManip->parse_datetime(shift);
130             },
131             );
132              
133             sub new {
134             shift if $_[0] && $_[0] eq __PACKAGE__;
135              
136             my $parse;
137             $parse = shift if @_ % 2;
138              
139             my %in = @_;
140             $parse = delete $in{parse} if exists $in{parse};
141             my $truncate = delete $in{truncate};
142             my $soft_time_zone_conversion = delete $in{soft_time_zone_conversion};
143             my $time_zone_if_floating = delete $in{default_time_zone};
144             $time_zone_if_floating = delete $in{time_zone_if_floating} if exists $in{time_zone_if_floating};
145             my $parser_order = delete $in{parser_order};
146             my $parser_exclude = delete $in{parser_exclude};
147             my $ambiguous = 1;
148             $ambiguous = delete $in{ambiguous} if exists $in{ambiguous};
149              
150             my ($time_zone);
151             $time_zone = delete $in{tz} if exists $in{tz};
152             $time_zone = delete $in{timezone} if exists $in{timezone};
153             $time_zone = delete $in{time_zone} if exists $in{time_zone}; # "time_zone" takes precedence over "timezone"
154              
155             my @delta;
156              
157             my $original_parse = $parse;
158             my $parse_dt;
159             if ($parse) {
160             if (blessed $parse && $parse->isa("DateTime")) { # We have a DateTime object as $parse
161             $parse_dt = $parse;
162             }
163             else {
164              
165             if (1) {
166             my $got_ambiguous;
167             my ($last_delta);
168             while ($parse =~ s/^\s*(start|first|last|(?:begin|end)(?:ning)?)\s+(year|month|day|hour|minute|second)\s+of\s+//i) {
169             my $first_or_last = $1;
170             $first_or_last = $_first_or_last{lc $first_or_last};
171             my $period = $2;
172             $last_delta->{add} = [ "${period}s" => 1 ] if $last_delta;
173             push @delta, $last_delta = my $delta = { period => $period };
174             if ($first_or_last ne "first") {
175             $delta->{last} = 1;
176             $delta->{subtract} = [ "${period}s" => 1 ];
177             }
178             else {
179             $delta->{first} = 1;
180             }
181             }
182             my $last_parse = $parse;
183             my $period;
184             if ($parse =~ s/^\s*(start|this|next|first|last|(?:begin|end)(?:ning)?)\s+(year|month|day|hour|minute|second)(?:\s+of\s+)?//) {
185             $period = $2;
186             $last_delta->{add} = [ "${period}s" => 1 ] if $last_delta && $last_delta->{last};
187             push @delta, { truncate => $period};
188             $parse = $last_parse unless $parse;
189             }
190             elsif ($parse =~ s/^\s*(year|month|day|hour|minute|second)\s+of\s+//i) {
191             $period = $1;
192             $last_delta->{add} = [ "${period}s" => 1 ] if $last_delta && $last_delta->{last};
193             push @delta, { truncate => $period };
194             }
195             elsif (@delta) {
196             $got_ambiguous = 1;
197             $period = $last_delta->{period};
198             my $truncate = $_truncate_range{$period};
199             push @delta, my $delta = { truncate => $truncate };
200             my $delta_range = $_delta_range{$period};
201             if ($delta_range) {
202             my ($add, $subtract) = @$delta_range;
203             if ($last_delta->{last}) {
204             $last_delta->{add} = [ "${add}" => 1 ];
205             }
206             }
207             }
208              
209             croak "Can't parse \"$original_parse\" since it's too ambiguous" if $got_ambiguous && ! $ambiguous;
210             }
211              
212             my @parser_order = $parser_order ? (ref $parser_order eq "ARRAY" ? @$parser_order : ($parser_order)) : @_parser_order;
213             my (%parser_exclude);
214             %parser_exclude = map { $_ => 1 } (ref $parser_exclude eq "ARRAY" ? @$parser_exclude : ($parser_exclude)) if $parser_exclude;
215             my %parser_source = %_parser_source;
216             if (DEBUG) {
217             warn "Parse $parse\n";
218             }
219             # TODO Kinda hackish
220             if ($parse =~ m/^[1-9]\d{3}$/) { # If it's a four digit year... yeah, arbitrary
221             $parse_dt = DateTime->new(year => $parse);
222             }
223             while (! $parse_dt && @parser_order) {
224             my $parser = shift @parser_order;
225             next if $parser_exclude{$parser};
226             # warn "Try $parser:\n" if DEBUG;
227             my $parser_code = $parser_source{$parser};
228             eval {
229             $parse_dt = $parser_code->($parse);
230             };
231             if (DEBUG) {
232             if ($@) {
233             warn "FAIL $parser: $@\n";
234             }
235             elsif ($parse_dt) {
236             warn "PASS $parser: $parse_dt\n";
237             }
238             else {
239             warn "FAIL $parser\n";
240             }
241             }
242             undef $parse_dt if $@;
243             }
244             }
245             return unless $parse_dt;
246             }
247              
248             my %DateTime;
249             $DateTime{time_zone} = "floating";
250             if ($parse_dt) {
251             $DateTime{$_} = $parse_dt->$_ for qw/year month day hour minute second nanosecond time_zone/;
252             }
253             @DateTime{keys %in} = values %in;
254            
255             return unless my $dt = DateTime->new(%DateTime);
256              
257             if ($time_zone) {
258             if ($soft_time_zone_conversion) {
259             $dt->set_time_zone("floating");
260             }
261             $dt->set_time_zone($time_zone);
262             }
263             elsif ($time_zone_if_floating && $dt->time_zone->is_floating) {
264             $dt->set_time_zone($time_zone_if_floating);
265             }
266              
267             if ($truncate) {
268             $truncate = $truncate->[1] if ref $truncate eq "ARRAY";
269             $truncate = (values %$truncate)[0] if ref $truncate eq "HASH";
270             $dt->truncate(to => $truncate);
271             }
272             elsif (@delta) {
273             if (DEBUG) {
274             require YAML;
275             warn "$original_parse => $parse => $dt";
276             }
277             for my $delta (reverse @delta) {
278             warn YAML::Dump($delta) if DEBUG;
279             if ($delta->{truncate}) {
280             $dt->truncate(to => $delta->{truncate});
281             }
282             else {
283             $dt->add(@{ $delta->{add} }) if $delta->{add};
284             $dt->subtract(@{ $delta->{subtract} }) if $delta->{subtract};
285             }
286             }
287             }
288              
289             return $dt;
290             }
291             *parse = \&new;
292             *parse_date = \&new;
293             *parse_datetime = \&new;
294             *date = \&new;
295             *datetime = \&new;
296             *new_date = \&new;
297             *new_datetime = \&new;
298              
299             1;
300              
301             __END__