File Coverage

blib/lib/Date/Span.pm
Criterion Covered Total %
statement 40 40 100.0
branch 34 34 100.0
condition 19 19 100.0
subroutine 11 11 100.0
pod 3 3 100.0
total 107 107 100.0


line stmt bran cond sub pod time code
1 2     2   934 use strict;
  2         8  
  2         50  
2 2     2   7 use warnings;
  2         4  
  2         63  
3              
4             package Date::Span 1.128;
5             # ABSTRACT: deal with date/time ranges than span multiple dates
6              
7 2     2   8 use Exporter;
  2         11  
  2         82  
8 2     2   1173 BEGIN { our @ISA = 'Exporter' }
9              
10             our @EXPORT = qw(range_expand range_durations range_from_unit); ## no critic
11              
12             #pod =head1 SYNOPSIS
13             #pod
14             #pod use Date::Span;
15             #pod
16             #pod @spanned = range_expand($start, $end);
17             #pod
18             #pod print "from $_->[0] to $_->[1]\n" for (@spanned);
19             #pod
20             #pod =head1 DESCRIPTION
21             #pod
22             #pod This module provides code for dealing with datetime ranges that span multiple
23             #pod calendar days. This is useful for computing, for example, the amount of
24             #pod seconds spent performing a task on each day. Given the following table:
25             #pod
26             #pod event | begun | ended
27             #pod ---------+------------------+------------------
28             #pod loading | 2004-01-01 00:00 | 2004-01-01 12:45
29             #pod venting | 2004-01-01 12:45 | 2004-01-02 21:15
30             #pod running | 2004-01-02 21:15 | 2004-01-03 00:00
31             #pod
32             #pod We may want to gather the following data:
33             #pod
34             #pod date | event | time spent
35             #pod ------------+---------+----------------
36             #pod 2004-01-01 | loading | 12.75 hours
37             #pod 2004-01-01 | venting | 11.25 hours
38             #pod 2004-01-02 | venting | 21.25 hours
39             #pod 2004-01-02 | running | 2.75 hours
40             #pod
41             #pod Date::Span takes a data like the first and produces data more like the second.
42             #pod (Details on exact interface are below.)
43             #pod
44             #pod =func range_durations
45             #pod
46             #pod my @durations = range_durations($start, $end)
47             #pod
48             #pod Given C<$start> and C<$end> as timestamps (in epoch seconds),
49             #pod C returns a list of arrayrefs. Each arrayref is a date
50             #pod (expressed as epoch seconds at midnight) and the number of seconds for which
51             #pod the given range intersects with the date.
52             #pod
53             #pod =cut
54              
55             sub _date_time {
56 8     8   12 my $date = $_[0] - (my $time = $_[0] % 86400);
57 8         15 ($date, $time)
58             }
59              
60             sub range_durations {
61 3     3 1 471 my ($start, $end) = @_;
62 3 100       10 return if $end < $start;
63              
64 2         4 my ($start_date, $start_time) = _date_time($start);
65 2         4 my ($end_date, $end_time) = _date_time($end);
66              
67 2 100       6 push my @results, [
68             $start_date,
69             (( $end_date != $start_date ) ? ( 86400 - $start_time ) : ($end - $start))
70             ];
71              
72             push @results,
73 2 100       7 map { [ $start_date + 86400 * $_, 86400 ] }
  11         17  
74             (1 .. ($end_date - $start_date - 86400) / 86400)
75             if ($end_date - $start_date > 86400);
76              
77 2 100       6 push @results, [ $end_date, $end_time ] if $start_date != $end_date;
78              
79 2         10 return @results;
80             }
81              
82             #pod =func range_expand
83             #pod
84             #pod my @endpoint_pairs = range_expand($start, $end);
85             #pod
86             #pod Given C<$start> and C<$end> as timestamps (in epoch seconds),
87             #pod C returns a list of arrayrefs. Each arrayref is a start and
88             #pod end timestamp. No pair of start and end times will cross a date boundary, and
89             #pod the set of ranges as a whole will be identical to the passed start and end.
90             #pod
91             #pod =cut
92              
93             sub range_expand {
94 3     3 1 6 my ($start, $end) = @_;
95 3 100       9 return if $end < $start;
96              
97 2         6 my ($start_date, $start_time) = _date_time($start);
98 2         3 my ($end_date, $end_time) = _date_time($end);
99              
100 2 100       6 push my @results, [
101             $start, ( ( $end_date != $start_date ) ? ( $start_date + 86399 ) : $end )
102             ];
103              
104             push @results,
105 2 100       7 map { [ $start_date + 86400 * $_, $start_date + 86400 * $_ + 86399 ] }
  11         16  
106             (1 .. ($end_date - $start_date - 86400) / 86400)
107             if ($end_date - $start_date > 86400);
108              
109 2 100       6 push @results, [ $end_date, $end ] if $start_date != $end_date;
110              
111 2         9 return @results;
112             }
113              
114             #pod =func range_from_unit
115             #pod
116             #pod my ($start, $end) = range_from_unit(@date_unit)
117             #pod
118             #pod C<@date_unit> is a specification of a unit of time, in the form:
119             #pod
120             #pod @date_unit = ($year, $month, $day, $hour, $minute);
121             #pod
122             #pod Only C<$year> is mandatory; other arguments may be added, in order. Month is
123             #pod given in the range (0 .. 11). This function will return the first and last
124             #pod second of the given unit.
125             #pod
126             #pod A code reference may be passed as the last object. It will be used to convert
127             #pod the date specification to a starting time. If no coderef is passed, a simple
128             #pod one using Time::Local (and C) will be used.
129             #pod
130             #pod =cut
131              
132             my @monthdays = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
133              
134             sub _is_leap {
135 7 100 100 7   47 not($_[0] % 4) and (($_[0] % 100) or not($_[0] % 400)) and $_[0] > 0
      100        
136             }
137              
138             sub _leap_secs {
139 5 100 100 5   9 _is_leap($_[0]) && $_[1] == 1 ? 86400 : 0
140             }
141              
142             sub _begin_secs {
143 9     9   40 require Time::Local;
144 9   100     54 Time::Local::timegm(
      100        
      100        
      100        
145             0, # $sec
146             $_[4]||0, # $min
147             $_[3]||0, # $hour
148             $_[2]||1, # $mday
149             $_[1]||0, # $mon
150             $_[0] # $year
151             );
152             }
153              
154             sub range_from_unit {
155 11 100 100 11 1 3047 my $code = (ref($_[-1])||'' eq 'CODE') ? pop : \&_begin_secs;
156 11 100       25 return unless @_;
157 10         19 my ($year,$month,$day,$hour,$min) = @_;
158 10         17 my $begin_secs = $code->(@_);
159 10 100       247 my $length = defined $min ? 60
    100          
    100          
    100          
    100          
160             : defined $hour ? 3600
161             : defined $day ? 86400
162             : defined $month ? 86400 * $monthdays[$month+0]
163             + _leap_secs($year, $month)
164             : 86400 * (_is_leap($year) ? 366 : 365);
165              
166 10         46 return ($begin_secs, $begin_secs + $length - 1);
167             }
168              
169             #pod =head1 TODO
170             #pod
171             #pod This code was just yanked out of a general purpose set of utility functions
172             #pod I've compiled over the years. It should be refactored (internally) and
173             #pod further tested. The interface should stay pretty stable, though.
174             #pod
175             #pod =cut
176              
177             1;
178              
179             __END__