File Coverage

blib/lib/Brick/Dates.pm
Criterion Covered Total %
statement 18 100 18.0
branch 0 32 0.0
condition 0 78 0.0
subroutine 6 24 25.0
pod 0 5 0.0
total 24 239 10.0


line stmt bran cond sub pod time code
1             package Brick::Dates;
2 5     5   35 use base qw(Exporter);
  5         10  
  5         480  
3 5     5   29 use vars qw($VERSION);
  5         10  
  5         252  
4              
5             $VERSION = '0.901';
6              
7             package Brick::Bucket;
8 5     5   41 use strict;
  5         11  
  5         136  
9              
10 5     5   26 use subs qw();
  5         7  
  5         107  
11              
12 5     5   23 use Carp qw(carp croak);
  5         20  
  5         264  
13 5     5   2496 use Time::Moment;
  5         7771  
  5         9500  
14              
15             =encoding utf8
16              
17             =head1 NAME
18              
19             Brick - This is the description
20              
21             =head1 SYNOPSIS
22              
23             use Brick;
24              
25             =head1 DESCRIPTION
26              
27              
28             =over 4
29              
30             =item _is_YYYYMMDD_date_format
31              
32             =cut
33              
34             sub _is_YYYYMMDD_date_format
35             {
36 0     0     my( $bucket, $setup ) = @_;
37              
38 0           my @caller = $bucket->__caller_chain_as_list();
39              
40             $bucket->add_to_bucket( {
41             name => $setup->{name} || $caller[0]{'sub'},
42             code => $bucket->_matches_regex( {
43             description => "The $setup->{field} is in the YYYYMMDD date format",
44             field => $setup->{field},
45 0   0       name => $caller[0]{'sub'},
46             regex => qr/
47             \A
48             \d\d\d\d # year
49             \d\d # month
50             \d\d # day
51             \z
52             /x,
53             } )
54             } );
55             }
56              
57             sub _is_valid_date
58             {
59 0     0     my( $bucket, $setup ) = @_;
60              
61 0           my @caller = $bucket->__caller_chain_as_list();
62              
63             $bucket->add_to_bucket( {
64             name => $setup->{name} || $caller[0]{'sub'},
65             code => sub {
66 0     0     my $eval_error = 'Could not parse YYYYMMMDD date';
67 0 0         if( my( $year, $month, $day ) =
68             $_[0]->{$setup->{field}} =~ m/(\d\d\d\d)(\d\d)(\d\d)/g )
69             {
70 0           $eval_error = '';
71 0           my $tm = eval {
72 0           Time::Moment->new(
73             year => $year,
74             month => $month,
75             day => $day,
76             ) };
77              
78 0 0         return 1 unless $@;
79 0           $eval_error = $@;
80             }
81              
82 0           my $date_error = do {
83 0 0         if( $eval_error =~ /^Parameter 'month'/ )
    0          
84 0           { 'The month is not right' }
85             elsif( $eval_error =~ /^Parameter 'day'/ )
86 0           { 'The day of the month is not right' }
87             else
88 0           { 'Could not parse YYYYMMMDD date' }
89             };
90              
91             die {
92             message => "The value in $setup->{field} [$_[0]->{$setup->{field}}] was not a valid date: $date_error",
93             failed_field => $setup->{field},
94 0 0         handler => $caller[0]{'sub'},
95             } if $eval_error;
96              
97             # 1;
98             },
99 0   0       } );
100              
101             }
102              
103             =item _is_YYYYMMDD_date_format
104              
105             =cut
106              
107             =pod
108              
109             sub _is_in_the_future
110             {
111             my( $bucket, $setup ) = @_;
112             croak "Not implemented";
113             }
114              
115             sub _is_tomorrow
116             {
117             my( $bucket, $setup ) = @_;
118             croak "Not implemented";
119             }
120              
121             sub _is_today
122             {
123             my( $bucket, $setup ) = @_;
124             croak "Not implemented";
125             }
126              
127             sub _is_yesterday
128             {
129             my( $bucket, $setup ) = @_;
130             croak "Not implemented";
131             }
132              
133             sub _is_in_the_past
134             {
135             my( $bucket, $setup ) = @_;
136             croak "Not implemented";
137             }
138              
139             =cut
140              
141             sub _date_is_after
142             {
143 0     0     my( $bucket, $setup ) = @_;
144              
145 0           my @caller = $bucket->__caller_chain_as_list();
146              
147             $bucket->add_to_bucket( {
148             name => $setup->{name} || $caller[0]{'sub'},
149             description => "Date is after the start date",
150             code => sub {
151 0   0 0     my $start = $setup->{start_date} || $_[0]->{$setup->{start_date_field}};
152 0   0       my $in_date = $setup->{input_date} || $_[0]->{$setup->{input_date_field}};
153              
154             #print STDERR "date after: $start --> $in_date\n";
155             die {
156             handler => $setup->{name} || $caller[0]{'sub'},
157             message => "Date [$in_date] is not after start date [$start]",
158             failed_field => $setup->{field},
159 0 0 0       } if $in_date <= $start;
160 0           1;
161             },
162 0   0       } );
163             }
164              
165             sub _date_is_before
166             {
167 0     0     my( $bucket, $setup ) = @_;
168              
169 0           my @caller = $bucket->__caller_chain_as_list();
170              
171             $bucket->add_to_bucket( {
172             name => $setup->{name} || $caller[0]{'sub'},
173             description => "Date is before the end date",
174             code => sub {
175 0   0 0     my $end = $setup->{end_date} || $_[0]->{$setup->{end_date_field}};
176 0   0       my $in_date = $setup->{input_date} || $_[0]->{$setup->{input_date_field}};
177              
178             #print STDERR "date before: $in_date --> $end\n";
179             die {
180             handler => $setup->{name} || $caller[0]{'sub'},
181             message => "Date [$in_date] is not before end date [$end]",
182             failed_field => $setup->{field},
183              
184 0 0 0       } if $end <= $in_date;
185             },
186 0   0       } );
187             }
188              
189             =item date_within_range
190              
191              
192              
193             =cut
194              
195             sub date_within_range # inclusive, negative numbers indicate past
196             {
197 0     0 0   my( $bucket, $setup ) = @_;
198              
199 0           my $before_sub = $bucket->_date_is_before( $setup );
200 0           my $after_sub = $bucket->_date_is_after( $setup );
201              
202 0           my $composed = $bucket->__compose_satisfy_all( $after_sub, $before_sub );
203              
204 0           $bucket->__make_constraint( $composed, $setup );
205             }
206              
207             =item days_between_dates_within_range( HASHREF )
208              
209             I can specify any of the dates as part of the setup by supplying them
210             as the values for these keys in the setup hash:
211              
212             start_date
213             end_date
214             input_date
215              
216             Instead of fixed values, I can tell the function to get values from
217             input fields. Put the field names in the values for these keys of
218             the setup hash"
219              
220             start_date_field
221             end_date_field
222             input_date_field
223              
224             I can use any combination of these setup fields, although the
225             start_date, end_date, and input_date take precedence.
226              
227             TO DO: Need to validate all the date formats before I use them
228             in the comparisons
229              
230             =cut
231              
232             sub days_between_dates_within_range # inclusive, negative numbers indicate past
233             {
234 0     0 0   my( $bucket, $setup ) = @_;
235              
236 0           my @caller = $bucket->__caller_chain_as_list();
237              
238             $bucket->__make_constraint(
239             $bucket->add_to_bucket( {
240             name => $setup->{name} || $caller[0]{'sub'},
241             description => "",
242             code => sub {
243 0   0 0     my $start = $setup->{start_date} || $_[0]->{$setup->{start_date_field}};
244 0   0       my $end = $setup->{end_date} || $_[0]->{$setup->{end_date_field}};
245 0   0       my $in_date = $setup->{input_date} || $_[0]->{$setup->{input_date_field}};
246              
247             die {
248             message => 'Dates were not within range',
249             handler => '',
250             failed_field => $setup->{field},
251 0 0 0       } unless $start <= $in_date && $in_date <= $end;
252             }
253             } )
254 0   0       );
255             }
256              
257             =item days_between_dates_outside_range( HASHREF )
258              
259             I can specify any of the dates as part of the setup by supplying them
260             as the values for these keys in the setup hash:
261              
262             start_date
263             end_date
264             input_date
265              
266             Instead of fixed values, I can tell the function to get values from
267             input fields. Put the field names in the values for these keys of
268             the setup hash"
269              
270             start_date_field
271             end_date_field
272             input_date_field
273              
274             I can use any combination of these setup fields, although the
275             start_date, end_date, and input_date take precedence.
276              
277             TO DO: Need to validate all the date formats before I use them
278             in the comparisons
279              
280             =cut
281              
282             sub days_between_dates_outside_range
283             {
284 0     0 0   my( $bucket, $setup ) = @_;
285              
286 0           my @caller = $bucket->__caller_chain_as_list();
287              
288             $bucket->__make_constraint(
289             $bucket->add_to_bucket( {
290             name => $setup->{name} || $caller[0]{'sub'},
291             description => "",
292             code => sub {
293 0   0 0     my $start = $setup->{start_date} || $_[0]->{$setup->{start_date_field}};
294 0   0       my $end = $setup->{end_date} || $_[0]->{$setup->{end_date_field}};
295 0   0       my $in_date = $setup->{input_date} || $_[0]->{$setup->{input_date_field}};
296              
297             die {
298             message => 'Dates were not outside range',
299             handler => '',
300             failed_field => $setup->{field},
301 0 0 0       } unless $in_date < $start || $end < $in_date;
302             }
303             } )
304 0   0       );
305             }
306              
307             =item at_least_N_days_between
308              
309             =cut
310              
311             sub at_least_N_days_between
312             {
313 0     0 0   my( $bucket, $setup ) = @_;
314              
315 0           my @caller = $bucket->__caller_chain_as_list();
316              
317             $bucket->__make_constraint(
318             $bucket->add_to_bucket( {
319             name => $setup->{name} || $caller[0]{'sub'},
320             description => "Dates within $setup->{number_of_days} days",
321             code => sub {
322 0   0 0     my $start = $setup->{start_date} || $_[0]->{$setup->{start_date_field}};
323 0   0       my $end = $setup->{end_date} || $_[0]->{$setup->{end_date_field}};
324              
325 0 0         print STDERR "Expected interval: $setup->{number_of_days}\n" if $ENV{DEBUG};
326              
327 0           my $interval = $bucket->_get_days_between( $start, $end );
328 0 0         print STDERR "Interval: $start --> $interval --> $end\n" if $ENV{DEBUG};
329              
330             die {
331             message => 'Dates were not within range',
332             handler => 'at_least_N_days_between',
333             failed_field => $setup->{field},
334              
335 0 0         } unless $interval >= $setup->{number_of_days};
336             }
337             } )
338 0   0       );
339             }
340              
341             =item at_most_N_days_between
342              
343             Like C, but the dates cannot be more than N days
344             apart.
345              
346             At the moment this has the curious result that if the end date in before the
347             start date, the duration between them is negative, so that duration is shorter
348             than any positive number. This isn't a bug but a loack of a design decision
349             if I should require the end date to be after the start date.
350              
351             =cut
352              
353             sub at_most_N_days_between
354             {
355 0     0 0   my( $bucket, $setup ) = @_;
356              
357 0           my @caller = $bucket->__caller_chain_as_list();
358              
359             $bucket->__make_constraint(
360             $bucket->add_to_bucket( {
361             name => $setup->{name} || $caller[0]{'sub'},
362             description => "",
363             code => sub {
364 0   0 0     my $start = $setup->{start_date} || $_[0]->{$setup->{start_date_field}};
365 0   0       my $end = $setup->{end_date} || $_[0]->{$setup->{end_date_field}};
366              
367 0           my $interval = $bucket->_get_days_between( $start, $end );
368 0 0         print STDERR "Interval: $start --> $interval --> $end\n" if $ENV{DEBUG};
369              
370             die {
371             message => 'Dates were outside the range',
372             handler => 'at_most_N_days_between',
373             failed_field => $setup->{field},
374              
375 0 0         } unless $setup->{number_of_days} >= $interval;
376             }
377             } )
378 0   0       );
379             }
380              
381             =pod
382              
383             sub at_most_N_days_after
384             {
385             my( $bucket, $setup ) = @_;
386              
387             croak "Not implemented!";
388             }
389              
390             sub at_most_N_days_before
391             {
392             my( $bucket, $setup ) = @_;
393              
394             croak "Not implemented!";
395             }
396              
397             sub before_fixed_date
398             {
399             my( $bucket, $setup ) = @_;
400              
401             croak "Not implemented!";
402             }
403              
404             sub after_fixed_date
405             {
406             my( $bucket, $setup ) = @_;
407              
408             croak "Not implemented!";
409             }
410              
411             =cut
412              
413             # return negative values if second date is earlier than first date
414              
415             =item __get_ymd_as_hashref( YYYYMMDD );
416              
417             Given two dates in YYYYMMDD format, return the number of days between
418             them, including the last date.
419              
420             For the dates 20070101 and 20070103, return 2 because it includes the
421             last day.
422              
423             For the dates 20070101 and 20060101, return -365 because the last date
424             is in the past.
425              
426             =cut
427              
428             sub _get_days_between
429             {
430 0     0     my( $bucket, $start, $stop ) = @_;
431              
432             my @dates =
433 0           map { Time::Moment->new(%{$bucket->__get_ymd_as_hashref($_)}) }
  0            
  0            
434             $start, $stop;
435              
436 0           my $days = $dates[0]->delta_days( $dates[1] );
437             }
438              
439             =item __get_ymd_as_hashref( YYYYMMDD );
440              
441             Given a date in YYYYMMDD format, return an anonymous hash with the
442             keys:
443              
444             year
445             month
446             day
447              
448             =cut
449              
450             sub __get_ymd_as_hashref
451             {
452 0     0     my( $bucket, $date ) = @_;
453              
454 0           my %hash = eval {
455 0 0         die "Could not parse date!"
456             unless $date =~ m/
457             \A
458             (\d\d\d\d)
459             (\d\d)
460             (\d\d)
461             \z
462             /x;
463              
464 0           my $tm = Time::Moment->new( year => $1, month => $2, day => $3 );
465             (
466 0           year => $tm->year,
467             month => $tm->month,
468             day => $tm->day_of_month,
469             );
470             };
471              
472 0 0         if( $@ )
473             {
474 0           $@ =~ s/\s+at\s+$0.*//s;
475 0           croak( "$@: I got [$date] but was expecting something in YYYYMMDD format!" );
476             }
477              
478 0           \%hash;
479             }
480              
481              
482             =back
483              
484             =head1 TO DO
485              
486             TBA
487              
488             =head1 SEE ALSO
489              
490             TBA
491              
492             =head1 SOURCE AVAILABILITY
493              
494             This source is in Github:
495              
496             https://github.com/briandfoy/brick
497              
498             =head1 AUTHOR
499              
500             brian d foy, C<< >>
501              
502             =head1 COPYRIGHT
503              
504             Copyright © 2007-2021, brian d foy . All rights reserved.
505              
506             You may redistribute this under the terms of the Artistic License 2.0.
507              
508             =cut
509              
510             1;