File Coverage

blib/lib/MooseX/TimestampTZ.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1              
2             package MooseX::TimestampTZ;
3              
4             =head1 NAME
5              
6             MooseX::Timestamp - simple timestamp type for Moose, with Time Zone
7              
8             =head1 SYNOPSIS
9              
10             use MooseX::TimestampTZ qw(:all);
11              
12             print zone 0; # +0000
13             print zone 0, 1; # Z
14             print zone 12*3600; # +1200
15              
16             print offset_s "Z"; # 0
17             print offset_s "+1200"; # 43200 (= 12 * 3600)
18              
19             # local times
20             print timestamptz; # 2007-12-06 23:23:22+1300
21             print timestamptz 0; # 1970-01-01 12:00:00+1200
22              
23             # UTC times
24             print gmtimestamptz; # 2007-12-06 10:23:22+0000
25             print gmtimestamptz 0; # 1970-01-01 00:00:00+0000
26              
27             # hires timestamps
28             print tmtimestamptz 0.123; # 1970-01-01 00:00:00.123+0000
29              
30             use MooseX::TimestampTZ ":all" => { hires => 1 };
31             print tmtimestamptz; # 2010-07-20 14:13:23.73418+1200
32              
33             # conversion the other way
34             print epoch "1970-01-01 00:00:00+0000"; # 0
35             print epoch "1970-01-01 12:00:00+1200"; # 0
36              
37             print for epochtz "1970-01-01 12:00:00+1200"; # 0, 43200
38              
39             # you can get these ISO forms if you want, too. functions
40             # that take a timestamptz accept either
41             package SomewhereElse;
42             use MooseX::TimestampTZ gmtimestamptz => { use_z => 1 };
43             print gmtimestamptz 0; # 1970-01-01 00:00:00Z
44              
45             package MyClass;
46             use Moose;
47             has 'stamp' =>
48             isa => "Timestamp",
49             is => "rw",
50             coerce => 1;
51              
52             package main;
53             my $obj = MyClass->new(stamp => "2007-01-02 12:00:12"); # ok
54             $obj->stamp("2007-01-02 12:01");
55             $obj->stamp("2007-01-02 12");
56             $obj->stamp("2007-01-02 12:00:00Gibbons"); #fail
57              
58             =head1 DESCRIPTION
59              
60             This module provides floating dates on the Gregorian calendar without
61             much code. It operates in (one particular variant of) ISO-8601 date
62             format with time zone, and epoch times. Sub-second resolution is not
63             yet supported.
64              
65             =cut
66              
67 3     3   44250 use strict;
  3         9  
  3         130  
68 3     3   19 use warnings;
  3         7  
  3         114  
69 3     3   16 use Carp;
  3         5  
  3         530  
70 3     3   842 use MooseX::Timestamp qw(:all);
  0            
  0            
