File Coverage

blib/lib/DateTimeX/Start.pm
Criterion Covered Total %
statement 54 54 100.0
branch 11 12 91.6
condition 11 14 78.5
subroutine 14 14 100.0
pod 4 4 100.0
total 94 98 95.9


line stmt bran cond sub pod time code
1              
2             package DateTimeX::Start;
3              
4 3     3   1236095 use strict;
  3         6  
  3         103  
5 3     3   15 use warnings;
  3         4  
  3         130  
6              
7 3     3   1943 use version; our $VERSION = qv('v1.6.0');
  3         6938  
  3         21  
8              
9 3     3   295 use Carp qw( );
  3         5  
  3         55  
10 3     3   1549 use DateTime qw( );
  3         429903  
  3         124  
11 3     3   18 use DateTime::TimeZone qw( );
  3         4  
  3         65  
12 3     3   13 use Exporter qw( import );
  3         4  
  3         112  
13 3     3   15 use Scalar::Util qw( );
  3         5  
  3         2075  
14              
15              
16             my @long = qw( start_of_date start_of_month start_of_year start_of_today );
17             my @short = qw( date month year today );
18             our @EXPORT_OK = ( @long, @short );
19             our %EXPORT_TAGS = (
20             'ALL' => \@EXPORT_OK,
21             'long' => \@long,
22             'short' => \@short,
23             );
24              
25              
26             sub _start_of_date {
27 49     49   4129 my ($trunc, $dt, $tz) = @_;
28              
29 49 100       306 if (Scalar::Util::blessed($dt)) {
30 18   66     101 $tz ||= $dt->time_zone;
31 18         139 $dt = $dt->clone->set_time_zone('floating')->truncate( to => $trunc );
32             } else {
33 31   100     160 $tz ||= 'local';
34 31   100     405 $dt = DateTime->new( year => $dt->[0], month => $dt->[1] || 1, day => $dt->[2] || 1 );
      100        
35             }
36              
37 49 100       21446 $tz = DateTime::TimeZone->new( name => $tz )
38             if !ref($tz);
39              
40 49         29889 my $target_day = ( $dt->local_rd_values )[0];
41 49         539 my $utc_day_start_epoch = int($dt->epoch / 60);
42              
43             # Most of the time, we don't need to do anything special.
44 49 100       719 if (eval { $dt->set_time_zone($tz); 1 }) {
  49         212  
  48         12369  
45             # Make sure we have the right midnight if this day has two midnights.
46 48 100       224 if (( $dt->clone->subtract( seconds => 1 )->local_rd_values )[0] < $target_day) {
47 47         71069 return $dt;
48             }
49             }
50              
51 2         1174 my $min_epoch = my $underflow_epoch = $utc_day_start_epoch - (24*60+1);
52 2         5 my $max_epoch = my $overflow_epoch = $utc_day_start_epoch + (24*60+1);
53 2         8 while ($max_epoch > $min_epoch) {
54 23         37 my $epoch = ( $min_epoch + $max_epoch ) >> 1;
55 23 100       71 if (( DateTime->from_epoch( epoch => $epoch*60, time_zone => $tz )->local_rd_values )[0] < $target_day) {
56 12         4924 $min_epoch = $epoch + 1;
57             } else {
58 11         4738 $max_epoch = $epoch;
59             }
60             }
61              
62 2 50 33     46 $underflow_epoch < $max_epoch && $max_epoch < $overflow_epoch
63             or Carp::croak("Unable to find date " . DateTime->from_epoch( epoch => $utc_day_start_epoch*60 )->ymd . " in time zone " . $tz->name);
64              
65 2         10 return DateTime->from_epoch(epoch => $max_epoch*60, time_zone => $tz);
66             }
67              
68 25     25 1 89188 sub start_of_date { _start_of_date('day', @_) }
69 12     12 1 22607 sub start_of_month { _start_of_date('month', @_) }
70 8     8 1 18162 sub start_of_year { _start_of_date('year', @_) }
71 4   100 4 1 8217 sub start_of_today { _start_of_date('day', DateTime->now( time_zone => $_[0] || 'local' )) }
72              
73             {
74 3     3   24 no warnings qw( once );
  3         10  
  3         355  
75             *date = \&start_of_date;
76             *month = \&start_of_month;
77             *year = \&start_of_year;
78             *today = \&start_of_today;
79             }
80              
81              
82             1;
83              
84             __END__
85              
86             =head1 NAME
87              
88             DateTimeX::Start - Find the time at which a day starts.
89              
90              
91             =head1 VERSION
92              
93             Version 1.6.0
94              
95              
96             =head1 SYNOPSIS
97              
98             use DateTimeX::Start qw( :ALL );
99              
100             my $dt = start_of_date([2013, 10, 20], 'America/Sao_Paulo');
101             print("$dt\n"); # 2013-10-20T01:00:00
102             $dt->subtract( seconds => 1 );
103             print("$dt\n"); # 2013-10-19T23:59:59
104              
105             # These three are equivalent.
106             my $dt = start_of_today();
107             my $dt = start_of_today('local');
108             my $dt = start_of_date( DateTime->now( time_zone => 'local' ) );
109              
110             # These three are equivalent.
111             my $dt = start_of_date([2014, 1, 1]);
112             my $dt = start_of_date([2014, 1]);
113             my $dt = start_of_date([2014]);
114              
115             my $dt = start_of_date( DateTime->now( time_zone => 'local' ) );
116             my $dt = start_of_month( DateTime->now( time_zone => 'local' ) );
117             my $dt = start_of_year( DateTime->now( time_zone => 'local' ) );
118              
119              
120             =head1 DESCRIPTION
121              
122             In Sao Paulo, in the night of Oct 19th, 2013, the clocks went from 23:59:59 to 01:00:00.
123             That's just one example of the fact that not all days have a midnight hour.
124             This module provides a mean of determining when a particular day (or month or year) starts.
125              
126              
127             =head1 FUNCTIONS
128              
129             =head2 start_of_date / date
130              
131             my $dt = start_of_date($date);
132             my $dt = start_of_date($date, $tz_or_tz_name);
133             my $dt = date($date);
134             my $dt = date($date, $tz_or_tz_name);
135              
136             Returns a DateTime object representing the earliest time of the specified date.
137              
138             C<$date> must be one of the following:
139              
140             =over 4
141              
142             =item * A DateTime object (to find the first time of C<< $date->strftime('%Y-%m-%d') >>),
143              
144             =item * A reference to an array containing a year, a month and a day (C<[$y,$m,$d]>, to find the first time of the specified date),
145              
146             =item * A reference to an array containing a year and month (C<[$y,$m]>, to find the first time of the specified month), or
147              
148             =item * A reference to an array containing a year (C<[$y]>, to find the first time of the specified year).
149              
150             =back
151              
152             C<$tz_or_tz_name> must be either a time zone name supported by DateTime::TimeZone or a DateTime::TimeZone object.
153             It defaults to C<< $dt->time_zone >> if C<$date> is a DateTime object, and C<'local'> otherwise.
154              
155              
156             =head2 start_of_month / month
157              
158             my $dt = start_of_month($date);
159             my $dt = start_of_month($date, $tz_or_tz_name);
160             my $dt = month($date);
161             my $dt = month($date, $tz_or_tz_name);
162              
163             Returns a DateTime object representing the earliest time of the specified month.
164              
165             C<$date> must be one of the following:
166              
167             =over 4
168              
169             =item * A DateTime object (to find the first time of the month given by C<< $date->strftime('%Y-%m') >>),
170              
171             =item * A reference to an array containing a year and month (C<[$y,$m]>, to find the first time of the specified month), or
172              
173             =item * A reference to an array containing a year (C<[$y]>, to find the first time of the specified year).
174              
175             =back
176              
177             C<$tz_or_tz_name> must be either a time zone name supported by DateTime::TimeZone or a DateTime::TimeZone object.
178             It defaults to C<< $dt->time_zone >> if C<$date> is a DateTime object, and C<'local'> otherwise.
179              
180              
181             =head2 start_of_year / year
182              
183             my $dt = start_of_year($date);
184             my $dt = start_of_year($date, $tz_or_tz_name);
185             my $dt = year($date);
186             my $dt = year($date, $tz_or_tz_name);
187              
188             Returns a DateTime object representing the earliest time of the specified year.
189              
190             C<$date> must be one of the following:
191              
192             =over 4
193              
194             =item * A DateTime object (to find the first time of the month given by C<< $date->year >>), or
195              
196             =item * A reference to an array containing a year (C<[$y]>, to find the first time of the specified year).
197              
198             =back
199              
200             C<$tz_or_tz_name> must be either a time zone name supported by DateTime::TimeZone or a DateTime::TimeZone object.
201             It defaults to C<< $dt->time_zone >> if C<$date> is a DateTime object, and C<'local'> otherwise.
202              
203              
204             =head2 start_of_today / today
205              
206             my $dt = start_of_today();
207             my $dt = start_of_today($tz_or_tz_name);
208             my $dt = today();
209             my $dt = today($tz_or_tz_name);
210              
211             Returns a DateTime object representing the earliest time of the current day.
212              
213             C<$tz_or_tz_name> must be either a time zone name supported by DateTime::TimeZone or a DateTime::TimeZone object.
214             It defaults to C<'local'>.
215              
216              
217             =head1 EXPORTS
218              
219             Nothing is exported by default. The following are exported on demand:
220              
221             =over 4
222              
223             =item * C<start_of_date>
224              
225             =item * C<start_of_month>
226              
227             =item * C<start_of_year>
228              
229             =item * C<start_of_today>
230              
231             =item * C<date>
232              
233             =item * C<month>
234              
235             =item * C<year>
236              
237             =item * C<today>
238              
239             =item * C<:ALL>
240              
241             For all of the above.
242              
243             =item * C<:long>
244              
245             For C<start_of_date>, C<start_of_month>, C<start_of_year> and C<start_of_today>.
246              
247             =item * C<:short>
248              
249             For C<date>, C<month>, C<year> and C<today>.
250              
251             =back
252              
253              
254             =head1 NOTES
255              
256             =over 4
257              
258             =item * This purpose of the functions of this module is to find the earliest time of the specified date.
259             As such, if a day has two midnights, the earlier of two will be returned.
260              
261             =item * Arguments passed to the functions of this module aren't modified.
262              
263             =back
264              
265              
266             =head1 ASSUMPTIONS
267              
268             The code makes the following assumptions about time zones:
269              
270             =over 4
271              
272             =item * There is no dt to which one can add time to obtain a dt with an earlier date.
273              
274             =item * In no time zone does a date starts more than 24*60*60 seconds before the same date starts in UTC.
275              
276             =item * In no time zone does a date starts more than 24*60*60 seconds after the same date starts in UTC.
277              
278             =item * There is no year without a January.
279              
280             =item * There is no month without a 1st.
281              
282             =item * Jumps in time zones only occur on times with zero seconds. (Optimization)
283              
284             =back
285              
286              
287             =head1 BUGS
288              
289             Please report any bugs or feature requests to C<bug-DateTimeX-Start at rt.cpan.org>,
290             or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=DateTimeX-Start>.
291             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
292              
293              
294             =head1 DOCUMENTATION AND SUPPORT
295              
296             You can find documentation for this module with the perldoc command.
297              
298             perldoc DateTimeX::Start
299              
300             You can also find it online at these locations:
301              
302             =over
303              
304             =item * L<http://search.cpan.org/dist/DateTimeX-Start>
305              
306             =item * L<https://metacpan.org/release/DateTimeX-Start>
307              
308             =back
309              
310             If you need help, the following are great resources:
311              
312             =over
313              
314             =item * L<https://stackoverflow.com/|StackOverflow>
315              
316             =item * L<http://www.perlmonks.org/|PerlMonks>
317              
318             =item * You may also contact the author directly.
319              
320             =back
321              
322             Bugs and improvements can be reported using any of the following systems:
323              
324             =over
325              
326             =item Using CPAN's request tracker by emailing C<bug-DateTimeX-Start at rt.cpan.org>
327              
328             =item Using CPAN's request tracker at L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=DateTimeX-Start>
329              
330             =item Using GitHub's issue tracker at L<https://github.com/ikegami/perl-DateTimeX-Start/issues>
331              
332             =back
333              
334              
335             =head1 REPOSITORY
336              
337             =over
338              
339             =item * Web: L<https://github.com/ikegami/perl-DateTimeX-Start>
340              
341             =item * git: L<https://github.com/ikegami/perl-DateTimeX-Start.git>
342              
343             =back
344              
345              
346             =head1 AUTHOR
347              
348             Eric Brine C<< <ikegami@adaelis.com> >>
349              
350              
351             =head1 COPYRIGHT AND LICENSE
352              
353             No rights reserved.
354              
355             The author has dedicated the work to the Commons by waiving all of his
356             or her rights to the work worldwide under copyright law and all related or
357             neighboring legal rights he or she had in the work, to the extent allowable by
358             law.
359              
360             Works under CC0 do not require attribution. When citing the work, you should
361             not imply endorsement by the author.
362              
363              
364             =cut