File Coverage

blib/lib/DateTimeX/Moment.pm
Criterion Covered Total %
statement 455 523 87.0
branch 199 260 76.5
condition 52 91 57.1
subroutine 138 160 86.2
pod 0 110 0.0
total 844 1144 73.7


line stmt bran cond sub pod time code
1             package DateTimeX::Moment;
2 33     33   444172 use 5.008001;
  33         82  
3 33     33   109 use strict;
  33         39  
  33         522  
4 33     33   103 use warnings;
  33         44  
  33         1205  
5              
6             our $VERSION = "0.04";
7              
8 33     33   11774 use Time::Moment 0.38;
  33         31470  
  33         730  
9 33     33   10771 use DateTimeX::Moment::Duration;
  33         55  
  33         767  
10 33     33   13506 use DateTime::Locale;
  33         1225870  
  33         1043  
11 33     33   15912 use DateTime::TimeZone;
  33         357502  
  33         866  
12 33     33   256 use Scalar::Util qw/blessed/;
  33         42  
  33         1483  
13 33     33   122 use Carp ();
  33         44  
  33         469  
14 33     33   14465 use POSIX qw/floor/;
  33         141548  
  33         168  
15 33     33   55456 use Class::Inspector;
  33         90872  
  33         2205  
16              
17             use overload (
18 33         302 'fallback' => 1,
19             '<=>' => \&_compare_overload,
20             'cmp' => \&_string_compare_overload,
21             '""' => \&_stringify,
22             '-' => \&_subtract_overload,
23             '+' => \&_add_overload,
24             'eq' => \&_string_equals_overload,
25             'ne' => \&_string_not_equals_overload,
26 33     33   164 );
  33         40  
27              
28 33     33   16918 use Class::Accessor::Lite ro => [qw/time_zone locale formatter/];
  33         27239  
  33         200  
