Excel-ValueReader-XLSX-1.16000755000000000000 014776134025 16032 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/Build.PL000444000000000000 243614776133531 17471 0ustar00unknownunknown000000000000use strict; use warnings; use Module::Build; my $builder = Module::Build->new( module_name => 'Excel::ValueReader::XLSX', license => 'perl', dist_author => 'Laurent Dami ', dist_version_from => 'lib/Excel/ValueReader/XLSX.pm', requires => { 'perl' => "5.012001", 'utf8' => 0, 'Carp' => 0, 'Archive::Zip' => "1.61", 'Module::Load' => 0, 'Moose' => 0, 'MooseX::StrictConstructor' => 0, 'Date::Calc' => 0, 'POSIX' => 0, 'Scalar::Util' => 0, 'Iterator::Simple' => 0, }, recommends => { 'XML::LibXML::Reader' => 0, }, test_requires => { 'Test::More' => "1.302195", 'List::Util' => 0, 'List::MoreUtils' => 0, 'Module::Load::Conditional' => "0.66", 'Clone' => 0, }, add_to_cleanup => [ 'Excel-ValueReader-XLSX-*' ], meta_merge => { resources => { repository => 'https://github.com/damil/Excel-ValueReader-XLSX', } }, ); $builder->create_build_script(); Excel-ValueReader-XLSX-1.16/Changes000444000000000000 300414774672555 17474 0ustar00unknownunknown000000000000Revision history for Excel-ValueReader-XLSX 1.16 07.04.2025 - new methods ivalues() and itable(), iterators for getting rows one at a time - tables with a 'totals' row can be retrieved without or with that final row 1.15 11.01.2025 - fix bug in table() with 'ref' param (Ephraim Stevens++) 1.14 16.05.2023 - better error message for invalid table name 1.13 06.11.2023 - minor fixes: typos, source improvements, avoid dependencies on unsafe modules (Merijn Brand++) 1.12 06.11.2023 - fix bug in test suite (useless attempt to open a file in RW mode) 1.11 05.11.2023 - new() can take a filehandle instead of a filename - additional method ->active_sheet - minimal perl version is now 5.12 because of a dependency chain Moose/Sub::Exporter/Data::OptList 1.10 12.02.2023 - support cells without an 'r' attribute (David Flink++) 1.09 22.01.2023 - fix bug on parsing empty string nodes with LibXML (ulibuck++) 1.08 11.02.2022 - added support to parse Excel tables 1.07 31.12.2021 - oops, forgot a file in MANIFEST 1.06 30.12.2021 - fix 2 bugs signaled by https://github.com/ulibuck 1.05 17.12.2021 - suppress warnings when a style is applied to a non-numeric cell 1.04 25.08.2021 - hack to avoid floating-point imprecisions in computing time values 1.03 19.08.2021 - fix failures from cpantesters 1.02 18.08.2021 - added support for decoding dates 1.01 01.08.2021 - bug fix : properly handle strings with embedded newlines characters (David Flink++) 1.0 31.05.2020 - initial release Excel-ValueReader-XLSX-1.16/MANIFEST000444000000000000 57614775655510 17316 0ustar00unknownunknown000000000000benchmark.pl Build.PL Changes lib/Excel/ValueReader/XLSX.pm lib/Excel/ValueReader/XLSX/Backend.pm lib/Excel/ValueReader/XLSX/Backend/LibXML.pm lib/Excel/ValueReader/XLSX/Backend/Regex.pm MANIFEST This list of files META.json META.yml README.md t/from_filehandle.t t/ulibuck.xlsx t/valuereader.t t/valuereader.xlsx t/valuereader1904.xlsx t/Mappe1.xlsx t/cells_without_r_attr.xlsx Excel-ValueReader-XLSX-1.16/META.json000444000000000000 417314776134025 17615 0ustar00unknownunknown000000000000{ "abstract" : "extracting values from Excel workbooks in XLSX format, fast", "author" : [ "Laurent Dami " ], "dynamic_config" : 1, "generated_by" : "Module::Build version 0.4234", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Excel-ValueReader-XLSX", "prereqs" : { "configure" : { "requires" : { "Module::Build" : "0.42" } }, "runtime" : { "recommends" : { "XML::LibXML::Reader" : "0" }, "requires" : { "Archive::Zip" : "1.61", "Carp" : "0", "Date::Calc" : "0", "Iterator::Simple" : "0", "Module::Load" : "0", "Moose" : "0", "MooseX::StrictConstructor" : "0", "POSIX" : "0", "Scalar::Util" : "0", "perl" : "5.012001", "utf8" : "0" } }, "test" : { "requires" : { "Clone" : "0", "List::MoreUtils" : "0", "List::Util" : "0", "Module::Load::Conditional" : "0.66", "Test::More" : "1.302195" } } }, "provides" : { "Excel::ValueReader::XLSX" : { "file" : "lib/Excel/ValueReader/XLSX.pm", "version" : "1.16" }, "Excel::ValueReader::XLSX::Backend" : { "file" : "lib/Excel/ValueReader/XLSX/Backend.pm" }, "Excel::ValueReader::XLSX::Backend::LibXML" : { "file" : "lib/Excel/ValueReader/XLSX/Backend/LibXML.pm" }, "Excel::ValueReader::XLSX::Backend::Regex" : { "file" : "lib/Excel/ValueReader/XLSX/Backend/Regex.pm" } }, "release_status" : "stable", "resources" : { "license" : [ "http://dev.perl.org/licenses/" ], "repository" : { "url" : "https://github.com/damil/Excel-ValueReader-XLSX" } }, "version" : "1.16", "x_serialization_backend" : "JSON::PP version 4.16" } Excel-ValueReader-XLSX-1.16/META.yml000444000000000000 257414776134025 17450 0ustar00unknownunknown000000000000--- abstract: 'extracting values from Excel workbooks in XLSX format, fast' author: - 'Laurent Dami ' build_requires: Clone: '0' List::MoreUtils: '0' List::Util: '0' Module::Load::Conditional: '0.66' Test::More: '1.302195' configure_requires: Module::Build: '0.42' dynamic_config: 1 generated_by: 'Module::Build version 0.4234, CPAN::Meta::Converter version 2.150010' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Excel-ValueReader-XLSX provides: Excel::ValueReader::XLSX: file: lib/Excel/ValueReader/XLSX.pm version: '1.16' Excel::ValueReader::XLSX::Backend: file: lib/Excel/ValueReader/XLSX/Backend.pm Excel::ValueReader::XLSX::Backend::LibXML: file: lib/Excel/ValueReader/XLSX/Backend/LibXML.pm Excel::ValueReader::XLSX::Backend::Regex: file: lib/Excel/ValueReader/XLSX/Backend/Regex.pm recommends: XML::LibXML::Reader: '0' requires: Archive::Zip: '1.61' Carp: '0' Date::Calc: '0' Iterator::Simple: '0' Module::Load: '0' Moose: '0' MooseX::StrictConstructor: '0' POSIX: '0' Scalar::Util: '0' perl: '5.012001' utf8: '0' resources: license: http://dev.perl.org/licenses/ repository: https://github.com/damil/Excel-ValueReader-XLSX version: '1.16' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Excel-ValueReader-XLSX-1.16/README.md000444000000000000 11013664571607 17424 0ustar00unknownunknown000000000000# Excel-ValueReader-XLSX Extracting values from Excel workbooks -- fast Excel-ValueReader-XLSX-1.16/benchmark.pl000444000000000000 1052014776130035 20471 0ustar00unknownunknown000000000000use utf8; use strict; use warnings; use Getopt::Long; use Excel::ValueReader::XLSX; # use Excel::Reader::XLSX; use Spreadsheet::ParseXLSX; use Data::XLSX::Parser; # options de la ligne de commande GetOptions \my %opt, 'xl_file=s', # fichier Excel des comparaisons 'valuereader!', 'ivaluereader!', 'vrlibxml!', 'ivrlibxml!', 'reader!', 'parsexlsx!', 'xparser!', ; $opt{xl_file} //= "d:/temp/Audit/Stats_acces_2024/Stats_DM_par_semaine_S1_2024.xlsx"; my ($start, $cpu, $system) = (time, times); valuereader($opt{xl_file}) if $opt{valuereader}; ivaluereader($opt{xl_file})if $opt{ivaluereader}; vrlibxml($opt{xl_file}) if $opt{vrlibxml}; ivrlibxml($opt{xl_file}) if $opt{ivrlibxml}; reader($opt{xl_file}) if $opt{reader}; parsexlsx($opt{xl_file}) if $opt{parsexlsx}; xparser($opt{xl_file}) if $opt{xparser}; my ($end, $ecpu, $esystem) = (time, times); printf "%d elapsed, %d cpu, %d system\n", $end-$start, $ecpu-$cpu, $esystem-$system; sub valuereader { my $xl_file = shift; warn "using ValueReader\n"; my $rv = Excel::ValueReader::XLSX->new(xlsx => $xl_file); foreach my $sheet_name ($rv->sheet_names) { my $vals = $rv->values($sheet_name); my $n_rows = @$vals; warn "sheet $sheet_name has $n_rows rows\n"; } } sub ivaluereader { my $xl_file = shift; warn "using ValueReader iterator\n"; my $rv = Excel::ValueReader::XLSX->new(xlsx => $xl_file); foreach my $sheet_name ($rv->sheet_names) { my $it = $rv->ivalues($sheet_name); my $n_rows = 0; $n_rows++ while $it->(); warn "sheet $sheet_name has $n_rows rows\n"; } } sub vrlibxml { my $xl_file = shift; warn "using ValueReader with LibXML\n"; my $rv = Excel::ValueReader::XLSX->new(xlsx => $xl_file, using => 'LibXML'); foreach my $sheet_name ($rv->sheet_names) { my $vals = $rv->values($sheet_name); my $n_rows = @$vals; warn "sheet $sheet_name has $n_rows rows\n"; } } sub ivrlibxml { my $xl_file = shift; warn "using ValueReader iterator with LibXML\n"; my $rv = Excel::ValueReader::XLSX->new(xlsx => $xl_file, using => 'LibXML'); foreach my $sheet_name ($rv->sheet_names) { my $it = $rv->ivalues($sheet_name); my $n_rows = 0; $n_rows++ while $it->(); warn "sheet $sheet_name has $n_rows rows\n"; } } sub reader { my $xl_file = shift; warn "using Excel::Reader::XLSX\n"; my $reader = Excel::Reader::XLSX->new(); my $workbook = $reader->read_file($xl_file); for my $worksheet ( $workbook->worksheets() ) { my $sheet_name = $worksheet->name(); my @rows; while ( my $row = $worksheet->next_row() ) { my @row; while ( my $cell = $row->next_cell() ) { push @row, $cell->value(); } push @rows, \@row; } my $n_rows = @rows; warn "sheet $sheet_name has $n_rows rows\n"; } } sub parsexlsx { my $xl_file = shift; warn "using Spreadsheet::ParseXLSX\n"; my $parser = Spreadsheet::ParseXLSX->new(); my $workbook = $parser->parse($xl_file) or die $parser->error; for my $worksheet ( $workbook->worksheets() ) { my $sheet_name = $worksheet->get_name(); my ( $row_min, $row_max ) = $worksheet->row_range(); warn "sheet $sheet_name has $row_max rows\n"; } } sub xparser { my $xl_file = shift; warn "using Data::XLSX::Parser\n"; my $parser = Data::XLSX::Parser->new; my @rows; $parser->add_row_event_handler(sub { my ($row) = @_; push @rows, $row; }); $parser->open($xl_file); foreach my $sheet_name ($parser->workbook->names) { @rows = (); $parser->sheet_by_rid( "rId" . $parser->workbook->sheet_id( $sheet_name ) ); my $n_rows = @rows; warn "sheet $sheet_name has $n_rows rows\n"; @rows = (); } } __END__ using ValueReader sheet Stats_DM_par_semaine_S1_2024 has 800131 rows 40 elapsed, 32 cpu, 0 system using ValueReader iterator sheet Stats_DM_par_semaine_S1_2024 has 800131 rows 34 elapsed, 30 cpu, 0 system using ValueReader with LibXML sheet Stats_DM_par_semaine_S1_2024 has 800131 rows 101 elapsed, 83 cpu, 0 system using ValueReader iterator with LibXML sheet Stats_DM_par_semaine_S1_2024 has 800131 rows 91 elapsed, 80 cpu, 0 system using Spreadsheet::ParseXLSX sheet Stats_DM_par_semaine_S1_2024 has 800130 rows 1272 elapsed, 870 cpu, 4 system using Data::XLSX::Parser sheet Stats_DM_par_semaine_S1_2024 has 800131 rows 125 elapsed, 107 cpu, 1 system Excel-ValueReader-XLSX-1.16/lib000755000000000000 014776134025 16600 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/lib/Excel000755000000000000 014776134025 17640 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader000755000000000000 014776134025 22037 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader/XLSX.pm000444000000000000 6731614776133425 23370 0ustar00unknownunknown000000000000package Excel::ValueReader::XLSX; use 5.12.1; use utf8; use Moose; use MooseX::StrictConstructor; use Moose::Util::TypeConstraints qw/union/; use Module::Load qw/load/; use Date::Calc qw/Add_Delta_Days/; use POSIX qw/strftime modf/; use Carp qw/croak/; use Iterator::Simple qw/iter/; #====================================================================== # GLOBALS #====================================================================== our $VERSION = '1.16'; our %A1_to_num_memoized; #====================================================================== # TYPES AND ATTRIBUTES #====================================================================== # TYPES my $XlsxSource = union([qw/Str FileHandle/]); # PUBLIC ATTRIBUTES has 'xlsx' => (is => 'ro', isa => $XlsxSource, required => 1); # path of xlsx file has 'using' => (is => 'ro', isa => 'Str', default => 'Regex'); # name of backend class has 'date_format' => (is => 'ro', isa => 'Str', default => '%d.%m.%Y'); has 'time_format' => (is => 'ro', isa => 'Str', default => '%H:%M:%S'); has 'datetime_format' => (is => 'ro', isa => 'Str', builder => '_datetime_format', lazy => 1); has 'date_formatter' => (is => 'ro', isa => 'Maybe[CodeRef]', builder => '_date_formatter', lazy => 1); # ATTRIBUTES USED INTERNALLY, NOT DOCUMENTED has 'backend' => (is => 'ro', isa => 'Object', builder => '_backend', lazy => 1, init_arg => undef, handles => [qw/base_year sheets active_sheet table_info/]); #====================================================================== # BUILDING #====================================================================== # syntactic sugar for supporting ->new($path) instead of ->new(xlsx => $path) around BUILDARGS => sub { my $orig = shift; my $class = shift; unshift @_, 'xlsx' if scalar(@_) % 2 and $XlsxSource->check($_[0]); $class->$orig(@_); }; #====================================================================== # ATTRIBUTE CONSTRUCTORS #====================================================================== sub _backend { my $self = shift; my $backend_class = ref($self) . '::Backend::' . $self->using; load $backend_class; return $backend_class->new(frontend => $self); } sub _datetime_format { my ($self) = @_; return $self->date_format . ' ' . $self->time_format; } sub _date_formatter { my ($self) = @_; # local copies of the various formats so that we can build a closure my @formats = (undef, # 0 -- error $self->date_format, # 1 -- just a date $self->time_format, # 2 -- just a time $self->datetime_format); # 3 -- date and time my $strftime_formatter = sub { my ($xl_date_format, $y, $m, $d, $h, $min, $s, $ms) = @_; # choose the proper format for strftime my $ix = 0; # index into the @formats array $ix += 1 if $xl_date_format =~ /[dy]/; # the Excel format contains a date portion $ix += 2 if $xl_date_format =~ /[hs]/; # the Excel format contains a time portion my $strftime_format = $formats[$ix] or die "cell with unexpected Excel date format : $xl_date_format"; # formatting through strftime my $formatted_date = strftime($strftime_format, $s, $min, $h, $d, $m-1, $y-1900); return $formatted_date; }; return $strftime_formatter; } #====================================================================== # GENERAL METHODS #====================================================================== sub sheet_names { my ($self) = @_; my $sheets = $self->sheets; # hashref of shape {$name => $sheet_position} my @sorted_names = sort {$sheets->{$a} <=> $sheets->{$b}} keys %$sheets; return @sorted_names; } sub values { my ($self, $sheet) = @_; $self->backend->_values($sheet, 0)} sub ivalues { my ($self, $sheet) = @_; $self->backend->_values($sheet, 1)} sub formatted_date { my ($self, $val, $date_format, $date_formatter) = @_; # separate date (integer part) from time (fractional part) my ($time, $n_days) = modf($val); # Convert $n_days into a date in Date::Calc format (year, month, day). # The algorithm is quite odd because in the 1900 system, 01.01.1900 == 0 while # in the 1904 system, 01.01.1904 == 1; furthermore, in the 1900 system, # Excel treats 1900 as a leap year. my $base_year = $self->base_year; if ($base_year == 1900) { my $is_after_february_1900 = $n_days > 60; $n_days -= $is_after_february_1900 ? 2 : 1; } my @d = Add_Delta_Days($base_year, 1, 1, $n_days); # decode the fractional part (the time) into hours, minutes, seconds, milliseconds my @t; foreach my $subdivision (24, 60, 60, 1000) { $time *= $subdivision; ($time, my $time_portion) = modf($time); push @t, $time_portion; } # dirty hack to deal with float imprecisions : if 999 millisecs, round to the next second my ($h, $m, $s, $ms) = @t; if ($ms == 999) { $s += 1, $ms = 0; if ($s == 60) { $m += 1, $s = 0; if ($m == 60) { $h += 1, $m = 0; } } } # NOTE : because of this hack, theoretically we could end up with a value # like 01.01.2000 24:00:00, semantically equal to 02.01.2000 00:00:00 but different # in its rendering. # call the date_formatter subroutine $date_formatter //= $self->date_formatter or die ref($self) . " has no date_formatter subroutine"; my $formatted_date = $date_formatter->($date_format, @d, $h, $m, $s, $ms); return $formatted_date; } #====================================================================== # METHODS FOR PARSING EXCEL TABLES #====================================================================== sub table_names { my ($self) = @_; my $table_info = $self->table_info; # sort on table id (field [1] in table_info arrayrefs) my @table_names = sort {$table_info->{$a}{id} <=> $table_info->{$b}{id}} keys %$table_info; return @table_names; } sub table {shift->_table(0, @_);} # 0 = does not want an iterator, just a regular arrayref sub itable {shift->_table(1, @_);} # 1 = does want an iterator my %valid_table_arg = map {$_ => 1} qw/name sheet ref columns no_headers with_totals want_records/; sub _table { my $self = shift; my $want_iterator = shift; # syntactic sugar : ->table('foo') is treated as ->table(name => 'foo') unshift @_, 'name' if scalar(@_) % 2; my %args = @_; # check for invalid args my @invalid_args = grep {!$valid_table_arg{$_}} keys %args; croak "invalid table args: ", join(", ", @invalid_args) if @invalid_args; # defaults $args{want_records} //= 1; # if called with a table name, derive positional args from the internal workbook info if (my $table_name = delete $args{name}) { my $table_info = $self->table_info->{$table_name} or croak sprintf "Excel file '%s' contains no table named '%s'", $self->xlsx, $table_name; $args{$_} //= $table_info->{$_} for keys %$table_info; } # get values from the sheet my ($sheet_ref, $vals_or_it) = $want_iterator ? $self->ivalues($args{sheet}) : $self->values($args{sheet}); # table boundaries my ($scol1, $srow1, $scol2, $srow2) = $self->range_from_ref($sheet_ref); my ($tcol1, $trow1, $tcol2, $trow2) = $self->range_from_ref($args{ref}); my $skip_initial_rows = $trow1 - $srow1; my $keep_rows = $trow2 - $trow1 + 1; my $skip_initial_cols = $tcol1 - $scol1; my $keep_cols = $tcol2 - $tcol1 + 1; # if a totals row is present, skip it, unless the 'with_totals' arg is present $keep_rows -=1 if $args{has_totals} and !$args{with_totals}; # skip initial rows if the table does not start at top row if ($skip_initial_rows) { if ($want_iterator) {$vals_or_it->() while $skip_initial_rows-- > 0;} else {splice @$vals_or_it, 0, $skip_initial_rows} } # read headers from first row -- even if this may be redundant with the 'columns' list declared in the table description my $headers; unless ($args{no_headers}) { $keep_rows--; $headers = $want_iterator ? $vals_or_it->() : shift @$vals_or_it; splice @$headers, 0, $skip_initial_cols if $skip_initial_cols; splice @$headers, $keep_cols; } $args{columns} //= $headers; croak "table contains undefined columns" if grep {!defined $_} @{$args{columns}}; # dual closure : can be used as an iterator or can compute all values in one call my @rows; my $get_values = sub { while (1) { $keep_rows-- and my $vals = $want_iterator ? $vals_or_it->() : shift @$vals_or_it or return; # no more records -- end of iterator splice @$vals, 0, $skip_initial_cols if $skip_initial_cols; splice @$vals, $keep_cols; my $row = $args{want_records} ? do {my %r; @r{@{$args{columns}}} = @$vals; \%r} : $vals; if ($want_iterator) {return $row} # current iteration successful else {push @rows, $row}; } }; # return either an iterator or the accumulated table records return ($args{columns}, $want_iterator ? iter($get_values) : do {$get_values->(); \@rows}); } sub A1_to_num { # convert Excel A1 reference format to a number my ($self, $A1) = @_; my $num = 0; foreach my $digit (unpack "C*", $A1) { $num = $num*26 + $digit-64; } return $num; } sub range_from_ref { # convert a range reference like 'C9:E21' into ($col1, $row1, $col2, $row2) my ($self, $range) = @_; $range =~ /^([A-Z]+)(\d+) # mandatory 1st col and row (?: # .. optionally followed by : # colon ([A-Z]+)(\d+) # and 2nd col and row )? $/x or croak "->range_from_ref($range) : invalid ref"; my @range = ($A1_to_num_memoized{$1} //= $self->A1_to_num($1), $2); # col, row of topleft cell push @range, ($3 ? ($A1_to_num_memoized{$3} //= $self->A1_to_num($3), $4) # col, row of bottomright cell, or .. : @range); # .. copy of topleft cell return @range; } 1; __END__ =head1 NAME Excel::ValueReader::XLSX - extracting values from Excel workbooks in XLSX format, fast =head1 SYNOPSIS my $reader = Excel::ValueReader::XLSX->new(xlsx => $filename_or_handle); # .. or with syntactic sugar : my $reader = Excel::ValueReader::XLSX->new($filename_or_handle); # .. or with LibXML backend : my $reader = Excel::ValueReader::XLSX->new(xlsx => $filename_or_handle, using => 'LibXML'); foreach my $sheet_name ($reader->sheet_names) { my $grid = $reader->values($sheet_name); my $n_rows = @$grid; print "sheet $sheet_name has $n_rows rows; ", "first cell contains : ", $grid->[0][0]; } foreach my $table_name ($reader->table_names) { my ($columns, $records) = $reader->table($table_name); my $n_records = @$records; my $n_columns = @$columns; print "table $table_name has $n_records records and $n_columns columns; ", "column 'foo' in first row contains : ", $records->[0]{foo}; } my $first_grid = $reader->values(1); # the arg can be a sheet index instead of a sheet name # iterator version of ->values() my $iterator = $reader->ivalues($sheet_name); while (my $row = $iterator->()) { process_row($row) } # iterator version of ->table() my ($columns, $iterator) = $reader->itable($table_name); while (my $record = $iterator->()) { process_record($record) } =head1 DESCRIPTION =head2 Purpose This module reads the contents of an Excel file in XLSX format. Unlike other modules like L or L, this module has no support for reading formulas, formats or other Excel internal information; all you get are plain values -- but you get them much faster ! Besides, this API has some features not found in concurrent parsers : =over =item * has support for parsing Excel tables =item * iterator methods for getting one row at a time from a worksheet or from a table -- very useful for sparing memory when dealing with large Excel files. =back =head2 Backends Two different backends may be used for extracting values : =over =item Regex using regular expressions to parse the XML content. =item LibXML using L to parse the XML content. It is probably safer but two to three times slower than the Regex backend (but still much faster than L). =back The default is the C backend. =head2 Sheet numbering Although worksheets are usually accessed by name, they may also be accessed by numerical indices, I. Some other Perl parsing modules use a different convention, where the first sheet has index 0. Here index 1 was chosen to be consistent with the common API for "collections" in Microsoft Office object model. =head1 NOTE ON ITERATORS Methods L and L return I. Each call to the iterator produces a new data row from the Excel content, until reaching the end of data where the iterator returns C. Following the L protocol, iterators support three different but semantically equivalent syntaxes : while (my $row = $iterator->()) { process($row) } while (my $row = $iterator->next) { process($row) } while (<$iterator>) { process($_) } Working with iterators is especially interesting when dealing with large Excel files, because rows can be processed one at a time instead of being loaded all at once in memory. For example a typical pattern for loading the Excel content into a database would be : my $iter = $valuereader->ivalues('MySheet'); my $sth = $dbh->prepare("INSERT INTO MYTABLE(col1, col2, col3) VALUES (?, ?, ?)"); while (my $row = $iter->()) { $sth->execute(@$row); } As another example, suppose a large population table, from which we want to produce a list of list of minor girls. This can be done with a combination of iterator operations : use Iterator::Simple qw/igrep imap/; use Iterator::Simple::Util qw/ireduce/; my $minor_girls = ireduce {"$a, $b"} # successive results joined with ", " imap {"$_->{firstname} $_->{lastname}"} # produce a flat string from an input record with first/last name igrep {$_->{gender} eq 'F' && $_->{age} < 18} # filter input records $valuereader->itable('Population'); # source iterator =head1 METHODS =head2 new my $reader = Excel::ValueReader::XLSX->new(xlsx => $filename_or_handle, %options); # .. or with syntactic sugar : my $reader = Excel::ValueReader::XLSX->new($filename_or_handle, %options); The C argument is mandatory and points to the C<.xlsx> file to be parsed, or to an open filehandle. Options are : =over =item C The backend to be used for parsing; default is 'Regex'. =item C, C, C, C Parameters for formatting date and time values; these are described in the L section below. =back =head2 sheet_names my @sheets = $reader->sheet_names; Returns the list of worksheet names, in the same order as in the Excel file. The first name in the list corresponds to sheet number 1. =head2 active_sheet my $active_sheet_number = $reader->active_sheet; Returns the numerical index (starting at 1) of the sheet that was active when the file was last saved. May return C. =head2 values my ($ref, $grid) = $reader->values($sheet); # or my $grid = $reader->values($sheet); Returns a pair where =over =item * the first item is a string that describes the range of the sheet, in Excel A1 format (like for example C =item * the second item is a bidimensional array of scalars (in other words, an arrayref of arrayrefs of scalars), corresponding to cell values in the specified worksheet. =back The C<$sheet> argument can be either a sheet name or a sheet position (starting at 1). When called in scalar context, this method only returns the grid of values. Unlike the original Excel cells, positions in the grid are zero-based, so for example the content of cell B3 is in C<< $grid->[1][2] >>. The grid is sparse : the size of each row depends on the position of the last non-empty cell in that row. Thanks to Perl's auto-vivification mechanism, any attempt to access a non-existent cell will automatically create the corresponding cell within the grid. The number of rows and columns in the grid can be computed like this : my $nb_rows = @$grid; my $nb_cols = max map {scalar @$_} @$grid; # must import List::Util::max Alternatively, these numbers can also be obtained through the L method. =head2 ivalues my ($ref, $iterator) = $reader->ivalues($sheet); # or my $iterator = $reader->ivalues($sheet); while (my $row = $iterator->()) { say join ", ", @$row; } Like the L method, except that it returns an iterator instead of a fully populated data grid. Data rows are retrieved through successive calls to the iterator. =head2 table_names my @table_names = $reader->table_names; Returns the list of names of tables registered in this workbook. =head2 table my $rows = $reader->table(name => $table_name); # or just : $reader->table($table_name) # or my ($columns, $rows) = $reader->table(name => $table_name); # or my ($columns, $rows) = $reader->table(sheet => $sheet [, ref => $ref] [, columns => \@columns] [, no_headers => 1] [, with_totals => 1] [, want_records => 0] ); In its simplest form, this method returns the content of an Excel table referenced by its table name (in Excel, the table name appears and can be modified through the ribbon tab entry "Table tools / Design"). The table name is passed either through the named argument C, or positionally as unique argument to the method. In list context, the method returns a pair, where the first element is an arrayref of column names, and the second element is an arrayref of rows. In scalar context, the method just returns the arrayref of rows. Rows are normally returned as hashrefs, where keys of the hashes correspond to column names in the table. Under option C<< want_records => 0>>, rows are returned as arrayrefs, and it is up to the client to make the correspondance with column names in C<$columns>. Instead of specifying a table name, it is also possible to give a sheet name or sheet number. By default, this considers the whole sheet content as a single table, where column names are on the first row. However, additional arguments can be supplied to change the default behaviour : =over =item ref a specific range of cells within the sheet that contain the table rows and columns. The range must be expressed using traditional Excel notation, like for example C<"C9:E23"> (columns 3 to 5, rows 9 to 23). =item columns an arrayref containing the list of column names. If absent, column names will be taken from the first row in the table. =item no_headers if true, the first row in the table will be treated as a regular data row, instead of being treated as a list of column names. In that case, since column names cannot be inferred from cell values in the first row, the C argument to the method must be present. =item with_totals For tables that have a "totals" row (turned on by a specific checkbox in the Excel ribbon), this row is normally not included in the result. To include it as a final row, pass a true value to the C option. =back =head1 AUXILIARY METHODS =head2 A1_to_num my $col_num = $reader->A1_to_num('A'); # 1 $col_num = $reader->A1_to_num('AZ'); # 52 $col_num = $reader->A1_to_num('AA'); # 26 $col_num = $reader->A1_to_num('ABC'); # 731 Converts a column expressed as a sequence of capital letters (in Excel's "A1" notation) into the corresponding numeric value. The module also has a global hash C<$Excel::ValueReader::XLSX::A1_to_num_memoized> where results from such conversions are memoized. =head2 range_from_ref my ($col1, $row1, $col2, $row2) = $reader->range_from_ref("C4:BB123"); Returns the coordinates of the topleft and bottomright cells corresponding to a given Excel range. =head2 table_info my $info = $reader->table_info->{$table_name}; Returns information about an Excel table in the form of a hashref with keys =over =item name the name of the table =item ref the range of the table, in Excel notation (e.g "G6:Z44") =item columns an arrayref of column names =item id numerical id of the table =item sheet numerical id of the sheet to which the table belongs =item no_headers boolean flag corresponding to the negation of the checkbox "Headers row" in Excel. By default tables have a header row, both in Excel and in this module. =item has_totals boolean flag corresponding to the checkbox "Totals row" in Excel. By default tables have no totals row, both in Excel and in this module. =back =head2 formatted_date my $date = $reader->formatted_date($numeric_date, $excel_date_format); Given a numeric date, this method returns a string date formatted according to the I routine explained in the next section. The C<$excel_date_format> argument should be the Excel format string for that specific cell; it is used only for for deciding if the numeric value should be presented as a date, as a time, or both. Optionally, a custom date formatter callback could be passed as third argument. =head1 DATE AND TIME FORMATS =head2 Date and time handling In Excel, date and times values are stored as numeric values, where the integer part represents the date, and the fractional part represents the time. What distinguishes such numbers from ordinary numbers is the I applied to the cells where they appear. Numeric formats in Excel are complex to reproduce, in particular because they are locale-dependent; therefore the present module does not attempt to faithfully interpret Excel formats. It just infers from formats which cells should be presented as date and/or time values. All such values are then presented through the same I routine. The default formatter is based on L; other behaviours may be specified through the C parameter (explained below). =head2 Parameters for the default strftime formatter When using the default strftime formatter, the following parameters may be passed to the constructor : =over =item date_format The L format for representing dates. The default is C<%d.%m.%Y>. =item time_format The L format for representing times. The default is C<%H:%M:%S>. =item datetime_format The L format for representing date and time together. The default is the concatenation of C and C, with a space in between. =back =head2 Writing a custom formatter A custom algorithm for date formatting can be specified as a parameter to the constructor my $reader = Excel::ValueReader::XLSX->new(xlsx => $filename, date_formatter => sub {...}); If this parameter is C, date formatting is canceled and therefore date and time values will be presented as plain numbers. If not C, the date formatting routine will we called as : $date_formater->($excel_date_format, $year, $month, $day, $hour, $minute, $second, $millisecond); where =over =item * C<$excel_date_format> is the Excel numbering format associated to that cell, like for example C or C. See the Excel documentation for the syntax description. This is useful to decide if the value should be presented as a date, a time, or both. The present module uses a simple heuristic : if the format contains C or C, it should be presented as a date; if the format contains C or C, it should be presented as a time. The letter C is not taken into consideration because it is ambiguous : depending on the position in the format string, it may represent either a "month" or a "minute". =item * C is the full year, such as 1993 or 2021. The date system of the Excel file (either 1900 or 1904, see L) is properly taken into account. Excel has no support for dates prior to 1900 or 1904, so the C component will always be above this value. =item * C is the numeric value of the month, starting at 1 =item * C is the numeric value of the day in month, starting at 1 =item * C<$hour>, C<$minute>, C<$second>, C<$millisecond> obviously contain the corresponding numeric values. =back =head1 CAVEATS =over =item * This module was optimized for speed, not for completeness of OOXML-SpreadsheetML support; so there may be some edge cases where the output is incorrect with respect to the original Excel data. =item * Embedded newline characters in strings are stored in Excel as C<< \r\n >>, following the old Windows convention. When retrieved through the C backend, the result contains the original C<< \r >> and C<< \n >> characters; but when retrieved through the C backend, C<< \r >> are silently removed by the C package. =back =head1 SEE ALSO The official reference for OOXML-SpreadsheetML format is in L. Introductory material on XLSX file structure can be found at L. Concurrent modules L or L. Another unpublished but working module for parsing Excel files in Perl can be found at L. Some test cases were borrowed from that distribution. Conversions from and to Excel internal date format can also be performed through the L module. =head1 BENCHMARKS Below are some comparative figures. The task computed here was to read a large Excel file with 800131 rows of 7 columns, and report the total number of rows. Reported figures are in seconds. Spreadsheet::ParseXLSX 1272 elapsed, 870 cpu, 4 system Data::XLSX::Parser 125 elapsed, 107 cpu, 1 system Excel::ValueReader::XLSX::Regex 40 elapsed, 32 cpu, 0 system Excel::ValueReader::XLSX::Regex, iterator 34 elapsed, 30 cpu, 0 system Excel::ValueReader::XLSX::LibXML 101 elapsed, 83 cpu, 0 system Excel::ValueReader::XLSX::LibXML, iterator 91 elapsed, 80 cpu, 0 system =head1 ACKNOWLEDGMENTS =over =item * David Flink signaled (and fixed) a bug about strings with embedded newline characters, and signaled that the 'r' attribute in cells is optional. =item * Ulibuck signaled bugs several minor bugs on the LibXML backend. =item * H.Merijn Brand suggested additions to the API and several improvements to the code source. =item * Ephraim Stevens signaled a bug in the table() method with 'ref' param. =back =head1 AUTHOR Laurent Dami, Edami at cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2020-2025 by Laurent Dami. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader/XLSX000755000000000000 014776134025 22635 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader/XLSX/Backend.pm000444000000000000 1366714775775075 24733 0ustar00unknownunknown000000000000package Excel::ValueReader::XLSX::Backend; use utf8; use 5.12.1; use Moose; use Archive::Zip 1.61 qw(AZ_OK); use Carp qw/croak/; use Scalar::Util qw/openhandle/; use Encode qw/decode/; #====================================================================== # ATTRIBUTES #====================================================================== has 'frontend' => (is => 'ro', isa => 'Excel::ValueReader::XLSX', required => 1, weak_ref => 1, handles => [qw/formatted_date/]); my %lazy_attrs = ( zip => 'Archive::Zip', date_styles => 'ArrayRef', strings => 'ArrayRef', workbook_data => 'HashRef', table_info => 'HashRef', sheet_for_table => 'ArrayRef', ); while (my ($name, $type) = each %lazy_attrs) { has $name => (is => 'ro', isa => $type, builder => "_$name", init_arg => undef, lazy => 1); } #====================================================================== # ATTRIBUTE CONSTRUCTORS #====================================================================== sub _zip { my $self = shift; my $zip = Archive::Zip->new; my $xlsx_source = $self->frontend->xlsx; my ($meth, $source_name) = openhandle($xlsx_source) ? (readFromFileHandle => 'filehandle') : (read => $xlsx_source); my $result = $zip->$meth($xlsx_source); $result == AZ_OK or die "cannot unzip from $source_name"; return $zip; } sub _table_info { my ($self) = @_; my %table_info; my @table_members = $self->zip->membersMatching(qr[^xl/tables/table\d+\.xml$]); foreach my $table_member (map {$_->fileName} @table_members) { my ($table_id) = $table_member =~ /table(\d+)\.xml/; my $table_xml = $self->_zip_member_contents($table_member); my $this_table_info = $self->_parse_table_xml($table_xml); # defined in subclass $this_table_info->{id} = $table_id; $this_table_info->{sheet} = $self->sheet_for_table->[$table_id] or croak "could not find sheet id for table $table_id"; $table_info{$this_table_info->{name}} = $this_table_info; } return \%table_info; } sub _sheet_for_table { my ($self) = @_; my @sheet_for_table; my @rel_members = $self->zip->membersMatching(qr[^xl/worksheets/_rels/sheet\d+\.xml\.rels$]); foreach my $rel_member (map {$_->fileName} @rel_members) { my ($sheet_id) = $rel_member =~ /sheet(\d+)\.xml/; my $rel_xml = $self->_zip_member_contents($rel_member); my @table_ids = $self->_table_targets($rel_xml); # defined in subclass $sheet_for_table[$_] = $sheet_id foreach @table_ids; } return \@sheet_for_table; } for my $abstract_meth (qw/_date_styles _strings _workbook_data/) { no strict 'refs'; *{$abstract_meth} = sub {die "->$abstract_meth() should be implemented in subclass"} } #====================================================================== # METHODS #====================================================================== # accessors to workbook data sub base_year {shift->workbook_data->{base_year} } sub sheets {shift->workbook_data->{sheets} } sub active_sheet {shift->workbook_data->{active_sheet}} sub Excel_builtin_date_formats { my @numFmt; # source : section 18.8.30 numFmt (Number Format) in ECMA-376-1:2016 # Office Open XML File Formats - Fundamentals and Markup Language Reference $numFmt[14] = 'mm-dd-yy'; $numFmt[15] = 'd-mmm-yy'; $numFmt[16] = 'd-mmm'; $numFmt[17] = 'mmm-yy'; $numFmt[18] = 'h:mm AM/PM'; $numFmt[19] = 'h:mm:ss AM/PM'; $numFmt[20] = 'h:mm'; $numFmt[21] = 'h:mm:ss'; $numFmt[22] = 'm/d/yy h:mm'; $numFmt[45] = 'mm:ss'; $numFmt[46] = '[h]:mm:ss'; $numFmt[47] = 'mmss.0'; return @numFmt; } sub _zip_member_contents { my ($self, $member) = @_; my $contents = $self->zip->contents($member) or die "no contents for member $member"; return decode('UTF-8', $contents); } sub _zip_member_name_for_sheet { my ($self, $sheet) = @_; # check that sheet name was given $sheet or die "->values(): missing sheet name"; # get sheet id my $id = $self->sheets->{$sheet}; $id //= $sheet if $sheet =~ /^\d+$/; $id or die "no such sheet: $sheet"; # construct member name for that sheet return "xl/worksheets/sheet$id.xml"; } 1; __END__ =head1 NAME Excel::ValueReader::XLSX::Backend -- abstract class, parent for the Regex and LibXML backends =head1 DESCRIPTION L has two possible implementation backends for parsing C files : L, based on regular expressions, or L, based on the libxml2 library. Both backends share some common features, so the present class implements those common features. This is about internal implementation; it should be of no interest to external users of the module. =head1 ATTRIBUTES A backend instance possesses the following attributes : =over =item frontend a weak reference to the frontend instance =item zip an L instance for accessing the contents of the C file =item date_styles an array of numeric styles for presenting dates and times. Styles are either Excel's builtin styles, or custom styles defined in the workbook. =item strings an array of all shared strings within the workbook =item workbook_data some metadata information about the workbook =back =head1 ABSTRACT METHODS Not defined in this abstract class, but implemented in subclasses. =over =item values Inspects all cells within the XSLX files and returns a bi-dimensional array of values. =back =head1 AUTHOR Laurent Dami, Edami at cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2021 by Laurent Dami. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader/XLSX/Backend000755000000000000 014776134025 24164 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader/XLSX/Backend/LibXML.pm000444000000000000 2724414775033705 26001 0ustar00unknownunknown000000000000package Excel::ValueReader::XLSX::Backend::LibXML; use utf8; use 5.12.1; use Moose; use Scalar::Util qw/looks_like_number/; use XML::LibXML::Reader qw/XML_READER_TYPE_END_ELEMENT/; use Iterator::Simple qw/iter/; extends 'Excel::ValueReader::XLSX::Backend'; #====================================================================== # LAZY ATTRIBUTE CONSTRUCTORS #====================================================================== sub _strings { my $self = shift; my $reader = $self->_xml_reader_for_zip_member('xl/sharedStrings.xml'); my @strings; my $last_string; NODE: while ($reader->read) { next NODE if $reader->nodeType == XML_READER_TYPE_END_ELEMENT; my $node_name = $reader->name; if ($node_name eq 'si') { push @strings, $last_string if defined $last_string; $last_string = ''; } elsif ($node_name eq '#text') { $last_string .= $reader->value; } } push @strings, $last_string if defined $last_string; return \@strings; } sub _workbook_data { my $self = shift; my %workbook_data = (sheets => {}, base_year => 1900); my $sheet_id = 1; my $reader = $self->_xml_reader_for_zip_member('xl/workbook.xml'); NODE: while ($reader->read) { next NODE if $reader->nodeType == XML_READER_TYPE_END_ELEMENT; if ($reader->name eq 'sheet') { my $name = $reader->getAttribute('name') or die "sheet node without name"; $workbook_data{sheets}{$name} = $sheet_id++; } elsif ($reader->name eq 'workbookPr' and my $date_attr = $reader->getAttribute('date1904')) { $workbook_data{base_year} = 1904 if $date_attr eq '1' or $date_attr eq 'true'; # this workbook uses the 1904 calendar } elsif ($reader->name eq 'workbookView' and my $active_attr = $reader->getAttribute('activeTab')) { $workbook_data{active_sheet} = $active_attr + 1 if defined $active_attr; } } return \%workbook_data; } sub _date_styles { my $self = shift; state $date_style_regex = qr{[dy]|\bmm\b}; my @date_styles; # read from the styles.xml zip member my $xml_reader = $self->_xml_reader_for_zip_member('xl/styles.xml'); # start with Excel builtin number formats for dates and times my @numFmt = $self->Excel_builtin_date_formats; my $expected_subnode = undef; # add other date formats explicitly specified in this workbook NODE: while ($xml_reader->read) { next NODE if $xml_reader->nodeType == XML_READER_TYPE_END_ELEMENT; # special treatment for some specific subtrees -- see 'numFmt' and 'xf' below if ($expected_subnode) { my ($name, $depth, $handler) = @$expected_subnode; if ($xml_reader->name eq $name && $xml_reader->depth == $depth) { # process that subnode and go to the next node $handler->(); next NODE; } elsif ($xml_reader->depth < $depth) { # finished handling subnodes; back to regular node treatment $expected_subnode = undef; } } # regular node treatement if ($xml_reader->name eq 'numFmts') { # start parsing nodes for numeric formats $expected_subnode = [numFmt => $xml_reader->depth+1 => sub { my $id = $xml_reader->getAttribute('numFmtId'); my $code = $xml_reader->getAttribute('formatCode'); $numFmt[$id] = $code if $id && $code && $code =~ $date_style_regex; }]; } elsif ($xml_reader->name eq 'cellXfs') { # start parsing nodes for cell formats $expected_subnode = [xf => $xml_reader->depth+1 => sub { state $xf_count = 0; my $numFmtId = $xml_reader->getAttribute('numFmtId'); my $code = $numFmt[$numFmtId]; # may be undef $date_styles[$xf_count++] = $code; }]; } } return \@date_styles; } #====================================================================== # METHODS #====================================================================== sub _xml_reader { my ($self, $xml) = @_; my $reader = XML::LibXML::Reader->new(string => $xml, no_blanks => 1, no_network => 1, huge => 1); return $reader; } sub _xml_reader_for_zip_member { my ($self, $member_name) = @_; my $contents = $self->_zip_member_contents($member_name); return $self->_xml_reader($contents); } sub _values { my ($self, $sheet, $want_iterator) = @_; # prepare for traversing the XML structure my $has_date_formatter = $self->frontend->date_formatter; my $sheet_member_name = $self->_zip_member_name_for_sheet($sheet); my $xml_reader = $self->_xml_reader_for_zip_member($sheet_member_name); # get sheet 'ref' attribute from the initial preamble my $ref; PREAMBLE: while ($xml_reader->read) { if ($xml_reader->name eq 'dimension') { $ref = $xml_reader->getAttribute('ref'); last PREAMBLE; } } my ($row_num, $col_num, @rows) = (0, 0); my ($cell_type, $cell_style, $seen_node); # dual closure : may be used as an iterator or as a regular sub, depending on $want_iterator. Of course # it would have been simpler to just write an iterator, and call it in a loop if the client wants all rows # at once ... but thousands of additional sub calls would slow down the process. So this more complex implementation # is for the sake of processing speed. my $get_values = sub { # in iterator mode, if we have a row ready, just return it return shift @rows if $want_iterator and @rows > 1; # otherwise loop on matching nodes NODE: while ($xml_reader->read) { my $node_name = $xml_reader->name; my $node_type = $xml_reader->nodeType; $xml_reader->finish and last NODE if $node_name eq 'sheetData' && $node_type == XML_READER_TYPE_END_ELEMENT; next NODE if $node_type == XML_READER_TYPE_END_ELEMENT; if ($node_name eq 'row') { my $prev_row = $row_num; $row_num = $xml_reader->getAttribute('r') // $row_num+1; $col_num = 0; push @rows, [] for 1 .. $row_num-$prev_row; # in iterator mode, if we have a closed empty row, just return it return shift @rows if $want_iterator and @rows > 1; } elsif ($node_name eq 'c') { my $A1_cell_ref = $xml_reader->getAttribute('r') // ''; my ($col_A1, $given_row) = ($A1_cell_ref =~ /^([A-Z]+)(\d+)$/); $given_row //= $row_num; if ($given_row < $row_num) {die "cell claims to be in row $given_row while current row is $row_num"} elsif ($given_row > $row_num) {push @rows, [] for 1 .. $given_row-$row_num; $col_num = 0; $row_num = $given_row;} # deal with the col number given in the 'r' attribute, if present if ($col_A1) {$col_num = $Excel::ValueReader::XLSX::A1_to_num_memoized{$col_A1} //= Excel::ValueReader::XLSX->A1_to_num($col_A1)} else {$col_num++} $cell_type = $xml_reader->getAttribute('t'); $cell_style = $xml_reader->getAttribute('s'); $seen_node = ''; } elsif ($node_name =~ /^[vtf]$/) { # remember that we have seen a 'value' or 'text' or 'formula' node $seen_node = $node_name; } elsif ($node_name eq '#text') { #start processing cell content my $val = $xml_reader->value; $cell_type //= ''; if ($seen_node eq 'v') { if ($cell_type eq 's') { if (looks_like_number($val)) { $val = $self->strings->[$val]; # string -- pointer into the global array of shared strings } else { warn "unexpected non-numerical value: $val inside a node of shape \n"; } } elsif ($cell_type eq 'e') { $val = undef; # error -- silently replace by undef } elsif ($cell_type =~ /^(n|d|b|str|)$/) { # number, date, boolean, formula string or no type : content is already in $val # if this is a date, replace the numeric value by the formatted date if ($has_date_formatter && $cell_style && looks_like_number($val) && $val >= 0) { my $date_style = $self->date_styles->[$cell_style]; $val = $self->formatted_date($val, $date_style) if $date_style; } } else { # handle unexpected cases warn "unsupported type '$cell_type' in cell L${row_num}C${col_num}\n"; $val = undef; } # insert this value into the last row $rows[-1][$col_num-1] = $val; } elsif ($seen_node eq 't' && $cell_type eq 'inlineStr') { # inline string -- accumulate all #text nodes until next cell no warnings 'uninitialized'; $rows[-1][$col_num-1] .= $val; } elsif ($seen_node eq 'f') { # formula -- just ignore it } else { # handle unexpected cases warn "unexpected text node in cell L${row_num}C${col_num}: $val\n"; } } } # end of XML nodes. In iterator mode, return a row if we have one return @rows ? shift @rows : undef if $want_iterator; }; # decide what to return depending on the dual mode my $retval = $want_iterator ? iter($get_values) : do {$get_values->(); \@rows}; # run the closure and return the rows return ($ref, $retval); } sub _table_targets { my ($self, $rel_xml) = @_; my $xml_reader = $self->_xml_reader($rel_xml); my @table_targets; # iterate through XML nodes NODE: while ($xml_reader->read) { my $node_name = $xml_reader->name; my $node_type = $xml_reader->nodeType; next NODE if $node_type == XML_READER_TYPE_END_ELEMENT; if ($node_name eq 'Relationship') { my $target = $xml_reader->getAttribute('Target'); if ($target =~ m[tables/table(\d+)\.xml]) { # just store the table id (positive integer) push @table_targets, $1; } } } return @table_targets; } sub _parse_table_xml { my ($self, $xml) = @_; my %table_info; my $xml_reader = $self->_xml_reader($xml); # iterate through XML nodes NODE: while ($xml_reader->read) { my $node_name = $xml_reader->name; my $node_type = $xml_reader->nodeType; next NODE if $node_type == XML_READER_TYPE_END_ELEMENT; if ($node_name eq 'table') { %table_info = ( name => $xml_reader->getAttribute('displayName'), ref => $xml_reader->getAttribute('ref'), no_headers => do {my $has_headers = $xml_reader->getAttribute('headerRowCount'); defined $has_headers && !$has_headers}, has_totals => $xml_reader->getAttribute('totalsRowCount'), ); } elsif ($node_name eq 'tableColumn') { push @{$table_info{columns}}, $xml_reader->getAttribute('name'); } } return \%table_info } 1; __END__ =head1 NAME Excel::ValueReader::XLSX::Backend::LibXML - using LibXML for extracting values from Excel workbooks =head1 DESCRIPTION This is one of two backend modules for L; the other possible backend is L. This backend parses OOXML structures using L. =head1 AUTHOR Laurent Dami, Edami at cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2020-2022 by Laurent Dami. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. Excel-ValueReader-XLSX-1.16/lib/Excel/ValueReader/XLSX/Backend/Regex.pm000444000000000000 2564414775030425 25762 0ustar00unknownunknown000000000000package Excel::ValueReader::XLSX::Backend::Regex; use utf8; use 5.12.1; use Moose; use Scalar::Util qw/looks_like_number/; use Iterator::Simple qw/iter/; extends 'Excel::ValueReader::XLSX::Backend'; #====================================================================== # LAZY ATTRIBUTE CONSTRUCTORS #====================================================================== sub _strings { my $self = shift; my @strings; # read from the sharedStrings zip member my $contents = $self->_zip_member_contents('xl/sharedStrings.xml'); # iterate on nodes while ($contents =~ m[(.*?)]sg) { my $innerXML = $1; # concatenate contents from all nodes (usually there is only 1) and decode XML entities my $string = join "", ($innerXML =~ m[]*>(.+?)]sg); _decode_xml_entities($string); push @strings, $string; } return \@strings; } sub _workbook_data { my $self = shift; my %workbook_data; # read from the workbook.xml zip member my $workbook = $self->_zip_member_contents('xl/workbook.xml'); # extract sheet names my @sheet_names = ($workbook =~ m[ $_+1} 0 .. $#sheet_names}; # does this workbook use the 1904 calendar ? my ($date1904) = $workbook =~ m[date1904="(.+?)"]; $workbook_data{base_year} = $date1904 && $date1904 =~ /^(1|true)$/ ? 1904 : 1900; # active sheet my ($active_tab) = $workbook =~ m[]+activeTab="(\d+)"]; $workbook_data{active_sheet} = $active_tab + 1 if defined $active_tab; return \%workbook_data; } sub _date_styles { my $self = shift; state $date_style_regex = qr{[dy]|\bmm\b}; # read from the styles.xml zip member my $styles = $self->_zip_member_contents('xl/styles.xml'); # start with Excel builtin number formats for dates and times my @numFmt = $self->Excel_builtin_date_formats; # add other date formats explicitly specified in this workbook while ($styles =~ m[]g) { my ($id, $code) = ($1, $2); $numFmt[$id] = $code if $code =~ $date_style_regex; } # read all cell formats, just rembember those that involve a date number format my ($cellXfs) = ($styles =~ m[(.+?)]); my @cell_formats = $self->_extract_xf($cellXfs); my @date_styles = map {$numFmt[$_->{numFmtId}]} @cell_formats; return \@date_styles; # array of shape (xf_index => numFmt_code) } sub _extract_xf { my ($self, $xml) = @_; state $xf_node_regex = qr{ /]*+) # attributes (captured in $1) (?: # non-capturing group for an alternation : /> # .. either an xml closing without content | # or > # .. closing for the xf tag .*? # .. then some formatting content # .. then the ending tag for the xf node ) }x; my @xf_nodes; while ($xml =~ /$xf_node_regex/g) { push @xf_nodes, _xml_attrs($1); } return @xf_nodes; } #====================================================================== # METHODS #====================================================================== sub _values { my ($self, $sheet, $want_iterator) = @_; # regex for the initial preamble state $preamble_regex = qr( # node specifying the range of defined cells .*? # start container node for actual rows and cells content )xs; # regex for extracting information from cell nodes state $row_or_cell_regex = qr( <(row) # row tag ($1) (?:\s+r="(\d+)")? # optional row number ($2) [^>/]*? # unused attrs > # end of tag | # .. or .. <(c) # cell tag ($3) (?: \s+ | (?=>) ) # either a space before attrs, or end of tag (?:r="([A-Z]+)(\d+)")? # capture col ($4) and row ($5) [^>/]*? # unused attrs (?:s="(\d+)"\s*)? # style attribute ($6) (?:t="(\w+)"\s*)? # type attribute ($7) (?: # non-capturing group for an alternation : /> # .. either an xml closing without content | # or > # .. closing xml tag, followed by .. (?: (.+?) # .. a value ($8) | # or (.+?) # .. some node content ($9) ) # followed by a closing cell tag ) )xs; # NOTE : this regex uses positional capturing groups; it would be more readable with named # captures instead, but this would double the execution time on big Excel files, so I # stick to plain old capturing groups. # does this instance want date formatting ? my $has_date_formatter = $self->frontend->date_formatter; # get worksheet XML my $contents = $self->_zip_member_contents($self->_zip_member_name_for_sheet($sheet)); # parse the preamble my ($ref) = $contents =~ /$preamble_regex/g; # /g to leave the pos() cursor before the 1st cell # variables for the closure below my ($row_num, $col_num, @rows) = (0, 0); # dual closure : may be used as an iterator or as a regular sub, depending on $want_iterator. Of course # it would have been simpler to just write an iterator, and call it in a loop if the client wants all rows # at once ... but thousands of additional sub calls would slow down the process. So this more complex implementation # is for the sake of processing speed. my $get_values = sub { # in iterator mode, if we have a row ready, just return it return shift @rows if $want_iterator and @rows > 1; # otherwise loop on matching nodes while ($contents =~ /$row_or_cell_regex/cg) { # /g allows the iterator to remember where the last cell left off if ($1) { # this is a 'row' tag my $prev_row = $row_num; $row_num = $2 // $row_num+1; # if present, capture group $2 is the row number $col_num = 0; push @rows, [] for 1 .. $row_num-$prev_row; # in iterator mode, if we have a closed empty row, just return it return shift @rows if $want_iterator and @rows > 1; } elsif ($3) { # this is a 'c' tag my ($col_A1, $given_row, $style, $cell_type, $val, $inner) = ($4, $5, $6, $7, $8, $9); # deal with the row number given in the 'r' attribute, if present $given_row //= $row_num; if ($given_row < $row_num) {die "cell claims to be in row $given_row while current row is $row_num"} elsif ($given_row > $row_num) {push @rows, [] for 1 .. $given_row-$row_num; $col_num = 0; $row_num = $given_row;} # deal with the col number given in the 'r' attribute, if present if ($col_A1) {$col_num = $Excel::ValueReader::XLSX::A1_to_num_memoized{$col_A1} //= Excel::ValueReader::XLSX->A1_to_num($col_A1)} else {$col_num++} # handle the cell value according to cell type $cell_type //= ''; if ($cell_type eq 'inlineStr') { # this is an inline string; gather all nodes within the cell node $val = join "", ($inner =~ m[(.+?)]g); _decode_xml_entities($val) if $val; } elsif ($cell_type eq 's') { # this is a string cell; $val is a pointer into the global array of shared strings $val = $self->strings->[$val]; } else { # this is a plain value ($val) = ($inner =~ m[(.*?)]) if !defined $val && $inner; _decode_xml_entities($val) if $val && $cell_type eq 'str'; # if necessary, transform the numeric value into a formatted date if ($has_date_formatter && $style && looks_like_number($val) && $val >= 0) { my $date_style = $self->date_styles->[$style]; $val = $self->formatted_date($val, $date_style) if $date_style; } } # insert this value into the last row $rows[-1][$col_num-1] = $val; } else {die "found a node which is neither a nor a (cell)"} } # end of regex matches. In iterator mode, return a row if we have one return @rows ? shift @rows : undef if $want_iterator; }; # decide what to return depending on the dual mode my $retval = $want_iterator ? iter($get_values) : do {$get_values->(); \@rows}; # run the closure and return the rows return ($ref, $retval); } sub _table_targets { my ($self, $rel_xml) = @_; my @table_targets = $rel_xml =~ m[]g and my $table_attrs = _xml_attrs($1) or die "invalid table XML: $xml"; # extract relevant attributes my %table_info = ( name => $table_attrs->{displayName}, ref => $table_attrs->{ref}, no_headers => exists $table_attrs->{headerRowCount} && !$table_attrs->{headerRowCount}, has_totals => $table_attrs->{totalsRowCount}, columns => [$xml =~ m{]+? name="([^"]+)"}gx], ); # decode entites for all string values _decode_xml_entities($_) for $table_info{name}, @{$table_info{columns}}; return \%table_info; } #====================================================================== # AUXILIARY FUNCTIONS #====================================================================== sub _decode_xml_entities { state $xml_entities = { amp => '&', lt => '<', gt => '>', quot => '"', apos => "'", }; state $entity_names = join '|', keys %$xml_entities; state $regex_entities = qr/&($entity_names);/; # substitute in-place $_[0] =~ s/$regex_entities/$xml_entities->{$1}/eg; } sub _xml_attrs { my $attrs_list = shift; my %attr = $attrs_list =~ m[(\w+)="(.+?)"]g; return \%attr; } 1; __END__ =head1 NAME Excel::ValueReader::XLSX::Backend::Regex - using regexes for extracting values from Excel workbooks =head1 DESCRIPTION This is one of two backend modules for L; the other possible backend is L. This backend parses OOXML structures using regular expressions. =head1 AUTHOR Laurent Dami, Edami at cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2020-2023 by Laurent Dami. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut Excel-ValueReader-XLSX-1.16/t000755000000000000 014776134025 16275 5ustar00unknownunknown000000000000Excel-ValueReader-XLSX-1.16/t/Mappe1.xlsx000444000000000000 2460014363164702 20514 0ustar00unknownunknown000000000000PK!d[Content_Types].xml (Ĕn0 US0M7C#$ `@ڥQ~qcf+h+E \qRO;!)Jp g;,EMĪFa8ޙ(8AU 59neZ 1>L--eOk%`Qd֫*k*EL*WNrwgMӡ`ʥFC6Q^Trm姏aJ? WAiEZF>Qa|ID?q;y~)̑#m,=\"w~j;> wrӫo6;, |7kץv)pv١3tLsmPK!U0#L _rels/.rels (MO0 HݐBKwAH!T~I$ݿ'TG~KAsc+EY5iQw~ om4]~ ɉ -i^Yy\YD>qW$KS3b2k T>:3[/%s* }+4?rV PK!ӏjvixl/worksheets/sheet1.xmlK0{w<6UmԽU٘!X8MVU{I*t%`30LGymɔ蕭u.ohFeg{( xzxnn[@P}Ic^`-ȀnցIcb:͘GpivpDt2`[Ҍ3mvHYEҝ/#JQ㺷NV}TxƧeVzۄ u9˙Tg:0zJ-$-q7b>+v.,$E$$,΢2KU3żK^DLb> {= d0Tv5w5G*~:>.`Rv>XsZ:*Fۓ"__l/J"һw,lL0 _;L`(3, m߾R`IoWX|r:|3:VP/wcSr|_ocVJht_p rڙkKu\[IƏU=9½>|=}5M5M5M5MHqUA,!DsrMrMrMrM%aYJќ%&&&&K8ZKќ%&&&&K]?hrM"}S{ި~3&;ݿVf4giyM:ޟCZ4giiivM: LOڗТ9K5MKش|>\v3= %Χ.&ֱ)IL'9a@Rd!3 Qg!sYwAQ B<> .ɝ%~fcQeϨPUo>>>72.X`3ӄ2zIFvFCt]- C@O'uPUoCcJAbdf|?(+D_Uonq1i\LL$"lU t jnq1bBgIWq`n|TE{C-DGbUEtO:_Uonbgt$GWtSW>2zKgrXbgs.[S'kUonbgt$nqB|Q9}NnoprAb7K퓋(M:`xzVhF:'?oO0ߛ-!nφ $cr(|{7#^nӫgR0ER)xԢV9󨐾/L͹hc)PY ń̟K=-2ɵ ķ)F)Xp&\9d$t kP+.GȜhf,NNyR)a`J4PK!!ڰxl/worksheets/sheet2.xmlMk0 BwG?R)!ٲew{Vq,bIFRޱCӅ\BAif̌?uG^yeMEdJ ike1)AZt@EO`η !%c^ zxu[{i =Jw 6r3A'[Op}I{DlTiRe5։M}y*$9:\13hʤt&L5__ y!]W. |6r^}[YsjYG=Ob>. ]ME|?ߙ ZXOc:Qώl^TZ=jF~ڶ< OkՆI!)HM-qI,IԷ)HLOM,J+VIM+U23WR(LπK &&`_RC2#51%$i_egPK!X7N xl/theme/theme1.xmlY͋7? sw5%l$dQV32%9R(Bo=@ $'#$lJZv G~ztҽzG ’_P=ؘ$Ӗk8(4|OHe n ,K۟~rmDlI9*f8&H#ޘ+R#^bP{}2!#o/)NP#RB,Drі3ߓGC/?}2!*7reÊOVA+@:_> @4Yt kүݽzZP},5`/Zx J>4;=[^j_"J5t)Uծ Fwf LylXeb)bt"IO.fxF]D'.Fx30åJiP 7Q!Kڐ#Nf_yO?GͭUYr;(r~WI>&_שx퓗O?zIw {7Y t؏$"@Cu_FQm2.]%q|5-cøW\dꞜMM\swQb?n-3oPH4 bXB,gMwxD.+rC\.!Ԗon{F]# QCL-7^Fsb!]$# >2q}!!SLc!\29U`w"\C]ĘnfD&3q)L{!‒M7- WӤύ`Wb]Av f"wRvO{'gMAԭԅSI^#PA8r]#ވuvg6{ Kbk &fp-XEԹNi0@ad;1IX({q0gPORvN8p ':gW5Uj6Z漖9e\o_l.[>B\P+tGx{,YM92gs"͠5TΩTO7c:FzX7Q ݺ48t˪P 8tdJNuui=dU DuF蕝M ~eWVQWn^[~dhAy>VqJi79%2H7V[D2H7# #xβleyH-+!7V$rhb2M_p2B?1|g;Bu!: fq!{HD5lGIW@!ڶr5 A' ZJ!;s~Ar8( [Y'vͫ(C8e'I)\ƒ:`}Q7C:@t\ G!FLwETUTh2|`?v =c2|om"+݅nZI-LMC0Zun$9+< dIþ J-[j n #Y>6J@fD}471o}qѝ]׼d/.ɒCb;fİGyZke;T꽲KA鯪pK蕧7HX"LBdA:Ȝ(*ౡ wn5\s ^OrȽ3c`s:gVs!&<̨V8;/CQL6`]IvLy*Xm!FÛ{ku ;m-TV6ZQs1 qZv_# Cs8ß K؍hۊõUK 0u7+/WC;{W[_E@م|ND{&S TЎc nυO%0zj6iMp>r+v}FMr]6z) uN׺ɷS@_W{a3|fKG"ç'wDsihOg%pf, z$z-an~L3^HCFU'5c#ۓ z4:Sx^'Qat,IP$Q7IL'|ICEÛ'+%\:*4H0 Q rPK!sSxl/sharedStrings.xml\n0 C;0)MHְ.~zj@>?eW)zY8Oi^s;QƉ|'= M3kZ9fڼg%8[Tgō/({X[d,`:B<C^^<&WPK-!d[Content_Types].xmlPK-!U0#L _rels/.relsPK-!oxl/workbook.xmlPK-!JaGr xl/_rels/workbook.xml.relsPK-!ӏjvi xl/worksheets/sheet1.xmlPK-!!ڰKxl/worksheets/sheet2.xmlPK-!X7N xl/theme/theme1.xmlPK-!^.u xl/styles.xmlPK-!sS] xl/sharedStrings.xmlPK-!yFgN!docProps/core.xmlPK-!ϣM#docProps/app.xmlPK &Excel-ValueReader-XLSX-1.16/t/cells_without_r_attr.xlsx000444000000000000 1700714372002554 23630 0ustar00unknownunknown000000000000PK!bhS[Content_Types].xmlMn0z[?nN i'زL~PK!U0#L _rels/.relsN0 HCnH *`Gm( н=ဠ=Ʊ??Ө9^mQbgR?n@D(58¡?H)ŮQe=b4O O#aEOfqW~3Z0jG{>y^Ö ?yإ3-βC4r`fhM!8%T4Šm@wlz\b0$ql#HLjkC^H?S⁁MW=_ ʥQ1):bըU!W(>Ჸy>-OPK!L;3[docProps/core.xmlQk0%mҖ 4a0do!jX$V[0KrӽX']4!(!Dy"n4M o1`vm7cǷKCqX| 6 G3<06#Hc 8hp`Y+N%M mۤ{kȟ5TSnV3/j",z-A<_8t,(|P>rdyLLdBɿWg:51%_@/PK!>xl/_rels/workbook.xml.relsMj0A̾RJlB!=Ɩ- ǷƁJ7Ҭ7 1Q(A7S<< !xT0!Z?97#IcRq8j*BDoڐF͹LurU2=>; -fжm0o#z>!! :d> ǯ܋ V%>BړC#ǟs$ 9оp6 #O6PK!ylR xl/styles.xml]o0'?XOJRHiR[8`6,lq$6+>~kһF gr2݄1Uꊫu.`uTUTh2|`ߥ{0 ƹ[nFoZIL͚حa~$1+fe J-ZF=6t%jhhlbԘSV}SGhkw\뚗)Z$ _Ggo̕1l}pZ9JS lًUᗼeTa OEQɺ{*p/Trq cp^$]v qvNShcF0AxyBy_Zi6`C;@ݕ6|}<vƏNo_tn:O+NZQᑧ% vS#tUӟB0t ;L7!c75%n ]*gD@f^Ue`T; 'VY_^cV?NEc_5ɺvD;3aax(`'%4/dt?_,i?=ofV@9h2HãKgKѤp)7Nm0ψ}i mJ?fyjy)DelNn~rsѤqh)hێ],W3~_חR{|MyW?~_ë'"/uBۧ/=}WDptIq Y LI D R@ޚ!uj»2x=>VgzI'|1G"N7 %Yp4 ؇;sjzV6׵^)BZSOfP 7]n3O-ʴVCZ)Uk]2Aڻ.s6XNv[Ndlt:RqjSzC0 Š=}:O7^܇R2Kr*T_,^jnԪu%mZn[Fosfk4NRݩv,+[VB*5jc7:;y85|0okPK!xB,xl/workbook.xmlU]o:}x'|`5RZEinRrnT1&7yX33peӵ#~adxuiE!k.'*/g?x^.F!mY5#Q`{[Z6#78V9Ǝj "hKЗ UuD7)Q#CMzݖmWW$WHNjmys-ӝn$۳44gPKKV3xl/sharedStrings.xml One two three four five six seven eight nine PKKV)Exl/worksheets/sheet1.xml 01234567811223344 PK!bhS[Content_Types].xmlPK!U0#L _rels/.relsPK!aI zdocProps/app.xmlPK!L;3[AdocProps/core.xmlPK!>xl/_rels/workbook.xml.relsPK!ylR xl/styles.xmlPK!  xl/theme/theme1.xmlPK!xB,Vxl/workbook.xmlPKKV3xl/sharedStrings.xmlPKKV)Exl/worksheets/sheet1.xmlPK qExcel-ValueReader-XLSX-1.16/t/from_filehandle.t000444000000000000 146114775126315 21741 0ustar00unknownunknown000000000000use utf8; use strict; use warnings; use Test::More 1.302195; use Module::Load::Conditional 0.66 qw/check_install/; use Excel::ValueReader::XLSX; (my $tst_dir = $0) =~ s/from_filehandle\.t$//; $tst_dir ||= "./"; my $xl_file = $tst_dir . "valuereader.xlsx"; my @backends = ('Regex'); push @backends, 'LibXML' if check_install(module => 'XML::LibXML::Reader'); foreach my $backend (@backends) { open my $fh, "<", $xl_file or die "open $xl_file : $!"; my $reader = Excel::ValueReader::XLSX->new($fh, using => $backend); # check sheet names my @sheet_names = $reader->sheet_names; my @expected_sheet_names = qw/Test Empty Entities Tab_entities Dates Tables RemoteCells/; is_deeply(\@sheet_names, \@expected_sheet_names, "filehandle, sheet names using $backend"); } done_testing; Excel-ValueReader-XLSX-1.16/t/ulibuck.xlsx000444000000000000 2653514521525034 21033 0ustar00unknownunknown000000000000PK!S _rels/.relsN0 {*5@LH!4$năD Îq~bLn,0&K^UY&c}qy/6͢~8GRoC*rOJAʤ{tJ MKcd=@r]Uw2dYqgVMmk5nIz>3W"!vJL|8 e ye} 0HM!ӷ!阘rpbͼ0gt{M#}HL3_JZPK4PK!S xl/styles.xml[Q8~_xBB†S%^NV:t8xۤl vn7I7$<3fQ&EWH9xg!Ϗ@Ù~4g[2[\3fYy)ߑ bYoil (Eж]+ 6S)ˍ@ɒ&{,'(tg|$R?AN)i(Bih?c 倜$Q~{޾e[X,<)7l-3=[q=$4⥻ÁY(1}f hAJ8"b M83hc$F"-}z߫]{z^qqç{y)-7 6 HRh}@IS% | ƚ;7-B\NbcI(DyM 0 Q=raMb1k/$Hj5)0 rDhimV{L;TƹOTSdJ f sL0#K3hЩhFfإh.ZE~bj3bjw9$?q<\zv*r+ {.kg|!o⬄=i3j/em|yj4v ִGuf47ٝ瀫嬳#'Q? \mXmtst&ig̳h䌑 aOu_Yu#*ٛ36R)K//_?!5W3OK5h[>%%XY>ISP[G 0An *AOBnk] 3:ipw*'HE Ş 5TUsv[]_H K_5a_#F{tjʏ.F՚yl{ښ3b\}xP?9}umbݷ/Z 9>=!'gu\ 7qxu~NG;-!Sjڏz4"9v\Sn85qt1]"Ƒ;sUuk[qPKRɧ8PK!S#xl/worksheets/_rels/sheet2.xml.relsMk0 ~E=QI/]nيm/,gd~1XaGlWlc Aދt9RGM < SJz`i#w1Q9fYCByAMpwb2E>A4D<[I(OqLRQ̚7ն当уBoWI}CБPQnO/g 1 ?sTZ(tj4pPK{RPK!S#xl/worksheets/_rels/sheet1.xml.relsMk0 ~E=QI/]nيm/,gd~1XaGlWlc Aދt9RGM < SJz`i#w1Q9fYCByAMpwb2E>A4D<[I(OqLRQ̚7ն当уBoWI}CБPQnO/g 1 ?sTZ(tj4pPK{RPK!Sxl/worksheets/sheet1.xml\]o}To$NmIH7n[7Ec";$%[;[7IRlyA/^AҚ2䃔MjYыy[+~]A &wKrŸdt,%92* Tr_0yƶ3;͵qFAVw>`Ƒn:[ч$BJ&5)1k}Wwhk 5[Axɷ!yr9N9κݬy ^&뜋֭}ORpCHZ b ? ~3"Mr`yn^'Ot-iQFŎ^D\WFE' Ss|JRN6Z[9-ĸ}\,pMq)FR#VM6nsmz_.VHj(f`&_ Rp4 8.#X P- opȌ6P;/2aљ˟'M ߤJhE {a%f':`P[Z:u8iGߟYWQ8={}~Lִʂa0:=lA-=5A:-x?1N ~߯N ~ N ~y.t *\T07pIL*gR< T8Ϥy&3pȤ! L*2pȤ! L*2pȤ! T8ߤ&7pIM*oR| T8ߤ&.0pI L*\`R T&.0pI M*\hRB TФ…&.4pI M*\dR" TȤE&.2pIL*\dR" Tؤ&.6pIM*\ t:yVO{9Vѡ>'pyG"l؄5>ܑb[3ª:F0*t{uYQ͵Կ#_&^LHX.(;ܶx&S꛼(_%7MۤUpYk-M'#_ F6+^w) \GzQAO"2UoIt!Ke^k8.UL$-k(*g;-ʱG?d}#mYR36T4Wg"IP*תFrOxnnw, /\s:﫭cּ- ̛̞1rY\2~iCI֑j3%)Ntpt`tͫ‘E\l"Y7ի @[V5wӦV̐'\I%USRm)IJ_$5%iKIf/_$M%))r?%aݦm35="jsmH~t%7[E.AQ57yYz͹^Yfįꌰ8G؝zW)PzR5ЃzzC=h<' !NO A7?p{z#jD\os~DyeMIMdɬI=zzH_~`.ἲNm^{&k=FY1=8nX;8C=~N_~`<"¹"\NݮpF=UNTr'\?0wn͞!lpp6ʓP: g署p3Q-Y41ڿ}9w:2sFv9>dHB6*Ik(Q'`}DװMuMIw~/6SWG%ktfyj:ME'//zjWI+y/OI[ 4o3mQqҖWCrJmн=<׫;.;O|.Դ;O{.>w}fw񝆺H!ntfH!C:t8p76(}86ޜVu0ˋf_oaP̠ca2$7ו?.B#@if zVa4 2yFzm7'mr]$ TΤ]"IrQRZHf-f*pSAG?OoZ磏f%Y ?,VmU}{w0S?Sk#Jd3;3U&ɮPK] PK!Sxl/_rels/workbook.xml.relsj0 } $c8A[Q6o?-Rv(;@j^x@U0؍a0{ujq'/fq\Ą!f+A'v@]学%#vhUvaǾn{1ȉr3Ҁb.2o৕>?dn.:o 'e,2F^PKpPK!Sxl/sharedStrings.xmln0E ^˔&NIA]( %-|uMVъ@hْgӔ3kݖ[ri[SXj]}.6S׃o6od@0Z0RY7PYt=*PU$^6; =y-PPbޫD. <zX0<_y>l+UHj}5AmT\d22eye8/#Dmiy!|Fk ўIj4 Q~hQ=p"PPKkYPK!SdocProps/core.xmlRN0D$(8P "7coSCX{I mggt1՞bї{ ՇFJbJiLNF 6{eWtmv #N 7PKdPK!SdocProps/app.xmlOk0 8 !+q;C6v ۠y7==n8ALڻWz6<R$N  {>@D zrD [J<ʶe[PT `4;ė{W4iYWdӗ6֕sJ3c p('"gvN 91h)zUBReՎM:1r&%enW jlP( Fh&ȱ@1$GmDKf'\tzmKsxay Ͳ SC ES8|!듍)#GcM䀇5J:.q#!#E+]zxyA*%0Q܈3PR=w*\۫fćfHgnDSb>J0긞&U?+5GmoPSY)0EC%H.зn]Z"Gn:^{`p?b|c݉3 {qƿo.w oN'xn=UCxwc:%Ni{#\1_PK Oy`PKdW鮌xl/workbook.xml PK!S4 _rels/.relsPK!SRɧ8 'xl/styles.xmlPK!S{R# xl/worksheets/_rels/sheet2.xml.relsPK!S{R#0xl/worksheets/_rels/sheet1.xml.relsPK!S U@Wxl/worksheets/sheet1.xmlPK!S] `xl/worksheets/sheet2.xmlPK!Spxl/_rels/workbook.xml.relsPK!SkYxl/sharedStrings.xmlPK!SddocProps/core.xmlPK!SZ{h!docProps/app.xmlPK!S9A"docProps/custom.xmlPK!S Oy`$[Content_Types].xmlPKdW鮌%xl/workbook.xmlPK h)Excel-ValueReader-XLSX-1.16/t/valuereader.t000444000000000000 3747514775551044 21161 0ustar00unknownunknown000000000000use utf8; use strict; use warnings; use Test::More 1.302195; use List::Util qw/max/; use List::MoreUtils qw/all/; use Scalar::Util qw/looks_like_number/; use Clone qw/clone/; use Module::Load::Conditional 0.66 qw/check_install/; use Iterator::Simple qw/list/; use Excel::ValueReader::XLSX; note "testing Excel::ValueReader::XLSX version $Excel::ValueReader::XLSX::VERSION"; (my $tst_dir = $0) =~ s/valuereader\.t$//; $tst_dir ||= "./"; my $xl_file = $tst_dir . "valuereader.xlsx"; my $xl_1904 = $tst_dir . "valuereader1904.xlsx"; my $xl_ulibuck = $tst_dir . "ulibuck.xlsx"; my $xl_mappe = $tst_dir . "Mappe1.xlsx"; my $xl_without_r = $tst_dir . "cells_without_r_attr.xlsx"; my @expected_sheet_names = qw/Test Empty Entities Tab_entities Dates Tables RemoteCells/; my @expected_values = ( ["Hello", undef, undef, 22, 33, 55], [123, undef, '<>'], ["This is bold text", undef, '&'], ["This is a Unicode string €", undef, '&<>'], [], [undef, "after an empty row and col", undef, undef, undef, "Hello after an empty row and col"], ["cell\r\nwith\r\nembedded newlines"], ); my $expected_active_sheet = 6; my @expected_tab_entities = ( [], [], ['Nombre de Name', "\x{c9}tiquettes de colonnes" ], ["\x{c9}tiquettes de lignes", 'capital', 'small', '(vide)', "Total g\x{e9}n\x{e9}ral"], ['A', '6', '6', undef, '12'], ['acute accent', '1', '1', undef, '2'], ['circumflex accent', '1', '1', undef, '2'], ['grave accent', '1', '1', undef, '2'], ['ring', '1', '1', undef, '2'], ['tilde', '1', '1', undef, '2'], ['dieresis or umlaut mark', '1', '1', undef, '2'], ['AE diphthong (ligature)', '1', '1', undef, '2'], ['(vide)', '1', '1', undef, '2'], ['C', '1', '1', undef, '2'], ['cedilla', '1', '1', undef, '2'], ['E', '4', '4', undef, '8'], ['acute accent', '1', '1', undef, '2'], ['circumflex accent', '1', '1', undef, '2'], ['grave accent', '1', '1', undef, '2'], ['dieresis or umlaut mark', '1', '1', undef, '2'], ['Eth', '1', '1', undef, '2'], ['Icelandic', '1', '1', undef, '2'], ['greater than', undef, undef, '1', '1'], ['(vide)', undef, undef, '1', '1'], ['I', '4', '4', undef, '8'], ['acute accent', '1', '1', undef, '2'], ['circumflex accent', '1', '1', undef, '2'], ['grave accent', '1', '1', undef, '2'], ['dieresis or umlaut mark', '1', '1', undef, '2'], ['less than', undef, undef, '1', '1'], ['(vide)', undef, undef, '1', '1'], ['N', '1', '1', undef, '2'], ['tilde', '1', '1', undef, '2'], ['O', '6', '6', undef, '12'], ['acute accent', '1', '1', undef, '2'], ['circumflex accent', '1', '1', undef, '2'], ['grave accent', '1', '1', undef, '2'], ['tilde', '1', '1', undef, '2'], ['dieresis or umlaut mark', '1', '1', undef, '2'], ['slash', '1', '1', undef, '2'], ['sharp s', undef, '1', undef, '1'], ['German (sz ligature)', undef, '1', undef, '1'], ['single quote', undef, undef, '1', '1'], ['(vide)', undef, undef, '1', '1'], ['THORN', '1', '1', undef, '2'], ['Icelandic', '1', '1', undef, '2'], ['U', '4', '4', undef, '8'], ['acute accent', '1', '1', undef, '2'], ['circumflex accent', '1', '1', undef, '2'], ['grave accent', '1', '1', undef, '2'], ['dieresis or umlaut mark', '1', '1', undef, '2'], ['Y', '1', '2', undef, '3'], ['acute accent', '1', '1', undef, '2'], ['dieresis or umlaut mark', undef, '1', undef, '1'], ['(vide)', undef, undef, '1', '1'], ['(vide)', undef, undef, '1', '1'], ['ampersand', undef, undef, '1', '1'], ['(vide)', undef, undef, '1', '1'], ["Total g\x{e9}n\x{e9}ral", '30', '32', '5', '67'], ); my @expected_dates_and_times = ( [ '10.07.2020', '10.07.2020', '01.02.1989', '10.07.2020 02:57:00', '02:57:59'], [ '10.07.2020', '10.07.2020', '31.12.1999', '10.07.2020 02:57:59', '01:23:00'], [ '10.07.2020', undef, '01.01.1900', undef, '01:26:18'], [ '10.07.2020', undef, '02.01.1900', ], [ '10.07.2020', undef, '28.02.1900' ], [ '10.07.2020', undef, '01.03.1900' ], [ '10.07.2020', undef, '01.03.1900' ], [ '10.07.2020', undef, '04.04.4444' ], [ '10.07.2020' ], [ '10.07.2020' ], [ '10.07.2020' ], ); # NOTE : cell C6 displays "29.02.1900" in Excel, but that date does not exist, so # this module gets 01.03.1900 instead. my @expected_dates_1904 = ( ['11.07.2024', '11.07.2024', '01.02.1989',], ['11.07.2024', '11.07.2024', '31.12.1999',], ['11.07.2024', undef, '02.01.1904',], ['11.07.2024', undef, '03.01.1904',], ['11.07.2024', undef, '29.02.1904',], ['11.07.2024', undef, '01.03.1904',], ['11.07.2024', undef, '02.03.1904',], ['11.07.2024', undef, '05.04.4448',], ['11.07.2024', ], ['11.07.2024', ], ['11.07.2024', ], ); my @expected_mappe = ( [qw/a b c d e a /], [qw/a b c d e b /], [qw/a b c d e c /], [qw/a b c d e d /], [qw/a b c d e e /], [qw/a b bla-bla-bla bla-bla-bla bla-bla-bla f /], [qw/a b bla-bla-bla bla-bla-bla bla-bla-bla 1 /], [qw/a b bla-bla-bla bla-bla-bla bla-bla-bla 2 /], [qw/a b bla-bla-bla d e 3 /], [qw/a b bla-bla-bla d e 5 /], [qw/a b c d e 6 /], [qw/1 11 bla-bla-bla bla-bla-bla bla-bla-bla z /], [qw/2 12 bla-bla-bla bla-bla-bla bla-bla-bla v /], [qw/3 13 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], [qw/4 14 c d e bla-bla-bla /], [qw/5 15 c d e bla-bla-bla /], [qw/6 16 c d e bla-bla-bla /], [qw/7 17 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], [qw/8 18 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], [qw/9 19 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], [qw/10 20 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], [qw/11 21 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], [qw/12 22 bla-bla-bla bla-bla-bla bla-bla-bla bla-bla-bla /], ); my @expected_tab_names = qw(Entities tab_foobar tab_in_middle_of_sheet tab_without_headers Cols_with_entities HasTotals); my @expected_tab_foobar = ( {foo => 11, bar => 22}, {foo => 33, bar => 44}, ); my @expected_tab_badambum = ( {badam => 99, bum => 88}, {badam => 77, bum => 66}, ); my @expected_tab_no_headers = ( {col1 => 'aa', col2 => 'bb', col3 => 'cc'}, {col1 => 'dd', col2 => undef, col3 => undef}, {col1 => 'ee', col2 => 'ff', col3 => 'gg'}, ); my @expected_tab_cols_with_entities = ( {'col<' => 'foo', 'col&' => 'bar', 'col>' => 'bim'}, ); my @expected_tab_HasTotals = ( {x => 11, y => 22, z => 33}, {x => 44, y => 55, z => 66}, {x => 77, y => 88, z => 99}, ); my @expected_tab_HasTotals_incl_totals = ( @expected_tab_HasTotals, {x => 77, y => 3, z => 198}, ); my @expected_tab_by_ref = ( {Name => 'amp', Char => '&'}, {Name => 'gt', Char => '>'}, {Name => 'lt', Char => '<'}, ); my @expected_without_r = ( [qw/One two three/], [qw/four five six /], [qw/seven eight nine /], [11, 22], [], [33, 44], ); my @backends = ('Regex'); push @backends, 'LibXML' if check_install(module => 'XML::LibXML::Reader'); foreach my $backend (@backends) { # regular tests on values and tables run_tests($xl_file, $backend, qw/values table/); # iterator tests run_tests($xl_file, $backend, sub {list(scalar shift->ivalues(@_))}, sub {my ($cols, $it) = shift->itable(@_); ($cols, list($it))}); } sub run_tests { my ($xl_source, $backend, $values_meth, $table_meth) = @_; my $context = $backend; $context .= "--iterator" if ref $values_meth; # dirty hack when testing with LibXML, because \r\n are silently transformed into \n local $expected_values[-1][0] = "cell\nwith\nembedded newlines" if $backend eq 'LibXML'; # instantiate the reader my $reader = Excel::ValueReader::XLSX->new(xlsx => $xl_source, using => $backend); # check sheet names my @sheet_names = $reader->sheet_names; is_deeply(\@sheet_names, \@expected_sheet_names, "sheet names using $context"); # check active_sheet is($reader->active_sheet, $expected_active_sheet, "active_sheet using $context"); # check a regular sheet my $values = $reader->$values_meth('Test'); is_deeply($values, \@expected_values, "values using $context"); my $nb_cols = max map {scalar @$_} @$values; is ($nb_cols, 6, "nb_cols using $context"); # check an empty sheet my $empty = $reader->$values_meth('Empty'); is_deeply($empty, [], "empty values using $context"); # sheet with holes my $shallow = $reader->$values_meth('RemoteCells'); is $shallow->[0][707], 'aaf1', 'remote horizontal cell'; is $shallow->[2555][0], 'a2556', 'remote vertical cell'; # tables my ($entity_columns, $entities) = $reader->$table_meth('Entities'); is_deeply($entity_columns, [qw(Num Name Char Cap/small Letter Variant)], "column names, using $context"); is $entities->[0]{Name}, 'amp' , "1st table row, name, using $context"; is $entities->[0]{Letter}, 'ampersand' , "1st table row, letter, using $context"; is $entities->[-1]{Name}, 'yuml' , "last table row, name, using $context"; is_deeply([$reader->table_names], \@expected_tab_names, "table names, using $context"); my $tab_foobar = $reader->$table_meth('tab_foobar', want_records => 1); # arg useless, this is the default is_deeply($tab_foobar, \@expected_tab_foobar, "tab_foobar, using $context"); my $rows_foobar = $reader->$table_meth('tab_foobar', want_records => 0); is_deeply($rows_foobar, [map {[@{$_}{qw/foo bar/}]} @expected_tab_foobar], "rows_foobar, using $context"); my $tab_badambum = $reader->$table_meth('tab_in_middle_of_sheet'); is_deeply($tab_badambum, \@expected_tab_badambum, "tab_badambum, using $context"); my ($col_headers, $tab_no_headers) = $reader->$table_meth('tab_without_headers'); is_deeply($tab_no_headers, \@expected_tab_no_headers, "tab_no_headers, using $context"); my $tab_cols_with_entities = $reader->$table_meth('Cols_with_entities'); is_deeply($tab_cols_with_entities, \@expected_tab_cols_with_entities, "tab_cols_with_entities, using $context"); my $tab_has_totals = $reader->$table_meth('HasTotals'); is_deeply($tab_has_totals, \@expected_tab_HasTotals, "tab_HasTotals, using $context"); my $tab_has_totals_incl_totals = $reader->$table_meth('HasTotals', with_totals => 1); is_deeply($tab_has_totals_incl_totals, \@expected_tab_HasTotals_incl_totals, "tab_HasTotals with_totals=>1, using $context"); my $tab_by_ref = $reader->$table_meth(sheet => "Entities", ref => "B1:C4"); is_deeply($tab_by_ref, \@expected_tab_by_ref, "tab_by_ref"); # check a pivot table my $tab_entities = $reader->$values_meth('Tab_entities'); is_deeply($tab_entities, \@expected_tab_entities, "tab_entities using $context"); # check date conversions my $dates = $reader->$values_meth('Dates'); is_deeply($dates, \@expected_dates_and_times, "dates using $context"); # check time conversions with rounding hack my $t1 = $reader->formatted_date("44022.123599537037", "[h]:mm:ss"); is($t1, '02:57:59', 'time conversion 1'); my $t2 = $reader->formatted_date("0.123599537037", "[h]:mm:ss"); is($t2, '02:57:59', 'time conversion 2'); # other date format my $expected_other_format = clone \@expected_dates_and_times; foreach my $row (@$expected_other_format) { $_ and s/^(\d\d)\.(\d\d)\.\d\d(\d\d)/$2-$1-$3/ foreach @$row; } my $other_reader = Excel::ValueReader::XLSX->new(xlsx => $xl_source, using => $backend, date_format => "%m-%d-%y"); my $other_dates = $other_reader->$values_meth('Dates'); is_deeply($other_dates, $expected_other_format, "dates with other format, using $context"); # no date format my $reader_no_date = Excel::ValueReader::XLSX->new(xlsx => $xl_source, using => $backend, date_formatter => undef); my $dates_raw_nums = $reader_no_date->$values_meth('Dates'); my @all_vals_flat = grep {$_} map {@$_} @$dates_raw_nums; my $are_all_numbers = all {looks_like_number($_)} @all_vals_flat; ok($are_all_numbers, "dates with no format, using $context"); # Excel file in 1904 date format my $reader_1904 = Excel::ValueReader::XLSX->new(xlsx => $xl_1904, using => $backend); my $dates_1904 = $reader_1904->$values_meth('Dates'); is_deeply($dates_1904, \@expected_dates_1904, "dates in 1904 format, using $context"); # some edge cases provided by https://github.com/ulibuck my $reader_ulibuck = Excel::ValueReader::XLSX->new(xlsx => $xl_ulibuck, using => $backend); my $example1 = $reader_ulibuck->$values_meth('Example'); is($example1->[3][2], '30.12.2021', "date1904=\"false\", using $context"); my $example2 = $reader_ulibuck->$values_meth('Example two'); is($example2->[12][2], '# Dummy', "# Dummy, using $context"); # in this workbook the active_sheet is deliberately empty ok(! defined $reader_ulibuck->active_sheet, "empty active_sheet, using $context"); # https://github.com/damil/Excel-ValueReader-XLSX/issues/2 : empty string (ulibuck++) my $reader_mappe = Excel::ValueReader::XLSX->new(xlsx => $xl_mappe, using => $backend); my $strings = $reader_mappe->$values_meth('Tabelle2'); is_deeply $strings, \@expected_mappe, "empty string nodes, using $context"; # cells do not always have a 'r' attribute my $reader_without_r = Excel::ValueReader::XLSX->new(xlsx => $xl_without_r, using => $backend); my $vals = $reader_without_r->$values_meth(1); is_deeply $vals, \@expected_without_r, "cells without 'r' attribute, using $context"; } done_testing(); Excel-ValueReader-XLSX-1.16/t/valuereader.xlsx000444000000000000 7504014774502607 21702 0ustar00unknownunknown000000000000PK!`  [Content_Types].xml (̗n0'"NĴi"m]0X$eRxʦ)%DuܐcǞl*ۀښ]cVi*F(7zP}<jrЛ@z+\Kk `7X [{OІe}ƪ`¹JK7Fc2˥|I:΃Pʝ zzB?CT9`.|_ph޼աOdw㭨)v녵Hlx\$i=9.q W4p|ONo,--f-Myt|ѭ@Z :ؐ*z(apWAHG.RxPHroecsI=Q8Cĩd"FH]IGls5?uM럺/E%g%IGSw޺@GM#!Coꈡ9(P-<OPK!U0#L _rels/.rels (MO0 HݐBKwAH!T~I$ݿ'TG~lCF*乒7JF C9.:aYƜi Kױ.f僉gcHQ])MZbTgdxdbīL<& B,M3RYN,+ &!KE}bb1>1aVIq5um/=woPK!O xl/workbook.xmlVmo8~ӼAa!\ە*ƪVoHx[.j<?̄/i" 3Yw&Hfo>g]g1f<#> -+5 %RaQBR\dp"(rAp\$ȔiFi4"ʔdr"/5Zb^z VQQ"-[^1{g?-GWR ^hcUiXY v9 eWWn,,VIv㛍kӞ8UbHcJ[%GD%|XRvDZ7t~ZBc@7\xJ q][ݲmW_LaIF<@CK {p 6'T7VyxU<`h`> _T.l"'J2#B>,S}q7 GWSA O)oh@fNg`k-'pts9F^q)ZP'4><|tX*`5(G樭{Y̷>r Ͷ:xLvN }K\K q$,Gm徭|љo޷>Z|3N-8Y=4 u5dC?xʂĖTwD^l_)gJ /#s\C&w FNuj@us}'ؙ6Ѯ49I9.a+uN#"QgvOijx0 ! hHBjgWNN ٿ'4ݡlZVԇCiw`4n ԫ)֟]M,ą S=i#\^M oTnJ~j0rrtl1Qw:^>*f`p`>|[_jOj@at֕7_WPK!4}mxl/worksheets/sheet1.xmlo0' f VQ6Mk#Xj&Ѵ}SHDEp6>swA+Kk*%)%`Vz*zOV_,| hBW2E v`XyWest4aF x` @Vv~iq Ns뻙CF*JX7 >dLZ gmBdv[0.&eWa9xQJn'V~|6`re/M߯`ҳ}jYK+⠩菬\lydx4)<(0`|f:#QC{΋$+y~;~#m0W>^`?U-r8y(8~./(Vc1?Sn0.@Z U"&9@`gXAYvfrlM u|}鈪j>JQvOY1yQؤ %Og|_a84A @ E2VӽzȤ3A4"xz[av}xFu O=~w`K.oO.F0mt0xn݉AQ&R 1,5!wn}K!PK!)xl/worksheets/sheet2.xmlj0=,4N( XĒl{GN@.!B͌&;U X'i܍(-L!2%s]h+WxrZyߤ9QkxRǥ]2XEH,!S\jz )K)`jZ{UqGuF5XZ} DtEq>dg&zG3%%5Δdv:1.No}fa#CϨ>a;a,|MײO_:8ơ:QKY!1!*bsL8k%a.$hqa*FVo,SW(z|%_l_A.+9$=-SpՆn2@)HM-qI,IԷ)HLOM,J+VIM+U23WR(LπK J I%%0^FjbJjg_egPK!CY ?xl/worksheets/sheet3.xml]O0MM  ;c.kl@ 7ez=g};kE>yiMAaB aKi}y^ &Mɕ5Pxz3 $_:&g̋4CۀLe?ݚ/EZ,ILsi葐s{+L8B(~_w4-i6f nJC D|6¾5dd%-Vadv)2.zigakC Ee+)vOظ, \|qH zJ;"ަbk *a$qe&& ƒA{I#q\}td=Y&PQBŷ*<upvEw@[07n6_%KnV{^Elp)QйK2YH ?ǟӯCxۿ{}ߚ-?}߿ǯǷEKڭ.M/[TgTU}4s㰹{kiX_\4R [tVc`x1Aeag]! by47 4հ$#c,mjK;a7^ 'ZJKreV-G )n؄-Xgs^*8-:=:\ʽܱnEu&ljs^Hhӵχч+oa&F8~Pu&՞B3 :.`,DABEQgӅ#!Hso4)TDD. jfp1N)JIDA\G WMD>͉)܄@匁6sw=˖sreMT9c@ݰ'^ػvչ0Z(X&۔U"1G"OGA su)I[n⏬!˄F-r2)H,)O:X\'E:n!X0*1>Q=a~ȄҨ' E2ȨwN3doI0'#3zzYdT`Fe }2cWQ: ]ddQ&XQ #%Cau2h,S0"eGQ(׎YjS7)r,GF}}\ddF1Fh9PVg]{z\ddQ>Ee F$sF}Vf]GYd()raŃ/1ropNaĬF%P0'#,~x2VdpN3F%@Ϥ(Er8Z'@'t|: }2g>$96\d4,d30P:0BYh޲>?L[ @ E""~B3C)wpA> VtEC&^&1P`r:#"V:i<,h]zs ^K 6PK!Wp2xl/worksheets/sheet4.xmlێ0+B QȪmԽ+sXU}'R0`97󧓨S:Gj* ^orr!OR5i(VQinLZ*A U@#E$ I QK! ^s^$ VXE ԯKӎ& jߍ;@x[EM-YU N ~Kܿ$8UR˵5߶? gm0x(vv/}%̊.fǥf{^O#^Q-]ys)}N(X쨯|q%>xvq?L@x$O0w'] Xoo{lxzZO_S!XW!~|ӎvPVV*"=㳠qqٽb"1;XH}<-$t|EFѠnEFwvұH!h>6ݎ6 F_ehIv Dyt^m`۾}m&EZ[(g;>ϯv?/?ؽ<~v{?_|~|v svXWx\zrVb!sX{| w/ u6zՁWNG^}꩜g9r:DČLF-l\G]v쬜HL^^ vJ'O+,Ko[bհV?{_aټ7n)7oVsXi͟_MrF-l5l>WO^>*o1õӯ@tDHP,Ir\<VZ>9Gq R"ŀ#)Bk\$MR"OPV9EpO>H>A)Bk\U8i %PHZF~Jn)BbzDqkS `GR#.%G6H`YGR/.1G6H`GR;.;G6b"F`s\vTD#)B#-xbH`GR;.;GZ\`n;H\(RdeH3/ױ7i2ž}4/srV PaV1`\Ê#]s@Mo-q$EhX. G0q$EhpkH`iGR . G0k PUc"4hxti8arpu0X(ߡ<л)HLOM,J+VIM+U23WR(LπK J I%%0^FjbJjg_egPK!9y xl/worksheets/sheet5.xmlۊ0 }X7 Yg)ن])ZDZ(}-a=#K7XQ+5%͒0VlJ}5Q7W@IOoA%mBh Ƽh@s imo Ҋi:eKC{Baغi08P<~_hZ܃mwHX"b- JX >fc.pIӭdR8m$^mq1n ') Nt劝J;=,tb^IdDN_Mn0 /28H]Mup4D`_1MK7,CS=>G8K>Mavl68GP(c(rz.ص0%% /_'=ûaGFТ<<ٯ,b<[=@ɏ[,Y*wZ,claTAP Dc4|Vn\_)FMpzyoRcGh#z|s@0Қ]g7qJbC\vkF%yG1 U-#O2Y9`s7Oىvi OcLD^}PK!? xl/worksheets/sheet6.xmlN0EH}$mV`xgX#KgR +q}L|3m"+p^ZSдPFJyA^{הMŕ5P-x:^^L-|/hB3EmLm_ݜMZ,I!w0l]KwV,58P<`Ӵ8[,۞ED) JXKuo!deYoG2|Ziga!sPRJGVvu >%dMJ Ǫ7i)N:KX1 |"D.G %݂"܂RH?w"( }'GJ֪Y%ϩK EvgށYLEU]k0+ҧ ƚZEeEU0tt~iܫ>Mɹ'zVEأ^O{!hiXˬeyf&esx"OKHJÎe.!sJT*2)1/elS^DHA6W5!Cf!m l Z[TTa ֭MymkKȢ+ȴ2b. LKb4}T$xe2Gh*r߽5T-,Lxbs[{wNإoz;( ^ =ŷx?<&Hf^:!zk]l:7t0 Wibb;_a-"%]/KӴZpE:+O5{@֑,U"̺FTjV4?|NAn(;~{sˌ_2n/wR PK!t!lxl/worksheets/sheet7.xmlo ߗ Wj6֥4۲3ҫ%hfk_ p㾳jX'i4)-L):/`B\1rzGoWWxrZ{f9Qn`Z詌UѮk-a aJ 7b@#B=jٺmZDd#RDUudoq8?azY$%5T~dv)2.N/D#fCv EK)JN6',=V9L&"$" FE4 E8Jp_t>+%vXrZDYQ,$I)zIع?{ire̦sYL.K_Q%T|g{=Z̽k~V Tqo)HM-qI,I)/W(U2TR(.H+ ,* MR*]RSJl LlAʁB@~~~2T UU U.t EFfH$z$W5)HLOM,J+VIM+U23WR(LπK J I%%0^FjbJjg_egPK!‡}xl/theme/theme1.xmlYKo7 aIȁ%Kq81l%EԊe].HʎnEr,PhZR׸MѦ@BJZZtl'8p]0t@yİT'(m\+|S'-O/0W O L+ZsukNZuN2g8 Ak+KfVih@2N^%veD ~-ʍfחfpd%|o٨xMKh^/>8V D<4OY.XobXiFF8Hd (:'v)KK/jffA_x^xzѣ?>zliTcdzo^=E?|ϟM ^~O_~=7}nCƕ vcL8];S|6qwO@!oL8b8q;6^Լ O\L=|;8u\۝dPMgAؾG]S#3>&ģ}JPpG ݧ$}:pii&Ogpc{͙O-r"!!0'1 8ɼ24.b?cQvw!7'%O/wi䈴d"UDbÇvy8ɘ42sъ&pVf+Ƭb:lj#)js˪ܚ 0`FvFEPI#*IXGz,-&'Ka+h֫8k#kץn,1~*T†luͦ?,+p+bS2! (!_Y/Jo!&u-H.;K)("!~ϐJ0Ak;mm-y/ ήc8/:Egl&2_VZ#(w~UL_*0Xjp,0 P1*4 D3~1i6, 0U=!A_<|Sr,GNDD4_nAyّy3avܮ Q b䬕 d?K9Ȇe*RpTz{;(Għy?p-~_ wssnZdyɖga 7l'{Bԅ9q)x[>'" Y?=D-l䞊}d@-ˤ+m~?'45kTثx^`I\|,w=j<,{̠'`B" R;b]JeSݡ0)TޗzOxء¢;C(Š| T6 3+wA\ZմHHuEu[~ :^(2[Vi@5bT hYeͣBC*jCF2⒮K"aJJ  &[C.F5u%WnWpejl otx\ ͆tpۂukb,o]4>JH }0kelBTf"ц 5B|4ёqohc RúubY]`[$rn71ruW7wd==!'1{΁G t>*TpUfᄚlG5[QkXae? [ 8FWI556~q\uR DINʿ& E' :vDd鿃3ÊҖ_z(`K r_߾=?-_LvrKh5q4KpOJ2eqn9|5&Hb5 :Ze؋3OtyIύ$vdt2~O7_9-DFNa7n;8yܨ{nӎyN4qC4G+|}$h>aWōPK!&m`xl/sharedStrings.xmlYkF 6CV.X1>&c[<$ss/ɝ&$)Կ y *T]V+&+DhJ[Vݝ+\HT.kj\!DEa%^LPM٦556ґ.( 7 ccbim긒ܺ:vlY^d`A6s '{?"I ~Lp>` 8U[Ӈ3rꎀS1`rN>. I'π|)V '+5oE| ,kd'%{NCi}ɱ8n<qSpb~LNݏIRԁܯI~F컖MPt,u [;MNGk8l5UMT:qVF^m*{[KCH*m;ɧ`$yԳʝq:n1xcnsbr%?H0S- ˃u1 ϴ 2V.U{tʽ~IvЙSq)nji.J鵻YGݤ\^No3V gZԂwGE=sMYRfYaE \] ƒ̔ @a15$ԯ%3C-;w? R PK!3Dxl/pivotTables/pivotTable1.xmlWn6?+lɉ oc,`7ݞiJJRE?됒Eђ7i[o 3mF$`B,D'9W$줌X’.^/A WN?Qr9"#@?.P[75cFd@a>b) /zUFwEN&rX `oqpT\8*ANoԙBRN^AYU%YETB>QŐgfiZMdFUftzWeF6dɆf<Iy0Ьjm_/N,}W:=RY}H9EbAwk}m PK!%#xl/worksheets/_rels/sheet3.xml.rels 0DnzЫ IFѿ7EA4fj$zrw Χj 'HÓz4aG<"SkS ;،4#KeqƔ8uQlTd@m!] 9?56K?"TDqAR,R_PK!ȡ4#xl/worksheets/_rels/sheet4.xml.rels 0DnzU IFѿ7 NռI<)NC) 4\f'HÛz.4aG<"SkS ،4#KeqƔ8mQT\2bs!D9?5t1K?"TOZMJ\yKUWkPK!,B#xl/worksheets/_rels/sheet5.xml.relsj0D{$;P%_J!&E^ۢJh%؄Bc0]Yc" n@!4D,|>X< ~IлKZ9fVBlaopqSFdLeRcLOhvM7ܝS 0N\8ƀ)Hτ%`9H=U˄bAGw swPK!^EE#xl/worksheets/_rels/sheet6.xml.rels͊0nR4^}tm2Yѷ7 ҽ)vס 9a!3d:h> 8w4܈aW>##n;"U,khc[ش4 KɦڅcچFy4glHYQoʗ8T¡Z8|ug˙߁l|3BE)145H8Dz #αs,sct͘c5ct_PK! A G)xl/pivotTables/_rels/pivotTable1.xml.rels 0 Pr<:UBm--m}{mcȟW_ ub˝ڂ 1ixS}\T0[EpЧwJEӈQ:O7 #]:`~He| % RN?:JPK!~2' 'xl/pivotCache/pivotCacheDefinition1.xmlVMo8/@Y؆"v]/M쑡~$uH)GN D GJA6`,jD݀b:j=>-:XGUJV0&o~|݌ n`w`2eA\> CNIAg_: Ie()WAahNɡW+FBruHls&)$5EaZ l\+ml>hW5#Q1CL#V%>*9Av?oC$IoU.ŰJV+Ù6Lʍ+$kEkwwuy]޺^խodʶ Àc6zxĈrC]d$e[p%ljL UȅtKDQҁ,*-f?-w ;޻IeFqQK`a(/>[|q@F?@ _iZI8lSAUg1ZQQr% ¾1%*sl}ܰE>n;0?*9*,lי RbNg NgH=rb T:eνV˴z;'  ڣA Wݥ;?NPzQ(8OcT48 1߉n.R\ڼu/:od0]>${&!Z8}QE(H2[@I0aC򶨧DY EgDQDY %Iq2ouEDQq*W j*̧(>GQ`B!X/hb r :80wϋ g|ڳɠ|P&)G%p^YLV8|{$eD?o;j~D+N_XxDPtJYFo7]Çi5erqA+2i|DWe+DM/Px&GY>`f|3Û2gfr}f$d;cVXos,nX:`hޯY8}> ػ*Td! U@Ģk̍tX$9SLj'Ṡ"=Ac'R}$\p*O4tѬ J>㿢J}!Ы%g";U1iJ)w_1{b+84~J4od9AlEr3ohIV$7DO=ǒLV?r,iSiO2 ׵2 F[S̯2˝bz^[,׃\Lb yC59M,:K7o94eJ˭94|ICF1n)XZn&\${Lr=)Me+5ǝYk eYbVuv;k/wM B`z I9bPd:6w%A\Ql'.ڧ7G^{{Fhjثtɭ39gkLP.j>vY&#[Lt~VU}MXNɶX`Y+0#SI) O S6ȾYR}]ꂠlU/ӻ[s͋SrD+Ke :|Y<ݙEZYOsjx~{inh>nX͖ΐIcb{?HịSrKLu9L"H)QM(ɨ QY'_0}D_!9i!|4+;ϖ_<|΃'i r vaOe#psʧ<_dʑ8@Թ } ㅰ1Zj1Y9:l-w jpUv}Zz!0 5",ͩ)|z|õP  PT힥1IR9=Ɓ^WˊWQFk;yv7TLju8clvPT]|cwUI:1>>)CVzӢ-1S5[Vfי@piQznO4zQB=6)S;Duބ$J'Pd=h߇ůk՚ORWLvxs- ;_,5¦D q!ҝ[-pFﻻCm;Y? )/#3w.tSoDr4|K!JE'{>xbb\(o#,I?e6;[ZiLT35 sir$oJN30B'NQĴxS4"f )/^>h|}5@2Zhg*4|u['xk!@+vE \R]Lhͬ _X"낪ٹsp- g`0fJBcdNʐ3jIVrb.Yb6 F6&t,uǹwJ x$H)o? NTy \_oX∮WK‘ߎD1 wֽ 6VxR@FܡsG?vqyڸgpڇV6W!{^#L#.M1LLD0Rcdr5c^Of^@xw? l ^/h\:_E=;_tFg!PK! -ΰxl/tables/table4.xmlo '@'ส6N"Ea+q &Ѵ}jIx>}︽{^LLB͋ ۚxX>rPF9-'gyTH3^9&N +߸F53 )oW6Py} ī<3 WhxYy]mb3:BжVk͋ҼdݜXmj~MX ySnj<ٚ8rYP#w;#Qx56ND4m73]<} 5#rL$_N5^|HLOG˷1"RR)¸Ż\477Dj-Fgr,DZe5ʾRۆkgeMw2ЫO OZmCM;b,NfwZ kz/y:)h6 f4*#N}&9'F%FT7k|TUӭ'[_÷~7Jx\ 31׶Jȉybjd]XK55=G|#ulcJv:`0Eֽ2X.hgeڻh-嘄B5ƫFQ `ԃG'qeR'E0LS,IIrV|@:Y!MxIp: W$IRpV!!QI4Ǜ4[DYz"qLlaC8=!) 8}Id:ذ{YS6}̇'Hݒ Mkh5o\(}x\ YPK!5b xl/calcChain.xmld 0EfoӊT]P!@% oQEs{fn\&A9ˠr hb$Dn{E7 Y%ʒdqRDCFisvF?0z}h$3+ N}Ggʇqu _NE;PK!drQbdocProps/core.xml (|]k0%WE~Vċ ̱5ICWKvAnNΛO XQ%QPBm (*NZA`Ѵə&62%Lh&[Im77lnq<QCY{Su0T A9(?YF?tAR w~٩٧Vi&4ߗnPvW PsFjSxXUԺF<3*E{ V9y`9_:ol=Ge'i3Y'g$K?Z_[Ӆ]Vd,i"5@Q(t!/e2B)Y' + cl| a֐сjOT8pgX@0 @$Q+tf- S؟V,46'J>xV[']AV`L5MVԭ1+^&`䛝 PЌ2&=~0dv KBקSEQx?PK!꫸'xl/printerSettings/printerSettings2.binrdHagHbHeP``papc 00aa G #?& H:2h.>PI0> ϰG TjPA Kq0``0af`07X*@!.?Rw& 57ÂA  "cJT"C#7@iPK!3xl/tables/table3.xmlMo ܽu5zsj)RCW|X]U`&ݪ:l᝗auu0K D ew|@BVp(\߾YEՒj:8a/  7IF w4L^rRF钱 jP^1jpfQmVY@IUOyܨƸ@uy~GWɚߨ?YO,ԥDIouXc䁡Y)~`qFٍQBhq& D0i~I^|l="{ݻ<}땎ғS(e5Gt]@77}Sن:МPf/t[.[e?H 'ɷRg)l.񞠍!9ѫIAʲ)ReEPK!+{32xl/pivotCache/_rels/pivotCacheDefinition1.xml.relsj0D{-;PimZUB1a̴2;% 4U ehAIp@Vuf%$ %)؉# EQGWIoz+7:9PKloPTXrrRF~YMUZv{PK!6xl/tables/table2.xmlSN0`ڡ+DM UB={^Z؞Ȟ@8ɶ qf<=;gٓр/y>i_AmVO"*_+ ^|#?_~@Q%"j Z)@p 7lDlVujΊ)OSpg@ ]UZfm}ř덇X|.ܙ*@g&iL'"'9BͿuz"^. aK˿r}$~|+G? Zj[&_%;x3ݲ˅VƢ2:%"Jr+UyL{y;^/i}wѿO&爹[x>ǽ׾qC>xkӹ3z dʄCeݨI(YOC!*DPK-!`  [Content_Types].xmlPK-!U0#L 7_rels/.relsPK-!M7\xl/_rels/workbook.xml.relsPK-!O  xl/workbook.xmlPK-!4}m xl/worksheets/sheet1.xmlPK-!)uxl/worksheets/sheet2.xmlPK-!CY ?xl/worksheets/sheet3.xmlPK-!Wp2Hxl/worksheets/sheet4.xmlPK-!9y &xl/worksheets/sheet5.xmlPK-!? +xl/worksheets/sheet6.xmlPK-!t!l.xl/worksheets/sheet7.xmlPK-!‡}1xl/theme/theme1.xmlPK-!h N8xl/styles.xmlPK-!&m`V=xl/sharedStrings.xmlPK-!3DAxl/pivotTables/pivotTable1.xmlPK-!;m2KB#Fxl/worksheets/_rels/sheet1.xml.relsPK-!%#Gxl/worksheets/_rels/sheet3.xml.relsPK-!ȡ4#Hxl/worksheets/_rels/sheet4.xml.relsPK-!,B#Ixl/worksheets/_rels/sheet5.xml.relsPK-!^EE#Jxl/worksheets/_rels/sheet6.xml.relsPK-! A G)Kxl/pivotTables/_rels/pivotTable1.xml.relsPK-!~2' 'Mxl/pivotCache/pivotCacheDefinition1.xmlPK-!*AK$Pxl/pivotCache/pivotCacheRecords1.xmlPK-!p&'YUxl/printerSettings/printerSettings1.binPK-!;J[xl/tables/table1.xmlPK-! -ΰ]xl/tables/table4.xmlPK-!s_xl/tables/table5.xmlPK-!#axl/tables/table6.xmlPK-!5b cxl/calcChain.xmlPK-!drQbddocProps/core.xmlPK-![_rbgdocProps/app.xmlPK-!꫸'Ljxl/printerSettings/printerSettings2.binPK-!3^kxl/tables/table3.xmlPK-!+{32Imxl/pivotCache/_rels/pivotCacheDefinition1.xml.relsPK-!6Znxl/tables/table2.xmlPK## 3pExcel-ValueReader-XLSX-1.16/t/valuereader1904.xlsx000444000000000000 2215014107234660 22201 0ustar00unknownunknown000000000000PK!A7n[Content_Types].xml (Tn0W?DV[$xX$(}'fQU%Ql[&<&YB@l.YO$` r=HEV5 ӵLb.j""%5 3NB?C%*=YK)ub8xR-JWQ23V$sU.)PI]h:C@im2 3 1 g/#ݺʸ2 x|`G㮶u_;ѐUOղwj s4ȥ-ZeN xe|o, 1ysi޺s V788wa:  CrhݝAPK!U0#L _rels/.rels (MO0 HݐBKwAH!T~I$ݿ'TG~xl/_rels/workbook.xml.rels (RMK0 0wvt/"Uɴ)&!3~*]XK/oyv5+zl;obG s>,8(%"D҆4j0u2jsMY˴S쭂 )fCy I< y!+EfMyk K5=|t G)s墙UtB),fPK!2Bmxl/workbook.xmlSn0?zjrIk -4ur&Wa>TJҴERݙy5M1%` fLY=of[k/hBGhǶ:nZEigf!we)9,,o4Ѓ8P,`4 fn#nu[d8uh/w:U81y? ZKe#T7y6oGIҏIy0Ьjm_/N,}W:=RY}H9EbAwk}m PK!1Gxxl/theme/theme1.xmlYn7;{O,ْcK6qbJ˘\݊X@Ѵ@o-P >m :$WiQq_lgqȽ|^!U/V"Dh4[If<'hLdtew.u u܌R%0yArx6" di raG(?n4&Dz\I=3erT5Be tY3E~#T 46./rS :槜WN,"OVƥ|`jtڝT8Z[\Z5yJR9V(mB ~*eo@_Z7 _w/5Vk>ހRF9h[JBmk_ a]ZŐjQe.]h ÊH 21qg}Aq s Jo|ix`gܐXB5Aj@^ApB£|@̓[~g}ڳM2ƌ\o= 0hT9=)|,.AJS\@_5D .74LN6[ 92j{V1'Ĝ'VWUr͔UU /jL3-md`pM{tKU3жi32~IXs L1랏Qi+4 H;OžW @$zoa{%]'ʑnq5F}hlu[Mij1tEN*ܜX-BHejS<*SRؿ\l+k G?d8$r팘[(HaNUXπJ1՞ysYtq̊tKtRnxjf5ڂŝ})i)nϖXp,0ڌP)*Rw\lax I 9+Ô53MT dar"Y)dc,}rHXOs#B6)iN~jcleb/RsH TS:x~ƭ2܊민p r""feP{|^! @ -=q6(۰QpC^vSPW˻ϳ9k dhr11aFLI2\*)þ5S7PK!Xk xl/styles.xmlWmo6>`q %RlVWP+ $,@KC/DgֆIْY:o{s^pH)«CT`r~MdAtᵴޤ~3ot ,M@iTE%Dö⦪))#$8 09 3%ԟv+Q63Z]Vl8$G0KzbDV*(Ū,YN:SL^:MaNdB7(W;@C]d!ȕ* 4!ngS_IV{$ZwV%Cm  Yg"$5TЀ އ8+>I^c6FY-PrCpNei+wՓK%"2njIef W:o@%4ט+jg,EANJpa+`ulw|A"v/;\1Ώ-@4eԭ ”e]&ml ceGJ眖R֪5 L:/*IIA[@89/ITr 6`Htmj'f y1Dpr~mC4v6yø\d}y&T /%(BZgvduY,^\V_}p|q_-h_\KkuqvB\\j'ot_QzGAf-5lQaA]x1<5ožn ~ڼIa\@$; _D GU{3a, xF-?oM`9#xj^g V ilxpx@]=m 6};( ag dYFdx{1fr&wu!. _nҿPK!$N4uZxl/worksheets/sheet1.xmlVn0?z F MgZYD$Q%i;wInH}1(hfj~ZWdH0 bJ*͆џ?on)179Twϟ{_L ` 246"#JJDsR]EiOˆL_¡B X)DC-oJٚ#[-.~ٶ7B-Re%'ӦQ+RheTaHD?y"dZs'pe' Fl$4Z}~I؛wkb*r%k^ b4(rK*FCl/KN GG߱o~ܖ(C_@nJc+M 2HNG I ֔gG([cU}[k'8)6-wsdҝ;Wb4t8>=0'9%^3ګIROz8'\/3:-DJ&zkQ^];+:Ns`Fǽz!ƳNt`F'!'qAft+:Ͽxd^tN{tv3:= gZW؄C{h׹='C |z#C*(btxkZgSֵǧW@Ë?-4c vے_հJK4m2*m5* TVbM;Jq3~ PK! LRbdocProps/core.xml (|AK0ߡ䤇6M;m:v No!yۂMZn޴je"{dӃ*/0VV:G$QWBm^p4MۛהWV8 6$m)ss[l77Qlq'Nx8&cuODg=ޛYL">:XκCf>X//ݨ8"rU+uKx,fL`Oܭ3|Dx#ztҧz$&I3^1S&ïBM%&$'! z+oPK!꫸'xl/printerSettings/printerSettings1.binrdHagHbHeP``papc 00aa G #?& H:2h.>PI0> ϰG TjPA Kq0``0af`07X*@!.?Rw& 57ÂA  "cJT"C#7@iPK!/ȧdocProps/app.xml (n0 (@V1-zI{d:*Kɞ~NSo$'JкÔmJQ7~[OQd_ +q,nojBDs>WbGRfeJR i4m0=˲x 5d(FEG_5X_1:k5)Pqw0蔜hQJNS6pƺQɷz@藶V-:4R?^ۥ(B$ o!v1S^k,x3"7NLc{C獽9Ӭ ')0r8@? 7Aﬗ,G_S܄uEAšSA=BM;[_{> x?_%?臘ۗPK-!A7n[Content_Types].xmlPK-!U0#L _rels/.relsPK-!>xl/_rels/workbook.xml.relsPK-!2Bmxl/workbook.xmlPK-!Ќ |3 xl/sharedStrings.xmlPK-!;m2KB# xl/worksheets/_rels/sheet1.xml.relsPK-!1Gx xl/theme/theme1.xmlPK-!Xk xl/styles.xmlPK-!$N4uZxl/worksheets/sheet1.xmlPK-! LRbdocProps/core.xmlPK-!꫸'Xxl/printerSettings/printerSettings1.binPK-!/ȧjdocProps/app.xmlPK &,!