File Coverage

blib/lib/MooseX/Types/ISO8601.pm
Criterion Covered Total %
statement 57 57 100.0
branch 1 2 50.0
condition n/a
subroutine 18 18 100.0
pod n/a
total 76 77 98.7


line stmt bran cond sub pod time code
1             package MooseX::Types::ISO8601; # git description: v0.15-19-g0e18bc1
2             # ABSTRACT: ISO8601 date and duration string type constraints and coercions for Moose
3             our $VERSION = '0.16';
4 6     6   1913451 use strict;
  6         14  
  6         188  
5 6     6   23 use warnings;
  6         7  
  6         135  
6              
7 6     6   1343 use utf8;
  6         27  
  6         33  
8 6     6   2175 use DateTime 0.41;
  6         254861  
  6         179  
9             # this alias lets us distinguish the class from the class_type in versions of
10             # MooseX::Types that can't figure that out for us (i.e. before 0.32)
11 6     6   2440 use aliased DateTime => 'DT';
  6         2653  
  6         30  
12 6     6   634 use DateTime::TimeZone;
  6         7  
  6         102  
13 6     6   24 use DateTime::Duration;
  6         6  
  6         103  
14 6     6   2166 use DateTime::Format::Duration 1.03;
  6         18596  
  6         281  
15 6     6   1161 use MooseX::Types::DateTime 0.03 qw(Duration DateTime);
  6         779777  
  6         30  
16 6     6   8519 use MooseX::Types::Moose qw/Str Num/;
  6         7  
  6         29  
17 6     6   19064 use List::MoreUtils qw/ zip /;
  6         10  
  6         48  
18 6     6   1824 use Scalar::Util qw/ looks_like_number /;
  6         6  
  6         300  
19 6     6   22 use Module::Runtime 'use_module';
  6         7  
  6         39  
20 6     6   206 use Try::Tiny;
  6         7  
  6         222  
21 6     6   2929 use Safe::Isa;
  6         1884  
  6         935  
22              
23             our $MYSQL;
24             BEGIN {
25 6     6   11 $MYSQL = 0;
26 6 50       6 if (eval { require MooseX::Types::DateTime::MySQL; 1 }) {
  6         1863  
  6         195338  
27 6         47 MooseX::Types::DateTime::MySQL->import(qw/ MySQLDateTime /);
28 6         6364 $MYSQL = 1;
29             }
30             }
31 6     6   41 use if MooseX::Types->VERSION >= 0.42, 'namespace::autoclean';
  6         8  
  6         97  
32              
33 6         40 use MooseX::Types 0.10 -declare => [qw(
34             ISO8601DateStr
35             ISO8601TimeStr
36             ISO8601DateTimeStr
37             ISO8601DateTimeTZStr
38              
39             ISO8601StrictDateStr
40             ISO8601StrictTimeStr
41             ISO8601StrictDateTimeStr
42             ISO8601StrictDateTimeTZStr
43              
44             ISO8601TimeDurationStr
45             ISO8601DateDurationStr
46             ISO8601DateTimeDurationStr
47             ISO8601DateTimeDurationStr
48 6     6   633 )];
  6         104  
