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   849643 use 5.006000;
  7         23  
4 7     7   36 use strict;
  7         10  
  7         195  
5 7     7   34 use warnings;
  7         14  
  7         439  
6            
7 7     7   49 use base 'Spreadsheet::ParseExcel::Workbook';
  7         24  
  7         3937  
8            
9             our $VERSION = '0.18';
10            
11 7     7   11358 use Archive::Zip;
  7         563631  
  7         289  
12 7     7   5665 use Spreadsheet::ParseExcel;
  7         394059  
  7         306  
13 7     7   4185 use Spreadsheet::XLSX::Fmt2007;
  7         21  
  7         15382  
14            
15             ################################################################################
16            
17             sub new {
18 5     5 0 1138 my ($class, $filename, $converter) = @_;
19            
20 5         9 my %shared_info; # shared_strings, styles, style_info, rels, converter
21 5         13 $shared_info{converter} = $converter;
22            
23 5         32 my $self = bless Spreadsheet::ParseExcel::Workbook->new(), $class;
24            
25 5         50 my $zip = __load_zip($filename);
26            
27 5         27 $shared_info{shared_strings}= __load_shared_strings($zip, $shared_info{converter});
28 5         17 my ($styles, $style_info) = __load_styles($zip);
29 5         17 $shared_info{styles} = $styles;
30 5         14 $shared_info{style_info} = $style_info;
31 5         18 $shared_info{rels} = __load_rels($zip);
32            
33 5         32 $self->_load_workbook($zip, \%shared_info);
34            
35 5         175666 return $self;
36             }
37            
38             sub _load_workbook {
39 5     5   17 my ($self, $zip, $shared_info) = @_;
40            
41 5 50       26 my $member_workbook = $zip->memberNamed('xl/workbook.xml') or die("xl/workbook.xml not found in this zip\n");
42 5         328 $self->{SheetCount} = 0;
43 5         42 $self->{FmtClass} = Spreadsheet::XLSX::Fmt2007->new;
44 5         12 $self->{Flg1904} = 0;
45 5 50       16 if ($member_workbook->contents =~ /date1904="1"/) {
46 0         0 $self->{Flg1904} = 1;
47             }
48            
49 5         5422 foreach ($member_workbook->contents =~ /\<(.*?)\/?\>/g) {
50            
51 74         6212 /^(\w+)\s+/;
52            
53 74         218 my ($tag, $other) = ($1, $');
54            
55 74         166 my @pairs = split /\" /, $other;
56            
57 74 100       183 $tag eq 'sheet' or next;
58            
59 14         58 my $sheet = {
60             MaxRow => 0,
61             MaxCol => 0,
62             MinRow => 1000000,
63             MinCol => 1000000,
64             };
65            
66 14         115 foreach ($other =~ /(\S+=".*?")/gsm) {
67            
68 45         178 my ($k, $v) = split /=?"/; #"
69            
70 45 100       125 if ($k eq 'name') {
    100          
71 14         32 $sheet->{Name} = $v;
72 14 50       48 $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         107 my $wsheet = Spreadsheet::ParseExcel::Worksheet->new(%$sheet);
82 14         269 $self->{Worksheet}[$self->{SheetCount}] = $wsheet;
83 14         55 $self->{SheetCount} += 1;
84            
85             }
86            
87            
88 5         18 foreach my $sheet (@{$self->{Worksheet}}) {
  5         18  
89            
90 14 50       119 my $member_sheet = $zip->memberNamed("xl/$sheet->{path}") or next;
91            
92 14         1074 my ($row, $col);
93            
94 14         26 my $parsing_v_tag = 0;
95 14         23 my $s = 0;
96 14         21 my $s2 = 0;
97 14         24 my $sty = 0;
98 14         46 foreach ($member_sheet->contents =~ /(\<.*?\/?\>|.*?(?=\<))/g) {
99 9954 100 66     78642 if (/^\
    100          
    100          
    100          
100            
101 1624         4088 ($row, $col) = __decode_cell_name($1, $2, $3);
102            
103 1624 100       4089 $s = m/t=\"s\"/ ? 1 : 0;
104 1624 100       3374 $s2 = m/t=\"str\"/ ? 1 : 0;
105 1624 100       7114 $sty = m/s="([0-9]+)"/ ? $1 : 0;
106            
107             } elsif (/^/) {
108 1622         2533 $parsing_v_tag = 1;
109             } elsif (/^<\/v>/) {
110 1622         2584 $parsing_v_tag = 0;
111             } elsif (length($_) && $parsing_v_tag) {
112 1622 100       4131 my $v = $s ? $shared_info->{shared_strings}->[$_] : $_;
113            
114 1622 50       3246 if ($v eq "") {
115 0         0 $v = "";
116             }
117 1622         2415 my $type = "Text";
118 1622         2286 my $thisstyle = "";
119            
120 1622 50 66     4156 if (not($s) && not($s2)) {
121 786         1209 $type = "Numeric";
122            
123 786 100 66     3346 if (defined $sty && defined $shared_info->{styles}->[$sty]) {
124 783         1997 $thisstyle = $shared_info->{style_info}->{$shared_info->{styles}->[$sty]};
125 783 100       2359 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       3974 $sheet->{MaxRow} = $row if $sheet->{MaxRow} < $row;
133 1622 100       3112 $sheet->{MaxCol} = $col if $sheet->{MaxCol} < $col;
134 1622 100       3233 $sheet->{MinRow} = $row if $sheet->{MinRow} > $row;
135 1622 100       2925 $sheet->{MinCol} = $col if $sheet->{MinCol} > $col;
136            
137 1622 100 66     4812 if ($v =~ /(.*)E\-(.*)/gsm && $type eq "Numeric") {
138 97         653 $v = $1 / (10**$2); # this handles scientific notation for very small numbers
139             }
140            
141 1622         6622 my $cell = Spreadsheet::ParseExcel::Cell->new(
142             Val => $v,
143             Format => $thisstyle,
144             Type => $type
145             );
146            
147 1622         20006 $cell->{_Value} = $self->{FmtClass}->ValFmt($cell, $self);
148 1622 100       3859 if ($type eq "Date") {
149 2 100       11 if ($v < 1) { #then this is Excel time field
150 1         4 $cell->{Type} = "Text";
151 1         4 $cell->{Val} = $cell->{_Value};
152             }
153             }
154 1622         5866 $sheet->{Cells}[$row][$col] = $cell;
155             }
156             }
157            
158 14 100       1061 $sheet->{MinRow} = 0 if $sheet->{MinRow} > $sheet->{MaxRow};
159 14 100       92 $sheet->{MinCol} = 0 if $sheet->{MinCol} > $sheet->{MaxCol};
160            
161             }
162            
163 5         20 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   7296 my ($letter1, $letter2, $digits) = @_;
170            
171 1624         3111 my $col = ord($letter1) - 65;
172            
173 1624 50       3365 if ($letter2) {
174 0         0 $col++;
175 0         0 $col *= 26;
176 0         0 $col += (ord($letter2) - 65);
177             }
178            
179 1624         3415 my $row = $digits - 1;
180            
181 1624         3734 return ($row, $col);
182             }
183            
184            
185             sub __load_shared_strings {
186 5     5   12 my ($zip, $converter) = @_;
187            
188 5         25 my $member_shared_strings = $zip->memberNamed('xl/sharedStrings.xml');
189            
190 5         395 my @shared_strings = ();
191            
192 5 100       17 if ($member_shared_strings) {
193            
194 4         30 my $mstr = $member_shared_strings->contents;
195 4         5661 $mstr =~ s//<\/t>/gsm; # this handles an empty t tag in the xml
196 4         1078 foreach my $si ($mstr =~ /(.*?)<\/si/gsm) {
197 830         789 my $str;
198 830         1770 foreach my $t ($si =~ /(.*?)<\/t/gsm) {
199 830 50       1090 $t = $converter->convert($t) if defined $converter;
200 830         1042 $str .= $t;
201             }
202 830         1096 push @shared_strings, $str;
203             }
204             }
205            
206 5         50 return \@shared_strings;
207             }
208            
209            
210             sub __load_styles {
211 5     5   38 my ($zip) = @_;
212            
213 5         56 my $member_styles = $zip->memberNamed('xl/styles.xml');
214            
215 5         331 my @styles = ();
216 5         9 my %style_info = ();
217            
218 5 50       18 if ($member_styles) {
219 5         49 my $formatter = Spreadsheet::XLSX::Fmt2007->new();
220            
221 5         51 foreach my $t ($member_styles->contents =~ /xf\ numFmtId="([^"]*)"(?!.*\/cellStyleXfs)/gsm) { #"
222 57         14053 push @styles, $t;
223             }
224            
225 5   100     952 my $default = $1 || '';
226            
227 5         17 foreach my $t1 (@styles) {
228 57         149 $member_styles->contents =~ /numFmtId="$t1" formatCode="([^"]*)/;
229 57   100     57289 my $formatCode = $1 || '';
230 57 100 100     223 if ($formatCode eq $default || not($formatCode)) {
231 41 100 66     197 if ($t1 == 9 || $t1 == 10) {
    100          
232 1         1 $formatCode = '0.00000%';
233             } elsif ($t1 == 14) {
234 1         4 $formatCode = 'yyyy-mm-dd';
235             } else {
236 39         61 $formatCode = '';
237             }
238             # $formatCode = $formatter->FmtStringDef($t1);
239             }
240 57         124 $style_info{$t1} = $formatCode;
241 57   100     242 $default = $1 || '';
242             }
243            
244             }
245 5         26 return (\@styles, \%style_info);
246             }
247            
248            
249             sub __load_rels {
250 5     5   14 my ($zip) = @_;
251            
252 5 50       30 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         231 my %rels = ();
255            
256 5         20 foreach ($member_rels->contents =~ /\/g) {
257            
258 28         7315 my ($id, $target);
259 28         93 ($id) = /Id="(.*?)"/;
260 28         80 ($target) = /Target="(.*?)"/;
261            
262 28 50 33     106 if (defined $id and defined $target) {
263 28         76 $rels{$id} = $target;
264             }
265            
266             }
267            
268 5         23 return \%rels;
269             }
270            
271             sub __load_zip {
272 5     5   10 my ($filename) = @_;
273            
274 5         32 my $zip = Archive::Zip->new();
275            
276 5 50       245 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       29 $zip->read($filename) == Archive::Zip::AZ_OK or die("Cannot open $filename as Zip archive");
280             }
281            
282 5         17226 return $zip;
283             }
284            
285            
286             1;
287             __END__