File Coverage

blib/lib/Spreadsheet/XLSX.pm
Criterion Covered Total %
statement 148 154 96.1
branch 65 80 81.2
condition 20 27 74.0
subroutine 14 14 100.0
pod 0 1 0.0
total 247 276 89.4


line stmt bran cond sub pod time code
1             package Spreadsheet::XLSX;
2              
3 6     6   251191 use 5.006000;
  6         22  
4 6     6   33 use strict;
  6         9  
  6         121  
5 6     6   28 use warnings;
  6         14  
  6         167  
6              
7 6     6   30 use base 'Spreadsheet::ParseExcel::Workbook';
  6         8  
  6         4886  
8              
9             our $VERSION = '0.15';
10              
11 6     6   9620 use Archive::Zip;
  6         450605  
  6         238  
12 6     6   7851 use Spreadsheet::ParseExcel;
  6         349598  
  6         203  
13 6     6   3871 use Spreadsheet::XLSX::Fmt2007;
  6         18  
  6         11510  
14              
15             ################################################################################
16              
17             sub new {
18 5     5 0 887 my ($class, $filename, $converter) = @_;
19              
20 5         10 my %shared_info; # shared_strings, styles, style_info, rels, converter
21 5         12 $shared_info{converter} = $converter;
22            
23 5         28 my $self = bless Spreadsheet::ParseExcel::Workbook->new(), $class;
24              
25 5         44 my $zip = __load_zip($filename);
26              
27 5         22 $shared_info{shared_strings}= __load_shared_strings($zip, $shared_info{converter});
28 5         18 my ($styles, $style_info) = __load_styles($zip);
29 5         18 $shared_info{styles} = $styles;
30 5         11 $shared_info{style_info} = $style_info;
31 5         44 $shared_info{rels} = __load_rels($zip);
32              
33 5         24 $self->_load_workbook($zip, \%shared_info);
34              
35 5         4378 return $self;
36             }
37              
38             sub _load_workbook {
39 5     5   11 my ($self, $zip, $shared_info) = @_;
40              
41 5 50       21 my $member_workbook = $zip->memberNamed('xl/workbook.xml') or die("xl/workbook.xml not found in this zip\n");
42 5         245 $self->{SheetCount} = 0;
43 5         36 $self->{FmtClass} = Spreadsheet::XLSX::Fmt2007->new;
44 5         12 $self->{Flg1904} = 0;
45 5 50       18 if ($member_workbook->contents =~ /date1904="1"/) {
46 0         0 $self->{Flg1904} = 1;
47             }
48              
49 5         3891 foreach ($member_workbook->contents =~ /\<(.*?)\/?\>/g) {
50              
51 74         4347 /^(\w+)\s+/;
52              
53 74         170 my ($tag, $other) = ($1, $');
54              
55 74         172 my @pairs = split /\" /, $other;
56              
57 74 100       213 $tag eq 'sheet' or next;
58              
59 14         49 my $sheet = {
60             MaxRow => 0,
61             MaxCol => 0,
62             MinRow => 1000000,
63             MinCol => 1000000,
64             };
65              
66 14         107 foreach ($other =~ /(\S+=".*?")/gsm) {
67              
68 45         219 my ($k, $v) = split /=?"/; #"
69              
70 45 100       207 if ($k eq 'name') {
    100          
71 14         31 $sheet->{Name} = $v;
72 14 50       50 $sheet->{Name} = $shared_info->{converter}->convert($sheet->{Name}) if defined $shared_info->{converter};
73             } elsif ($k eq 'r:id') {
74              
75 14         47 $sheet->{path} = $shared_info->{rels}->{$v};
76              
77             }
78              
79             }
80              
81 14         109 my $wsheet = Spreadsheet::ParseExcel::Worksheet->new(%$sheet);
82 14         285 $self->{Worksheet}[$self->{SheetCount}] = $wsheet;
83 14         56 $self->{SheetCount} += 1;
84              
85             }
86              
87              
88 5         20 foreach my $sheet (@{$self->{Worksheet}}) {
  5         16  
89              
90 14 50       100 my $member_sheet = $zip->memberNamed("xl/$sheet->{path}") or next;
91              
92 14         839 my ($row, $col);
93              
94 14         24 my $parsing_v_tag = 0;
95 14         18 my $s = 0;
96 14         20 my $s2 = 0;
97 14         19 my $sty = 0;
98 14         78 foreach ($member_sheet->contents =~ /(\<.*?\/?\>|.*?(?=\<))/g) {
99 9954 100 66     77121 if (/^\
    100          
    100          
    100          
100              
101 1624         3502 ($row, $col) = __decode_cell_name($1, $2, $3);
102              
103 1624 100       3989 $s = m/t=\"s\"/ ? 1 : 0;
104 1624 100       3438 $s2 = m/t=\"str\"/ ? 1 : 0;
105 1624 100       6442 $sty = m/s="([0-9]+)"/ ? $1 : 0;
106              
107             } elsif (/^/) {
108 1622         2727 $parsing_v_tag = 1;
109             } elsif (/^<\/v>/) {
110 1622         2736 $parsing_v_tag = 0;
111             } elsif (length($_) && $parsing_v_tag) {
112 1622 100       3818 my $v = $s ? $shared_info->{shared_strings}->[$_] : $_;
113              
114 1622 50       3343 if ($v eq "") {
115 0         0 $v = "";
116             }
117 1622         1910 my $type = "Text";
118 1622         1954 my $thisstyle = "";
119              
120 1622 50 66     4243 if (not($s) && not($s2)) {
121 786         982 $type = "Numeric";
122              
123 786 100 66     3610 if (defined $sty && defined $shared_info->{styles}->[$sty]) {
124 783         1670 $thisstyle = $shared_info->{style_info}->{$shared_info->{styles}->[$sty]};
125 783 100       2433 if ($thisstyle =~ /\b(mmm|m|d|yy|h|hh|mm|ss)\b/) {
126 2         4 $type = "Date";
127             }
128             }
129             }
130              
131              
132 1622 100       3878 $sheet->{MaxRow} = $row if $sheet->{MaxRow} < $row;
133 1622 100       3317 $sheet->{MaxCol} = $col if $sheet->{MaxCol} < $col;
134 1622 100       3384 $sheet->{MinRow} = $row if $sheet->{MinRow} > $row;
135 1622 100       3623 $sheet->{MinCol} = $col if $sheet->{MinCol} > $col;
136              
137 1622 100 66     4914 if ($v =~ /(.*)E\-(.*)/gsm && $type eq "Numeric") {
138 97         345 $v = $1 / (10**$2); # this handles scientific notation for very small numbers
139             }
140              
141 1622         4817 my $cell = Spreadsheet::ParseExcel::Cell->new(
142             Val => $v,
143             Format => $thisstyle,
144             Type => $type
145             );
146              
147 1622         16041 $cell->{_Value} = $self->{FmtClass}->ValFmt($cell, $self);
148 1622 100       4180 if ($type eq "Date") {
149 2 100       7 if ($v < 1) { #then this is Excel time field
150 1         2 $cell->{Type} = "Text";
151             }
152 2         4 $cell->{Val} = $cell->{_Value};
153             }
154 1622         4662 $sheet->{Cells}[$row][$col] = $cell;
155             }
156             }
157              
158 14 100       686 $sheet->{MinRow} = 0 if $sheet->{MinRow} > $sheet->{MaxRow};
159 14 100       54 $sheet->{MinCol} = 0 if $sheet->{MinCol} > $sheet->{MaxCol};
160              
161             }
162              
163 5         17 return $self;
164             }
165              
166             # Convert cell name in the format AA1 to a row and column number.
167              
168             sub __decode_cell_name {
169 1624     1624   3995 my ($letter1, $letter2, $digits) = @_;
170              
171 1624         2687 my $col = ord($letter1) - 65;
172              
173 1624 50       3415 if ($letter2) {
174 0         0 $col++;
175 0         0 $col *= 26;
176 0         0 $col += (ord($letter2) - 65);
177             }
178              
179 1624         2457 my $row = $digits - 1;
180              
181 1624         3216 return ($row, $col);
182             }
183              
184              
185             sub __load_shared_strings {
186 5     5   11 my ($zip, $converter) = @_;
187              
188 5         25 my $member_shared_strings = $zip->memberNamed('xl/sharedStrings.xml');
189              
190 5         355 my @shared_strings = ();
191              
192 5 100       19 if ($member_shared_strings) {
193              
194 4         32 my $mstr = $member_shared_strings->contents;
195 4         4699 $mstr =~ s//<\/t>/gsm; # this handles an empty t tag in the xml
196 4         897 foreach my $si ($mstr =~ /(.*?)<\/si/gsm) {
197 830         913 my $str;
198 830         2747 foreach my $t ($si =~ /(.*?)<\/t/gsm) {
199 830 50       1711 $t = $converter->convert($t) if defined $converter;
200 830         1465 $str .= $t;
201             }
202 830         1562 push @shared_strings, $str;
203             }
204             }
205              
206 5         60 return \@shared_strings;
207             }
208              
209              
210             sub __load_styles {
211 5     5   11 my ($zip) = @_;
212              
213 5         22 my $member_styles = $zip->memberNamed('xl/styles.xml');
214              
215 5         324 my @styles = ();
216 5         12 my %style_info = ();
217              
218 5 50       22 if ($member_styles) {
219 5         38 my $formatter = Spreadsheet::XLSX::Fmt2007->new();
220              
221 5         24 foreach my $t ($member_styles->contents =~ /xf\ numFmtId="([^"]*)"(?!.*\/cellStyleXfs)/gsm) { #"
222 57         15271 push @styles, $t;
223             }
224              
225 5   100     968 my $default = $1 || '';
226            
227 5         20 foreach my $t1 (@styles) {
228 57         200 $member_styles->contents =~ /numFmtId="$t1" formatCode="([^"]*)/;
229 57   100     46565 my $formatCode = $1 || '';
230 57 100 100     262 if ($formatCode eq $default || not($formatCode)) {
231 41 100 66     232 if ($t1 == 9 || $t1 == 10) {
    100          
232 1         3 $formatCode = '0.00000%';
233             } elsif ($t1 == 14) {
234 1         2 $formatCode = 'yyyy-mm-dd';
235             } else {
236 39         59 $formatCode = '';
237             }
238             # $formatCode = $formatter->FmtStringDef($t1);
239             }
240 57         113 $style_info{$t1} = $formatCode;
241 57   100     269 $default = $1 || '';
242             }
243              
244             }
245 5         82 return (\@styles, \%style_info);
246             }
247              
248              
249             sub __load_rels {
250 5     5   9 my ($zip) = @_;
251              
252 5 50       25 my $member_rels = $zip->memberNamed('xl/_rels/workbook.xml.rels') or die("xl/_rels/workbook.xml.rels not found in this zip\n");
253              
254 5         174 my %rels = ();
255              
256 5         22 foreach ($member_rels->contents =~ /\/g) {
257              
258 28         4373 my ($id, $target);
259 28         94 ($id) = /Id="(.*?)"/;
260 28         92 ($target) = /Target="(.*?)"/;
261            
262 28 50 33     136 if (defined $id and defined $target) {
263 28         81 $rels{$id} = $target;
264             }
265              
266             }
267              
268 5         20 return \%rels;
269             }
270              
271             sub __load_zip {
272 5     5   10 my ($filename) = @_;
273              
274 5         38 my $zip = Archive::Zip->new();
275              
276 5 50       217 if (ref $filename) {
277 0 0       0 $zip->readFromFileHandle($filename) == Archive::Zip::AZ_OK or die("Cannot open data as Zip archive");
278             } else {
279 5 50       30 $zip->read($filename) == Archive::Zip::AZ_OK or die("Cannot open $filename as Zip archive");
280             }
281            
282 5         13802 return $zip;
283             }
284              
285              
286             1;
287             __END__