29              
30             BEGIN {
31 33     33   3872 local $@;
32 33 50       46 if (eval { require Data::Util; 1 }) {
  33         13533  
  33         18644  
33 33         179734 *is_instance = \&Data::Util::is_instance;
34             }
35             else {
36 0 0       0 *is_instance = sub { blessed($_[0]) && $_[0]->isa($_[1]) };
  0         0  
37             }
38             }
39              
40             my $_DEFAULT_LOCALE = DateTime::Locale->load('en_US');
41             my $_FLOATING_TIME_ZONE = DateTime::TimeZone->new(name => 'floating');
42             my $_UTC_TIME_ZONE = DateTime::TimeZone->new(name => 'UTC');
43 784     784   1850 sub _default_locale { $_DEFAULT_LOCALE }
44 1588     1588   2706 sub _default_formatter { undef }
45 97     97   341 sub _default_time_zone { $_FLOATING_TIME_ZONE }
46              
47             sub _inflate_locale {
48 793     793   830 my ($class, $locale) = @_;
49 793 50       1143 return $class->_default_locale unless defined $locale;
50 793 100       1101 return $locale if _isa_locale($locale);
51 24         92 return DateTime::Locale->load($locale);
52             }
53              
54             sub _inflate_formatter {
55 790     790   2296 my ($class, $formatter) = @_;
56 790 100       1416 return $class->_default_formatter unless defined $formatter;
57 5 100       11 return $formatter if _isa_formatter($formatter);
58 1         203 Carp::croak 'formatter should can format_datetime.';
59             }
60              
61             sub _inflate_time_zone {
62 773     773   700 my ($class, $time_zone) = @_;
63 773 50       965 return $class->_default_time_zone unless defined $time_zone;
64 773 100       894 return $time_zone if _isa_time_zone($time_zone);
65 685         1811 return DateTime::TimeZone->new(name => $time_zone);
66             }
67              
68             sub isa {
69 258     258 0 2612 my ($invocant, $a) = @_;
70 258 100       448 return !!1 if $a eq 'DateTime';
71 249         940 return $invocant->SUPER::isa($a);
72             }
73              
74             sub _moment_resolve_instant {
75 104     104   120 my ($moment, $time_zone) = @_;
76 104 100       344 if ($time_zone->is_floating) {
77 6         27 return $moment->with_offset_same_local(0);
78             }
79             else {
80 98         431 my $offset = $time_zone->offset_for_datetime($moment) / 60;
81 98         6913171 return $moment->with_offset_same_instant($offset);
82             }
83             }
84              
85             sub _moment_resolve_local {
86 612     612   553 my ($moment, $time_zone) = @_;
87 612 100       986 if ($time_zone->is_floating) {
88 55         190 return $moment->with_offset_same_local(0);
89             }
90             else {
91 557         1554 my $offset = $time_zone->offset_for_local_datetime($moment) / 60;
92 555         6236700 return $moment->with_offset_same_local($offset);
93             }
94             }
95              
96             sub new {
97 761     761 0 298670 my $class = shift;
98 761 50 33     3499 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
99              
100 761 100       1159 $args{locale} = delete $args{language} if exists $args{language};
101 761   66     2060 my $locale = delete $args{locale} || $class->_default_locale;
102 761   66     1783 my $formatter = delete $args{formatter} || $class->_default_formatter;
103 761   66     1370 my $time_zone = delete $args{time_zone} || $class->_default_time_zone;
104              
105 761         826 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
106              
107 761         4276 my $self = bless {
108             _moment => Time::Moment->new(%args),
109             locale => $class->_inflate_locale($locale),
110             formatter => $class->_inflate_formatter($formatter),
111             time_zone => $class->_inflate_time_zone($time_zone),
112             } => $class;
113 748         89577 return $self->_adjust_to_current_offset();
114             }
115              
116             sub now {
117 20     20 0 23808 my $class = shift;
118 20 50 33     114 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
119              
120 20 50       52 $args{locale} = delete $args{language} if exists $args{language};
121 20   66     103 my $locale = delete $args{locale} || $class->_default_locale;
122 20   33     90 my $formatter = delete $args{formatter} || $class->_default_formatter;
123 20 100       48 my $time_zone = exists $args{time_zone} ? $class->_inflate_time_zone(delete $args{time_zone}) : $_UTC_TIME_ZONE;
124 20 50       3065 if (%args) {
125 0         0 my $msg = 'Invalid args: '.join ',', keys %args;
126 0         0 Carp::croak $msg;
127             }
128              
129 20         32 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
130              
131 20         1545 return bless {
132             _moment => _moment_resolve_instant(Time::Moment->now, $time_zone),
133             locale => $class->_inflate_locale($locale),
134             formatter => $class->_inflate_formatter($formatter),
135             time_zone => $time_zone,
136             } => $class;
137             }
138              
139             sub from_object {
140 7     7 0 27208 my $class = shift;
141 7 50 33     41 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
142             my $object = delete $args{object}
143 7 50       29 or Carp::croak 'object is required.';
144              
145 7 50       20 $args{locale} = delete $args{language} if exists $args{language};
146 7   66     36 my $locale = delete $args{locale} || $class->_default_locale;
147 7   66     22 my $formatter = delete $args{formatter} || $class->_default_formatter;
148 7 100       60 my $time_zone = $object->can('time_zone') ? $object->time_zone : $_FLOATING_TIME_ZONE;
149 7 50       45 if (%args) {
150 0         0 my $msg = 'Invalid args: '.join ',', keys %args;
151 0         0 Carp::croak $msg;
152             }
153              
154 7 100       23 if ($object->isa(__PACKAGE__)) {
155 4         13 my $self = $object->clone;
156 4         12 $self->set_locale($locale);
157 4 100       21 $self->set_formatter($formatter) if $formatter;
158 4         9 return $self;
159             }
160              
161 3         5 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
162              
163 3         4 my $moment;
164 3 50       7 if (_isa_moment_convertable($object)) {
165 3         22 $moment = Time::Moment->from_object($object);
166             }
167             else {
168 0         0 require DateTime; # fallback
169 0         0 my $object = DateTime->from_object(object => $object);
170 0 0       0 if ($object->time_zone->is_floating) {
171 0         0 $time_zone = $object->time_zone;
172 0         0 $object->set_time_zone($_UTC_TIME_ZONE);
173             }
174 0         0 $moment = Time::Moment->from_object($object);
175             }
176              
177 3         68 return bless {
178             _moment => _moment_resolve_instant($moment, $time_zone),
179             locale => $class->_inflate_locale($locale),
180             formatter => $class->_inflate_formatter($formatter),
181             time_zone => $time_zone,
182             } => $class;
183             }
184              
185             sub from_epoch {
186 19     19 0 4816 my $class = shift;
187 19 50 33     81 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
188 19 50       36 Carp::croak 'epoch is required.' unless exists $args{epoch};
189              
190 19         27 my $epoch = delete $args{epoch};
191              
192 19 50       34 $args{locale} = delete $args{language} if exists $args{language};
193 19   66     52 my $locale = delete $args{locale} || $class->_default_locale;
194 19   66     50 my $formatter = delete $args{formatter} || $class->_default_formatter;
195 19 100       29 my $time_zone = exists $args{time_zone} ? $class->_inflate_time_zone(delete $args{time_zone}) : $_UTC_TIME_ZONE;
196 19 50       139 if (%args) {
197 0         0 my $msg = 'Invalid args: '.join ',', keys %args;
198 0         0 Carp::croak $msg;
199             }
200              
201 19         24 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
202              
203 19         17 my $moment = do {
204 19     3   84 local $SIG{__WARN__} = sub { die @_ };
  3         21  
205 19         216 Time::Moment->from_epoch($epoch);
206             };
207              
208 16         49 return bless {
209             _moment => _moment_resolve_instant($moment, $time_zone),
210             locale => $class->_inflate_locale($locale),
211             formatter => $class->_inflate_formatter($formatter),
212             time_zone => $time_zone,
213             } => $class;
214             }
215              
216 2     2 0 281 sub today { shift->now(@_)->truncate(to => 'day') }
217              
218             sub last_day_of_month {
219 28     28 0 6662 my $class = shift;
220 28 50 33     126 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
221 28         37 for my $key (qw/year month/) {
222 56 50       112 Carp::croak "Parameter: $key is required." unless defined $args{$key};
223             }
224 28 100 66     235 Carp::croak q{Parameter 'month' is out of the range [1, 12]} if 0 > $args{month} || $args{month} > 12;
225              
226 27         45 my ($year, $month) = @args{qw/year month/};
227 27         36 my $day = _month_length($year, $month);
228              
229 27         31 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
230 27         75 return $class->new(%args, day => $day);
231             }
232              
233             my @_MONTH_LENGTH = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
234             sub _month_length {
235 36     36   26 my ($year, $month) = @_;
236 36         42 my $day = $_MONTH_LENGTH[$month-1];
237 36 100 100     79 $day++ if $month == 2 && _is_leap_year($year);
238 36         73 return $day;
239             }
240              
241             sub _is_leap_year {
242 15     15   1223 my $year = shift;
243 15 100       45 return 0 if $year % 4;
244 9 100       23 return 1 if $year % 100;
245 6 100       18 return 0 if $year % 400;
246 3         12 return 1;
247             }
248              
249             sub from_day_of_year {
250 26     26 0 5960 my $class = shift;
251 26 50 33     111 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
252 26         28 for my $key (qw/year day_of_year/) {
253 52 50       95 Carp::croak "Parameter: $key is required." unless defined $args{$key};
254             }
255              
256 26         30 my $day_of_year = delete $args{day_of_year};
257              
258 26         25 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
259 26         58 my $self = $class->new(%args);
260 26         96 $self->{_moment} = $self->{_moment}->with_day_of_year($day_of_year);
261 25         42 return $self->_adjust_to_current_offset();
262             }
263              
264             sub _adjust_to_current_offset {
265 844     844   689 my $self = shift;
266 844 100       2615 return $self if $self->{time_zone}->is_floating;
267              
268 710         2625 my $offset = $self->{time_zone}->offset_for_local_datetime($self->{_moment}) / 60;
269 707         3428 $self->{_moment} = $self->{_moment}->with_offset_same_local($offset);
270              
271 707         1604 return $self;
272             }
273              
274 72     72 0 6446 sub clone { bless { %{$_[0]} }, ref $_[0] }
  72         410  