71             use Moose::Util::TypeConstraints;
72              
73             sub _curry {
74             my $class = shift;
75             my $name = shift;
76             my $arg_h = shift;
77             my $col_h = shift;
78              
79             my $chain = \&$name;
80              
81             if ( defined $arg_h->{use_z} or defined $col_h->{defaults}{use_z} ) {
82             my $use_z = defined $arg_h->{use_z} ?
83             $arg_h->{use_z} : $col_h->{defaults}{use_z};
84             my $old_chain = $chain;
85             $chain = sub { $old_chain->($_[0], $use_z) };
86             }
87              
88             if ( defined $arg_h->{hires} or defined $col_h->{defaults}{hires} ) {
89             my $hires = defined $arg_h->{hires} ?
90             $arg_h->{hires} : $col_h->{defaults}{hires};
91             if ( $hires ) {
92             require Time::HiRes;
93             my $old_chain = $chain;
94             $chain = sub {
95             $old_chain->(
96             defined($_[0])?$_[0]:&Time::HiRes::time,
97             @_[1..$#_],
98             );
99             };
100             }
101             }
102              
103             $chain;
104             }
105              
106             sub _curry_epoch {
107             my $class = shift;
108             my $name = shift;
109             my $arg_h = shift;
110             my $col_h = shift;
111              
112             my $chain = \&$name;
113              
114             if ( defined $arg_h->{hires} or defined $col_h->{defaults}{hires} ) {
115             my $hires = defined $arg_h->{hires} ?
116             $arg_h->{hires} : $col_h->{defaults}{hires};
117             if ( $hires ) {
118             require Time::HiRes;
119             my $old_chain = $chain;
120             $chain = sub {
121             if ( @_ ) {
122             $old_chain->(@_);
123             }
124             else {
125             return &Time::HiRes::time;
126             }
127             };
128             }
129             }
130              
131             $chain;
132             }
133              
134             use Sub::Exporter -setup =>
135             { exports =>
136             [ qw(offset_s timestamp posixtime epochtz),
137             epoch => \&_curry_epoch,
138             map { ($_ => \&_curry) } qw(zone timestamptz gmtimestamptz),
139             ],
140             groups =>
141             { default => [ qw(timestamptz gmtimestamptz epoch) ] },
142             collectors => { defaults => sub {
143             1;
144             } },
145             };
146              
147             subtype "TimestampTZ"
148             => as "Str"
149             => where {
150             m{^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[\-+]\d{4}|Z)$}
151             and do {
152             my $x;
153             eval { $x = epoch($_) };
154             warn "Error: $@ on $_" if $@;
155             !$@
156             };
157             };
158              
159             use Time::Local;
160             use Memoize;
161             memoize qw(zone);
162             sub zone {
163             my $offset_s = shift;
164             my $use_z = shift;
165             if ( $use_z and $offset_s == 0 ) {
166             "Z";
167             }
168             else {
169             my $hh = sprintf("%.2d", int(abs($offset_s)/3600));
170             my $mm = sprintf("%.2d", int((abs($offset_s)-$hh*3600)/60));
171             my $s = ( $offset_s >= 0 ? "+" : "-" );
172             "$s$hh$mm";
173             }
174             }
175              
176             sub offset_s {
177             my $zone = shift or croak "no zone passed to offset_s!";
178             if ( $zone eq "Z" ) {
179             return 0;
180             }
181             elsif ( $zone =~ m{^([\-+])(\d{2}):?(\d{2})?$}) {
182             return ( ($1 eq "-" ? -1 : 1) *
183             (($2 * 60) + ($3||0)) * 60 );
184             }
185             else {
186             croak "no timezone on '$zone'";
187             }
188             }
189              
190             sub timestamptz {
191             my $time = shift;
192             defined($time)||($time = time);
193             my $use_z = shift;
194             my @lt = localtime(int($time));
195             my $offset_s = timegm(@lt) - int($time);
196             if ( int($time) != $time ) {
197             $lt[0] += $time - int($time);
198             }
199             timestamp(@lt).zone($offset_s, $use_z);
200             }
201              
202             sub gmtimestamptz {
203             my $time = shift;
204             defined($time)||($time = time);
205             my $use_z = shift;
206             my @gt = gmtime($time);
207             if ( int($time) != $time ) {
208             $gt[0] += $time - int($time);
209             }
210             timestamp(@gt).zone(0, $use_z);
211             }
212              
213             sub epochtz {
214             my $timestamptz = shift || timestamptz;
215             my ($timestamp, $zone) =
216             ($timestamptz =~ m{^(.*)([\-+]\d{2}(?::?\d{2})?|Z)$}x)
217             or die "bad TimestampTZ passed to epoch: '$timestamptz'";
218             my @wct = posixtime($timestamp);
219             my $frac = $wct[0] - int($wct[0]);
220             $wct[0] = int($wct[0]);
221             my $offset_s = offset_s($zone);
222             (timegm(@wct) - $offset_s + $frac, $offset_s);
223             }
224              
225             sub epoch {
226             return time unless @_;
227             return (epochtz(@_))[0];
228             }
229              
230             subtype 'time_t'
231             => as "Int";
232              
233             sub _looks_like_timestamp {
234             my ($epoch, $off) = eval { epochtz $_ };
235             if ( defined $epoch ) {
236             }
237             elsif ( eval { valid_posixtime(posixtime($_)) } and !$@ ) {
238             my @gt = posixtime($_);
239             my $frac = $gt[0] - int($gt[0]);
240             $gt[0] = int($gt[0]);
241             my $gmtime = timegm(@gt);
242             my $localtime = timelocal(@gt);
243             $epoch = $localtime + $frac;
244             $off = ($localtime - $gmtime);
245             }
246             else {
247             croak "bad TimestampTZ $_";
248             }
249             if ( wantarray ) {
250             return ($epoch, $off);
251             }
252             else {
253             return $epoch;
254             }
255             }
256              
257             coerce 'time_t'
258             => from "Num"
259             => via { $_ },
260             => from "TimestampTZ"
261             => via { epoch($_) }
262             => from "Str"
263             => via \&_looks_like_timestamp;
264              
265             coerce 'Timestamp'
266             => from "TimestampTZ"
267             => via {
268             my $epoch = epoch($_);
269             my $frac = $epoch - int($epoch);
270             my @lt = localtime(int($epoch));
271             $lt[0] += $frac;
272             timestamp(@lt);
273             };
274              
275             # traditionally, coercing a timestamp to one with time zone and back
276             # uses the local time, with the resultant ambiguities
277             coerce 'TimestampTZ'
278             => from "TimestampTZ"
279             => via { $_ },
280             => from "time_t"
281             => via { timestamptz($_) }
282             => from "Timestamp"
283             => via {
284             my @lt = posixtime($_);
285             my $frac = $lt[0] - int($lt[0]);
286             $lt[0] = int($lt[0]);
287             timestamptz(timelocal(@lt)+$frac);
288             },
289             => from "Str"
290             => via {
291             my ($epoch, $off) = _looks_like_timestamp;
292             my $frac = $epoch - int($epoch);
293             my @gt = gmtime(int($epoch)+$off);
294             $gt[0] += $frac;
295             timestamp(@gt).zone($off);
296             };
297              
298             =head1 FUNCTIONS
299              
300             The following functions are available for import. If you want to
301             import them all, use the C<:all> import group, as below:
302              
303             use MooseX::TimestampTZ qw(:all);
304              
305             =head2 zone(Int $offset, Bool $use_z = false)
306              
307             Returns the timezone of the given offset. Pass $use_z to select
308             returning "Z" as a timezone if the offset is 0.
309              
310             =head2 offset_s(Str)
311              
312             Returns the offset corresponding to the given timezone. Does NOT
313             accept nicknames like "EST", etc (which EST did you mean, US or
314             Australian Eastern Standard Time?).
315              
316             =head2 timestamptz(time_t $time_t = time(), Bool $use_z = false)
317              
318             Returns the passed epoch time as a valid TimestampTZ, according to the
319             local time zone rules in effect. C<$use_z> functions as with C<zone>.
320              
321             =head2 gmtimestamptz(time_t $time_t = time(), Bool $use_z = false)
322              
323             Returns the passed epoch time as a valid TimestampTZ, corresponding to
324             the time in UTC. C<$use_z> functions as with C<zone>, and if passed
325             this function will always return TimestampTZs ending with C<Z>.
326              
327             =head2 epoch()
328              
329             Synonym for the built-in C<time()>.
330              
331             =head2 epoch(TimestampTZ)
332              
333             Converts the passed TimestampTZ value to an epoch time. Does B<not>
334             perform any coercion - the passed value must already have a time zone
335             on it. You may omit any part of the time, specify the time zone in
336             hours or with a C<Z>, and optionally separate your time from your date
337             with a C<T>. Single digit values for fields are accepted.
338              
339             Example valid forms:
340              
341             2007-12-07 16:34:02+1200
342             2007-12-07 16:34+12
343             2007-12-07 04Z
344             2007-12-7T4Z
345             2007-12-7+12
346             2007120704:12:32 # Date::Manip format
347              
348             Examples of ISO-8601 valid forms which are not currently accepted:
349              
350             07-12-07Z
351             071207Z
352             20071207Z # seperators required
353             2007120704Z
354             -12-07Z # no year specified
355              
356             No locale-specific date forms, such as C</> delimited dates, are
357             accepted.
358              
359             =head2 epochtz(...)
360              
361             Just like C<epoch()>, except returns the timezone as well.
362              
363             =head1 TYPES AND COERCIONS
364              
365             The following subtypes are defined by this module:
366              
367             =head2 TimestampTZ
368              
369             This is a subtype of C<Str> which conforms to one of the two
370             normalized forms of a TimestampTZ (either with a Z, or without).
371              
372             Rules exist to coerce C<Str>, C<Timestamp> and C<time_t> to this type,
373             and are available by using the C<coerce =E<gt> 1> flag on a Moose
374             attribute declaration:
375              
376             package Widget;
377             use MooseX::TimestampTZ;
378             has 'created' =>
379             isa => TimestampTZ,
380             is => "rw",
381             coerce => 1;
382              
383             With the above, if you set C<created> to a time_t value, it will
384             automatically get converted into a TimestampTZ in the current time
385             zone.
386              
387             A TimestampTZ value with a fractional second part is considered valid,
388             regardless of whether C<hires> is passed to the importer.
389              
390             B<New in 0.07>: Timestamp to TimestampTZ conversion now happens in
391             I<local time>, not UTC.
392              
393             =head2 time_t
394              
395             C<time_t> is a nicer way of writing an epoch time. If you set
396             C<coerce =E<gt> 1> on your accessors, then you can happily pass in
397             timestamps. As of MooseX::Timestamp 0.07, B<time_t> is a C<Num>, not
398             merely an C<Int>.
399              
400             =head1 EXPORTS
401              
402             The default exporting action of this module is to export the
403             C<timestamptz>, C<gmtimestamptz> and C<epoch> methods. To avoid this,
404             pass an empty argument list to the use statement:
405              
406             use MooseX::TimestampTZ ();
407              
408             =head2 ISO-8601 "Z" TIMEZONE
409              
410             Several of the functions which return a timezone may be told to return
411             "Z" if the offset is 0, that is, the time is in UTC. To select this,
412             pass a true second argument to any of the three functions (C<zone>,
413             C<timestamptz> and C<gmtimestamptz>), or curry them on import;
414              
415             use MooseX::TimestampTZ qw(:default), defaults => { use_z => 1 };
416              
417             You can also curry individual functions like this:
418              
419             use MooseX::TimestampTZ zone => { use_z => 1 };
420              
421             =cut
422              
423             =head1 BUGS
424              
425             This module is relatively slow, as conversions and calls to C<timegm>
426             and friends happen far too often, really - especially with coercion.
427              
428             =head1 AUTHOR AND LICENSE
429              
430             Sam Vilain, <samv@cpan.org>
431              
432             Copyright 2007, Sam Vilain. All Rights Reserved. This program is
433             Free Software; you may use it and/or redistribute it under the terms
434             of Perl itself.
435              
436             =cut
437              
438             1;