| line | stmt | bran | cond | sub | pod | time | code | 
| 1 | 4 |  |  | 4 |  | 288336 | use 5.014; | 
|  | 4 |  |  |  |  | 47 |  | 
| 2 |  |  |  |  |  |  | package Excel::ValueWriter::XLSX; | 
| 3 | 4 |  |  | 4 |  | 23 | use strict; | 
|  | 4 |  |  |  |  | 9 |  | 
|  | 4 |  |  |  |  | 82 |  | 
| 4 | 4 |  |  | 4 |  | 20 | use warnings; | 
|  | 4 |  |  |  |  | 15 |  | 
|  | 4 |  |  |  |  | 161 |  | 
| 5 | 4 |  |  | 4 |  | 25 | use utf8; | 
|  | 4 |  |  |  |  | 6 |  | 
|  | 4 |  |  |  |  | 28 |  | 
| 6 | 4 |  |  | 4 |  | 3188 | use Archive::Zip          qw/AZ_OK COMPRESSION_LEVEL_DEFAULT/; | 
|  | 4 |  |  |  |  | 382823 |  | 
|  | 4 |  |  |  |  | 276 |  | 
| 7 | 4 |  |  | 4 |  | 40 | use Scalar::Util          qw/looks_like_number/; | 
|  | 4 |  |  |  |  | 11 |  | 
|  | 4 |  |  |  |  | 227 |  | 
| 8 | 4 |  |  | 4 |  | 27 | use List::Util            qw/none/; | 
|  | 4 |  |  |  |  | 9 |  | 
|  | 4 |  |  |  |  | 457 |  | 
| 9 | 4 |  |  | 4 |  | 2215 | use Params::Validate      qw/validate_with SCALAR SCALARREF UNDEF/; | 
|  | 4 |  |  |  |  | 37800 |  | 
|  | 4 |  |  |  |  | 314 |  | 
| 10 | 4 |  |  | 4 |  | 2056 | use POSIX                 qw/strftime/; | 
|  | 4 |  |  |  |  | 26403 |  | 
|  | 4 |  |  |  |  | 34 |  | 
| 11 | 4 |  |  | 4 |  | 7700 | use Date::Calc            qw/Delta_Days/; | 
|  | 4 |  |  |  |  | 23538 |  | 
|  | 4 |  |  |  |  | 332 |  | 
| 12 | 4 |  |  | 4 |  | 31 | use Carp                  qw/croak/; | 
|  | 4 |  |  |  |  | 9 |  | 
|  | 4 |  |  |  |  | 175 |  | 
| 13 | 4 |  |  | 4 |  | 23 | use Encode                qw/encode_utf8/; | 
|  | 4 |  |  |  |  | 7 |  | 
|  | 4 |  |  |  |  | 3515 |  | 
| 14 |  |  |  |  |  |  |  | 
| 15 |  |  |  |  |  |  | our $VERSION = '1.03'; | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | #====================================================================== | 
| 18 |  |  |  |  |  |  | # GLOBALS | 
| 19 |  |  |  |  |  |  | #====================================================================== | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | my $DATE_STYLE = 1;                        # 0-based index into the  format for dates .. | 
| 22 |  |  |  |  |  |  | # .. defined in the styles() method | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | my $SHEET_NAME = qr(^[^\\/?*\[\]]{1,31}$); # valid sheet names: <32 chars, no chars \/?*[] | 
| 25 |  |  |  |  |  |  | my $TABLE_NAME = qr(^\w{3,}$);             # valid table names: >= 3 chars, no spaces | 
| 26 |  |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  |  | 
| 28 |  |  |  |  |  |  | # specification in Params::Validate format for checking parameters to the new() method | 
| 29 |  |  |  |  |  |  | my %params_spec = ( | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | # date_regex : for identifying dates in data cells. Should capture into $+{d}, $+{m} and $+{y}. | 
| 32 |  |  |  |  |  |  | date_regex        => {type => SCALARREF|UNDEF, optional => 1, default => | 
| 33 |  |  |  |  |  |  | qr[^(?: (?\d\d?)    \. (?\d\d?) \. (?\d\d\d\d)  # dd.mm.yyyy | 
| 34 |  |  |  |  |  |  | | (?\d\d\d\d) -  (?\d\d?) -  (?\d\d?)     # yyyy-mm-dd | 
| 35 |  |  |  |  |  |  | | (?\d\d?)    /  (?\d\d?) /  (?\d\d\d\d)) # mm/dd/yyyy | 
| 36 |  |  |  |  |  |  | $]x}, | 
| 37 |  |  |  |  |  |  |  | 
| 38 |  |  |  |  |  |  | # bool_regex : for identifying booleans in data cells. If true, should capture into $1 | 
| 39 |  |  |  |  |  |  | bool_regex        => {type => SCALARREF|UNDEF, optional => 1, default => qr[^(?:(TRUE)|FALSE)$]}, | 
| 40 |  |  |  |  |  |  |  | 
| 41 |  |  |  |  |  |  | compression_level => {type => SCALAR, regex => qr/^\d$/, optional => 1, default => COMPRESSION_LEVEL_DEFAULT}, | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | ); | 
| 44 |  |  |  |  |  |  |  | 
| 45 |  |  |  |  |  |  |  | 
| 46 |  |  |  |  |  |  | my %entity       = ( '<' => '<', '>' => '>', '&' => '&' ); | 
| 47 |  |  |  |  |  |  | my $entity_regex = do {my $chars = join "", keys %entity; qr/[$chars]/}; | 
| 48 |  |  |  |  |  |  |  | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | #====================================================================== | 
| 51 |  |  |  |  |  |  | # CONSTRUCTOR | 
| 52 |  |  |  |  |  |  | #====================================================================== | 
| 53 |  |  |  |  |  |  |  | 
| 54 |  |  |  |  |  |  | sub new { | 
| 55 | 5 |  |  | 5 | 1 | 15888 | my $class = shift; | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | # check parameters and create $self | 
| 58 | 5 |  |  |  |  | 141 | my $self = validate_with( params      => \@_, | 
| 59 |  |  |  |  |  |  | spec        => \%params_spec, | 
| 60 |  |  |  |  |  |  | allow_extra => 0, | 
| 61 |  |  |  |  |  |  | ); | 
| 62 |  |  |  |  |  |  |  | 
| 63 |  |  |  |  |  |  | # initial values for internal data structures | 
| 64 | 5 |  |  |  |  | 47 | $self->{sheets}                = []; # array of sheet names | 
| 65 | 5 |  |  |  |  | 15 | $self->{tables}                = []; # array of table names | 
| 66 | 5 |  |  |  |  | 13 | $self->{shared_string}         = {}; # ($string => $string_index) | 
| 67 | 5 |  |  |  |  | 14 | $self->{n_strings_in_workbook} = 0;  # total nb of strings (including duplicates) | 
| 68 | 5 |  |  |  |  | 14 | $self->{last_string_id}        = 0;  # index for the next shared string | 
| 69 | 5 |  |  |  |  | 12 | $self->{defined_names}         = {}; # ($name => [$formula, $comment]) | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | # immediately open a Zip archive | 
| 72 | 5 |  |  |  |  | 31 | $self->{zip} = Archive::Zip->new; | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | # return the constructed object | 
| 75 | 5 |  |  |  |  | 264 | bless $self, $class; | 
| 76 |  |  |  |  |  |  | } | 
| 77 |  |  |  |  |  |  |  | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | #====================================================================== | 
| 80 |  |  |  |  |  |  | # GATHERING DATA | 
| 81 |  |  |  |  |  |  | #====================================================================== | 
| 82 |  |  |  |  |  |  |  | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | sub add_sheet { | 
| 85 |  |  |  |  |  |  | # 3rd parameter ($headers) may be omitted -- so we insert an undef if necessary | 
| 86 | 15 | 100 |  | 15 | 1 | 1024 | splice @_, 3, 0, undef if @_ < 5; | 
| 87 |  |  |  |  |  |  |  | 
| 88 |  |  |  |  |  |  | # now we can parse the parameters | 
| 89 | 15 |  |  |  |  | 48 | my ($self, $sheet_name, $table_name, $headers, $code_or_array) = @_; | 
| 90 |  |  |  |  |  |  |  | 
| 91 |  |  |  |  |  |  | # check if the given sheet name is valid | 
| 92 | 15 | 50 |  |  |  | 158 | $sheet_name =~ $SHEET_NAME | 
| 93 |  |  |  |  |  |  | or croak "'$sheet_name' is not a valid sheet name"; | 
| 94 | 15 | 50 |  | 30 |  | 83 | none {$sheet_name eq $_} @{$self->{sheets}} | 
|  | 30 |  |  |  |  | 58 |  | 
|  | 15 |  |  |  |  | 88 |  | 
| 95 |  |  |  |  |  |  | or croak "this workbook already has a sheet named '$sheet_name'"; | 
| 96 |  |  |  |  |  |  |  | 
| 97 |  |  |  |  |  |  | # local copies for convenience | 
| 98 | 15 |  |  |  |  | 62 | my $date_regex = $self->{date_regex}; | 
| 99 | 15 |  |  |  |  | 30 | my $bool_regex = $self->{bool_regex}; | 
| 100 |  |  |  |  |  |  |  | 
| 101 |  |  |  |  |  |  | # iterator for generating rows; either received as argument or built as a closure upon an array | 
| 102 |  |  |  |  |  |  | my $next_row | 
| 103 |  |  |  |  |  |  | = ref $code_or_array eq 'CODE'  ? $code_or_array | 
| 104 |  |  |  |  |  |  | : ref $code_or_array ne 'ARRAY' ? croak 'add_sheet() : missing or invalid $rows argument' | 
| 105 | 15 | 100 |  | 46 |  | 62 | : do {my $i = 0; sub { $i < @$code_or_array ? $code_or_array->[$i++] : undef}}; | 
|  | 13 | 50 |  |  |  | 26 |  | 
|  | 13 | 100 |  |  |  | 52 |  | 
|  | 46 |  |  |  |  | 162 |  | 
| 106 |  |  |  |  |  |  |  | 
| 107 |  |  |  |  |  |  | # if $headers were not given explicitly, the first row will do | 
| 108 | 15 |  | 100 |  |  | 55 | $headers //= $next_row->(); | 
| 109 |  |  |  |  |  |  |  | 
| 110 |  |  |  |  |  |  | # array of column references in A1 Excel notation | 
| 111 | 15 |  |  |  |  | 38 | my @col_letters = ('A'); # this array will be expanded on demand in the loop below | 
| 112 |  |  |  |  |  |  |  | 
| 113 |  |  |  |  |  |  | # register the sheet name | 
| 114 | 15 |  |  |  |  | 23 | push @{$self->{sheets}}, $sheet_name; | 
|  | 15 |  |  |  |  | 36 |  | 
| 115 |  |  |  |  |  |  |  | 
| 116 |  |  |  |  |  |  | # start building XML for the sheet | 
| 117 | 15 |  |  |  |  | 41 | my @xml = ( | 
| 118 |  |  |  |  |  |  | q{}, | 
| 119 |  |  |  |  |  |  | q{ | 
| 120 |  |  |  |  |  |  | q{ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">}, | 
| 121 |  |  |  |  |  |  | q{}, | 
| 122 |  |  |  |  |  |  | ); | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | # loop over rows and columns | 
| 125 | 15 |  |  |  |  | 25 | my $row_num = 0; | 
| 126 |  |  |  |  |  |  | ROW: | 
| 127 | 15 |  |  |  |  | 42 | for (my $row = $headers; $row; $row = $next_row->()) { | 
| 128 | 1040 |  |  |  |  | 291262 | $row_num++; | 
| 129 | 1040 | 50 |  |  |  | 2471 | my $last_col = @$row or next ROW; | 
| 130 | 1040 |  |  |  |  | 1624 | my @cells; | 
| 131 |  |  |  |  |  |  |  | 
| 132 |  |  |  |  |  |  | COLUMN: | 
| 133 | 1040 |  |  |  |  | 2875 | foreach my $col (0 .. $last_col-1) { | 
| 134 |  |  |  |  |  |  |  | 
| 135 |  |  |  |  |  |  | # if this column letter is not known yet, compute it using Perl's increment op on strings | 
| 136 |  |  |  |  |  |  | my $col_letter = $col_letters[$col] | 
| 137 | 300704 |  | 66 |  |  | 606404 | //= do {my $prev_letter = $col_letters[$col-1]; ++$prev_letter}; | 
|  | 615 |  |  |  |  | 1013 |  | 
|  | 615 |  |  |  |  | 1768 |  | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | # get the value; if the cell is empty, no need to write it into the XML | 
| 140 | 300704 |  |  |  |  | 427254 | my $val = $row->[$col]; | 
| 141 | 300704 | 100 | 66 |  |  | 1132619 | defined $val and length $val or next COLUMN; | 
| 142 |  |  |  |  |  |  |  | 
| 143 |  |  |  |  |  |  | # choose XML attributes and inner value | 
| 144 |  |  |  |  |  |  | (my $tag, my $attrs, $val) | 
| 145 |  |  |  |  |  |  | = looks_like_number $val             ? (v => ""                  , $val                          ) | 
| 146 | 4 | 100 | 66 | 4 |  | 1903 | : $date_regex && $val =~ $date_regex ? (v => qq{ s="$DATE_STYLE"}, n_days($+{y}, $+{m}, $+{d})   ) | 
|  | 4 | 100 | 100 |  |  | 1501 |  | 
|  | 4 | 100 |  |  |  | 12266 |  | 
|  | 300702 | 100 |  |  |  | 736553 |  | 
|  |  | 100 |  |  |  |  |  | 
| 147 |  |  |  |  |  |  | : $bool_regex && $val =~ $bool_regex ? (v => qq{ t="b"}          , $1 ? 1 : 0                    ) | 
| 148 |  |  |  |  |  |  | : $val =~ /^=/                       ? (f => "",                   escape_formula($val)          ) | 
| 149 |  |  |  |  |  |  | :                                      (v => qq{ t="s"}          , $self->add_shared_string($val)); | 
| 150 |  |  |  |  |  |  |  | 
| 151 |  |  |  |  |  |  | # add the new XML cell | 
| 152 | 300702 |  |  |  |  | 1229373 | my $cell = sprintf qq{<%s>%s%s>}, $col_letter, $row_num, $attrs, $tag, $val, $tag; | 
| 153 | 300702 |  |  |  |  | 634036 | push @cells, $cell; | 
| 154 |  |  |  |  |  |  | } | 
| 155 |  |  |  |  |  |  |  | 
| 156 |  |  |  |  |  |  | # generate the row XML and add it to the sheet | 
| 157 | 1040 |  |  |  |  | 42929 | my $row_xml = join "", qq{ }, @cells, qq{}; | 
| 158 | 1040 |  |  |  |  | 21292 | push @xml, $row_xml; | 
| 159 |  |  |  |  |  |  | } | 
| 160 |  |  |  |  |  |  |  | 
| 161 |  |  |  |  |  |  | # close sheet data | 
| 162 | 15 |  |  |  |  | 49 | push @xml, q{}; | 
| 163 |  |  |  |  |  |  |  | 
| 164 |  |  |  |  |  |  | # if required, add the table corresponding to this sheet into the zip archive, and refer to it in XML | 
| 165 | 15 |  |  |  |  | 27 | my @table_rels; | 
| 166 | 15 | 100 | 100 |  |  | 61 | if ($table_name && $row_num) { | 
| 167 | 9 |  |  |  |  | 70 | my $table_id = $self->add_table($table_name, $col_letters[-1], $row_num, @$headers); | 
| 168 | 9 |  |  |  |  | 31 | push @table_rels, $table_id; | 
| 169 | 9 |  |  |  |  | 28 | push @xml, q{}; | 
| 170 |  |  |  |  |  |  | } | 
| 171 |  |  |  |  |  |  |  | 
| 172 |  |  |  |  |  |  | # close the worksheet xml | 
| 173 | 15 |  |  |  |  | 31 | push @xml, q{}; | 
| 174 |  |  |  |  |  |  |  | 
| 175 |  |  |  |  |  |  | # insert the sheet and its rels into the zip archive | 
| 176 | 15 |  |  |  |  | 55 | my $sheet_id   = $self->n_sheets; | 
| 177 | 15 |  |  |  |  | 51 | my $sheet_file = "sheet$sheet_id.xml"; | 
| 178 |  |  |  |  |  |  | $self->{zip}->addString(encode_utf8(join("", @xml)), | 
| 179 |  |  |  |  |  |  | "xl/worksheets/$sheet_file", | 
| 180 | 15 |  |  |  |  | 59939 | $self->{compression_level}); | 
| 181 |  |  |  |  |  |  | $self->{zip}->addString($self->worksheet_rels(@table_rels), | 
| 182 |  |  |  |  |  |  | "xl/worksheets/_rels/$sheet_file.rels", | 
| 183 | 15 |  |  |  |  | 19773 | $self->{compression_level}); | 
| 184 |  |  |  |  |  |  |  | 
| 185 | 15 |  |  |  |  | 3783 | return $sheet_id; | 
| 186 |  |  |  |  |  |  | } | 
| 187 |  |  |  |  |  |  |  | 
| 188 |  |  |  |  |  |  |  | 
| 189 |  |  |  |  |  |  |  | 
| 190 |  |  |  |  |  |  | sub add_sheets_from_database { | 
| 191 | 0 |  |  | 0 | 1 | 0 | my ($self, $dbh, $sheet_prefix, @table_names) = @_; | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  | # in absence of table names, get them from the database metadata | 
| 194 | 0 | 0 |  |  |  | 0 | if (!@table_names) { | 
| 195 | 0 |  |  |  |  | 0 | my $tables = $dbh->table_info(undef, undef, undef, 'TABLE')->fetchall_arrayref({}); | 
| 196 | 0 |  |  |  |  | 0 | @table_names = map {$_->{TABLE_NAME}} @$tables; | 
|  | 0 |  |  |  |  | 0 |  | 
| 197 |  |  |  |  |  |  | } | 
| 198 |  |  |  |  |  |  |  | 
| 199 | 0 |  | 0 |  |  | 0 | $sheet_prefix //= "S."; | 
| 200 |  |  |  |  |  |  |  | 
| 201 | 0 |  |  |  |  | 0 | foreach my $table (@table_names) { | 
| 202 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare("select * from $table"); | 
| 203 | 0 |  |  |  |  | 0 | $sth->execute; | 
| 204 | 0 |  |  |  |  | 0 | my $headers = $sth->{NAME}; | 
| 205 | 0 |  |  |  |  | 0 | my $rows    = $sth->fetchall_arrayref; | 
| 206 | 0 |  |  |  |  | 0 | $self->add_sheet("$sheet_prefix$table", $table, $headers, $rows); | 
| 207 |  |  |  |  |  |  | } | 
| 208 |  |  |  |  |  |  | } | 
| 209 |  |  |  |  |  |  |  | 
| 210 |  |  |  |  |  |  |  | 
| 211 |  |  |  |  |  |  |  | 
| 212 |  |  |  |  |  |  | sub add_shared_string { | 
| 213 | 654 |  |  | 654 | 0 | 1406 | my ($self, $string) = @_; | 
| 214 |  |  |  |  |  |  |  | 
| 215 |  |  |  |  |  |  | # single quote before an initial equal sign is ignored (escaping the '=' like in Excel) | 
| 216 | 654 |  |  |  |  | 1039 | $string =~ s/^'=/=/; | 
| 217 |  |  |  |  |  |  |  | 
| 218 |  |  |  |  |  |  | # keep a global count of how many strings are in the workbook | 
| 219 | 654 |  |  |  |  | 960 | $self->{n_strings_in_workbook}++; | 
| 220 |  |  |  |  |  |  |  | 
| 221 |  |  |  |  |  |  | # if that string was already stored, return its id, otherwise create a new id | 
| 222 | 654 |  | 100 |  |  | 2988 | $self->{shared_strings}{$string} //= $self->{last_string_id}++; | 
| 223 |  |  |  |  |  |  | } | 
| 224 |  |  |  |  |  |  |  | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  |  | 
| 227 |  |  |  |  |  |  | sub add_table { | 
| 228 | 9 |  |  | 9 | 0 | 137 | my ($self, $table_name, $last_col, $last_row, @col_names) = @_; | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | # check if the given table name is valid | 
| 231 | 9 | 50 |  |  |  | 95 | $table_name =~ $TABLE_NAME | 
| 232 |  |  |  |  |  |  | or croak "'$table_name' is not a valid table name"; | 
| 233 | 9 | 50 |  | 6 |  | 56 | none {$table_name eq $_} @{$self->{tables}} | 
|  | 6 |  |  |  |  | 22 |  | 
|  | 9 |  |  |  |  | 52 |  | 
| 234 |  |  |  |  |  |  | or croak "this workbook already has a table named '$table_name'"; | 
| 235 |  |  |  |  |  |  |  | 
| 236 |  |  |  |  |  |  | # register this table | 
| 237 | 9 |  |  |  |  | 42 | push @{$self->{tables}}, $table_name; | 
|  | 9 |  |  |  |  | 28 |  | 
| 238 | 9 |  |  |  |  | 54 | my $table_id = $self->n_tables; | 
| 239 |  |  |  |  |  |  |  | 
| 240 |  |  |  |  |  |  | # build column headers from first data row | 
| 241 | 9 |  |  |  |  | 59 | unshift @col_names, undef; # so that the first index is at 1, not 0 | 
| 242 | 9 |  |  |  |  | 68 | my @columns = map {qq{}} 1 .. $#col_names; | 
|  | 618 |  |  |  |  | 1623 |  | 
| 243 |  |  |  |  |  |  |  | 
| 244 |  |  |  |  |  |  | # Excel range of this table | 
| 245 | 9 |  |  |  |  | 78 | my $ref = "A1:$last_col$last_row"; | 
| 246 |  |  |  |  |  |  |  | 
| 247 |  |  |  |  |  |  | # assemble XML for the table | 
| 248 | 9 |  |  |  |  | 176 | my @xml = ( | 
| 249 |  |  |  |  |  |  | qq{}, | 
| 250 |  |  |  |  |  |  | qq{ 
},
| 251 |  |  |  |  |  |  | qq{ id="$table_id" displayName="$table_name" ref="$ref" totalsRowShown="0">}, |  
| 252 |  |  |  |  |  |  | qq{}, |  
| 253 |  |  |  |  |  |  | qq{}, |  
| 254 |  |  |  |  |  |  | @columns, |  
| 255 |  |  |  |  |  |  | qq{}, |  
| 256 |  |  |  |  |  |  | qq{}, |  
| 257 |  |  |  |  |  |  | qq{ |  | 
| 258 |  |  |  |  |  |  | ); | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | # insert into the zip archive | 
| 261 |  |  |  |  |  |  | $self->{zip}->addString(encode_utf8(join "", @xml), | 
| 262 |  |  |  |  |  |  | "xl/tables/table$table_id.xml", | 
| 263 | 9 |  |  |  |  | 342 | $self->{compression_level}); | 
| 264 |  |  |  |  |  |  |  | 
| 265 | 9 |  |  |  |  | 3302 | return $table_id; | 
| 266 |  |  |  |  |  |  | } | 
| 267 |  |  |  |  |  |  |  | 
| 268 |  |  |  |  |  |  |  | 
| 269 |  |  |  |  |  |  | sub add_defined_name { | 
| 270 | 2 |  |  | 2 | 1 | 17 | my ($self, $name, $formula, $comment) = @_; | 
| 271 |  |  |  |  |  |  |  | 
| 272 | 2 | 50 | 33 |  |  | 12 | $name && $formula                        or croak 'add_defined_name($name, $formula): empty argument'; | 
| 273 | 2 | 50 |  |  |  | 9 | not exists $self->{defined_names}{$name} or croak "add_defined_name(): name '$name' already in use"; | 
| 274 | 2 |  |  |  |  | 8 | $self->{defined_names}{$name} = [$formula, $comment]; | 
| 275 |  |  |  |  |  |  | } | 
| 276 |  |  |  |  |  |  |  | 
| 277 |  |  |  |  |  |  |  | 
| 278 |  |  |  |  |  |  | sub worksheet_rels { | 
| 279 | 15 |  |  | 15 | 0 | 48 | my ($self, $table_id) = @_; | 
| 280 |  |  |  |  |  |  |  | 
| 281 | 15 |  |  |  |  | 26 | my @rels; | 
| 282 | 15 | 100 |  |  |  | 68 | push @rels, "officeDocument/2006/relationships/table" => "../tables/table$table_id.xml" if $table_id; | 
| 283 | 15 |  |  |  |  | 61 | return $self->relationships(@rels); | 
| 284 |  |  |  |  |  |  | } | 
| 285 |  |  |  |  |  |  |  | 
| 286 |  |  |  |  |  |  |  | 
| 287 |  |  |  |  |  |  | #====================================================================== | 
| 288 |  |  |  |  |  |  | # BUILDING THE ZIP CONTENTS | 
| 289 |  |  |  |  |  |  | #====================================================================== | 
| 290 |  |  |  |  |  |  |  | 
| 291 |  |  |  |  |  |  | sub save_as { | 
| 292 | 5 |  |  | 5 | 1 | 1192 | my ($self, $target) = @_; | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | # assemble all parts within the zip, except sheets and tables that were already added previously | 
| 295 | 5 |  |  |  |  | 13 | my $zip = $self->{zip}; | 
| 296 | 5 |  |  |  |  | 19 | $zip->addString($self->content_types,  "[Content_Types].xml"        , $self->{compression_level}); | 
| 297 | 5 |  |  |  |  | 1019 | $zip->addString($self->core,           "docProps/core.xml"          , $self->{compression_level}); | 
| 298 | 5 |  |  |  |  | 1011 | $zip->addString($self->app,            "docProps/app.xml"           , $self->{compression_level}); | 
| 299 | 5 |  |  |  |  | 1050 | $zip->addString($self->workbook,       "xl/workbook.xml"            , $self->{compression_level}); | 
| 300 | 5 |  |  |  |  | 1062 | $zip->addString($self->_rels,          "_rels/.rels"                , $self->{compression_level}); | 
| 301 | 5 |  |  |  |  | 996 | $zip->addString($self->workbook_rels,  "xl/_rels/workbook.xml.rels" , $self->{compression_level}); | 
| 302 | 5 |  |  |  |  | 1045 | $zip->addString($self->shared_strings, "xl/sharedStrings.xml"       , $self->{compression_level}); | 
| 303 | 5 |  |  |  |  | 1076 | $zip->addString($self->styles,         "xl/styles.xml"              , $self->{compression_level}); | 
| 304 |  |  |  |  |  |  |  | 
| 305 |  |  |  |  |  |  | # write the Zip archive | 
| 306 | 5 | 100 |  |  |  | 994 | my $write_result = ref $target ? $zip->writeToFileHandle($target) : $zip->writeToFileNamed($target); | 
| 307 | 5 | 50 | 0 |  |  | 1088954 | $write_result == AZ_OK | 
| 308 |  |  |  |  |  |  | or croak "could not save Zip archive into " . (ref($target) || $target); | 
| 309 |  |  |  |  |  |  | } | 
| 310 |  |  |  |  |  |  |  | 
| 311 |  |  |  |  |  |  |  | 
| 312 |  |  |  |  |  |  | sub _rels { | 
| 313 | 5 |  |  | 5 |  | 19 | my ($self) = @_; | 
| 314 |  |  |  |  |  |  |  | 
| 315 | 5 |  |  |  |  | 24 | return $self->relationships("officeDocument/2006/relationships/extended-properties" => "docProps/app.xml", | 
| 316 |  |  |  |  |  |  | "package/2006/relationships/metadata/core-properties"   => "docProps/core.xml", | 
| 317 |  |  |  |  |  |  | "officeDocument/2006/relationships/officeDocument"      => "xl/workbook.xml"); | 
| 318 |  |  |  |  |  |  | } | 
| 319 |  |  |  |  |  |  |  | 
| 320 |  |  |  |  |  |  | sub workbook_rels { | 
| 321 | 5 |  |  | 5 | 0 | 14 | my ($self) = @_; | 
| 322 |  |  |  |  |  |  |  | 
| 323 | 5 |  |  |  |  | 18 | my @rels = map {("officeDocument/2006/relationships/worksheet"     => "worksheets/sheet$_.xml")} | 
|  | 15 |  |  |  |  | 60 |  | 
| 324 |  |  |  |  |  |  | 1 .. $self->n_sheets; | 
| 325 | 5 |  |  |  |  | 19 | push @rels,      "officeDocument/2006/relationships/sharedStrings" => "sharedStrings.xml", | 
| 326 |  |  |  |  |  |  | "officeDocument/2006/relationships/styles"        => "styles.xml"; | 
| 327 |  |  |  |  |  |  |  | 
| 328 | 5 |  |  |  |  | 16 | return $self->relationships(@rels); | 
| 329 |  |  |  |  |  |  | } | 
| 330 |  |  |  |  |  |  |  | 
| 331 |  |  |  |  |  |  |  | 
| 332 |  |  |  |  |  |  | sub workbook { | 
| 333 | 5 |  |  | 5 | 0 | 15 | my ($self) = @_; | 
| 334 |  |  |  |  |  |  |  | 
| 335 |  |  |  |  |  |  | # opening XML | 
| 336 | 5 |  |  |  |  | 17 | my @xml = ( | 
| 337 |  |  |  |  |  |  | qq{}, | 
| 338 |  |  |  |  |  |  | qq{ | 
| 339 |  |  |  |  |  |  | qq{ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">}, | 
| 340 |  |  |  |  |  |  | ); | 
| 341 |  |  |  |  |  |  |  | 
| 342 |  |  |  |  |  |  | # references to the worksheets | 
| 343 | 5 |  |  |  |  | 13 | push @xml, q{}; | 
| 344 | 5 |  |  |  |  | 13 | my $sheet_id = 1; | 
| 345 | 5 |  |  |  |  | 30 | foreach my $sheet_name (@{$self->{sheets}}) { | 
|  | 5 |  |  |  |  | 35 |  | 
| 346 | 15 |  |  |  |  | 53 | push @xml, qq{}; | 
| 347 | 15 |  |  |  |  | 35 | $sheet_id++; | 
| 348 |  |  |  |  |  |  | } | 
| 349 | 5 |  |  |  |  | 25 | push @xml, q{}; | 
| 350 |  |  |  |  |  |  |  | 
| 351 | 5 | 50 |  |  |  | 28 | if (my $names = $self->{defined_names}) { | 
| 352 | 5 |  |  |  |  | 13 | push @xml, q{}; | 
| 353 | 5 |  |  |  |  | 29 | while (my ($name, $content) = each %$names) { | 
| 354 | 2 |  |  |  |  | 7 | my $attrs = qq{name="$name"}; | 
| 355 | 2 | 100 |  |  |  | 9 | $attrs   .= qq{ comment="$content->[1]"} if $content->[1]; | 
| 356 | 2 |  |  |  |  | 44 | $content->[0] =~ s/($entity_regex)/$entity{$1}/g; | 
| 357 | 2 |  |  |  |  | 14 | push @xml, qq{$content->[0]}; | 
| 358 |  |  |  |  |  |  | } | 
| 359 | 5 |  |  |  |  | 14 | push @xml, q{}; | 
| 360 |  |  |  |  |  |  | } | 
| 361 |  |  |  |  |  |  |  | 
| 362 |  |  |  |  |  |  |  | 
| 363 |  |  |  |  |  |  | # closing XML | 
| 364 | 5 |  |  |  |  | 13 | push @xml, q{}; | 
| 365 |  |  |  |  |  |  |  | 
| 366 | 5 |  |  |  |  | 54 | return encode_utf8(join "", @xml); | 
| 367 |  |  |  |  |  |  | } | 
| 368 |  |  |  |  |  |  |  | 
| 369 |  |  |  |  |  |  |  | 
| 370 |  |  |  |  |  |  | sub content_types { | 
| 371 | 5 |  |  | 5 | 0 | 15 | my ($self) = @_; | 
| 372 |  |  |  |  |  |  |  | 
| 373 | 5 |  |  |  |  | 12 | my $spreadsheetml = "application/vnd.openxmlformats-officedocument.spreadsheetml"; | 
| 374 |  |  |  |  |  |  |  | 
| 375 |  |  |  |  |  |  | my @sheets_xml | 
| 376 | 5 |  |  |  |  | 19 | = map {qq{}} 1 .. $self->n_sheets; | 
|  | 15 |  |  |  |  | 56 |  | 
| 377 |  |  |  |  |  |  |  | 
| 378 |  |  |  |  |  |  | my @tables_xml | 
| 379 | 5 |  |  |  |  | 33 | = map {qq{  }} 1 .. $self->n_tables; | 
|  | 9 |  |  |  |  | 41 |  | 
| 380 |  |  |  |  |  |  |  | 
| 381 | 5 |  |  |  |  | 44 | my @xml = ( | 
| 382 |  |  |  |  |  |  | qq{}, | 
| 383 |  |  |  |  |  |  | qq{}, | 
| 384 |  |  |  |  |  |  | qq{}, | 
| 385 |  |  |  |  |  |  | qq{}, | 
| 386 |  |  |  |  |  |  | qq{}, | 
| 387 |  |  |  |  |  |  | qq{}, | 
| 388 |  |  |  |  |  |  | qq{}, | 
| 389 |  |  |  |  |  |  | qq{}, | 
| 390 |  |  |  |  |  |  | qq{}, | 
| 391 |  |  |  |  |  |  | @sheets_xml, | 
| 392 |  |  |  |  |  |  | @tables_xml, | 
| 393 |  |  |  |  |  |  | qq{}, | 
| 394 |  |  |  |  |  |  | ); | 
| 395 |  |  |  |  |  |  |  | 
| 396 | 5 |  |  |  |  | 54 | return join "", @xml; | 
| 397 |  |  |  |  |  |  | } | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  |  | 
| 400 |  |  |  |  |  |  | sub core { | 
| 401 | 5 |  |  | 5 | 0 | 15 | my ($self) = @_; | 
| 402 |  |  |  |  |  |  |  | 
| 403 | 5 |  |  |  |  | 247 | my $now = strftime "%Y-%m-%dT%H:%M:%SZ", gmtime; | 
| 404 |  |  |  |  |  |  |  | 
| 405 | 5 |  |  |  |  | 72 | my @xml = ( | 
| 406 |  |  |  |  |  |  | qq{}, | 
| 407 |  |  |  |  |  |  | qq{ | 
| 408 |  |  |  |  |  |  | qq{ xmlns:dc="http://purl.org/dc/elements/1.1/"}, | 
| 409 |  |  |  |  |  |  | qq{ xmlns:dcterms="http://purl.org/dc/terms/"}, | 
| 410 |  |  |  |  |  |  | qq{ xmlns:dcmitype="http://purl.org/dc/dcmitype/"}, | 
| 411 |  |  |  |  |  |  | qq{ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">}, | 
| 412 |  |  |  |  |  |  | qq{$now}, | 
| 413 |  |  |  |  |  |  | qq{$now}, | 
| 414 |  |  |  |  |  |  | qq{}, | 
| 415 |  |  |  |  |  |  | ); | 
| 416 |  |  |  |  |  |  |  | 
| 417 | 5 |  |  |  |  | 61 | return join "", @xml; | 
| 418 |  |  |  |  |  |  | } | 
| 419 |  |  |  |  |  |  |  | 
| 420 |  |  |  |  |  |  | sub app { | 
| 421 | 5 |  |  | 5 | 0 | 16 | my ($self) = @_; | 
| 422 |  |  |  |  |  |  |  | 
| 423 | 5 |  |  |  |  | 22 | my @xml = ( | 
| 424 |  |  |  |  |  |  | qq{}, | 
| 425 |  |  |  |  |  |  | qq{ | 
| 426 |  |  |  |  |  |  | qq{ xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">}, | 
| 427 |  |  |  |  |  |  | qq{Microsoft Excel}, | 
| 428 |  |  |  |  |  |  | qq{}, | 
| 429 |  |  |  |  |  |  | ); | 
| 430 |  |  |  |  |  |  |  | 
| 431 | 5 |  |  |  |  | 52 | return join "", @xml; | 
| 432 |  |  |  |  |  |  | } | 
| 433 |  |  |  |  |  |  |  | 
| 434 |  |  |  |  |  |  |  | 
| 435 |  |  |  |  |  |  |  | 
| 436 |  |  |  |  |  |  |  | 
| 437 |  |  |  |  |  |  | sub shared_strings { | 
| 438 | 5 |  |  | 5 | 0 | 18 | my ($self) = @_; | 
| 439 |  |  |  |  |  |  |  | 
| 440 |  |  |  |  |  |  | # array of XML nodes for each shared string | 
| 441 | 5 |  |  |  |  | 10 | my @si_nodes; | 
| 442 | 5 |  |  |  |  | 11 | $si_nodes[$self->{shared_strings}{$_}] = si_node($_) foreach keys %{$self->{shared_strings}}; | 
|  | 5 |  |  |  |  | 149 |  | 
| 443 |  |  |  |  |  |  |  | 
| 444 |  |  |  |  |  |  | # assemble XML | 
| 445 | 5 |  |  |  |  | 122 | my @xml = ( | 
| 446 |  |  |  |  |  |  | qq{}, | 
| 447 |  |  |  |  |  |  | qq{ | 
| 448 |  |  |  |  |  |  | qq{ count="$self->{n_strings_in_workbook}" uniqueCount="$self->{last_string_id}">}, | 
| 449 |  |  |  |  |  |  | @si_nodes, | 
| 450 |  |  |  |  |  |  | qq{}, | 
| 451 |  |  |  |  |  |  | ); | 
| 452 |  |  |  |  |  |  |  | 
| 453 | 5 |  |  |  |  | 161 | return encode_utf8(join "", @xml); | 
| 454 |  |  |  |  |  |  | } | 
| 455 |  |  |  |  |  |  |  | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | sub styles { | 
| 458 | 5 |  |  | 5 | 0 | 16 | my ($self) = @_; | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | # minimal stylesheet | 
| 461 |  |  |  |  |  |  | # style "1" will be used for displaying dates; it uses the default numFmtId for dates, which is 14 (Excel builtin). | 
| 462 |  |  |  |  |  |  | # other nodes are empty but must be present | 
| 463 | 5 |  |  |  |  | 27 | my @xml = ( | 
| 464 |  |  |  |  |  |  | q{}, | 
| 465 |  |  |  |  |  |  | q{}, | 
| 466 |  |  |  |  |  |  | q{}, | 
| 467 |  |  |  |  |  |  | q{}, | 
| 468 |  |  |  |  |  |  | q{}, | 
| 469 |  |  |  |  |  |  | q{}, | 
| 470 |  |  |  |  |  |  | q{}, | 
| 471 |  |  |  |  |  |  | q{}, | 
| 472 |  |  |  |  |  |  | q{}, | 
| 473 |  |  |  |  |  |  | ); | 
| 474 |  |  |  |  |  |  |  | 
| 475 | 5 |  |  |  |  | 22 | my $xml = join "", @xml; | 
| 476 |  |  |  |  |  |  |  | 
| 477 | 5 |  |  |  |  | 32 | return $xml; | 
| 478 |  |  |  |  |  |  | } | 
| 479 |  |  |  |  |  |  |  | 
| 480 |  |  |  |  |  |  |  | 
| 481 |  |  |  |  |  |  | #====================================================================== | 
| 482 |  |  |  |  |  |  | # UTILITY METHODS | 
| 483 |  |  |  |  |  |  | #====================================================================== | 
| 484 |  |  |  |  |  |  |  | 
| 485 |  |  |  |  |  |  | sub relationships { | 
| 486 | 25 |  |  | 25 | 0 | 76 | my ($self, @rels) = @_; | 
| 487 |  |  |  |  |  |  |  | 
| 488 |  |  |  |  |  |  | # build a "rel" file from a list of relationships | 
| 489 | 25 |  |  |  |  | 54 | my @xml = ( | 
| 490 |  |  |  |  |  |  | qq{}, | 
| 491 |  |  |  |  |  |  | qq{}, | 
| 492 |  |  |  |  |  |  | ); | 
| 493 |  |  |  |  |  |  |  | 
| 494 | 25 |  |  |  |  | 46 | my $id = 1; | 
| 495 | 25 |  |  |  |  | 103 | while (my ($type, $target) = splice(@rels, 0, 2)) { | 
| 496 | 49 |  |  |  |  | 179 | push @xml, qq{}; | 
| 497 | 49 |  |  |  |  | 145 | $id++; | 
| 498 |  |  |  |  |  |  | } | 
| 499 |  |  |  |  |  |  |  | 
| 500 | 25 |  |  |  |  | 51 | push @xml, qq{}; | 
| 501 |  |  |  |  |  |  |  | 
| 502 | 25 |  |  |  |  | 212 | return join "", @xml; | 
| 503 |  |  |  |  |  |  | } | 
| 504 |  |  |  |  |  |  |  | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | sub n_sheets { | 
| 507 | 25 |  |  | 25 | 0 | 51 | my ($self) = @_; | 
| 508 | 25 |  |  |  |  | 39 | return scalar @{$self->{sheets}}; | 
|  | 25 |  |  |  |  | 63 |  | 
| 509 |  |  |  |  |  |  | } | 
| 510 |  |  |  |  |  |  |  | 
| 511 |  |  |  |  |  |  | sub n_tables { | 
| 512 | 14 |  |  | 14 | 0 | 36 | my ($self) = @_; | 
| 513 | 14 |  |  |  |  | 21 | return scalar @{$self->{tables}}; | 
|  | 14 |  |  |  |  | 41 |  | 
| 514 |  |  |  |  |  |  | } | 
| 515 |  |  |  |  |  |  |  | 
| 516 |  |  |  |  |  |  |  | 
| 517 |  |  |  |  |  |  | #====================================================================== | 
| 518 |  |  |  |  |  |  | # UTILITY ROUTINES | 
| 519 |  |  |  |  |  |  | #====================================================================== | 
| 520 |  |  |  |  |  |  |  | 
| 521 |  |  |  |  |  |  |  | 
| 522 |  |  |  |  |  |  | sub si_node { | 
| 523 | 650 |  |  | 650 | 0 | 1068 | my ($string) = @_; | 
| 524 |  |  |  |  |  |  |  | 
| 525 |  |  |  |  |  |  | # build XML node for a single shared string | 
| 526 | 650 |  |  |  |  | 1506 | $string =~ s/($entity_regex)/$entity{$1}/g; | 
| 527 | 650 | 50 |  |  |  | 1735 | my $maybe_preserve_space = $string =~ /^\s|\s$/ ? ' xml:space="preserve"' : ''; | 
| 528 | 650 |  |  |  |  | 1286 | my $node = qq{$string}; | 
| 529 |  |  |  |  |  |  |  | 
| 530 | 650 |  |  |  |  | 1753 | return $node; | 
| 531 |  |  |  |  |  |  | } | 
| 532 |  |  |  |  |  |  |  | 
| 533 |  |  |  |  |  |  | sub escape_formula { | 
| 534 | 2 |  |  | 2 | 0 | 9 | my ($string) = @_; | 
| 535 |  |  |  |  |  |  |  | 
| 536 | 2 |  |  |  |  | 8 | $string =~ s/^=//; | 
| 537 | 2 |  |  |  |  | 45 | $string =~ s/($entity_regex)/$entity{$1}/g; | 
| 538 | 2 |  |  |  |  | 9 | return $string; | 
| 539 |  |  |  |  |  |  | } | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  |  | 
| 542 |  |  |  |  |  |  | sub n_days { | 
| 543 | 14 |  |  | 14 | 0 | 97 | my ($y, $m, $d) = @_; | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | # convert the given date into a number of days since 1st January 1900 | 
| 546 | 14 |  |  |  |  | 57 | my $n_days = Delta_Days(1900, 1, 1, $y, $m, $d) + 1; | 
| 547 | 14 |  |  |  |  | 28 | my $is_after_february_1900 = $n_days > 59; | 
| 548 | 14 | 100 |  |  |  | 28 | $n_days += 1 if $is_after_february_1900; # because Excel wrongly treats 1900 as a leap year | 
| 549 |  |  |  |  |  |  |  | 
| 550 | 14 |  |  |  |  | 32 | return $n_days; | 
| 551 |  |  |  |  |  |  | } | 
| 552 |  |  |  |  |  |  |  | 
| 553 |  |  |  |  |  |  |  | 
| 554 |  |  |  |  |  |  |  | 
| 555 |  |  |  |  |  |  |  | 
| 556 |  |  |  |  |  |  |  | 
| 557 |  |  |  |  |  |  | 1; | 
| 558 |  |  |  |  |  |  |  | 
| 559 |  |  |  |  |  |  | __END__ |