File Coverage

blib/lib/DateTimeX/Moment.pm
Criterion Covered Total %
statement 468 531 88.1
branch 210 270 77.7
condition 57 100 57.0
subroutine 141 161 87.5
pod 0 111 0.0
total 876 1173 74.6


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