49              
50             my $date_re = qr/^(\d{4})-(\d{2})-(\d{2})$/;
51             my $time_re = qr/^(\d{2}):(\d{2}):(\d{2})(?:(?:\.|,)(\d+))?Z?$/;
52             my $datetime_re = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:(?:\.|,)(\d+))?Z?$/;
53             my $datetimetz_re = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:(?:\.|,)(\d+))?((?:(?:\+|-)\d\d:\d\d)|Z)$/;
54              
55             subtype ISO8601DateStr,
56             as Str,
57             where { /$date_re/ };
58              
59             # XXX TODO: this doesn't match all the ISO Time formats in the spec:
60             # hhmmss
61             # hhmm
62             # hh
63             # hh:mm
64             subtype ISO8601TimeStr,
65             as Str,
66             where { /$time_re/ };
67              
68             subtype ISO8601DateTimeStr,
69             as Str,
70             where { /$datetime_re/ };
71              
72             # XXX TODO: this doesn't match these offset indicators:
73             # ±hhmm
74             # ±hh
75             subtype ISO8601DateTimeTZStr,
76             as Str,
77             where { /$datetimetz_re/ };
78              
79             subtype ISO8601StrictDateStr,
80             as ISO8601DateStr,
81             where { (try { use_module('DateTime::Format::ISO8601')->parse_datetime($_) })->$_isa('DateTime') };
82              
83             subtype ISO8601StrictTimeStr,
84             as ISO8601TimeStr,
85             where {
86             ( try { use_module('DateTime::Format::ISO8601')->parse_datetime($_) }
87             || try { DateTime::Format::ISO8601->parse_time($_) }
88             )->$_isa('DateTime')
89             };
90              
91             subtype ISO8601StrictDateTimeStr,
92             as ISO8601DateTimeStr,
93             where { (try { use_module('DateTime::Format::ISO8601')->parse_datetime($_) })->$_isa('DateTime') };
94              
95             subtype ISO8601StrictDateTimeTZStr,
96             as ISO8601DateTimeTZStr,
97             where { (try { use_module('DateTime::Format::ISO8601')->parse_datetime($_) })->$_isa('DateTime') };
98              
99              
100             # TODO: According to ISO 8601:2004(E), the lowest order components may be
101             # omitted, if less accuracy is required. The lowest component may also have
102             # a decimal fraction. We don't support these both together, you may only have
103             # a fraction on the seconds component.
104              
105             my $timeduration_re = qr/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d{0,2})(?:(?:\.|,)(\d+))?S)?$/;
106             subtype ISO8601TimeDurationStr,
107             as Str,
108             where { grep { looks_like_number($_) } /$timeduration_re/; };
109              
110             my $dateduration_re = qr/^P(?:(\d+)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?$/;
111             subtype ISO8601DateDurationStr,
112             as Str,
113             where { grep { looks_like_number($_) } /$dateduration_re/ };
114              
115             my $datetimeduration_re = qr/^P(?:(\d+)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d{0,2})(?:(?:\.|,)(\d+))?)S)?$/;
116             subtype ISO8601DateTimeDurationStr,
117             as Str,
118             where { grep { looks_like_number($_) } /$datetimeduration_re/ };
119              
120             {
121             my %coerce = (
122             ISO8601TimeDurationStr, 'PT%02HH%02MM%02S.%06NS',
123             ISO8601DateDurationStr, 'P%02YY%02mM%02dD',
124             ISO8601DateTimeDurationStr, 'P%02YY%02mM%02dDT%02HH%02MM%02S.%06NS',
125             );
126              
127             foreach my $type_name (keys %coerce) {
128              
129             my $code = sub {
130             my $str = DateTime::Format::Duration->new(
131             normalize => 1,
132             pattern => $coerce{$type_name},
133             )
134             ->format_duration( shift );
135              
136             # Remove fractional seconds if there aren't any.
137             $str =~ s/\.0+S$/S/;
138             return $str;
139             };
140              
141             coerce $type_name,
142             from Duration,
143             via { $code->($_) },
144             from Num,
145             via { $code->(to_Duration($_)) };
146             # FIXME - should be able to say => via_type 'DateTime::Duration';
147             # nothingmuch promised to make that syntax happen if I got
148             # Stevan to approve and/or wrote a test case.
149             }
150             }
151              
152             {
153             my %coerce = (
154             ISO8601TimeStr, sub { die "cannot coerce non-UTC time" if ($_[0]->offset!=0); $_[0]->hms(':') . 'Z' },
155             ISO8601DateStr, sub { $_[0]->ymd('-') },
156             ISO8601DateTimeStr, sub { die "cannot coerce non-UTC time" if ($_[0]->offset!=0); $_[0]->ymd('-') . 'T' . $_[0]->hms(':') . 'Z' },
157             ISO8601DateTimeTZStr, sub {
158             DateTime::TimeZone->offset_as_string($_[0]->offset) =~ /(.\d\d)(\d\d)/;
159             $_[0]->ymd('-') . 'T' . $_[0]->hms(':') . "$1:$2"
160             },
161             );
162             @coerce{(ISO8601StrictTimeStr, ISO8601StrictDateStr, ISO8601StrictDateTimeStr, ISO8601StrictDateTimeTZStr)} =
163             @coerce{(ISO8601TimeStr, ISO8601DateStr, ISO8601DateTimeStr, ISO8601DateTimeTZStr)};
164              
165             foreach my $type_name (keys %coerce) {
166              
167             coerce $type_name,
168             from DateTime,
169             via { $coerce{$type_name}->($_) },
170             from Num,
171             via { $coerce{$type_name}->(DT->from_epoch( epoch => $_ )) };
172              
173             if ($MYSQL) {
174             coerce $type_name, from MySQLDateTime(),
175             via { $coerce{$type_name}->(to_DateTime($_)) };
176             }
177             }
178             }
179              
180             {
181             my %coerce = (
182             ISO8601TimeStr, sub {
183             $_ =~ s/^(\d\d) \:? (\d\d) \:? (\d\d([\.\,]\d+)?) (([+-]00\:?(00)?)|Z) $
184             /${1}:${2}:${3}Z/x;
185             return $_;
186             },
187             ISO8601DateStr, sub {
188             $_ =~ s/^(\d{4}) \-? (\d\d) \-? (\d\d)$
189             /${1}-${2}-${3}/x;
190             return $_;
191             },
192             ISO8601DateTimeStr, sub {
193             $_ =~ s/^(\d{4}) \-? (\d\d) \-? (\d\d)
194             T(\d\d) \:? (\d\d) \:? (\d\d([\.\,]\d+)?)
195             (([+-]00\:?(00)?)|Z)$
196             /${1}-${2}-${3}T${4}:${5}:${6}Z/x;
197             return $_;
198             },
199             );
200             @coerce{(ISO8601StrictTimeStr, ISO8601StrictDateStr, ISO8601StrictDateTimeStr, ISO8601StrictDateTimeTZStr)} =
201             @coerce{(ISO8601TimeStr, ISO8601DateStr, ISO8601DateTimeStr, ISO8601DateTimeTZStr)};
202              
203             foreach my $type_name (keys %coerce) {
204              
205             coerce $type_name,
206             from Str,
207             via { $coerce{$type_name}->($_) },
208             }
209             }
210              
211             {
212             my @datefields = qw/ years months days /;
213             my @timefields = qw/ hours minutes seconds nanoseconds /;
214             my @datetimefields = (@datefields, @timefields);
215             coerce Duration,
216             from ISO8601DateTimeDurationStr,
217             via {
218             my @fields = map { $_ || 0 } $_ =~ /$datetimeduration_re/;
219             if ($fields[6]) {
220             my $missing = 9 - length($fields[6]);
221             $fields[6] .= "0" x $missing;
222             }
223             DateTime::Duration->new( zip @datetimefields, @fields );
224             },
225             from ISO8601DateDurationStr,
226             via {
227             my @fields = map { $_ || 0 } $_ =~ /$dateduration_re/;
228             DateTime::Duration->new( zip @datefields, @fields );
229             },
230             from ISO8601TimeDurationStr,
231             via {
232             my @fields = map { $_ || 0 } $_ =~ /$timeduration_re/;
233             if ($fields[3]) {
234             my $missing = 9 - length($fields[3]);
235             $fields[3] .= "0" x $missing;
236             }
237             DateTime::Duration->new( zip @timefields, @fields );
238             };
239             }
240              
241             {
242             my @datefields = qw/ year month day /;
243             my @timefields = qw/ hour minute second nanosecond /;
244             my @datetimefields = (@datefields, @timefields);
245             my @datetimetzfields = (@datefields, @timefields, "time_zone");
246             coerce DateTime,
247             from ISO8601DateTimeStr,
248             via {
249             # TODO: surely we should be using
250             # DateTime::Format::ISO8601->parse_datetime for this
251             my @fields = map { $_ || 0 } $_ =~ /$datetime_re/;
252             if ($fields[6]) {
253             my $missing = 9 - length($fields[6]);
254             $fields[6] .= "0" x $missing;
255             }
256             DT->new( zip(@datetimefields, @fields), time_zone => 'UTC' );
257             },
258             from ISO8601DateTimeTZStr,
259             via {
260             my @fields = map { $_ || 0 } $_ =~ /$datetimetz_re/;
261             if ($fields[6]) {
262             my $missing = 9 - length($fields[6]);
263             $fields[6] .= "0" x $missing;
264             }
265             DT->new( zip(@datetimetzfields, @fields ) );
266             },
267             from ISO8601DateStr,
268             via {
269             my @fields = map { $_ || 0 } $_ =~ /$date_re/;
270             DT->new( zip @datefields, @fields );
271             },
272              
273             # XXX This coercion does not work as DateTime requires a year.
274             from ISO8601TimeStr,
275             via {
276             my @fields = map { $_ || 0 } $_ =~ /$time_re/;
277             if ($fields[3]) {
278             my $missing = 9 - length($fields[3]);
279             $fields[3] .= "0" x $missing;
280             }
281             DT->new( zip(@timefields, @fields), 'time_zone' => 'UTC' );
282             };
283             }
284              
285             1;
286              
287             __END__
288              
289             =pod
290              
291             =encoding UTF-8
292              
293             =head1 NAME
294              
295             MooseX::Types::ISO8601 - ISO8601 date and duration string type constraints and coercions for Moose
296              
297             =head1 VERSION
298              
299             version 0.16
300              
301             =head1 SYNOPSIS
302              
303             use MooseX::Types::ISO8601 qw/
304             ISO8601DateTimeStr
305             ISO8601TimeDurationStr
306             /;
307              
308             has datetime => (
309             is => 'ro',
310             isa => ISO8601DateTimeStr,
311             );
312              
313             has duration => (
314             is => 'ro',
315             isa => ISO8601TimeDurationStr,
316             coerce => 1,
317             );
318              
319             Class->new( datetime => '2012-01-01T00:00:00' );
320              
321             Class->new( duration => 60 ); # 60s => PT00H01M00S
322             Class->new( duration => DateTime::Duration->new(%args) )
323              
324             =head1 DESCRIPTION
325              
326             This module packages several L<TypeConstraints|Moose::Util::TypeConstraints> with
327             coercions for working with ISO8601 date strings and the DateTime suite of objects.
328              
329             =head1 DATE CONSTRAINTS
330              
331             =head2 ISO8601DateStr
332              
333             An ISO8601 date string. E.g. C<< 2009-06-11 >>
334              
335             =head2 ISO8601TimeStr
336              
337             An ISO8601 time string. E.g. C<< 12:06:34Z >>
338              
339             =head2 ISO8601DateTimeStr
340              
341             An ISO8601 combined datetime string. E.g. C<< 2009-06-11T12:06:34Z >>
342              
343             =head2 ISO8601DateTimeTZStr
344              
345             An ISO8601 combined datetime string with a fully specified timezone. E.g. C<< 2009-06-11T12:06:34+00:00 >>
346              
347             =head2 ISO8601StrictDateStr
348              
349             =head2 ISO8601StrictTimeStr
350              
351             =head2 ISO8601StrictDateTimeStr
352              
353             =head2 ISO8601StrictDateTimeTZStr
354              
355             As above, only in addition to validating the strings against regular
356             expressions, an attempt is made to actually parse the data into a L<DateTime>
357             object. This will catch cases like C<< 2013-02-31 >> which look correct but do not
358             correspond to real-world values. Note that this bears a computation
359             penalty.
360              
361             =head2 COERCIONS
362              
363             The date types will coerce from:
364              
365             =over
366              
367             =item C< Num >
368              
369             The number is treated as a time in seconds since the unix epoch
370              
371             =item C< DateTime >
372              
373             The duration represented as a L<DateTime> object.
374              
375             =item C< Str >
376              
377             Non-expanded date and time string representations.
378              
379             e.g.:-
380              
381             20120113 => 2012-01-13
382             170500Z => 17:05:00Z
383             20120113T170500Z => 2012-01-13T17:05:00Z
384              
385             Representations of UTC time zone (only an offset of zero is supported)
386              
387             e.g.:-
388              
389             17:05:00+00:00 => 17:05:00Z
390             17:05:00+00 => 17:05:00Z
391             170500+0000 => 17:05:00Z
392              
393             2012-01-13T17:05:00+00:00 => 2012-01-13T17:05:00Z
394             2012-01-13T17:05:00+00 => 2012-01-13T17:05:00Z
395             20120113T170500+0000 => 2012-01-13T17:05:00Z
396              
397             Also supports non-standards mixing of expanded and non-expanded representations
398              
399             e.g.:-
400              
401             2012-01-13T170500Z => 2012-01-13T17:05:00Z
402             20120113T17:05:00Z => 2012-01-13T17:05:00Z
403              
404             In addition, there are coercions from these string types to L<DateTime>.
405              
406             =back
407              
408             =head1 DURATION CONSTRAINTS
409              
410             =head2 ISO8601DateDurationStr
411              
412             An ISO8601 date duration string. E.g. C<< P01Y01M01D >>
413              
414             =head2 ISO8601TimeDurationStr
415              
416             An ISO8601 time duration string. E.g. C<< PT01H01M01S >>
417              
418             =head2 ISO8601DateTimeDurationStr
419              
420             An ISO8601 combined date and time duration string. E.g. C<< P01Y01M01DT01H01M01S >>
421              
422             =head2 COERCIONS
423              
424             The duration types will coerce from:
425              
426             =over
427              
428             =item C< Num >
429              
430             The number is treated as a time in seconds
431              
432             =item C< DateTime::Duration >
433              
434             The duration represented as a L<DateTime::Duration> object.
435              
436             =back
437              
438             The duration types will coerce to:
439              
440             =over
441              
442             =item C< Duration >
443              
444             A L<DateTime::Duration>, i.e. the C< Duration > constraint from
445             L<MooseX::Types::DateTime>.
446              
447             =back
448              
449             =head1 FEATURES
450              
451             =head2 Fractional seconds
452              
453             If provided, the number of seconds in time types is represented to microsecond
454             accuracy. A full stop character is used as the decimal separator, which is
455             allowed, but deprecated in preference to the comma character in
456             I<ISO 8601:2004>.
457              
458             =head1 BUGS
459              
460             Probably full of them, patches are very welcome.
461              
462             Specifically missing features:
463              
464             =over 4
465              
466             =item *
467              
468             No timezone support - all times are assumed UTC
469              
470             =item *
471              
472             No week number type
473              
474             =item *
475              
476             "Basic format", which lacks separator characters, is not supported for reading or writing.
477              
478             =item *
479              
480             Tests are rubbish.
481              
482             =back
483              
484             =head1 SEE ALSO
485              
486             =over 4
487              
488              
489              
490             =back
491              
492             * L<MooseX::Types::DateTime>
493             * L<DateTime>
494             * L<DateTime::Duration>
495             * L<DateTime::Format::ISO8601>
496             * L<DateTime::Format::Duration>
497             * L<http://en.wikipedia.org/wiki/ISO_8601>
498             * L<http://dotat.at/tmp/ISO_8601-2004_E.pdf>
499              
500             =head1 ACKNOWLEDGEMENTS
501              
502             The development of this code was sponsored by my employer L<http://www.state51.co.uk>.
503              
504             =head1 AUTHORS
505              
506             =over 4
507              
508             =item *
509              
510             Tomas Doran (t0m) <bobtfish@bobtfish.net>
511              
512             =item *
513              
514             Dave Lambley <davel@state51.co.uk>
515              
516             =back
517              
518             =head1 COPYRIGHT AND LICENSE
519              
520             This software is copyright (c) 2009 by Tomas Doran.
521              
522             This is free software; you can redistribute it and/or modify it under
523             the same terms as the Perl 5 programming language system itself.
524              
525             =head1 CONTRIBUTORS
526              
527             =for stopwords Karen Etheridge Dave Lambley zebardy Aaron Moses Gregory Oschwald
528              
529             =over 4
530              
531             =item *
532              
533             Karen Etheridge <ether@cpan.org>
534              
535             =item *
536              
537             Dave Lambley <dave@lambley.me.uk>
538              
539             =item *
540              
541             zebardy <zebardy@gmail.com>
542              
543             =item *
544              
545             Aaron Moses <zebardy@gmail.com>
546              
547             =item *
548              
549             Gregory Oschwald <goschwald@maxmind.com>
550              
551             =back
552              
553             =cut