File Coverage

blib/lib/Date/ISO.pm
Criterion Covered Total %
statement 49 55 89.0
branch 14 18 77.7
condition n/a
subroutine 13 13 100.0
pod 2 7 28.5
total 78 93 83.8


line stmt bran cond sub pod time code
1             #$Header: /cvsroot/date-iso/lib/Date/ISO.pm,v 1.30 2003/01/21 15:36:59 rbowen Exp $
2             package Date::ISO;
3              
4 4     4   135460 use strict;
  4         9  
  4         155  
5 4     4   22 use Exporter;
  4         6  
  4         163  
6 4     4   4180 use Date::ICal;
  4         70674  
  4         151  
7              
8 4     4   31 use vars qw( $VERSION @ISA @EXPORT );
  4         8  
  4         379  
9              
10             @ISA = qw( Exporter Date::ICal );
11             $VERSION = (qw'$Revision: 1.31 $')[1];
12              
13             @EXPORT = qw(iso inverseiso localiso);
14              
15 4     4   23 use Date::Leapyear qw();
  4         5  
  4         63  
16 4     4   4176 use Memoize;
  4         11567  
  4         6780  
17              
18             # Docs {{{
19              
20             =head1 NAME
21              
22             Date::ISO - Perl extension for converting dates between ISO and
23             Gregorian formats.
24              
25             =head1 SYNOPSIS
26              
27             use Date::ISO;
28             $iso = Date::ISO->new( iso => $iso_date_string );
29             $iso = Date::ISO->new( epoch => $epoch_time );
30             $iso = Date::ISO->new( ical => $ical_string );
31             $iso = Date::ISO->new( year => $year, month => $month,
32             day => $day );
33             $iso = Date::ISO->new( year => $year, week => $week,
34             weekday => $weekday );
35              
36             $year = $iso->year;
37              
38             $iso_year = $iso->iso_year()
39             $iso_week = $iso->iso_week();
40             $week_day = $iso->iso_week_day();
41              
42             $month = $iso->month();
43             $day = $iso->day();
44              
45             And, for backward compatibility:
46              
47             ($yearnumber, $weeknumber, $weekday) = iso($year, $month, $day);
48             ($yearnumber, $weeknumber, $weekday) = localiso(time);
49             ($year, $month, $day) = inverseiso($iso_year, $iso_week,
50             $iso_week_day);
51              
52             =head1 DESCRIPTION
53              
54             Convert dates between ISO and Gregorian formats.
55              
56             =head2 new
57              
58             my $iso = Date::ISO->new( iso => $iso_date_string );
59             my $iso = Date::ISO->new( epoch = $epoch_time );
60              
61              
62             Set the time to 2:30:25 on the date specified in $iso_date_string
63             my $iso = Date::ISO->new( iso => $iso_date_string, hour => 2, min => 30, sec => 25 );
64              
65              
66             And, since this is a Date::ICal subclass ...
67              
68             my $iso = Date::ISO->new( ical => $ical_string );
69             $ical = $iso->ical;
70              
71             Accepted ISO date string formats are:
72              
73             1997-02-05 (Feb 5, 1997)
74             19970205 (Same)
75             199702 (February 1997)
76             1997-W06 (6th week, 1997)
77             1997W06 (Same)
78             1997-W06-2 (6th week, 2nd day)
79             1997W062 (Same as above)
80             1997-035 (35th day of 1997)
81             1997035 (Same as above)
82              
83             2-digit representations of the year are not supported at this time.
84              
85             =cut
86              
87             # }}}
88              
89             # sub new {{{
90              
91             sub new {
92 16     16 1 10566 my $class = shift;
93 16         134 my %args = ( day => 0,
94             hour => 0,
95             min => 0,
96             sec => 0,
97             offset => 0,
98             @_);
99 16         25 my $offset = $args{offset};
100 16         24 my $self;
101              
102             # Deprecated argument form {{{
103 16 50       52 if (defined $args{ISO}) {
104 0         0 $args{iso} = $args{ISO};
105 0         0 warn "'ISO' is a deprecated arg. Use 'iso' instead.";
106             }
107 16 50       45 if (defined $args{EPOCH}) {
108 0         0 $args{epoch} = $args{EPOCH};
109 0         0 warn "'EPOCH' is a deprecated arg. Use 'epoch' instead.";
110             } # }}}
111              
112             # ISO date string passed in?
113 16 100       63 if ( $args{iso} ) {
114              
115             # 1997-02-05 or 19970205 formats
116 11 100       121 if ( $args{iso} =~ m/^(\d\d\d\d)-?(\d\d)-?(\d\d$)/ ) {
    100          
    50          
117              
118 2         21 $self = $class->SUPER::new( year => $1,
119             month => $2, day => $3, hour => $args{hour},
120             min => $args{min}, sec => $args{sec}, offset => $offset );
121             }
122              
123             # 199702 format
124             elsif ( $args{iso} =~ m/^(\d\d\d\d)(\d\d)$/ ) {
125            
126 1         7 $self = $class->SUPER::new( year => $1, month => $2,
127             day => 1, hour => 0, min => 0, sec => 0,
128             offset => $offset );
129             }
130              
131             # 1997-W06-2, 1997W062,, 1997-06-2, 1997062, 1996-06, 1997W06 formats
132             # 199706 has already matched above, and means something else.
133             elsif ( $args{iso} =~ m/^(\d\d\d\d)-?W?(\d\d)-?(\d)?$/ ) {
134              
135 8 100       48 my $iso_day = (defined($3) ? $3 : 1);
136 8         207 my ( $year, $month, $day ) =
137             from_iso( $1, $2, $iso_day );
138              
139 8         242 $self = $class->SUPER::new( year => $year, month => $month,
140             day => $day, hour => $args{hour}, min => $args{min}, sec => $args{sec},
141             offset => $offset );
142              
143             # Don't know what the format was
144             }
145             else {
146 0         0 warn('Did not recognize this as valid ISO date string format');
147             }
148             }
149              
150             # Otherwise, just pass arguments to Date::ICal
151             else {
152             # year/week/weekday args passed in?
153 5 50       23 if ( defined $args{week}) {
154 0         0 @args{qw(year month day)} =
155             inverseiso($args{year}, $args{week}, $args{weekday});
156             }
157              
158 5         45 $self = $class->SUPER::new( %args, offset => $offset );
159             }
160              
161 16         2145 bless $self, $class;
162 16         57 return $self;
163             } #}}}
164              
165             # Test::Inline tests #{{{
166              
167             =begin testing
168              
169             use lib '../blib/lib';
170             use Date::ISO;
171              
172             my $t1 = Date::ISO->new( day => 25, month => 10, year => 1971 );
173             is($t1->day, 25, 'day()');
174             is($t1->month, 10, 'month()');
175             is($t1->year, 1971, 'year()');
176             $t1->offset(0);
177             is($t1->ical, '19711025Z', 'ical()');
178             is($t1->epoch, 57196800, 'epoch()');
179              
180             my $t2 = Date::ISO->new( iso => '1971-W43-1' );
181             is($t2->day, 25, 'day()' );
182             is($t2->month, 10, 'month()');
183             is($t2->year, 1971, 'year()');
184             =end testing
185              
186             #}}}
187              
188             # sub to_iso {{{
189              
190             =head2 to_iso
191              
192             ( $isoyear, $isoweek, $isoday ) = to_iso( $year, $month, $day );
193              
194             Returns the iso year, week, and day, given the gregorian year, month,
195             and day. This should be considered an internal method, and is subject
196             to change at any time.
197              
198             This algorithm is at http://personal.ecu.edu/mccartyr/ISOwdALG.txt
199              
200             =cut
201              
202             memoize( 'to_iso' );
203             sub to_iso {
204             my ($y, $m, $d) = @_;
205             my @mnth=( 0, 31, 59, 90, 120, 151,
206             181, 212, 243, 273, 304, 334 );
207             my $doy = $d + $mnth[ $m - 1 ];
208              
209             if ( Date::Leapyear::isleap( $y ) && $m > 2 ) {
210             $doy ++;
211             }
212              
213             my $yy = ( $y - 1) % 100;
214             my $c = ( $y - 1 ) - $yy;
215             my $g = $yy + int( $yy / 4 );
216             my $jan_one = 1 + ((((( int($c/100) )%4) * 5 ) + $g ) % 7 );
217              
218             my $h = $doy + ( $jan_one - 1 );
219             my $weekday = 1 + ( ( $h - 1 ) % 7 );
220              
221             my ( $year_no, $week_no );
222             if ( ( $doy <= ( 8 - $jan_one ) ) && ( $jan_one > 4 ) ) {
223             $year_no = $y - 1;
224             if ( $jan_one == 5 || ( $jan_one == 6 &&
225             Date::Leapyear::isleap( $y -1 ) ) ) {
226             $week_no = 53;
227             } else {
228             $week_no = 52;
229             }
230             } else {
231             $year_no = $y;
232             my $i;
233             if ( Date::Leapyear::isleap( $y )) {
234             $i = 366;
235             } else {
236             $i = 365;
237             }
238             if ( ($i - $doy) < (4 - $weekday) ) {
239             $year_no = $y + 1;
240             $week_no = 1;
241             } else {
242             my $j = $doy + ( 7 - $weekday ) + ( $jan_one - 1 );
243             $week_no = int( $j/7 );
244             if ( $jan_one > 4 ) {
245             $week_no --;
246             }
247             }
248             }
249              
250             return ( $year_no, $week_no, $weekday );
251             } #}}}
252              
253             # sub from_iso {{{
254              
255             =head2 from_iso
256              
257             ($year, $month, $day) = from_iso($year, $week, $day);
258              
259             Given an ISO year, week, and day, returns year, month, and day, as
260             localtime would give them to you. This should be considered an
261             internal method, and is subject to change in future versions.
262              
263             =cut
264              
265 2     2 0 3767 sub inverseiso { return from_iso( @_ ) }
266             memoize( 'from_iso' );
267             sub from_iso {
268             my ( $yearnumber, $weeknumber, $weekday ) = @_;
269             my ( $yy, $c, $g, $janone, $eoy, $year, $month, $day, $doy, );
270             $yy = ( $yearnumber - 1 ) % 100;
271             $c = ( $yearnumber - 1 ) - $yy;
272             $g = $yy + int( $yy / 4 );
273             $janone = 1 + ( ( ( ( int( $c / 100 ) % 4 ) * 5 ) + $g ) % 7 );
274              
275             if ( ( $weeknumber == 1 ) && ( $janone < 5 ) &&
276             ( $weekday < $janone ) ) {
277             $year = $yearnumber - 1;
278             $month = 12;
279             $day = 32 - ( $janone - $weekday );
280             return ($year, $month, $day);
281             }
282             else {
283             $year = $yearnumber;
284             }
285             $doy = ( $weeknumber - 1 ) * 7;
286              
287             if ( $janone < 5 ) {
288             $doy += $weekday - ( $janone - 1 );
289             }
290             else {
291             $doy += $weekday + ( 8 - $janone );
292             }
293              
294             if ( Date::Leapyear::isleap($yearnumber) ) {
295             $eoy = 366;
296             }
297             else {
298             $eoy = 365;
299             }
300              
301             if ( $doy > $eoy ) {
302             $year = $yearnumber + 1;
303             $month = 1;
304             $day = $doy - $eoy;
305             }
306             else {
307             $year = $yearnumber;
308             my @month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
309             $month[2] = 29 if ( Date::Leapyear::isleap($year) );
310             my $h = 0;
311             my $i = 1;
312              
313             while ($h < $doy) {
314             $h += $month[$i];
315             $i++
316             }
317             $month = $i-1;
318              
319             $day = $doy - ( $h - $month[$i-1] );
320             }
321              
322             return ( $year, $month, $day );
323             } #}}}
324              
325             # Attribute acessor methods#{{{
326              
327             sub iso {
328 5 100   5 1 4395 if ( ref $_[0] ) {
329 2         5 my $self = shift;
330 2         6 return sprintf( '%04d-W%02d-%01d',
331             $self->iso_year, $self->iso_week, $self->iso_day );
332              
333             } else {
334 3         6 my ($year, $month, $day) = @_;
335 3         14 my $self = Date::ISO->new( year => $year, month => $month,
336             day => $day, offset=>0 );
337 3         73 return ( $self->iso_year, $self->iso_week,
338             $self->iso_week_day);
339             }
340             }
341              
342             sub iso_year {
343 16     16 0 19595 my $self = shift;
344 16         55 return (to_iso( $self->year, $self->month, $self->day ))[0];
345             }
346              
347             sub iso_week {
348 16     16 0 6567 my $self = shift;
349 16         162 return (to_iso( $self->year, $self->month, $self->day ))[1];
350             }
351              
352             sub iso_week_day {
353 16     16 0 6975 my $self = shift;
354 16         265 return (to_iso( $self->year, $self->month, $self->day ))[2];
355             }
356 2     2 0 131 sub iso_day{iso_week_day(@_)}
357              
358             #}}}
359              
360             # Testing other methods inherited from ICal #{{{
361              
362             =begin testing
363              
364             my $t3 = Date::ISO->new( iso => '1973-W12-4' );
365             is( $t3->iso, '1973-W12-4', 'Return the ISO string we started with');
366             $t3->offset(0);
367             is ( $t3->ical, '19730322Z', 'ical()');
368             $t3->add( week => 2 );
369             is( $t3->ical, '19730405Z', 'ical()');
370             is( $t3->iso_week, 14, 'Two weeks later' );
371             is( $t3->iso_week_day, 4, 'Should be the same dow' );
372             is($t3->iso, '1973-W14-4', 'Adding 2 weeks');
373              
374             =end testing
375              
376             =cut
377              
378             #}}}
379              
380             =head1 Backwards compatibiilty methods
381              
382             The following functional interface is provided for backwards
383             compatibility with former versions.
384              
385             =head2 iso
386              
387             $iso = iso($year, $month, $day);
388              
389             =cut
390              
391             1;
392              
393             # Documentation {{{
394              
395             =head1 AUTHOR
396              
397             Rich Bowen (rbowen@rcbowen.com)
398              
399             =head1 DATE
400              
401             $Date: 2003/01/21 15:36:59 $
402              
403             =head1 Additional comments
404              
405             For more information about this calendar, please see:
406              
407             http://personal.ecu.edu/mccartyr/ISOwdALG.txt
408              
409             http://personal.ecu.edu/mccartyr/isowdcal.html
410              
411             http://personal.ecu.edu/mccartyr/aboutwdc.htm
412              
413             http://www.cl.cam.ac.uk/~mgk25/iso-time.html
414              
415             http://www.fourmilab.ch/documents/calendar/
416              
417             Finally, many many many thanks to Rick McCarty who provided me with
418             the algorithms that I'm using for conversions to and from ISO dates.
419             All the errors in previous versions of this module were entirely my
420             fault for miscopying something from his algorithm.
421              
422             =head1 To Do, Bugs
423              
424             Need to flesh out test suite some more. Particularly need to test some dates
425             immediately before and after the first day of the year - days in which you
426             might be in a different Gregorian and ISO years.
427              
428             ISO date format also supports a variety of time formats. I suppose I should
429             accept those as valid arguments.
430              
431             Creating a Date::ISO object with an ISO string, and then immediately
432             getting the ISO string representation of that object, is not giving
433             back what we started with. I'm not at all sure what is going on.
434              
435             =cut
436              
437             # }}}
438              
439             # CVS History #{{{
440              
441             =head1 Version History
442              
443             $Log: ISO.pm,v $
444             Revision 1.30 2003/01/21 15:36:59 rbowen
445             Patch submitted by Winifred Plapper for a stupid typo.
446              
447             Revision 1.29 2002/11/08 12:57:28 rbowen
448             Patch by Martijn van Beers to make it possible to construct objects with
449             a week number and week day, as per the spec.
450              
451             Revision 1.28 2002/01/21 02:13:57 rbowen
452             Patch from Jesse Vincent, to permit the setting of times in ISO dates.
453              
454             Revision 1.27 2001/11/29 18:03:16 rbowen
455             If offsets are not specified, use GMT. This fixes a problem that has
456             been in the last several releases. Need to add additional tests to test
457             in the system's local time zone.
458              
459             Revision 1.26 2001/11/28 22:36:42 rbowen
460             Jesse's patch to make offsets work as passed in, rather than setting to
461             0.
462              
463             Revision 1.25 2001/11/27 02:44:43 rbowen
464             If an offset is not provided, explicitly set to 0. We are dealing with
465             dates, not times.
466              
467             Revision 1.24 2001/11/27 02:15:15 rbowen
468             Explicitly set offset to 0 always.
469              
470             Revision 1.23 2001/11/25 03:55:23 rbowen
471             Code fold. Nothing to see here.
472              
473             Revision 1.22 2001/11/24 16:03:11 rbowen
474             Offsets must be explicitly set to 0 in order to get the right epoch
475             time. See Date::ICal for details
476              
477             Revision 1.21 2001/09/12 03:21:31 rbowen
478             remove warnings for 5.005 compatibility
479              
480             Revision 1.20 2001/08/23 02:04:00 rbowen
481             Thanks to Rick McCarty, conversions from ISO to gregorian are now
482             working correctly. They never worked correctly in earlier versions.
483             All of the tests have been updated to use is() rather than ok() so
484             that I could actually see what was failing. Schwern++
485              
486             Revision 1.19 2001/07/30 00:50:07 rbowen
487             Update for the new Date::ICal
488              
489             Revision 1.18 2001/07/24 16:08:11 rbowen
490             perltidy
491              
492             Revision 1.17 2001/04/30 13:23:35 rbowen
493             Removed AutoLoader from ISA, since it really isn't.
494              
495             Revision 1.16 2001/04/29 21:31:04 rbowen
496             Added new tests, and fixed a lot of bugs in the process. Apparently the
497             inverseiso function had never actually worked, and various other functions
498             had some off-by-one problems.
499              
500             Revision 1.15 2001/04/29 02:42:03 rbowen
501             New Tests.
502             Updated MANIFEST, Readme for new files, functionality
503             Fixed CVS version number in ISO.pm
504              
505             Revision 1.14 2001/04/29 02:36:50 rbowen
506             Added OO interface.
507             Changed functions to accept 4-digit years and 1-based months.
508              
509             =cut
510              
511             #}}}
512