File Coverage

blib/lib/DateTime/Fiction/JRRTolkien/Shire/Duration.pm
Criterion Covered Total %
statement 119 139 85.6
branch 34 68 50.0
condition 13 26 50.0
subroutine 37 42 88.1
pod 7 23 30.4
total 210 298 70.4


line stmt bran cond sub pod time code
1             package DateTime::Fiction::JRRTolkien::Shire::Duration;
2              
3 9     9   1164 use 5.008004;
  9         54  
4              
5 9     9   67 use strict;
  9         24  
  9         228  
6 9     9   57 use warnings;
  9         22  
  9         303  
7              
8 9     9   51 use Carp ();
  9         21  
  9         258  
9 9     9   614 use DateTime::Duration 0.140 ();
  9         568920  
  9         201  
10 9     9   5039 use DateTime::Fiction::JRRTolkien::Shire::Types ();
  9         45  
  9         585  
11 9     9   99 use Params::ValidationCompiler 0.13 ();
  9         263  
  9         215  
12 9     9   63 use Scalar::Util ();
  9         27  
  9         714  
13              
14             *__t = \&DateTime::Fiction::JRRTolkien::Shire::Types::t;
15              
16             use overload
17 9         103 fallback => 1,
18             '+' => '_add_overload',
19             '-' => '_subtract_overload',
20             '*' => '_multiply_overload',
21             '<=>' => '_compare_overload',
22             'cmp' => '_compare_overload',
23 9     9   111 ;
  9         26  
