File Coverage

blib/lib/DateTimeX/Format/Excel.pm
Criterion Covered Total %
statement 76 77 98.7
branch 15 16 93.7
condition 4 6 66.6
subroutine 18 18 100.0
pod 2 2 100.0
total 115 119 96.6


line stmt bran cond sub pod time code
1             package DateTimeX::Format::Excel;
2             our $AUTHORITY = 'cpan:JANDREW';
3 3     3   298734 use version 0.77; our $VERSION = version->declare("v0.12.48");
  3         3758  
  3         16  
4 3     3   221 use 5.010;
  3         10  
5 3     3   10 use strict;
  3         6  
  3         45  
6 3     3   9 use warnings;
  3         3  
  3         71  
7 3     3   8 use List::Util 1.33;
  3         52  
  3         136  
8 3     3   1402 use Moose 2.1213;
  3         802912  
  3         20  
9 3     3   15051 use MooseX::StrictConstructor;
  3         39843  
  3         9  
10 3     3   17978 use MooseX::HasDefaults::RO;
  3         13571  
  3         9  
11 3     3   18833 use DateTime;
  3         128942  
  3         95  
12 3     3   31 use Carp qw( cluck );
  3         4  
  3         152  
13 3     3   1452 use Types::Standard -types;
  3         136996  
  3         28  
14             if( $ENV{ Smart_Comments } ){
15 3     3   8101 use Smart::Comments -ENV;
  3         23148  
  3         23  
16             ### Smart-Comments turned on for DateTimeX-Format-Excel ...
17             }
18 3     3   2196 use lib '../../../lib',;
  3         4  
  3         16  
19 3         36 use DateTimeX::Format::Excel::Types 0.012 qw(
20             DateTimeHash
21             DateTimeInstance
22             HashToDateTime
23             is_ExcelEpoch
24             ExcelEpoch
25             SystemName
26 3     3   1443 );
  3         56  
