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 7     7   614548 use 5.006000;
  7         85  
4 7     7   41 use strict;
  7         11  
  7         159  
5 7     7   35 use warnings;
  7         14  
  7         255  
6              
7 7     7   43 use base 'Spreadsheet::ParseExcel::Workbook';
  7         14  
  7         3749  
8              
9             our $VERSION = '0.17';
10              
11 7     7   9270 use Archive::Zip;
  7         530717  
  7         320  
12 7     7   6121 use Spreadsheet::ParseExcel;
  7         377433  
  7         293  
13 7     7   3746 use Spreadsheet::XLSX::Fmt2007;
  7         23  
  7         13467  
14              
15             ################################################################################
16              
17             sub new {
18 5     5 0 888 my ($class, $filename, $converter) = @_;
19              
20 5         10 my %shared_info; # shared_strings, styles, style_info, rels, converter
21 5         16 $shared_info{converter} = $converter;
22            
23 5         26 my $self = bless Spreadsheet::ParseExcel::Workbook->new(), $class;
24              
25 5         60 my $zip = __load_zip($filename);
26              
27 5         31 $shared_info{shared_strings}= __load_shared_strings($zip, $shared_info{converter});
28 5         17 my ($styles, $style_info) = __load_styles($zip);
29 5         16 $shared_info{styles} = $styles;
30 5         11 $shared_info{style_info} = $style_info;
31 5         15 $shared_info{rels} = __load_rels($zip);
32              
33 5         27 $self->_load_workbook($zip, \%shared_info);
34              
35 5         4305 return $self;
36             }
37              
38             sub _load_workbook {
39 5     5   15 my ($self, $zip, $shared_info) = @_;
40              
41 5 50       23 my $member_workbook = $zip->memberNamed('xl/workbook.xml') or die("xl/workbook.xml not found in this zip\n");
42 5         276 $self->{SheetCount} = 0;
43 5         31 $self->{FmtClass} = Spreadsheet::XLSX::Fmt2007->new;
44 5         13 $self->{Flg1904} = 0;
45 5 50       17 if ($member_workbook->contents =~ /date1904="1"/) {
46 0         0 $self->{Flg1904} = 1;
47             }
48              
49 5         5270 foreach ($member_workbook->contents =~ /\<(.*?)\/?\>/g) {
50              
51 74         5560 /^(\w+)\s+/;
52              
53 74         182 my ($tag, $other) = ($1, $');
54              
55 74         183 my @pairs = split /\" /, $other;
56              
57 74 100       171 $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         96 foreach ($other =~ /(\S+=".*?")/gsm) {
67              
68 45         175 my ($k, $v) = split /=?"/; #"
69              
70 45 100       130 if ($k eq 'name') {
    100          
71 14         30 $sheet->{Name} = $v;
72 14 50       42 $sheet->{Name} = $shared_info->{converter}->convert($sheet->{Name}) if defined $shared_info->{converter};
73             } elsif ($k eq 'r:id') {
74              
75 14         42 $sheet->{path} = $shared_info->{rels}->{$v};
76              
77             }
78              
79             }
80              
81 14         112 my $wsheet = Spreadsheet::ParseExcel::Worksheet->new(%$sheet);
82 14         248 $self->{Worksheet}[$self->{SheetCount}] = $wsheet;
83 14         46 $self->{SheetCount} += 1;
84              
85             }
86              
87              
88 5         17 foreach my $sheet (@{$self->{Worksheet}}) {
  5         17  
89              
90 14 50       98 my $member_sheet = $zip->memberNamed("xl/$sheet->{path}") or next;
91              
92 14         995 my ($row, $col);
93              
94 14         27 my $parsing_v_tag = 0;
95 14         21 my $s = 0;
96 14         22 my $s2 = 0;
97 14         21 my $sty = 0;
98 14         43 foreach ($member_sheet->contents =~ /(\<.*?\/?\>|.*?(?=\<))/g) {
99 9954 100 66     63287 if (/^\
    100          
    100          
    100          
100              
101 1624         3659 ($row, $col) = __decode_cell_name($1, $2, $3);
102              
103 1624 100       3886 $s = m/t=\"s\"/ ? 1 : 0;
104 1624 100       2929 $s2 = m/t=\"str\"/ ? 1 : 0;
105 1624 100       5956 $sty = m/s="([0-9]+)"/ ? $1 : 0;
106              
107             } elsif (/^/) {
108 1622         2507 $parsing_v_tag = 1;
109             } elsif (/^<\/v>/) {
110 1622         2839 $parsing_v_tag = 0;
111             } elsif (length($_) && $parsing_v_tag) {
112 1622 100       3885 my $v = $s ? $shared_info->{shared_strings}->[$_] : $_;
113              
114 1622 50       3186 if ($v eq "") {
115 0         0 $v = "";
116             }
117 1622         2263 my $type = "Text";
118 1622         2282 my $thisstyle = "";
119              
120 1622 50 66     4000 if (not($s) && not($s2)) {
121 786         1058 $type = "Numeric";
122              
123 786 100 66     2972 if (defined $sty && defined $shared_info->{styles}->[$sty]) {
124 783         1645 $thisstyle = $shared_info->{style_info}->{$shared_info->{styles}->[$sty]};
125 783 100       2106 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       3469 $sheet->{MaxRow} = $row if $sheet->{MaxRow} < $row;
133 1622 100       2940 $sheet->{MaxCol} = $col if $sheet->{MaxCol} < $col;
134 1622 100       3024 $sheet->{MinRow} = $row if $sheet->{MinRow} > $row;
135 1622 100       2878 $sheet->{MinCol} = $col if $sheet->{MinCol} > $col;
136              
137 1622 100 66     4215 if ($v =~ /(.*)E\-(.*)/gsm && $type eq "Numeric") {
138 97         471 $v = $1 / (10**$2); # this handles scientific notation for very small numbers
139             }
140              
141 1622         5135 my $cell = Spreadsheet::ParseExcel::Cell->new(
142             Val => $v,
143             Format => $thisstyle,
144             Type => $type
145             );
146              
147 1622         17088 $cell->{_Value} = $self->{FmtClass}->ValFmt($cell, $self);
148 1622 100       3643 if ($type eq "Date") {
149 2 100       6 if ($v < 1) { #then this is Excel time field
150 1         3 $cell->{Type} = "Text";
151             }
152 2         5 $cell->{Val} = $cell->{_Value};
153             }
154 1622         4461 $sheet->{Cells}[$row][$col] = $cell;
155             }
156             }
157              
158 14 100       595 $sheet->{MinRow} = 0 if $sheet->{MinRow} > $sheet->{MaxRow};
159 14 100       73 $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   6574 my ($letter1, $letter2, $digits) = @_;
170              
171 1624         2962 my $col = ord($letter1) - 65;
172              
173 1624 50       3164 if ($letter2) {
174 0         0 $col++;
175 0         0 $col *= 26;
176 0         0 $col += (ord($letter2) - 65);
177             }
178              
179 1624         3331 my $row = $digits - 1;
180              
181 1624         3416 return ($row, $col);
182             }
183              
184              
185             sub __load_shared_strings {
186 5     5   16 my ($zip, $converter) = @_;
187              
188 5         24 my $member_shared_strings = $zip->memberNamed('xl/sharedStrings.xml');
189              
190 5         388 my @shared_strings = ();
191              
192 5 100       70 if ($member_shared_strings) {
193              
194 4         48 my $mstr = $member_shared_strings->contents;
195 4         5984 $mstr =~ s//<\/t>/gsm; # this handles an empty t tag in the xml
196 4         893 foreach my $si ($mstr =~ /(.*?)<\/si/gsm) {
197 830         1005 my $str;
198 830         2602 foreach my $t ($si =~ /(.*?)<\/t/gsm) {
199 830 50       1533 $t = $converter->convert($t) if defined $converter;
200 830         1422 $str .= $t;
201             }
202 830         1566 push @shared_strings, $str;
203             }
204             }
205              
206 5         57 return \@shared_strings;
207             }
208              
209              
210             sub __load_styles {
211 5     5   11 my ($zip) = @_;
212              
213 5         34 my $member_styles = $zip->memberNamed('xl/styles.xml');
214              
215 5         359 my @styles = ();
216 5         11 my %style_info = ();
217              
218 5 50       18 if ($member_styles) {
219 5         32 my $formatter = Spreadsheet::XLSX::Fmt2007->new();
220              
221 5         22 foreach my $t ($member_styles->contents =~ /xf\ numFmtId="([^"]*)"(?!.*\/cellStyleXfs)/gsm) { #"
222 57         11641 push @styles, $t;
223             }
224              
225 5   100     1245 my $default = $1 || '';
226            
227 5         18 foreach my $t1 (@styles) {
228 57         490 $member_styles->contents =~ /numFmtId="$t1" formatCode="([^"]*)/;
229 57   100     66380 my $formatCode = $1 || '';
230 57 100 100     237 if ($formatCode eq $default || not($formatCode)) {
231 41 100 66     195 if ($t1 == 9 || $t1 == 10) {
    100          
232 1         5 $formatCode = '0.00000%';
233             } elsif ($t1 == 14) {
234 1         3 $formatCode = 'yyyy-mm-dd';
235             } else {
236 39         65 $formatCode = '';
237             }
238             # $formatCode = $formatter->FmtStringDef($t1);
239             }
240 57         143 $style_info{$t1} = $formatCode;
241 57   100     245 $default = $1 || '';
242             }
243              
244             }
245 5         24 return (\@styles, \%style_info);
246             }
247              
248              
249             sub __load_rels {
250 5     5   12 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         192 my %rels = ();
255              
256 5         19 foreach ($member_rels->contents =~ /\/g) {
257              
258 28         5733 my ($id, $target);
259 28         88 ($id) = /Id="(.*?)"/;
260 28         91 ($target) = /Target="(.*?)"/;
261            
262 28 50 33     110 if (defined $id and defined $target) {
263 28         69 $rels{$id} = $target;
264             }
265              
266             }
267              
268 5         19 return \%rels;
269             }
270              
271             sub __load_zip {
272 5     5   14 my ($filename) = @_;
273              
274 5         50 my $zip = Archive::Zip->new();
275              
276 5 50       253 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       28 $zip->read($filename) == Archive::Zip::AZ_OK or die("Cannot open $filename as Zip archive");
280             }
281            
282 5         18150 return $zip;
283             }
284              
285              
286             1;
287             __END__