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