27              
28             #########1 Dispatch Tables 3#########4#########5#########6#########7#########8#########9
29              
30             my $input_scrub ={
31             win_excel =>{
32             action => sub{
33             ### <where> - Reached win_excel scrub with: @_
34             if(int( $_[0] ) == 60){
35             cluck "-1900-February-29- is not a real date (contrary to excel implementation)";
36             }elsif($_[0] == 0){
37             cluck "-1900-January-0- is not a real date (contrary to excel implementation)";
38             }
39             ### <where> - Finished testing Lotus 123 date error warnings ...
40             my $return =(
41             (int( $_[0] ) > 60) ? ($_[0] - 1) :
42             (int( $_[0] ) == 0) ? ($_[0] + 1) : $_[0] );
43             ### <where> - updated date: $return
44             return $return;
45             },
46             output => sub{
47             ### <where> - Reached win_excel output with: @_
48             my $return =( (defined( $_[0] ) and int( $_[0] ) > 59) ? ($_[0] + 1) : $_[0] );
49             if( defined( $return ) and $return < 1 ){
50             $return = undef;
51             }
52             ### <where> - updated date: $return
53             return $return;
54             },
55             date_time_hash =>{
56             year => 1899,# actually 1900
57             month => 12,# actually 01
58             day => 31,# actually 00
59             },
60             },
61             apple_excel =>{
62             date_time_hash =>{
63             year => 1904,
64             month => 1,
65             day => 1,
66             },
67             }
68             };
69              
70             #########1 Public Attributes 3#########4#########5#########6#########7#########8#########9
71              
72             has system_type =>(
73             isa => SystemName,
74             reader => 'get_system_type',
75             writer => 'set_system_type',
76             default => 'win_excel',
77             trigger => \&_set_system,
78             );
79              
80             #########1 Public Methods 3#########4#########5#########6#########7#########8#########9
81              
82             sub parse_datetime{
83 36     36 1 13161 my ( $self, $date_num, $timezone_flag, $timezone ) = @_;
84             ### <where> - Reached parse_datetime for: $date_num
85 36 100       91 if( !is_ExcelEpoch( $date_num ) ){
86             ### <where> - not and excel epoch: $date_num
87 1         13 return $date_num;
88             }
89             ### <where> - Passed the type constraint ...
90 35 100       1329 if( my $action = $input_scrub->{$self->get_system_type}->{action} ){
91             ### <where> - There is an action: $action
92             ### <where> - Using system name: $self->get_system_type
93 28         54 $date_num = $action->( $date_num );
94             }
95             ### <where> - Updated date num: $date_num
96 35         171 $date_num =~ /^ (\d+ (?: (\.\d+ ) )? ) $/x;
97 35         57 my $excel_days = $1;
98 35         36 my $excel_secs = $2;
99             ### <where> - Excel added days: $excel_days
100             ### <where> - Excel seconds: $excel_secs
101 35         1048 my $dt = $self->_get_epoch_start->clone();
102             ### <where> - DateTime: $dt
103 35 100       315 if(defined $excel_secs){
104 7         19 $excel_secs = $excel_secs * (60*60*24);# Seconds in most days
105 7         11 my $excel_nanoseconds = ($excel_secs - int($excel_secs)) * 1_000_000_000;
106             ### <where> - Excel days: $excel_days
107             ### <where> - Excel seconds: $excel_secs
108             ### <where> - Excel nano seconds: $excel_nanoseconds
109 7         18 $dt->add( days => $excel_days,
110             seconds => $excel_secs,
111             nanoseconds => $excel_nanoseconds);
112             } else {
113             ### <where> - No seconds in the epoch ...
114 28         65 $dt->add( days => $excel_days );
115             }
116 35 50 33     13870 if( $timezone_flag and $timezone_flag eq 'time_zone' ){
117             ### <where> - Setting timezone to: $timezone
118 0         0 $dt->set_time_zone( $timezone );
119             };
120             ### <where> - DateTime: $dt
121              
122 35         128 return $dt;
123             }
124              
125             sub format_datetime{
126 156     156 1 104981 my ( $self, $date_time ) = @_;
127             ### <where> - Reached format_datetime with: $date_time
128 156         338 DateTimeInstance->( $date_time );
129 156         8692 my $base = $self->_get_epoch_start->clone();
130 156         1381 my $test = DateTime->compare_ignore_floating( $date_time, $base );
131             ### <where> - DateTime base is: $base
132             ### <where> - Using system name: $self->get_system_type
133             ### <where> - Test result: $test
134 156         3935 my $excel = undef;
135 156         132 my $return_string = 0;
136 156 100       230 if( $test < 0 ){
137 3         5 $return_string = 1;
138             }else{
139 153         278 $excel = $date_time->jd - $base->jd;
140             }
141             ### <where> - Initial excel epoch: $excel
142 156 100 100     6724 if( defined $excel and my $action = $input_scrub->{$self->get_system_type}->{output} ){
143             ### <where> - There is an action: $action
144             ### <where> - For: $self->get_system_type
145 85         133 $excel = $action->( $excel );
146 85 100       137 $return_string = 1 if !defined $excel;
147             }
148             ### <where> - Should return a date string: $return_string
149             ### <where> - Final excel epoch: $excel
150             ### <where> - Original DateTime: $date_time
151              
152 156 100       897 return ( $return_string ) ? $date_time : $excel;
153             }
154              
155             #########1 Private Attributes 3#########4#########5#########6#########7#########8#########9
156              
157             has _epoch_start =>(
158             isa => DateTimeInstance->plus_coercions( HashToDateTime ),
159             writer => '_set_epoch_start',
160             reader => '_get_epoch_start',
161             coerce => 1,
162             default => sub{ DateTime->new(
163             $input_scrub->{win_excel}->{date_time_hash}
164             ) },
165             );
166              
167             #########1 Private Methods 3#########4#########5#########6#########7#########8#########9
168              
169             sub _set_system{
170 4     4   6 my ( $self, $system_type ) = @_;
171 4         128 $self->_set_epoch_start( $input_scrub->{$system_type}->{date_time_hash} );
172 4         24 return $system_type;
173             }
174              
175             #########1 Phinish 3#########4#########5#########6#########7#########8#########9
176              
177 3     3   3150 no Moose;
  3         4  
  3         19  
