File Coverage

blib/lib/DateTime/Format/Excel.pm
Criterion Covered Total %
statement 64 64 100.0
branch 21 30 70.0
condition 4 5 80.0
subroutine 15 15 100.0
pod 7 9 77.7
total 111 123 90.2


line stmt bran cond sub pod time code
1             package DateTime::Format::Excel;
2             # $Id: Excel.pm 4458 2010-10-20 09:53:33Z achim66 $
3            
4             =head1 NAME
5            
6             DateTime::Format::Excel - Convert between DateTime and Excel dates.
7            
8             =cut
9            
10 3     3   106178 use strict;
  3         9  
  3         200  
11 3     3   79 use 5.005;
  3         10  
  3         118  
12 3     3   20 use Carp;
  3         8  
  3         771  
13 3     3   5928 use DateTime 0.1705;
  3         786706  
  3         121  
14 3     3   34 use vars qw( $VERSION );
  3         5  
  3         2699  
15            
16             $VERSION = '0.31';
17            
18             =head1 SYNOPSIS
19            
20             use DateTime::Format::Excel;
21            
22             # From Excel via class method:
23            
24             my $datetime = DateTime::Format::Excel->parse_datetime( 37680 );
25             print $datetime->ymd(); # prints 2003-02-28
26            
27             my $datetime = DateTime::Format::Excel->parse_datetime( 40123.625 );
28             print $datetime->iso8601(); # prints 2009-11-06T15:00:00
29            
30             # or via an object
31            
32             my $excel = DateTime::Format::Excel->new();
33             print $excel->parse_datetime( 25569 )->ymd; # prints 1970-01-01
34            
35             # Back to Excel number:
36            
37             use DateTime;
38             my $dt = DateTime->new( year => 1979, month => 7, day => 16 );
39             my $daynum = DateTime::Format::Excel->format_datetime( $dt );
40             print $daynum; # prints 29052
41            
42             my $dt_with_time = DateTime->new( year => 2010, month => 7, day => 23
43             , hour => 18, minute => 20 );
44             my $excel_date = DateTime::Format::Excel->format_datetime( $dt_with_time );
45             print $excel_date; # prints 40382.763888889
46            
47             # or via the object created above
48             my $other_daynum = $excel->format_datetime( $dt );
49             print $other_daynum; # prints 29052
50            
51             =head1 DESCRIPTION
52            
53             Excel uses a different system for its dates than most Unix programs.
54             This module allows you to convert between a few of the Excel raw formats
55             and C objects, which can then be further converted via any
56             of the other C modules, or just with C's
57             methods.
58            
59             If you happen to be dealing with dates between S<1 Jan 1900> and
60             S<1 Mar 1900> please read the notes on L.
61            
62             Since version 0.30 this modules handles the time part (the decimal
63             fraction of the Excel time number) correctly, so you can convert
64             a single point in time to and from Excel format. (Older versions
65             did only calculate the day number, effectively loosing the time
66             of day information).
67             The H:M:S is stored as a fraction where 1 second = 1 / (60*60*24).
68            
69             If you're wanting to handle actual spreadsheet files, you may find
70             L and L of use.
71            
72             =head1 CONSTRUCTORS
73            
74             =head2 new
75            
76             Creates a new C instance. This is generally
77             not required for simple operations. If you wish to use a different
78             epoch, however, then you'll need to create an object.
79            
80             my $excel = DateTime::Format::Excel->new()
81             my $copy = $excel->new();
82            
83             It takes no parameters. If called on an existing object then it
84             clones the object.
85            
86             =cut
87            
88             sub new
89             {
90 12     12 1 3205 my $class = shift;
91 12 100       241 croak "${class}->new takes no parameters." if @_;
92            
93 11   66     65 my $self = bless {}, ref($class)||$class;
94 11 100       39 if (ref $class)
95             {
96             # If called on an object, clone
97 6         19 $self->_epoch( scalar $class->epoch );
98             # and that's it. we don't store that much info per object
99             }
100            
101 11         33 $self;
102             }
103            
104             =head2 clone
105            
106             This method is provided For those who prefer to explicitly clone via a
107             method called C. If called as a class method it will die.
108            
109             my $clone = $original->clone();
110            
111             =cut
112            
113             sub clone
114             {
115 3     3 1 530 my $self = shift;
116 3 50       10 croak 'Calling object method as class method!' unless ref $self;
117 3         9 return $self->new();
118             }
119            
120             =head1 CLASS/OBJECT METHODS
121            
122             These methods work as both class and object methods.
123            
124             =head2 parse_datetime
125            
126             Given an Excel day number, return a C object representing that
127             date and time.
128            
129             # As a class method
130             my $datetime = DateTime::format::Excel->parse_datetime( 37680 );
131             print $datetime->ymd('.'); # '2003.02.28'
132            
133             # Or via an object
134             my $excel = DateTime::Format::Excel->new();
135             my $viaobj $excel->parse_datetime( 25569 );
136             print $viaobj->ymd; # '1970-01-01'
137            
138             =cut
139            
140             sub parse_datetime
141             {
142 35     35 1 23761 my $self = shift;
143 35 50       117 croak 'No date specified.' unless @_;
144 35 50       263 croak 'Invalid number of days' unless $_[0] =~ /^ (\d+ (?: (\.\d+ ) )? ) $/x;
145 35         88 my $excel_days = $1;
146 35         54 my $excel_secs = $2;
147 35         92 my $dt = DateTime->new( $self->epoch );
148 35 100       10191 if(defined $excel_secs){
149 5         18 $excel_secs = $excel_secs * 86400; # RT7498
150 5         14 my $excel_nanoseconds = ($excel_secs - int($excel_secs)) * 1_000_000_000;
151 5         20 $dt->add( days => $excel_days,
152             seconds => $excel_secs,
153             nanoseconds => $excel_nanoseconds);
154             } else {
155 30         250 $dt->add( days => $excel_days );
156             }
157 35         24578 return $dt;
158             }
159            
160             =head2 format_datetime
161            
162             Given a C object, return the Excel daynum time.
163            
164             use DateTime;
165             my $dt = DateTime->new( year => 1979, month => 7, day => 16 );
166             my $daynum = DateTime::Format::Excel->format_datetime( $dt );
167             print $daynum; # 29052
168            
169             # or via an object
170             my $excel = DateTime::Format::Excel->new();
171             $excel->epoch_mac(); # Let's imagine we want the Mac number
172             my $mac_daynum = $excel->format_datetime( $dt );
173             print $mac_daynum; # 27590
174            
175            
176             =cut
177            
178             sub format_datetime
179             {
180 35     35 1 26210 my $self = shift;
181 35 50       113 croak 'No DateTime object specified.' unless @_;
182 35         55 my $dt = shift;
183            
184 35         88 my $base = DateTime->new( $self->epoch );
185 35         7999 my $excel = $dt->jd - $base->jd; # RT7498
186            
187 35         808 return $excel;
188             }
189            
190             =begin _development
191            
192             =head1 BETA METHODS
193            
194             I don't really know whether durations should be handled by this module.
195             They're nothing interesting.
196            
197             =cut
198            
199             sub parse_duration
200             {
201 8     8 0 3952 my $self = shift;
202 8 50       22 croak 'No duration specified.' unless @_;
203 8 50       44 croak 'Invalid number of days' unless $_[0] =~ /^ (\d+ (?: \.\d+ )? ) $/x;
204 8         21 my $days = $1;
205            
206 8         33 return DateTime::Duration->new( days => $days );
207             }
208            
209             sub format_duration
210             {
211 8     8 0 5583 my $self = shift;
212 8 50       26 croak 'No DateTime::Duration object specified.' unless @_;
213            
214 8         28 return $_[0]->delta_days();
215             }
216            
217             =end _development
218            
219             =head1 OBJECT METHODS
220            
221             =head2 epoch
222            
223             In scalar context, returns a string identifying the current epoch.
224            
225             my $epoch = $excel->epoch();
226            
227             Currently either `mac' or `win' with the default being `win'.
228            
229             In list context, returns appropriate parameters with which to
230             create a C object representing the start of the epoch.
231            
232             my $base = DateTime->new( $excel->epoch );
233            
234             =cut
235            
236 88     88 1 2992 sub epoch { $_[0]->_epoch() }
237            
238             =head2 epoch_mac
239            
240             Set the object to use a Macintosh epoch.
241            
242             $excel->epoch_mac(); # epoch is now 1 Jan 1904
243            
244             Thus, 1 maps to C<2 Jan 1904>.
245            
246             =cut
247            
248 3     3 1 1266 sub epoch_mac { $_[0]->_epoch('mac') }
249            
250             =head2 epoch_win
251            
252             Set the object to use a Windows Excel epoch.
253            
254             $excel->epoch_win(); # epoch is now 30 Dec 1899
255            
256             Thus, 2 maps to C<1 Jan 1900>.
257            
258             =cut
259            
260 2     2 1 1116 sub epoch_win { $_[0]->_epoch('win') }
261            
262             =head1 EPOCHS
263            
264             Excel uses ``number of days since S<31 Dec 1899>''. Naturally, Microsoft
265             messed this up because they happened to believe that 1900 was a leap
266             year. In this module, we assume what Psion assumed for their Abacus /
267             Sheet program: S<1 Jan 1900> maps to 2 rather than 1. Thus, 61 maps to
268             S<1 Mar 1900> in both Excel and this module (and Abacus).
269            
270             I has a little option hidden away in its
271             calculations preferences. It can use either the Windows epoch, or it can
272             use the Macintosh epoch, which means that the day number is calculated
273             as ``number of days since S< 1 Jan 1904>''. This module supports both
274             notations.
275            
276             B: the results of this module have only been compared with
277             I and I on the
278             I. Where they have differed, I've opted for I's
279             result rather than I's.
280            
281             =cut
282            
283             {
284             my %epochs = (
285             win => [ year => 1899, month => 12, day => 30 ],
286             mac => [ year => 1904, month => 1, day => 1 ],
287             );
288            
289             sub _epoch
290             {
291 99     99   134 my $self = shift;
292 99 100       220 if (@_)
293             {
294 11 50       31 croak 'Calling object method as class method!' unless ref $self;
295 11 50       34 croak 'Invalid epoch' unless exists $epochs{$_[0]};
296 11         48 $self->{epoch} = $_[0];
297 11         29 return $self; # more useful this way, I feel.
298             }
299             else
300             {
301 88         105 my $epoch;
302 88 100       229 $epoch = $self->{epoch} if ref $self;
303 88   100     302 $epoch ||= 'win';
304 88 100       217 return wantarray ? @{ $epochs{$epoch} } : $epoch;
  70         475  
305             }
306             }
307             }
308            
309             1;
310            
311             __END__