24              
25             our $VERSION = '0.906';
26              
27             {
28              
29             my $validate = Params::ValidationCompiler::validation_for(
30             name => '_validation_for_new',
31             name_is_optional => 1,
32             params => {
33             years => { type => __t( 'IntOrUndef' ) },
34             months => { type => __t( 'IntOrUndef' ) },
35             weeks => { type => __t( 'IntOrUndef' ) },
36             },
37             );
38              
39             sub new {
40 43     43 1 1638 my ( $class, %arg ) = @_;
41              
42             $validate->(
43             years => $arg{years},
44             months => $arg{months},
45             weeks => $arg{weeks},
46 43         1126 );
47              
48 43   100     2081 $arg{$_} ||= 0 foreach qw{ years months weeks days };
49              
50 43         80 my $default_mode;
51             ( my $mode_specified = $arg{end_of_month} || $arg{holiday} )
52 43 100 100     227 or $default_mode = _compute_default_mode( \%arg );
53              
54 43         105 my $years = delete $arg{years};
55 43         84 my $weeks = delete $arg{weeks};
56              
57 43 100       126 if ( defined $arg{holiday} ) {
58             defined $arg{end_of_month}
59 6 50       18 and Carp::croak(
60             q<You may not specify both end_of_month and holiday> );
61 6         21 $arg{end_of_month} = _map_holiday_mode( delete $arg{holiday} );
62             }
63              
64             defined $arg{end_of_month}
65 43 100       136 or $arg{end_of_month} = $default_mode;
66              
67 43   66     209 return bless {
68             duration => DateTime::Duration->new( %arg ),
69             mode_specified => $mode_specified,
70             weeks => $weeks,
71             years => $years,
72             }, ref $class || $class;
73             }
74             }
75              
76             sub add {
77 1     1 0 654 my ( $self, @arg ) = @_;
78 1         7 return $self->add_duration( _make_duration( @arg ) );
79             }
80              
81             sub add_duration {
82 4     4 0 155 my ( $self, $dur ) = @_;
83 4 50       12 if ( _isa( $dur, __PACKAGE__ ) ) {
    0          
84 4         13 $self->{weeks} += $dur->{weeks};
85 4         16 $self->{duration}->add_duration( $dur->{duration} );
86             } elsif ( _isa( $dur, 'DateTime::Duration' ) ) {
87 0         0 $self->{duration}->add_duration( $dur );
88             } else {
89 0         0 Carp::croak( "Can not do arithmetic on $dur" );
90             }
91 4         82 return $self;
92             }
93              
94             sub calendar_duration {
95 1     1 0 795 my ( $self ) = @_;
96 1         6 return $self->new(
97             years => $self->delta_years(),
98             months => $self->delta_months(),
99             weeks => $self->delta_weeks(),
100             days => $self->delta_days(),
101             end_of_month => $self->end_of_month_mode(),
102             );
103             }
104              
105             sub clock_duration {
106 1     1 0 642 my ( $self ) = @_;
107 1         6 return $self->new(
108             minutes => $self->delta_minutes(),
109             seconds => $self->delta_seconds(),
110             nanoseconds => $self->delta_nanoseconds(),
111             end_of_month => $self->end_of_month_mode(),
112             );
113             }
114              
115             sub clone {
116 3     3 0 7 my ( $self ) = @_;
117 3         7 my %clone = %{ $self };
  3         13  
118 3         16 $clone{duration} = $self->{duration}->clone();
119 3         40 return bless \%clone, ref $self;
120             }
121              
122             require DateTime::Fiction::JRRTolkien::Shire;
123              
124             sub compare {
125 0     0 0 0 my ( undef, $left, $right, $base ) = @_;
126              
127 0   0     0 $base ||= DateTime::Fiction::JRRTolkien::Shire->now();
128              
129 0         0 return DateTime::Fiction::JRRTolkien::Shire->compare(
130             $base->clone()->add_duration( $left ),
131             $base->clone()->add_duration( $right ),
132             );
133             }
134              
135             sub delta_weeks {
136 2     2 1 596 my ( $self ) = @_;
137 2         13 return $self->{weeks};
138             }
139              
140             sub delta_years {
141 2     2 1 747 my ( $self ) = @_;
142 2         15 return $self->{years};
143             }
144              
145             # sub delta_months; sub delta_days; sub delta_minutes;
146             # sub delta_seconds; sub delta_nanoseconds;
147             # sub end_of_month_mode; is_wrap_mode; is_limit_mode; is_preserve_mode;
148             # sub months; sub days; sub hours; sub minutes; sub seconds;
149             # sub nanoseconds;
150             foreach my $method ( qw{
151             delta_months delta_days delta_minutes delta_seconds
152             delta_nanoseconds
153             end_of_month_mode is_wrap_mode is_limit_mode is_preserve_mode
154             months days hours minutes seconds nanoseconds
155             } ) {
156 9     9   9201 no strict qw{ refs };
  9         48  
  9         13903  
157 47     47   4456 *$method = sub { return $_[0]->{duration}->$method() };
158             }
159              
160 25 100   25 1 75 sub is_forward_mode { return $_[0]->is_wrap_mode() ? 1 : 0 }
161              
162 0 0   0 1 0 sub is_backward_mode { return $_[0]->is_wrap_mode() ? 0 : 1 }
163              
164 0     0 1 0 sub holiday_mode { return ( qw{ backward forward } )[
165             $_[0]->is_forward_mode() ] }
166              
167             sub deltas {
168 62     62 1 762 my ( $self ) = @_;
169             return (
170             $self->{duration}->deltas(),
171             weeks => $self->{weeks},
172             years => $self->{years},
173 62         175 );
174             }
175              
176             {
177             my %on_side = map { $_ => 1 } qw{ years weeks };
178              
179             sub in_units {
180 0     0 0 0 my ( $self, @units ) = @_;
181 0         0 my @rslt = $self->{duration}->in_units( @units );
182 0         0 foreach my $inx ( 0 .. $#units ) {
183             $on_side{$units[$inx]}
184 0 0       0 and $rslt[$inx] = $self->{$units[$inx]};
185             }
186 0 0       0 return wantarray ? @rslt : $rslt[0];
187             }
188             }
189              
190             # Note that we always specify am end-of-month mode to the contained
191             # DateTime::Duration, because it does not have enough information to
192             # properly default, AND if an end-of-month mode was originally specified
193             # it is not preserved across the inversion.
194             sub inverse {
195 20     20 0 695 my ( $self, %arg ) = @_;
196              
197 20 50       88 if ( $arg{holiday} ) {
    50          
    100          
198 0         0 $arg{end_of_month} = _map_holiday_mode( delete $arg{holiday} );
199             } elsif ( $arg{end_of_month} ) {
200             # Do nothing
201             } elsif ( $self->{mode_specified} ) {
202 4         16 $arg{end_of_month} = $self->end_of_month_mode();
203             } else {
204 16         42 my %delta = $self->deltas();
205 16         289 $arg{end_of_month} = _compute_default_mode( \%delta, 1 );
206             }
207              
208 20         65 my %inverse = %{ $self };
  20         90  
209             $inverse{weeks}
210 20 100       65 and $inverse{weeks} *= -1;
211             $inverse{years}
212 20 100       54 and $inverse{years} *= -1;
213 20         77 $inverse{duration} = $self->{duration}->inverse( %arg );
214 20         2589 return bless \%inverse, ref $self;
215             }
216              
217             sub is_negative {
218 1     1 0 5 my ( $self ) = @_;
219 1 50       8 $self->{weeks} > 0
220             and return 0;
221 0 0       0 $self->{years} > 0
222             and return 0;
223             ( $self->{weeks} || $self->{years} )
224 0 0 0     0 and return $self->{duration}->is_negative() ? 1 : 0;
    0          
225 0         0 return $self->{duration}->is_negative();
226             }
227              
228             sub is_positive {
229 1     1 0 219 my ( $self ) = @_;
230 1 50       31 $self->{weeks} < 0
231             and return 0;
232 1 50       5 $self->{years} < 0
233             and return 0;
234             ( $self->{weeks} || $self->{years} )
235 1 50 33     8 and return $self->{duration}->is_positive() ? 1 : 0;
    50          
236 0         0 return $self->{duration}->is_positive();
237             }
238              
239             sub is_zero {
240 34     34 0 79 my ( $self ) = @_;
241             return ( $self->{duration}->is_zero() && 0 == $self->{weeks} &&
242 34 50 66     128 $self->{years} == 0 ) ? 1 : 0;
243             }
244              
245             sub multiply {
246 1     1 0 4 my ( $self, $multiplier ) = @_;
247 1         2 $self->{weeks} *= $multiplier;
248 1         3 $self->{years} *= $multiplier;
249 1         6 $self->{duration}->multiply( $multiplier );
250 1         69 return $self;
251             }
252              
253             sub subtract {
254 1     1 0 646 my ( $self, @arg ) = @_;
255 1         4 return $self->subtract_duration( _make_duration( @arg ) );
256             }
257              
258             sub subtract_duration {
259 2     2 0 273 my ( $self, $dur ) = @_;
260 2         43 return $self->add_duration( $dur->inverse() );
261             }
262              
263             sub weeks {
264 1     1 0 606 my ( $self ) = @_;
265 1         7 return abs $self->{weeks};
266             }
267              
268             sub years {
269 1     1 0 600 my ( $self ) = @_;
270 1         7 return abs $self->{years};
271             }
272              
273             sub _add_overload {
274 1     1   118 my ( $left, $right, $reverse ) = @_;
275              
276 1 50       6 $reverse
277             and ( $left, $right ) = ( $right, $left );
278              
279 1 50       4 _isa( $right, 'DateTime::Fiction::JRRTolkien::Shire' )
280             and return $right->clone()->add_duration( $left );
281              
282 1         6 return $left->clone()->add_duration( $right );
283             }
284              
285             sub _compare_overload {
286 0     0   0 Carp::croak(
287             'DateTime::Fiction::JRRTolkien::Shire::Duration does not overload comparison' );
288             }
289              
290             # Compute the default mode. Arguments are a reference to the argument
291             # hash to compute from, and an optional invert flag. The basic
292             # computation is to return 'preserve' if $arg->{months} * 30 +
293             # $arg->{weeks} * 7 is negative, and 'wrap' otherwise. If the invert
294             # flag is true, the opposite is returned.
295             sub _compute_default_mode {
296 51     51   120 my ( $arg, $invert ) = @_;
297             my $inx = ( $arg->{years} * 365 + $arg->{months} * 30 +
298 51 100       185 $arg->{weeks} * 7 ) >= 0 ? 1 : 0;
299 51 100       125 $invert
300             and $inx = 1 - $inx;
301 51         143 return ( qw{ preserve wrap } )[$inx];
302             }
303              
304             sub _isa {
305 6     6   15 my ( $obj, $class ) = @_;
306 6   66     88 return Scalar::Util::blessed( $obj ) && $obj->isa( $class );
307             }
308              
309             sub _make_duration {
310 2     2   6 my @arg = @_;
311 2 50 33     11 if ( 1 == @arg && Scalar::Util::blessed( $arg[0] ) ) {
312 0 0       0 $arg[0]->isa( __PACKAGE__ )
313             and return $arg[0];
314 0 0       0 $arg[0]->isa( 'DateTime::Duration' )
315             and return __PACKAGE__->new( $arg[0]->deltas() );
316             }
317 2         8 return __PACKAGE__->new( @arg );
318             }
319              
320             {
321             my %mode = (
322             forward => 'wrap',
323             backward => 'preserve',
324             );
325              
326             sub _map_holiday_mode {
327 6     6   13 my ( $m ) = @_;
328 6 50       22 my $rslt = $mode{$m}
329             or Carp::croak( "Invalid holiday mode '$m'");
330 6         15 return $rslt;
331             }
332             }
333              
334             sub _multiply_overload {
335 1     1   633 my ( $left, $right ) = @_;
336 1         6 return $left->clone()->multiply( $right );
337             }
338              
339             sub _subtract_overload {
340 1     1   687 my ( $left, $right, $reverse ) = @_;
341              
342 1 50       5 $reverse
343             and ( $left, $right ) = ( $right, $left );
344              
345 1 50       5 _isa( $right, 'DateTime::Fiction::JRRTolkien::Shire' )
346             and Carp::croak(
347             'Can not subtract a DateTime::Fiction::JRRTolkien::Shire from a DateTime::Fiction::JRRTolkien::Shire::Duration' );
348              
349 1         8 return $left->clone()->subtract_duration( $right );
350             }
351              
352             1;
353              
354             __END__
355              
356             =head1 NAME
357              
358             DateTime::Fiction::JRRTolkien::Shire::Duration - Duration objects for Shire calendar date math
359              
360             =head1 SYNOPSIS
361              
362             use DateTime::Fiction::JRRTolkien::Shire;
363             use DateTime::Fiction::JRRTolkien::Shire::Duration;
364            
365             my $dt = DateTime::Fiction::JRRTolkien::Shire->new(
366             year => 1419,
367             month => 3,
368             day => 25,
369             );
370             my $dur = DateTime::Fiction::JRRTolkien::Shire::Duration->new(
371             years => 1,
372             months => 2,
373             weeks => 3,
374             days => 4,
375             hours => 5,
376             minutes => 6,
377             seconds => 7,
378             nanoseconds => 8,
379             holiday => 'forward',
380             );
381             print $dt->add( $dur )->iso8601(), "\n";
382              
383             =head1 DESCRIPTION
384              
385             This is a simple class for representing durations in the Shire calendar.
386             It is B<not> a subclass of L<DateTime::Duration|DateTime::Duration>,
387             though it implements the same interface, plus some extra bells and
388             whistles. Objects of this class are used whenever you do date math with
389             L<DateTime::Fiction::JRRTolkien::Shire|DateTime::Fiction::JRRTolkien::Shire>.
390              
391             Unlike L<DateTime::Duration|DateTime::Duration>, this class preserves
392             years and weeks rather than folding them into months and days
393             respectively. This is because the Shire calendar contains days that are
394             not part of any week or month. An example may clarify this.
395              
396             You would expect adding a week to a Monday to produce the following
397             Monday. But adding seven days to 30 Forelithe (a Mersday) gives you 4
398             Afterlithe (a Hevensday) because the interval between these two dates
399             contains Midsummer's day, which is not part of any week. In a leap year
400             this would give 3 Afterlithe (a Trewsday) because the leap year day also
401             falls in this interval and is part of no week. The issues for months are
402             similar.
403              
404             A related issue with this calendar is what happens when you try, for
405             example, to add a month to a date that is not part of any month. When
406             something like this happens, the date is first adjusted to a nearby date
407             that B<is> part of a month (or week). By default the adjustment is
408             forward for a positive delta and backward for a negative delta, though
409             you can specify the direction of adjustment when the object is
410             instantiated. So adding a month to 1 Lithe gives 1 Wedmath by default,
411             but 30 Afterlithe if the adjustment is backward.
412              
413             =head1 METHODS
414              
415             This class supports the following public methods over and above those
416             supplied by L<DateTime::Duration|DateTime::Duration>:
417              
418             =head2 new
419              
420             This static method takes the same arguments as the corresponding
421             L<DateTime::Duration|DateTime::Duration> method. As (maybe) a
422             convenience, it also takes a C<holiday> parameter in lieu of the
423             C<end_of_month> parameter.
424              
425             The C<holiday> parameter must be either C<forward> or C<backward>, and
426             specifies how a date should be adjusted (if needed) before doing
427             arithmetic on it.
428              
429             If you specify C<end_of_month> (a misnomer in this case since all Shire
430             months have 30 days), C<wrap> specifies a forward adjustment, and
431             anything else specifies a backward adjustment.
432              
433             =head2 deltas
434              
435             This method returns the deltas stored in the object. Possible keys are
436             C<years>, C<months>, C<weeks>, C<days>, C<minutes>, C<seconds>, and
437             C<nanoseconds>.
438              
439             =head2 delta_weeks
440              
441             This method returns the C<weeks> element of the object.
442              
443             =head2 delta_years
444              
445             This method returns the C<years> element of the object.
446              
447             =head2 holiday_mode
448              
449             This method returns one of the strings C<forward> or C<backward>,
450             representing how dates are to be adjusted (if necessary) before
451             performing arithmetic on them.
452              
453             =head2 is_backward_mode
454              
455             This method returns C<1> if dates are to be adjusted backward (if
456             necessary) before doing arithmetic on them, and C<0> otherwise.
457              
458             =head2 is_forward_mode
459              
460             This method returns C<1> if dates are to be adjusted forward (if
461             necessary) before doing arithmetic on them, and C<0> otherwise.
462              
463             =head1 SEE ALSO
464              
465             L<DateTime::Fiction::JRRTolkien::Shire|DateTime::Fiction::JRRTolkien::Shire>
466              
467             L<DateTime|DateTime>
468              
469             L<DateTime::Duration|DateTime::Duration>
470              
471             =head1 SUPPORT
472              
473             Support is by the author. Please file bug reports at
474             L<https://rt.cpan.org/Public/Dist/Display.html?Name=DateTime-Fiction-JRRTolkien-Shire>,
475             L<https://github.com/trwyant/perl-DateTime-Fiction-JRRTolkien-Shire/issues>, or in
476             electronic mail to the author.
477              
478             =head1 AUTHOR
479              
480             Thomas R. Wyant, III F<wyant at cpan dot org>
481              
482             =head1 COPYRIGHT AND LICENSE
483              
484             Copyright (C) 2017-2021 by Thomas R. Wyant, III
485              
486             This program is free software; you can redistribute it and/or modify it
487             under the same terms as Perl 5.10.0. For more details, see the full text
488             of the licenses in the directory LICENSES.
489              
490             This program is distributed in the hope that it will be useful, but
491             without any warranty; without even the implied warranty of
492             merchantability or fitness for a particular purpose.
493              
494             =cut
495              
496             # ex: set textwidth=72 :