275              
276 85     85 0 1681 sub year { shift->{_moment}->year }
277 0     0 0 0 sub year_0 { shift->{_moment}->year - 1 }
278 10     10 0 83 sub month_0 { shift->{_moment}->month - 1 }
279 84     84 0 2041 sub month { shift->{_moment}->month }
280 13     13 0 343 sub day_of_week { shift->{_moment}->day_of_week }
281 11     11 0 91 sub day_of_week_0 { shift->{_moment}->day_of_week - 1 }
282 103     103 0 1927 sub day_of_month { shift->{_moment}->day_of_month }
283 4     4 0 21 sub day_of_month_0 { shift->{_moment}->day_of_month - 1 }
284 6     6 0 30 sub day_of_quarter { shift->{_moment}->day_of_quarter }
285 2     2 0 11 sub day_of_quarter_0 { shift->{_moment}->day_of_quarter - 1 }
286 27     27 0 119 sub day_of_year { shift->{_moment}->day_of_year }
287 3     3 0 16 sub day_of_year_0 { shift->{_moment}->day_of_year - 1 }
288 5     5 0 45 sub quarter { shift->{_moment}->quarter }
289 2     2 0 18 sub quarter_0 { shift->{_moment}->quarter - 1 }
290 3     3 0 30 sub weekday_of_month { int((shift->{_moment}->day_of_month - 1) / 7) + 1 }
291 33     33 0 1634 sub hour { shift->{_moment}->hour }
292 4 100   4 0 27 sub hour_1 { shift->{_moment}->hour || 24 }
293 3 100   3 0 8 sub hour_12 { shift->hour_12_0 || 12 }
294 6     6 0 33 sub hour_12_0 { shift->{_moment}->hour % 12 }
295 28     28 0 1600 sub minute { shift->{_moment}->minute }
296 25     25 0 1519 sub second { shift->{_moment}->second }
297              
298             sub fractional_second {
299 1     1 0 1 my $moment = shift->{_moment};
300 1         11 return $moment->second + $moment->nanosecond / 1_000_000_000;
301             }
302              
303 20     20 0 1513 sub nanosecond { shift->{_moment}->nanosecond }
304 5     5 0 22 sub millisecond { shift->{_moment}->millisecond }
305 5     5 0 26 sub microsecond { shift->{_moment}->microsecond }
306              
307 2     2 0 9 sub is_leap_year { _is_leap_year(shift->{_moment}->year) }
308 0     0 0 0 sub leap_seconds { 0 } ## XXX: time moment doesn't have a leap seconds. So always leap seconds are zero.
309              
310 0     0 0 0 sub week_number { shift->{_moment}->week }
311 0     0 0 0 sub week_year { Carp::croak 'not yet implemented' }
312              
313             # ISO says that the first week of a year is the first week containing
314             # a Thursday. Extending that says that the first week of the month is
315             # the first week containing a Thursday. ICU agrees.
316             sub week_of_month {
317 3     3 0 5 my $self = shift;
318 3         5 my $thu = $self->day + 4 - $self->day_of_week;
319 3         13 return int(($thu + 6) / 7);
320             }
321              
322 6     6 0 355 sub offset { $_[0]->{time_zone}->offset_for_datetime($_[0]) }
323              
324             sub ymd {
325 629     629 0 2246 my $moment = shift->{_moment};
326 629 100       807 my $separator = defined $_[0] ? shift : '-';
327 629         4236 return sprintf '%04d%s%02d%s%02d',
328             $moment->year, $separator,
329             $moment->month, $separator,
330             $moment->day_of_month;
331             }
332              
333             sub mdy {
334 3     3 0 6 my $moment = shift->{_moment};
335 3 100       5 my $separator = defined $_[0] ? shift : '-';
336 3         31 return sprintf '%02d%s%02d%s%04d',
337             $moment->month, $separator,
338             $moment->day_of_month, $separator,
339             $moment->year;
340             }
341              
342             sub dmy {
343 3     3 0 6 my $moment = shift->{_moment};
344 3 100       5 my $separator = defined $_[0] ? shift : '-';
345 3         26 return sprintf '%02d%s%02d%s%04d',
346             $moment->day_of_month, $separator,
347             $moment->month, $separator,
348             $moment->year;
349             }
350              
351             sub hms {
352 108     108 0 113 my $moment = shift->{_moment};
353 108 100       140 my $separator = defined $_[0] ? shift : ':';
354 108         1100 return sprintf '%02d%s%02d%s%02d',
355             $moment->hour, $separator,
356             $moment->minute, $separator,
357             $moment->second;
358             }
359              
360 104     104 0 1348 sub iso8601 { $_[0]->ymd('-').'T'.$_[0]->hms(':') }
361              
362             # NOTE: no nanoseconds, no leap seconds
363 110     110 0 523 sub utc_rd_values { $_[0]->{_moment}->utc_rd_values }
364 2     2 0 1860 sub local_rd_values { $_[0]->{_moment}->local_rd_values }
365 5     5 0 53 sub utc_rd_as_seconds { $_[0]->{_moment}->utc_rd_as_seconds }
366 0     0 0 0 sub local_rd_as_seconds { $_[0]->{_moment}->local_rd_as_seconds }
367 0     0 0 0 sub utc_year { shift->{_moment}->utc_year }
368              
369             ## NOTE: Time::Moment supports date in anno Domini omly.
370 6     6 0 29 sub ce_year { shift->{_moment}->year }
371 1     1 0 234 sub era_name { $_[0]->{locale}->era_wide->[1] }
372 5     5 0 246 sub era_abbr { $_[0]->{locale}->era_abbreviated->[1] }
373 2     2 0 241 sub christian_era { 'AD' }
374 2     2 0 6 sub secular_era { 'CE' }
375 1     1 0 235 sub year_with_era { ( abs $_[0]->ce_year ) . $_[0]->era_abbr }
376 1     1 0 3 sub year_with_christian_era { ( abs $_[0]->ce_year ) . $_[0]->christian_era }
377 1     1 0 3 sub year_with_secular_era { ( abs $_[0]->ce_year ) . $_[0]->secular_era }
378              
379 5     5 0 18 sub month_name { $_[0]->{locale}->month_format_wide->[ $_[0]->month_0() ] }
380 4     4 0 14 sub month_abbr { $_[0]->{locale}->month_format_abbreviated->[ $_[0]->month_0() ] }
381 4     4 0 15 sub day_name { $_[0]->{locale}->day_format_wide->[ $_[0]->day_of_week_0() ] }
382 4     4 0 13 sub day_abbr { $_[0]->{locale}->day_format_abbreviated->[ $_[0]->day_of_week_0() ] }
383 20 100   20 0 54 sub am_or_pm { $_[0]->{locale}->am_pm_abbreviated->[ $_[0]->{_moment}->hour < 12 ? 0 : 1 ] }
384 1     1 0 5 sub quarter_name { $_[0]->{locale}->quarter_format_wide->[ $_[0]->quarter_0() ] }
385 1     1 0 235 sub quarter_abbr { $_[0]->{locale}->quarter_format_abbreviated->[ $_[0]->quarter_0() ] }
386              
387             sub local_day_of_week {
388 0     0 0 0 my $self = shift;
389 0         0 return 1 + ($self->{_moment}->day_of_week - $self->{locale}->first_day_of_week) % 7;
390             }
391              
392 7     7 0 86 sub mjd { $_[0]->{_moment}->mjd }
393 7     7 0 54 sub jd { $_[0]->{_moment}->jd }
394 0     0 0 0 sub rd { $_[0]->{_moment}->rd }
395              
396 17     17 0 370 sub epoch { shift->{_moment}->epoch }
397              
398             sub hires_epoch {
399 1     1 0 3 my $moment = shift->{_moment};
400 1         6 return $moment->epoch + $moment->nanosecond / 1_000_000_000;
401             }
402              
403 0     0 0 0 sub is_finite { 1 }
404 0     0 0 0 sub is_infinite { 0 }
405              
406             sub _mod_and_keep_sign {
407 0     0   0 my ($lhs, $rhs) = @_;
408 0 0       0 my $sign = $lhs < 0 ? -1 : 1;
409 0         0 return $sign * ($lhs % $rhs);
410             }
411              
412             sub _delta_days {
413 25     25   22 my ($lhs, $rhs) = @_;
414 25 100       101 return $lhs->day_of_month - $rhs->day_of_month if $lhs->day_of_month >= $rhs->day_of_month;
415 9         29 return $lhs->day_of_month + (_month_length($rhs->year, $rhs->month) - $rhs->day_of_month);
416             }
417              
418             sub subtract_datetime {
419 25     25 0 741 my ($lhs, $rhs) = @_;
420 25         29 my $class = ref $lhs;
421              
422             # normalize
423 25 50       31 $rhs = $class->from_object(object => $rhs) unless $rhs->isa($class);
424 25 50       60 $rhs = $rhs->clone->set_time_zone($lhs->time_zone) unless $lhs->time_zone->name eq $rhs->time_zone->name;
425              
426 25         263 my ($lhs_moment, $rhs_moment) = map { $_->{_moment} } ($lhs, $rhs);
  50         101  
427              
428 25 100       136 my $sign = $lhs_moment < $rhs_moment ? -1 : 1;
429 25 100       39 ($lhs_moment, $rhs_moment) = ($rhs_moment, $lhs_moment) if $sign == -1;
430              
431 25         66 my $months = $rhs_moment->delta_months($lhs_moment);
432 25         34 my $days = _delta_days($lhs_moment, $rhs_moment);
433 25         87 my $minutes = 60 * ($lhs_moment->hour - $rhs_moment->hour) + ($lhs_moment->minute - $rhs_moment->minute);
434 25         46 my $seconds = $lhs_moment->second - $rhs_moment->second;
435 25         46 my $nanoseconds = $lhs_moment->nanosecond - $rhs_moment->nanosecond;
436              
437 25 100       38 if ($nanoseconds < 0) {
438 6         6 $nanoseconds += 1_000_000_000;
439 6         6 $seconds--;
440             }
441 25 100       31 if ($seconds < 0) {
442 5         4 $seconds += 60;
443 5         6 $minutes--;
444             }
445 25 100       34 if ($minutes < 0) {
446 4         5 $minutes += 24 * 60;
447 4         4 $days--;
448             }
449 25 50       32 if ($days < 0) {
450 0         0 my $max_days = _month_length($rhs->year, $rhs_moment->month);
451 0         0 $days += $max_days;
452 0         0 $months--;
453             }
454              
455 25         85 return DateTimeX::Moment::Duration->new(
456             months => $sign * $months,
457             days => $sign * $days,
458             minutes => $sign * $minutes,
459             seconds => $sign * $seconds,
460             nanoseconds => $sign * $nanoseconds,
461             );
462             }
463              
464             sub subtract_datetime_absolute {
465 2     2 0 7 my ($lhs, $rhs) = @_;
466 2         3 my $class = ref $lhs;
467              
468             # normalize
469 2 50       4 $rhs = $class->from_object(object => $rhs) unless $rhs->isa($class);
470 2 50       5 $rhs = $rhs->clone->set_time_zone($lhs->time_zone) unless $lhs->time_zone eq $rhs->time_zone;
471              
472 2         17 my ($lhs_moment, $rhs_moment) = map { $_->{_moment} } ($lhs, $rhs);
  4         7  
473 2 100       7 my $sign = $lhs_moment < $rhs_moment ? -1 : 1;
474 2 100       5 ($lhs_moment, $rhs_moment) = ($rhs_moment, $lhs_moment) if $sign == -1;
475              
476 2         5 my $seconds = $rhs_moment->delta_seconds($lhs_moment);
477 2         5 my $nanoseconds = $rhs_moment->delta_nanoseconds($lhs_moment) % 1_000_000_000;
478              
479 2         7 return DateTimeX::Moment::Duration->new(
480             seconds => $sign * $seconds,
481             nanoseconds => $sign * $nanoseconds,
482             );
483             }
484              
485             sub _stringify {
486 63     63   874 my $self = shift;
487 63 100       156 return $self->iso8601 unless defined $self->{formatter};
488 10         19 return $self->{formatter}->format_datetime($self);
489             }
490              
491             sub _compare_overload {
492 17     17   1209 my ($lhs, $rhs, $flip) = @_;
493 17 100       59 return undef unless defined $rhs;
494 15 50       35 return $flip ? -$lhs->compare($rhs) : $lhs->compare($rhs);
495             }
496              
497             sub _string_compare_overload {
498 15     15   1038 my ($lhs, $rhs, $flip) = @_;
499 15 50       25 return undef unless defined $rhs;
500 15 100       21 goto \&_compare_overload if _isa_datetime_compareble($rhs);
501              
502             # One is a DateTimeX::Moment object, one isn't. Just stringify and compare.
503 12 100       20 my $sign = $flip ? -1 : 1;
504 12         13 return $sign * ("$lhs" cmp "$rhs");
505             }
506              
507 6     6   349 sub _string_not_equals_overload { !_string_equals_overload(@_) }
508             sub _string_equals_overload {
509 12 50   12   485 my ($class, $lhs, $rhs) = ref $_[0] ? (ref $_[0], @_) : @_;
510 12 50       18 return undef unless defined $rhs;
511 12 100       19 return !$class->compare($lhs, $rhs) if _isa_datetime_compareble($rhs);
512 10         15 return "$lhs" eq "$rhs";
513             }
514              
515             sub _add_overload {
516 6     6   712 my ($dt, $dur, $flip) = @_;
517 6 50       16 ($dur, $dt) = ($dt, $dur) if $flip;
518              
519 6 100       14 unless (_isa_duration($dur)) {
520 2         3 my $class = ref $dt;
521 2         5 Carp::croak("Cannot add $dur to a $class object ($dt).\n"
522             . ' Only a DateTime::Duration object can '
523             . " be added to a $class object.");
524             }
525              
526 4         10 return $dt->clone->add_duration($dur);
527             }
528              
529             sub _subtract_overload {
530 12     12   611 my ($date1, $date2, $flip) = @_;
531 12 50       21 ($date2, $date1) = ($date1, $date2) if $flip;
532              
533 12 100       18 if (_isa_duration($date2)) {
    100          
534 1         4 my $new = $date1->clone;
535 1         3 $new->add_duration($date2->inverse);
536 1         4 return $new;
537             }
538             elsif (_isa_datetime($date2)) {
539 9         17 return $date1->subtract_datetime($date2);
540             }
541              
542 2         2 my $class = ref $date1;
543 2         7 Carp::croak(
544             "Cannot subtract $date2 from a $class object ($date1).\n"
545             . ' Only a DateTime::Duration or DateTimeX::Moment object can '
546             . " be subtracted from a $class object." );
547             }
548              
549 50     50 0 179 sub compare { shift->_compare(@_, 0) }
550 2     2 0 4 sub compare_ignore_floating { shift->_compare(@_, 1) }
551              
552             sub _compare {
553 52 100   52   106 my ($class, $lhs, $rhs, $consistent) = ref $_[0] ? (__PACKAGE__, @_) : @_;
554 52 50       81 return undef unless defined $rhs;
555              
556 52 100 33     73 if (!_isa_datetime_compareble($lhs) || !_isa_datetime_compareble($rhs)) {
557 2         4 Carp::croak("A DateTimeX::Moment object can only be compared to another DateTimeX::Moment object ($lhs, $rhs).");
558             }
559              
560 50 100 66     319 if (!$consistent && $lhs->can('time_zone') && $rhs->can('time_zone')) {
      100        
561 46         103 my $is_floating1 = $lhs->time_zone->is_floating;
562 46         270 my $is_floating2 = $rhs->time_zone->is_floating;
563              
564 46 50 66     354 if ($is_floating1 && !$is_floating2) {
    100 100        
565 0         0 $lhs = $lhs->clone->set_time_zone($rhs->time_zone);
566             }
567             elsif ($is_floating2 && !$is_floating1) {
568 3         7 $rhs = $rhs->clone->set_time_zone($lhs->time_zone);
569             }
570             }
571              
572 50 100 66     85 if ($lhs->isa(__PACKAGE__) && $rhs->isa(__PACKAGE__)) {
573 48         239 return $lhs->{_moment}->compare($rhs->{_moment});
574             }
575              
576 2         3 my @lhs_components = $lhs->utc_rd_values;
577 2         4 my @rhs_components = $rhs->utc_rd_values;
578              
579 2         11 for my $i (0 .. 2) {
580 4 100       14 return $lhs_components[$i] <=> $rhs_components[$i] if $lhs_components[$i] != $rhs_components[$i];
581             }
582              
583 0         0 return 0;
584             }
585              
586             sub set {
587 81     81 0 409 my $self = shift;
588 81 50 33     286 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
589              
590 81         153 my $src = $self->{_moment};
591              
592 81         68 my %params;
593 81         118 for my $unit (qw/year month day hour minute second nanosecond/) {
594 567 100       606 my $key = $unit eq 'day' ? 'day_of_month' : $unit;
595 567 100       1388 $params{$unit} = exists $args{$unit} ? delete $args{$unit} : $src->$key();
596             }
597 81 50       138 if (%args) {
598 0         0 my $msg = 'Invalid args: '.join ',', keys %args;
599 0         0 Carp::croak $msg;
600             }
601              
602 81         440 $self->{_moment} = Time::Moment->new(%params);
603              
604 71         145 return $self->_adjust_to_current_offset();
605             }
606              
607 1     1 0 5 sub set_year { $_[0]->set(year => $_[1]) }
608 1     1 0 2 sub set_month { $_[0]->set(month => $_[1]) }
609 1     1 0 3 sub set_day { $_[0]->set(day => $_[1]) }
610 1     1 0 3 sub set_hour { $_[0]->set(hour => $_[1]) }
611 1     1 0 4 sub set_minute { $_[0]->set(minute => $_[1]) }
612 1     1 0 3 sub set_second { $_[0]->set(second => $_[1]) }
613 1     1 0 3 sub set_nanosecond { $_[0]->set(nanosecond => $_[1]) }
614              
615 602     602 0 2143 sub add { shift->_calc_date(plus => @_) }
616 45     45 0 91 sub subtract { shift->_calc_date(minus => @_) }
617              
618             sub _calc_date {
619 647     647   483 my $self = shift;
620 647         516 my $type = shift;
621 647 50 33     1162 return $self->_calc_duration($type => @_) if @_ == 1 && _isa_duration($_[0]);
622              
623 647 50 33     1620 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
624              
625 647         561 my $moment = $self->{_moment};
626              
627             {
628 647 50 66     1347 if (exists $args{years} && exists $args{months}) {
629 0 0       0 my $factor = ($type eq 'plus') ? 12 : -12;
630 0         0 $args{months} += delete($args{years}) * $factor;
631             }
632              
633 647         447 my $result = $moment;
634 647         786 for my $unit (qw/weeks days months years/) {
635 2588 100       3288 next unless exists $args{$unit};
636 633         757 my $method = $type.'_'.$unit;
637 633         2571 $result = $result->$method(delete $args{$unit});
638             }
639              
640 647 100       1393 if (!$moment->is_equal($result)) {
641 607         750 $moment = _moment_resolve_local($result, $self->{time_zone});
642             }
643             }
644              
645             {
646 647         500 my $result = $moment;
  646         549  
  646         453  
647 646         633 for my $unit (qw/nanoseconds seconds minutes hours/) {
648 2584 100       3385 next unless exists $args{$unit};
649 112         134 my $method = $type.'_'.$unit;
650 112         312 $result = $result->$method(delete $args{$unit});
651             }
652              
653 646 100       1090 if (!$moment->is_equal($result)) {
654 49         83 $moment = _moment_resolve_instant($result, $self->{time_zone});
655             }
656             }
657              
658 646 50       943 if (%args) {
659 0         0 my $msg = 'Invalid args: '.join ',', keys %args;
660 0         0 Carp::croak $msg;
661             }
662              
663 646         576 $self->{_moment} = $moment;
664 646         1057 return $self;
665             }
666              
667             sub delta_md {
668 2     2 0 345 my ($lhs, $rhs) = reverse sort { $a <=> $b } @_;
  2         6  
669 2         5 return $lhs->clone->truncate(to => 'day')->subtract_datetime($rhs->clone->truncate(to => 'day'));
670             }
671              
672             sub delta_ms {
673 3     3 0 9 my ($lhs, $rhs) = reverse sort { $a <=> $b } @_;
  3         7  
674 3         52 my $days = floor($lhs->{_moment}->jd - $rhs->{_moment}->jd);
675 3         5 my $duration = $lhs->subtract_datetime($rhs);
676 3         9 return DateTimeX::Moment::Duration->new(
677             hours => $duration->hours + ($days * 24),
678             minutes => $duration->minutes,
679             seconds => $duration->seconds,
680             );
681             }
682              
683 0     0 0 0 sub delta_years { shift->_delta(years => @_) }
684 0     0 0 0 sub delta_months { shift->_delta(months => @_) }
685 0     0 0 0 sub delta_weeks { shift->_delta(weeks => @_) }
686 3     3 0 13 sub delta_days { shift->_delta(days => @_) }
687 0     0 0 0 sub delta_hours { shift->_delta(hours => @_) }
688 0     0 0 0 sub delta_minutes { shift->_delta(minutes => @_) }
689 0     0 0 0 sub delta_seconds { shift->_delta(seconds => @_) }
690 0     0 0 0 sub delta_milliseconds { shift->_delta(milliseconds => @_) }
691 0     0 0 0 sub delta_microseconds { shift->_delta(microseconds => @_) }
692 0     0 0 0 sub delta_nanoseconds { shift->_delta(nanoseconds => @_) }
693              
694             sub _delta {
695 3     3   5 my ($self, $unit, $another) = @_;
696 3         3 my $lhs = $self->{_moment};
697 3 50       6 my $rhs = $another->isa(__PACKAGE__) ? $another->{_moment} : Time::Moment->from_object($another);
698 3 50       6 $rhs = Time::Moment->from_object($rhs) unless _isa_moment($rhs);
699              
700 3         7 my $method = "delta_$unit";
701 3 100       20 my $diff = $lhs > $rhs ? $rhs->$method($lhs) : $lhs->$method($rhs);
702              
703             # normalize
704 3 50       11 if ($unit eq 'milliseconds') {
    50          
705 0         0 $unit = 'nanoseconds';
706 0         0 $diff *= 1_000_000;
707             }
708             elsif ($unit eq 'microseconds') {
709 0         0 $unit = 'nanoseconds';
710 0         0 $diff *= 1_000;
711             }
712              
713 3         9 return DateTimeX::Moment::Duration->new($unit => $diff);
714             }
715              
716             # strftime
717             {
718             my %CUSTOM_HANDLER = (
719             a => sub { $_[0]->day_abbr },
720             A => sub { $_[0]->day_name },
721             b => sub { $_[0]->month_abbr },
722             B => sub { $_[0]->month_name },
723             c => sub { $_[0]->format_cldr($_[0]->{locale}->datetime_format_default()) },
724             p => sub { $_[0]->am_or_pm },
725             P => sub { lc $_[0]->am_or_pm },
726             r => sub { $_[0]->strftime('%I:%M:%S %p') },
727             x => sub { $_[0]->format_cldr($_[0]->{locale}->date_format_default()) },
728             X => sub { $_[0]->format_cldr($_[0]->{locale}->time_format_default()) },
729             Z => sub { $_[0]->{time_zone}->short_name_for_datetime($_[0]) },
730             );
731              
732             my $CUSTOM_HANDLER_REGEXP = '(?:(?<=[^%])((?:%%)*)|\A)%(['.(join '', keys %CUSTOM_HANDLER).'])';
733              
734             sub strftime {
735 164     164 0 58547 my ($self, @formats) = @_;
736 164         199 my $moment = $self->{_moment};
737              
738 164         148 my @ret;
739 164         186 for my $format (@formats) {
740             # XXX: follow locale/time_zone
741 165   50     732 $format =~ s/$CUSTOM_HANDLER_REGEXP/($1||'').$CUSTOM_HANDLER{$2}->($self)/omsge;
  41         226  
742 165 100 50     1862 $format =~ s/(?:(?<=[^%])((?:%%)*)|\A)%\{(\w+)\}/($1||'').($self->can($2) ? $self->$2 : "%{$2}")/omsge;
  7         68  
743              
744 165         646 my $ret = $moment->strftime($format);
745 165 100       848 return $ret unless wantarray;
746              
747 2         4 push @ret => $ret;
748             }
749              
750 1         3 return @ret;
751             }
752             }
753              
754             sub format_cldr {
755 105     105 0 63518 my $self = shift;
756              
757             # fallback
758 105         419 require DateTime;
759             return DateTime->from_object(
760             object => $self,
761             locale => $self->{locale},
762 105         340 )->format_cldr(@_);
763             }
764              
765             sub set_time_zone {
766 23     23 0 15826 my ($self, $time_zone) = @_;
767 23 50       57 Carp::croak 'required time_zone' if @_ != 2;
768              
769 23         58 $time_zone = $self->_inflate_time_zone($time_zone);
770 23 100       54579 return $self if $time_zone == $self->{time_zone};
771 21 50       97 return $self if $time_zone->name eq $self->{time_zone}->name;
772              
773 21         143 $self->{_moment} = do {
774 21 100       51 if ($self->{time_zone}->is_floating) {
775 5         23 _moment_resolve_local($self->{_moment}, $time_zone)
776             }
777             else {
778 16         69 _moment_resolve_instant($self->{_moment}, $time_zone);
779             }
780             };
781 20         39 $self->{time_zone} = $time_zone;
782 20         50 return $self;
783             }
784              
785 1     1 0 10 sub is_dst { $_[0]->{time_zone}->is_dst_for_datetime($_[0]) }
786              
787 0     0 0 0 sub time_zone_long_name { $_[0]->{time_zone}->name }
788 1     1 0 329 sub time_zone_short_name { $_[0]->{time_zone}->short_name_for_datetime($_[0]) }
789              
790             sub truncate :method {
791 69     69 0 1282 my $self = shift;
792 69 50 33     202 my %args = (@_ == 1 && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  0         0  
793              
794             my $to = delete $args{to}
795 69 50       116 or Carp::croak "Parameter: to is required.";
796 69 50       95 if (%args) {
797 0         0 my $msg = 'Invalid args: '.join ',', keys %args;
798 0         0 Carp::croak $msg;
799             }
800              
801 69 100 100     227 if ($to eq 'week' || $to eq 'local_week') {
802 25 100       59 my $first_day_of_week = $to eq 'local_week' ? $self->{locale}->first_day_of_week : 1;
803 25         82 my $day_diff = ($self->{_moment}->day_of_week - $first_day_of_week) % 7;
804 25 100       47 $self->subtract(days => $day_diff) if $day_diff;
805 25         22 eval {
806 25         33 $self->truncate(to => 'day');
807             };
808 25 100       189 if ($@) {
809 1         2 $self->add(days => $day_diff); # rollback
810 1         3 die $@;
811             }
812 24         33 return $self;
813             }
814              
815 44         53 my %param;
816 44 100       43 $param{nanosecond} = 0; goto DO_TRUNCATE if $to eq 'second';
  44         71  
817 43 100       36 $param{second} = 0; goto DO_TRUNCATE if $to eq 'minute';
  43         65  
818 41 100       33 $param{minute} = 0; goto DO_TRUNCATE if $to eq 'hour';
  41         62  
819 40 100       36 $param{hour} = 0; goto DO_TRUNCATE if $to eq 'day';
  40         95  
820 7 100       6 $param{day} = 1; goto DO_TRUNCATE if $to eq 'month';
  7         12  
821 5 100       5 $param{month} = 1; goto DO_TRUNCATE if $to eq 'year';
  5         8  
822              
823 4         297 Carp::croak "The 'to' parameter '$to' is unsupported.";
824              
825 40         100 DO_TRUNCATE:
826             return $self->set(%param);
827             }
828              
829             my %CALC_DURATION_METHOD = (plus => 'add_duration', minus => 'subtract_duration');
830             sub _calc_duration {
831 0     0   0 my ($self, $type, $duration) = @_;
832 0         0 my $method = $CALC_DURATION_METHOD{$type};
833 0         0 return $self->$method($duration);
834             }
835              
836 2     2 0 9 sub subtract_duration { $_[0]->add_duration($_[1]->inverse) }
837             sub add_duration {
838 20     20 0 343 my ($self, $duration) = @_;
839 20 50       25 Carp::croak 'required duration object' unless _isa_duration($duration);
840              
841             # simple optimization
842 20 50       46 return $self if $duration->is_zero;
843              
844 20 50       52 if (!$duration->is_limit_mode) {
845 0         0 Carp::croak 'DateTimeX::Moment supports limit mode only.';
846             }
847              
848 20         38 return $self->add($duration->deltas);
849             }
850              
851             sub set_locale {
852 6     6 0 9 my ($self, $locale) = @_;
853 6 50       22 Carp::croak 'required locale' if @_ != 2;
854 6         17 $self->{locale} = $self->_inflate_locale($locale);
855 6         533 return $self;
856             }
857              
858             sub set_formatter {
859 3     3 0 55 my ($self, $formatter) = @_;
860 3         6 $self->{formatter} = $self->_inflate_formatter($formatter);
861 2         2 return $self;
862             }
863              
864             # internal utilities
865 793 100   793   4352 sub _isa_locale { is_instance($_[0] => 'DateTime::Locale::FromData') || is_instance($_[0] => 'DateTime::Locale::Base') }
866 5 100   5   11 sub _isa_formatter { _isa_invocant($_[0]) && $_[0]->can('format_datetime') }
867 773     773   1995 sub _isa_time_zone { is_instance($_[0] => 'DateTime::TimeZone') }
868 11     11   25 sub _isa_datetime { is_instance($_[0] => 'DateTime') }
869 131 100   131   779 sub _isa_datetime_compareble { blessed($_[0]) && $_[0]->can('utc_rd_values') }
870 38     38   200 sub _isa_duration { is_instance($_[0] => 'DateTime::Duration') }
871 3     3   31 sub _isa_moment { is_instance($_[0] => 'Time::Moment') }
872 3 50   3   35 sub _isa_moment_convertable { blessed($_[0]) && $_[0]->can('__as_Time_Moment') }
873 5 100   5   71 sub _isa_invocant { blessed $_[0] || Class::Inspector->loaded("$_[0]") }
874              
875             # define aliases
876             {
877             my %aliases = (
878             month => [qw/mon/],
879             day_of_month => [qw/day mday/],
880             day_of_month_0 => [qw/day_0 mday_0/],
881             day_of_week => [qw/wday dow/],
882             day_of_week_0 => [qw/wday_0 dow_0/],
883             day_of_quarter => [qw/doq/],
884             day_of_quarter_0 => [qw/doq_0/],
885             day_of_year => [qw/doy/],
886             day_of_year_0 => [qw/doy_0/],
887             ymd => [qw/date/],
888             hms => [qw/time/],
889             iso8601 => [qw/datetime/],
890             minute => [qw/min/],
891             second => [qw/sec/],
892             locale => [qw/language/],
893             era_abbr => [qw/era/],
894             );
895              
896             for my $src (keys %aliases) {
897             my $code = do {
898 33     33   206 no strict qw/refs/;
  33         40  
  33         1829  
899             \&{$src};
900             };
901              
902             for my $dst (@{ $aliases{$src} }) {
903 33     33   131 no strict qw/refs/;
  33         59  
  33         2085  
904             *{$dst} = $code;
905             }
906             }
907             }
908              
909             1;
910             __END__