178             __PACKAGE__->meta->make_immutable(
179             inline_constructor => 0,
180             );
181              
182             1;
183              
184             #########1 Documentation 3#########4#########5#########6#########7#########8#########9
185             __END__
186              
187             =head1 NAME
188              
189             DateTimeX::Format::Excel - Microsofty conversion of Excel epochs
190              
191             =begin html
192              
193             <a href="https://www.perl.org">
194             <img src="https://img.shields.io/badge/perl-5.10+-brightgreen.svg" alt="perl version">
195             </a>
196              
197             <a href="https://travis-ci.org/jandrew/DateTimeX-Format-Excel">
198             <img alt="Build Status" src="https://travis-ci.org/jandrew/DateTimeX-Format-Excel.png?branch=master" alt='Travis Build'/>
199             </a>
200              
201             <a href='https://coveralls.io/r/jandrew/DateTimeX-Format-Excel?branch=master'>
202             <img src='https://coveralls.io/repos/jandrew/DateTimeX-Format-Excel/badge.svg?branch=master' alt='Coverage Status' />
203             </a>
204              
205             <a href='https://github.com/jandrew/DateTimeX-Format-Excel'>
206             <img src="https://img.shields.io/github/tag/jandrew/DateTimeX-Format-Excel.svg?label=github level" alt="github level"/>
207             </a>
208              
209             <a href="https://metacpan.org/pod/DateTimeX::Format::Excel">
210             <img src="https://badge.fury.io/pl/DateTimeX-Format-Excel.svg?label=cpan version" alt="CPAN version" height="20">
211             </a>
212              
213             <a href='http://cpants.cpanauthors.org/dist/DateTimeX-Format-Excel'>
214             <img src='http://cpants.cpanauthors.org/dist/DateTimeX-Format-Excel.png' alt='kwalitee' height="20"/>
215             </a>
216              
217             =end html
218              
219             =head1 SYNOPSIS
220              
221             #!/usr/bin/env perl
222             use DateTimeX::Format::Excel;
223              
224             # From an Excel date number
225              
226             my $parser = DateTimeX::Format::Excel->new();
227             print $parser->parse_datetime( 25569 )->ymd ."\n";
228             my $datetime = $parser->parse_datetime( 37680 );
229             print $datetime->ymd() ."\n";
230             $datetime = $parser->parse_datetime( 40123.625 );
231             print $datetime->iso8601() ."\n";
232              
233             # And back to an Excel number from a DateTime object
234              
235             use DateTime;
236             my $dt = DateTime->new( year => 1979, month => 7, day => 16 );
237             my $daynum = $parser->format_datetime( $dt );
238             print $daynum ."\n";
239              
240             my $dt_with_time = DateTime->new( year => 2010, month => 7, day => 23
241             , hour => 18, minute => 20 );
242             my $parser_date = $parser->format_datetime( $dt_with_time );
243             print $parser_date ."\n";
244              
245             ###########################
246             # SYNOPSIS Screen Output
247             # 01: 1970-01-01
248             # 02: 2003-02-28
249             # 03: 2009-11-06T15:00:00
250             # 04: 29052
251             # 05: 40382.763888889
252             ###########################
253              
254             =head1 DESCRIPTION
255              
256             Excel uses a different system for its dates than most Unix programs.
257             This package allows you to convert between the Excel raw format and
258             and L<DateTime> objects, which can then be further converted via any
259             of the other L<DateTime::Format::*
260             |https://metacpan.org/search?q=DateTime%3A%3AFormat> modules, or just
261             with L<DateTime>'s methods. The L<DateTime::Format::Excel> module states
262             "we assume what Psion assumed for their Abacus / Sheet program". As a
263             consequence the output does not follow exactly the output of Excel.
264             Especially in the Windows range of 0-60. This module attempts to more
265             faithfully follow actual Microsoft Excel with a few notable exceptions.
266              
267             Excel has a few date quirks. First, it allows two different epochs. One
268             for the Windows world and one for the Apple world. The windows epoch
269             starts in 0-January-1900 and allows for 29-February-1900 (both non real
270             dates). Most of the explanations for the difference between windows
271             implementations and Apple implementations focus on the fact that there
272             was no leap year in 1900 L<(the Gregorian vs Julian calendars)
273             |http://en.wikipedia.org/wiki/Gregorian_calendar> and the Apple
274             version wanted to skip that issue. Both non real dates appear to have
275             been a known issue in the original design of VisiCalc that was carried
276             through Lotus 1-2-3 and into Excel for L<compatibility
277             |http://support.microsoft.com/kb/214326>. (Spreadsheets were arguably the
278             first personal computer killer app and Excel was a L<johnny come lately
279             |http://en.wikipedia.org/wiki/Lotus_1-2-3#VisiCalc> trying to gain an entry
280             into the market at the time.) The closest microsoft discussion I could find
281             on this issue is L<here|http://www.joelonsoftware.com/items/2006/06/16.html>.
282             In any case the apple version starts 1-January-1904. (counting from 0 while
283             also avoiding the leap year issue). In both cases the Windows and Apple
284             version use integers from the epoch start to represent days and the decimal
285             portion to represent a portion of a day. Both Windows and Apple Excel will
286             attempt to convert recognized date strings to an Excel epoch for storage with
287             the exception that any date prior to the epoch start will be stored as a
288             string. (31-December-1899 and earlier for Windows and 31-December-1903 and
289             earlier for Apple). Next, Excel does not allow for a time zone component of
290             each number. Finally, in the Windows version when dealing with epochs that
291             do not have a date component just a time component all values will fall
292             between 0 and 1 which is a non real date (0-January-1900).
293              
294             =head2 Caveat utilitor
295              
296             This explanation is not intended to justify Microsofts decisions with Excel
297             dates just replicate them as faithfully as possible. This module makes the
298             assumption that you already know if your date is a string or a number in Excel
299             and that you will handle string to DateTime conversions elsewhere. see
300             L<DateTime::Format::Flexible>. Any passed strings will die. (As a failure
301             of a L<Type::Tiny> test) This module also makes several unilateral decisions
302             to deal with corner cases. When a 0 date is requested to be converted to
303             DateTime it will use L<Carp> to cluck that it received a bad date and then
304             provide a DateTime object dated 1-January-1900 (Excel would provide
305             0-January-1900). If a value between 0 and 1 is requested to be converted to
306             a DateTime object the module will B<NOT> cluck and provide an object dated
307             1-January-1900 with the appropriate time component. All Apple times are provide
308             as 1-January-1904. Any requested numerical conversion for Windows >= 60 and
309             < 61 will cluck and provide a DateTime object dated 1-March-1900 (Excel would
310             provide 29-Febrary-1900). All requests for conversion of negative numbers to
311             DateTime objects will die . If a DateTime object is provided for conversion
312             to the Excel value and it falls earlier than 1-January-1900 for Windows and
313             1-January-1904 for Apple then the DateTime object itself will be returned.
314             If you accept the output of that L<method|/format_datetime( $date_time )>
315             as a scalar, DateTime will stringify itself and give you a text equivalent
316             date. For time zones you can L<pass|/parse_datetime( @arg_list )> a time zone
317             with the excel number for conversion to the DateTime object. In reverse,
318             the conversion to Excel Epoch uses the L<-E<gt>jd
319             |https://metacpan.org/pod/DateTime#dt-jd-dt-mjd> method for calculation so
320             the time zone is stripped out. No clone or duration calculations are provided
321             with this module. Finally this is a L<Moose> based module and does
322             not provide a functional interface. I<(Moose would allow it I just chose not
323             to for design purposes)>.
324              
325             The Types module for this package uses L<Type::Tiny> which can, in the background,
326             use L<Type::Tiny::XS>. While in general this is a good thing you will need to make
327             sure that Type::Tiny::XS is version 0.010 or newer since the older ones didn't support
328             the 'Optional' method.
329              
330             =head2 Attributes
331              
332             Data passed to new when creating an instance (parser). For modification of
333             these attributes see the listed L</Methods> of the instance.
334              
335             =head3 system_type
336              
337             =over
338              
339             B<Definition:> This attribute identifies whether the translation will be done
340             for Windows Excel => 'win_excel' or Apple Excel => 'apple_excel'.
341              
342             B<Default> win_excel (0-January-1900T00:00:00 = 0, range includes 29-February-1900)
343              
344             B<Range> win_excel|apple_excel (1-January-1904T00:00:00 = 0)
345              
346             =back
347              
348             =head2 Methods
349              
350             These include methods to adjust attributes as well as providing methods to
351             provide the conversion functionality of the module.
352              
353             =head3 get_system_type
354              
355             =over
356              
357             B<Definition:> This is the way to see whether the conversion is Windows or Apple based
358              
359             B<Accepts:>Nothing
360              
361             B<Returns:> win_excel|apple_excel
362              
363             =back
364              
365             =head3 set_system_type( $system )
366              
367             =over
368              
369             B<Definition:> This is the way to set the base epoch for the translator
370              
371             B<Accepts:> win_excel|apple_excel (see the L</DESCRIPTION> for details)
372              
373             B<Returns:> Nothing
374              
375             =back
376              
377             =head3 parse_datetime( @arg_list )
378              
379             =over
380              
381             B<Definition:> This is how positive excel numbers are translated to L<DateTime> objects
382              
383             B<Accepts:> @arg_list - the order is important!
384              
385             =over
386              
387             B<0. > $the_excel_number_for_translation - must be positive - no strings allowed
388              
389             B<1. > 'time_zone' (the only useful option - other values here will ignore position 2)
390              
391             B<2. > A recognizable time zone string or L<DateTime::TimeZone> object
392              
393             B<example: > ( 12345, time_zone => 'America/Los_Angeles' )
394              
395             =back
396              
397             B<Returns:> A DateTime object set to match the passed values. A floating time zone is default.
398              
399             =back
400              
401             =head3 format_datetime( $date_time )
402              
403             =over
404              
405             B<Definition:> This is how DateTime objects can be translated to Excel epoch numbers
406              
407             B<Accepts:> A L<DateTime> object
408              
409             B<Returns:> An excel epoch number or DateTime object if it is before the relevant epoch start.
410              
411             =back
412              
413             =head2 A note on text dates
414              
415             Dates saved in Excel prior to 1-January-1900 for Windows or 1-January-1904 for Apple are stored as text.
416             I suggest using L<Type::Tiny::Manual::Coercions/Chained Coercions>. Or use an Excel reader
417             that implements this for you like L<Spreadsheet::XLSX::Reader::LibXML> (self promotion).
418             Here is one possible way to integrate text and dates in the same field into a consistent DateTime
419             output. (I know it's a bit clunky but it's a place to start)
420              
421             my $system_lookup = {
422             '1900' => 'win_excel',
423             '1904' => 'apple_excel',
424             };
425             my @args_list = ( system_type => $system_lookup->{$workbook->get_epoch_year} );
426             my $converter = DateTimeX::Format::Excel->new( @args_list );
427             my $string_via = sub{
428             my $str = $_[0];
429             return DateTime::Format::Flexible->parse_datetime( $str );
430             };
431             my $num_via = sub{
432             my $num = $_[0];
433             return $converter->parse_datetime( $num );
434             };
435             my $date_time_from_value = Type::Coercion->new(
436             type_coercion_map => [ Num, $num_via, Str, $string_via, ],
437             );
438             my $date_time_type = Type::Tiny->new(
439             name => 'Custom_date_type',
440             constraint => sub{ ref($_) eq 'DateTime' },
441             coercion => $date_time_from_value,
442             );
443             my $string_type = Type::Tiny->new(
444             name => 'YYYYMMDD',
445             constraint => sub{
446             !$_ or (
447             $_ =~ /^\d{4}\-(\d{2})-(\d{2})$/ and
448             $1 > 0 and $1 < 13 and $2 > 0 and $2 < 32 )
449             },
450             coercion => Type::Coercion->new(
451             type_coercion_map =>[
452             $date_time_type->coercibles, sub{
453             my $tmp = $date_time_type->coerce( $_ );
454             $tmp->format_cldr( 'yyyy-MM-dd' )
455             },
456             ],
457             ),
458             );
459              
460              
461             =head1 THANKS
462              
463             Dave Rolsky (L<DROLSKY>) for kickstarting the DateTime project.
464             Iain Truskett, Dave Rolsky, and Achim Bursian for maintaining L<DateTime::Format::Excel>.
465             I used it heavily till I wrote this.
466             Peter (Stig) Edwards and Bobby Metz for contributing to L<DateTime::Format::Excel>.
467              
468             =head1 Build/Install from Source
469              
470             B<1.> Download a compressed file with the code
471              
472             B<2.> Extract the code from the compressed file. If you are using tar this should work:
473              
474             tar -zxvf DateTimeX-Format-Excel-v1.xx.tar.gz
475              
476             B<3.> Change (cd) into the extracted directory
477              
478             B<4.> Run the following commands
479              
480             =over
481              
482             (For Windows find what version of make was used to compile your perl)
483              
484             perl -V:make
485              
486             (then for Windows substitute the correct make function (s/make/dmake/g)?)
487              
488             =back
489              
490             >perl Makefile.PL
491              
492             >make
493              
494             >make test
495              
496             >make install # As sudo/root
497              
498             >make clean
499              
500             =head1 SUPPORT
501              
502             =over
503              
504             L<github DateTimeX::Format::Excel/issues|https://github.com/jandrew/DateTimeX-Format-Excel/issues>
505              
506             =back
507              
508             =head1 TODO
509              
510             =over
511              
512             B<1.> Add an error attribute to load soft failures or warnings to
513              
514             B<2.> Convert Smart::Comments to L<Log::Shiras|https://github.com/jandrew/Log-Shiras> debug lines
515              
516             B<3.> Allow localization as an input to the data so the object output will localize (DateTime::Local)
517              
518             =back
519              
520             =head1 AUTHOR
521              
522             =over
523              
524             =item Jed Lund
525              
526             =item jandrew@cpan.org
527              
528             =back
529              
530             =head1 COPYRIGHT
531              
532             This program is free software; you can redistribute
533             it and/or modify it under the same terms as Perl itself.
534              
535             The full text of the license can be found in the
536             LICENSE file included with this module.
537              
538             This software is copyrighted (c) 2014 - 2016 by Jed Lund
539              
540             =head1 DEPENDENCIES
541              
542             =over
543              
544             B<5.010> - (L<perl>)
545              
546             L<version> - 0.77
547              
548             L<Moose>
549              
550             L<MooseX::StrictConstructor>
551              
552             L<MooseX::HasDefaults::RO>
553              
554             L<DateTime>
555              
556             L<Carp>
557              
558             L<Types::Standard>
559              
560             L<DateTimeX::Format::Excel::Types>
561              
562             =back
563              
564             =head1 SEE ALSO
565              
566             =over
567              
568             L<DateTime::Format::Excel>
569              
570             L<Smart::Comments> - Turned on with $ENV{ Smart_Comments }
571              
572             =back
573              
574             =cut
575              
576             #########1#########2 main pod documentation end 5#########6#########7#########8#########9