File Coverage

lib/Spreadsheet/Engine/Functions.pm
Criterion Covered Total %
statement 491 582 84.3
branch 97 168 57.7
condition 28 77 36.3
subroutine 105 107 98.1
pod 9 9 100.0
total 730 943 77.4


line stmt bran cond sub pod time code
1             package Spreadsheet::Engine::Functions;
2             ## no critic
3              
4             =head1 NAME
5              
6             Spreadsheet::Engine::Functions - Spreadsheet functions (SUM, MAX, etc)
7              
8             =head1 SYNOPSIS
9              
10             my $ok = calculate_function($fname, \@operand, \$errortext, \%typelookup, \%sheetdata);
11              
12             =head1 DESCRIPTION
13              
14             This provides all the spreadsheet functions (SUM, MAX, IRR, ISNULL,
15             etc).
16              
17             =cut
18              
19 28     28   155 use strict;
  28         49  
  28         1215  
20              
21 28     28   152 use Spreadsheet::Engine::Sheet; # bah!
  28         52  
  28         13400  
22 28     28   26058 use Time::Local; # For timegm in NOW and TODAY
  28         55116  
  28         2072  
23 28     28   28610 use Encode;
  28         416861  
  28         3714  
24              
25 28     28   271 use base 'Exporter';
  28         64  
  28         120463  
26             our @EXPORT = qw(calculate_function);
27             our @EXPORT_OK = qw(cr_to_coord);
28              
29             #0 = no arguments
30             #>0 = exactly that many arguments
31             #<0 = that many arguments (abs value) or more
32              
33             our %function_list = (
34             COLUMNS => [ \&columns_rows_function, 1 ],
35             COUNTIF => [ \&countif_sumif_functions, 2 ],
36             DAVERAGE => [ \&dseries_functions, 3 ],
37             DCOUNT => [ \&dseries_functions, 3 ],
38             DCOUNTA => [ \&dseries_functions, 3 ],
39             DGET => [ \&dseries_functions, 3 ],
40             DMAX => [ \&dseries_functions, 3 ],
41             DMIN => [ \&dseries_functions, 3 ],
42             DPRODUCT => [ \&dseries_functions, 3 ],
43             DSTDEV => [ \&dseries_functions, 3 ],
44             DSTDEVP => [ \&dseries_functions, 3 ],
45             DSUM => [ \&dseries_functions, 3 ],
46             DVAR => [ \&dseries_functions, 3 ],
47             DVARP => [ \&dseries_functions, 3 ],
48             INDEX => [ \&index_function, -1 ],
49             ROWS => [ \&columns_rows_function, 1 ],
50             SUMIF => [ \&countif_sumif_functions, -2 ],
51             HTML => [ \&html_function, -1 ],
52             PLAINTEXT => [ \&text_function, -1 ],
53             );
54              
55             =head1 EXTENDING
56              
57             =head2 register
58              
59             Spreadsheet::Engine->register(SUM => 'Spreadsheet::Engine::Function::SUM');
60              
61             If you wish to make a new function available you should register it
62             here. A series of base classes are provided that do all the argument
63             checking etc., allowing you to concentrate on the calculations. Have a
64             look at how the existing functions are implemented for details (it
65             should hopefully be mostly self-explanatory!)
66              
67             information on how many arguments should be passed:
68              
69             =cut
70              
71             my $_reg = {};
72              
73             sub register {
74 28     28 1 2356 my ($class, %to_reg) = @_;
75 28         363 while (my ($name, $where) = each %to_reg) {
76 28     28   15940 eval "use $where";
  28     28   104  
  28     28   686  
  28     28   15164  
  28     28   82  
  28     28   583  
  28     28   12950  
  28     28   90  
  28     28   577  
  28     28   13433  
  28     28   85  
  28     28   707  
  28     28   14485  
  28     28   76  
  28     28   502  
  28     28   19587  
  28     28   85  
  28     28   550  
  28     28   18963  
  28     28   83  
  28     28   540  
  28     28   14278  
  28     28   93  
  28     28   516  
  28     28   18655  
  28     28   85  
  28     28   579  
  28     28   13034  
  28     28   85  
  28     28   550  
  28     28   12174  
  28     28   84  
  28     28   569  
  28     28   15069  
  28     28   79  
  28     28   494  
  28     28   12633  
  28     28   83  
  28     28   524  
  28     28   12602  
  28     28   79  
  28     28   582  
  28     28   13391  
  28     28   86  
  28     28   517  
  28     28   12049  
  28     28   266  
  28     28   511  
  28     28   12116  
  28     28   74  
  28     28   538  
  28     28   13094  
  28     28   81  
  28     28   871  
  28     28   12092  
  28     28   270  
  28     28   489  
  28     28   12195  
  28     28   166  
  28     28   501  
  28     28   12603  
  28     28   87  
  28     28   514  
  28     28   12764  
  28     28   73  
  28     28   473  
  28     28   15016  
  28     28   77  
  28     28   484  
  28     28   11199  
  28     28   82  
  28     28   709  
  28     28   13626  
  28     28   95  
  28     28   509  
  28     28   11449  
  28     28   74  
  28     28   500  
  28     28   12861  
  28     28   86  
  28     28   719  
  28     28   12202  
  28     28   528  
  28     28   585  
  28     28   12932  
  28     28   272  
  28     28   486  
  28     28   12396  
  28     28   82  
  28     28   483  
  28     28   12783  
  28     28   79  
  28     28   489  
  28         12680  
  28         76  
  28         483  
  28         25626  
  28         90  
  28         551  
  28         11972  
  28         84  
  28         617  
  28         12013  
  28         83  
  28         511  
  28         12728  
  28         80  
  28         519  
  28         12257  
  28         82  
  28         798  
  28         15609  
  28         82  
  28         478  
  28         12267  
  28         81  
  28         551  
  28         12135  
  28         84  
  28         494  
  28         13162  
  28         85  
  28         569  
  28         11748  
  28         84  
  28         509  
  28         11575  
  28         83  
  28         503  
  28         12252  
  28         85  
  28         506  
  28         10754  
  28         85  
  28         492  
  28         12103  
  28         85  
  28         495  
  28         11244  
  28         79  
  28         487  
  28         11542  
  28         79  
  28         527  
  28         11616  
  28         77  
  28         487  
  28         11659  
  28         93  
  28         477  
  28         12028  
  28         84  
  28         519  
  28         11568  
  28         77  
  28         493  
  28         11618  
  28         81  
  28         477  
  28         11394  
  28         117  
  28         490  
  28         12145  
  28         75  
  28         505  
  28         12000  
  28         76  
  28         506  
  28         11732  
  28         79  
  28         487  
  28         11209  
  28         84  
  28         511  
  28         11913  
  28         85  
  28         479  
  28         13941  
  28         78  
  28         470  
  28         11838  
  28         110  
  28         513  
  28         12161  
  28         113  
  28         465  
  28         11545  
  28         79  
  28         482  
  28         11843  
  28         77  
  28         491  
  28         11566  
  28         88  
  28         499  
  28         12261  
  28         92  
  28         531  
  28         12315  
  28         83  
  28         472  
  28         11721  
  28         81  
  28         543  
  28         11875  
  28         88  
  28         481  
  28         11639  
  28         87  
  28         1471  
  28         12507  
  28         95  
  28         534  
  28         11909  
  28         1069  
  28         490  
  28         13963  
  28         78  
  28         1893  
  28         13820  
  28         74  
  28         1621  
  28         12584  
  28         76  
  28         1523  
  28         11109  
  28         80  
  28         471  
  28         11351  
  28         83  
  28         508  
  28         11425  
  28         76  
  28         467  
  28         11117  
  28         79  
  28         488  
  28         11288  
  28         86  
  28         478  
  28         11402  
  28         78  
  28         479  
  28         11554  
  28         77  
  28         565  
  28         11911  
  28         97  
  28         473  
  28         17347  
  28         84  
  28         477  
  28         10864  
  28         78  
  28         474  
  28         11243  
  28         102  
  28         537  
  28         11366  
  28         90  
  28         506  
  28         12252  
  28         83  
  28         576  
  28         17999  
  28         84  
  28         478  
  28         11387  
  28         85  
  28         488  
  28         12133  
  28         85  
  28         491  
  28         10693  
  28         471  
  28         473  
  28         16629  
  28         1147  
  28         831  
  2604         137865  
77 2604 50       9427 die $@ if $@;
78 2604         16458 $_reg->{$name} = $where;
79             }
80             }
81              
82             __PACKAGE__->register(
83             map +($_ => "Spreadsheet::Engine::Function::$_"),
84             qw/ ABS ACOS AND ASIN ATAN ATAN2 AVERAGE CHOOSE COS COUNT COUNTA
85             COUNTBLANK DATE DAY DDB DEGREES ERRCELL EVEN EXACT EXP FACT FALSE FIND
86             FV HLOOKUP HOUR IF INT IRR ISBLANK ISERR ISERROR ISLOGICAL ISNA
87             ISNONTEXT ISNUMBER ISTEXT LEFT LEN LN LOG LOG10 LOWER MATCH MAX MID
88             MIN MINUTE MOD MONTH N NA NOT NOW NPER NPV ODD OR PI PMT POWER PRODUCT
89             PROPER PV RADIANS RATE REPLACE REPT RIGHT ROUND SECOND SIN SLN SQRT
90             STDEV STDEVP SUBSTITUTE SUM SYD T TAN TIME TODAY TRIM TRUE TRUNC UPPER
91             VALUE VAR VARP VLOOKUP WEEKDAY YEAR /
92             );
93              
94             =head1 EXPORTS
95              
96             =head2 calculate_function
97              
98             my $ok = calculate_function($fname, \@operand, \$errortext, \%typelookup, \%sheetdata);
99              
100             =cut
101              
102             sub calculate_function {
103              
104 18769     18769 1 34590 my ($fname, $operand, $errortext, $typelookup, $sheetdata) = @_;
105              
106             # has the function been registered? (new style)
107 18769 100       68372 if (my $fclass = $_reg->{$fname}) {
108 18252         531103 my $fn = $fclass->new(
109             fname => $fname,
110             operand => $operand,
111             errortext => $errortext,
112             typelookup => $typelookup,
113             sheetdata => $sheetdata,
114             );
115 18252         9242520 $fn->execute;
116 18252         136727 return 1;
117             }
118              
119             # Otherwise is it in our function_list (old style)
120 517         900 my ($function_sub, $want_args) = @{ $function_list{$fname} }[ 0, 1 ];
  517         1757  
121              
122 517 100       1258 if ($function_sub) {
123 244         1095 copy_function_args($operand, \my @foperand);
124              
125 244         456 my $have_args = scalar @foperand;
126              
127 244 50 66     1807 if ( ($want_args < 0 and $have_args < -$want_args)
      66        
      33        
128             or ($want_args >= 0 and $have_args != $want_args)) {
129 0         0 function_args_error($fname, $operand, $errortext);
130 0         0 return 0;
131             }
132              
133 244         768 $function_sub->(
134             $fname, $operand, \@foperand, $errortext, $typelookup, $sheetdata
135             );
136             } else {
137 273         503 my $ttext = $fname;
138 273 50 33     1608 if (@$operand && $operand->[ @$operand - 1 ]->{type} eq "start")
139             { # no arguments - name or zero arg function
140 273         353 pop @$operand;
141 273         1412 push @$operand, { type => "name", value => $ttext };
142             } else {
143 0         0 $$errortext = "Unknown function $ttext. ";
144             }
145             }
146 517         2667 return 1;
147             }
148              
149             =head1 FUNCTION providers
150              
151             =head2 dseries_functions
152              
153             =over
154              
155             =item DAVERAGE(databaserange, fieldname, criteriarange)
156              
157             =item DCOUNT(databaserange, fieldname, criteriarange)
158              
159             =item DCOUNTA(databaserange, fieldname, criteriarange)
160              
161             =item DGET(databaserange, fieldname, criteriarange)
162              
163             =item DMAX(databaserange, fieldname, criteriarange)
164              
165             =item DMIN(databaserange, fieldname, criteriarange)
166              
167             =item DPRODUCT(databaserange, fieldname, criteriarange)
168              
169             =item DSTDEV(databaserange, fieldname, criteriarange)
170              
171             =item DSTDEVP(databaserange, fieldname, criteriarange)
172              
173             =item DSUM(databaserange, fieldname, criteriarange)
174              
175             =item DVAR(databaserange, fieldname, criteriarange)
176              
177             =item DVARP(databaserange, fieldname, criteriarange)
178              
179             =back
180              
181             =cut
182              
183             # Calculate all of these and then return the desired one (overhead is in accessing not calculating)
184             # If this routine is changed, check the series_functions, too.
185              
186             sub dseries_functions {
187              
188 182     182 1 348 my ($fname, $operand, $foperand, $errortext, $typelookup, $sheetdata) = @_;
189              
190 182         214 my ($value1, $tostype, $cr);
191              
192 182         258 my $sum = 0;
193 182         255 my $resulttypesum = "";
194 182         294 my $count = 0;
195 182         266 my $counta = 0;
196 182         211 my $countblank = 0;
197 182         207 my $product = 1;
198 182         255 my $maxval;
199             my $minval;
200 0         0 my ($mk, $sk, $mk1, $sk1); # For variance, etc.: M sub k, k-1, and S sub k-1
201             # as per Knuth "The Art of Computer Programming" Vol. 2 3rd edition, page 232
202              
203 182         662 my ($dbrange, $dbrangetype) =
204             top_of_stack_value_and_type($sheetdata, $foperand, $errortext);
205 182         324 my $fieldtype;
206 182         663 my $fieldname =
207             operand_value_and_type($sheetdata, $foperand, $errortext, \$fieldtype);
208 182         576 my ($criteriarange, $criteriarangetype) =
209             top_of_stack_value_and_type($sheetdata, $foperand, $errortext);
210              
211 182 50 33     936 if ($dbrangetype ne "range" || $criteriarangetype ne "range") {
212 0         0 function_args_error($fname, $operand, $errortext);
213 0         0 return 0;
214             }
215              
216 182         671 my ($dbsheetdata, $dbcol1num, $ndbcols, $dbrow1num, $ndbrows) =
217             decode_range_parts($sheetdata, $dbrange, $dbrangetype);
218             my (
219 182         585 $criteriasheetdata, $criteriacol1num, $ncriteriacols,
220             $criteriarow1num, $ncriteriarows
221             )
222             = decode_range_parts($sheetdata, $criteriarange, $criteriarangetype);
223              
224 182         664 my $fieldasnum =
225             field_to_colnum($dbsheetdata, $dbcol1num, $ndbcols, $dbrow1num,
226             $fieldname, $fieldtype);
227 182         245 $fieldasnum = int($fieldasnum);
228 182 50       415 if ($fieldasnum <= 0) {
229 0         0 push @$operand, { type => "e#VALUE!", value => 0 };
230 0         0 return;
231             }
232              
233 182         300 my $targetcol = $dbcol1num + $fieldasnum - 1;
234              
235 182         284 my (@criteriafieldnums, $criteriafieldname, $criteriafieldtype,
236             $criterianum);
237              
238 182         440 for (my $i = 0 ; $i < $ncriteriacols ; $i++) { # get criteria field colnums
239 218         604 my $criteriacr = cr_to_coord($criteriacol1num + $i, $criteriarow1num);
240 218         562 $criteriafieldname = $criteriasheetdata->{datavalues}->{$criteriacr};
241 218         438 $criteriafieldtype = $criteriasheetdata->{valuetypes}->{$criteriacr};
242 218         525 $criterianum =
243             field_to_colnum($dbsheetdata, $dbcol1num, $ndbcols, $dbrow1num,
244             $criteriafieldname, $criteriafieldtype);
245 218         356 $criterianum = int($criterianum);
246 218 50       568 if ($criterianum <= 0) {
247 0         0 push @$operand, { type => "e#VALUE!", value => 0 };
248 0         0 return;
249             }
250 218         635 push @criteriafieldnums, $dbcol1num + $criterianum - 1;
251             }
252              
253 182         220 my ($testok, $criteria, $testcol, $testcr);
254              
255 182         490 for (my $i = 1 ; $i < $ndbrows ; $i++)
256             { # go through each row of the database
257 2366         2602 $testok = 0;
258             CRITERIAROW:
259 2366         4501 for (my $j = 1 ; $j < $ncriteriarows ; $j++)
260             { # go through each criteria row
261 2709         4882 for (my $k = 0 ; $k < $ncriteriacols ; $k++) { # look at each column
262 2840         7174 my $criteriacr =
263             cr_to_coord($criteriacol1num + $k, $criteriarow1num + $j)
264             ; # where criteria is
265 2840         6029 $criteria = $criteriasheetdata->{datavalues}->{$criteriacr};
266 2840 50       4720 next unless $criteria; # blank items are OK
267 2840         10579 $testcol =
268             $criteriasheetdata->{datavalues}
269             ->{ cr_to_coord($criteriacol1num + $k, $criteriarow1num) };
270 2840         5163 $testcol = $criteriafieldnums[$k];
271 2840         8953 $testcr = cr_to_coord($testcol, $dbrow1num + $i); # cell to check
272             next CRITERIAROW
273 2840 100 50     13367 unless test_criteria($criteriasheetdata->{datavalues}->{$testcr},
274             ($criteriasheetdata->{valuetypes}->{$testcr} || "b"), $criteria);
275             }
276 500         578 $testok = 1;
277 500         693 last CRITERIAROW;
278             }
279 2366 100       6958 next unless $testok;
280              
281 500         1306 $cr =
282             cr_to_coord($targetcol, $dbrow1num + $i)
283             ; # get cell of this row to do the function on
284 500         1350 $value1 = $dbsheetdata->{datavalues}->{$cr};
285 500         958 $tostype = $dbsheetdata->{valuetypes}->{$cr};
286 500   50     984 $tostype ||= "b";
287 500 50       988 if ($tostype eq "b") { # blank
288 0         0 $value1 = 0;
289             }
290              
291 500 50       1104 $count += 1 if substr($tostype, 0, 1) eq "n";
292 500 50       1086 $counta += 1 if substr($tostype, 0, 1) ne "b";
293 500 50       957 $countblank += 1 if substr($tostype, 0, 1) eq "b";
294              
295 500 50 0     957 if (substr($tostype, 0, 1) eq "n") {
    0          
296 500         587 $sum += $value1;
297 500         542 $product *= $value1;
298 500 100       1113 $maxval =
    100          
299             (defined $maxval) ? ($value1 > $maxval ? $value1 : $maxval) : $value1;
300 500 50       975 $minval =
    100          
301             (defined $minval) ? ($value1 < $minval ? $value1 : $minval) : $value1;
302 500 100       810 if ($count eq 1)
303             { # initialize with with first values for variance used in STDEV, VAR, etc.
304 174         341 $mk1 = $value1;
305 174         232 $sk1 = 0;
306             } else { # Accumulate S sub 1 through n as per Knuth noted above
307 326         523 $mk = $mk1 + ($value1 - $mk1) / $count;
308 326         439 $sk = $sk1 + ($value1 - $mk1) * ($value1 - $mk);
309 326         329 $sk1 = $sk;
310 326         351 $mk1 = $mk;
311             }
312 500   66     2086 $resulttypesum =
313             lookup_result_type($tostype, $resulttypesum || $tostype,
314             $typelookup->{plus});
315             } elsif (substr($tostype, 0, 1) eq "e"
316             && substr($resulttypesum, 0, 1) ne "e") {
317 0         0 $resulttypesum = $tostype;
318             }
319             }
320              
321 182   100     423 $resulttypesum ||= "n";
322              
323 182 100       940 if ($fname eq "DSUM") {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
324 94         659 push @$operand, { type => $resulttypesum, value => $sum };
325             } elsif ($fname eq "DPRODUCT")
326             { # may handle cases with text differently than some other spreadsheets
327 6         33 push @$operand, { type => $resulttypesum, value => $product };
328             } elsif ($fname eq "DMIN") {
329 7   50     59 push @$operand, { type => $resulttypesum, value => ($minval || 0) };
330             } elsif ($fname eq "DMAX") {
331 8   50     81 push @$operand, { type => $resulttypesum, value => ($maxval || 0) };
332             } elsif ($fname eq "DCOUNT") {
333 12         74 push @$operand, { type => "n", value => $count };
334             } elsif ($fname eq "DCOUNTA") {
335 11         90 push @$operand, { type => "n", value => $counta };
336             } elsif ($fname eq "DAVERAGE") {
337 13 50       24 if ($count > 0) {
338 13         61 push @$operand, { type => $resulttypesum, value => ($sum / $count) };
339             } else {
340 0         0 push @$operand, { type => "e#DIV/0!", value => 0 };
341             }
342             } elsif ($fname eq "DSTDEV") {
343 5 50       19 if ($count > 1) {
344 5         36 push @$operand,
345             { type => $resulttypesum, value => (sqrt($sk / ($count - 1))) };
346             } else {
347 0         0 push @$operand, { type => "e#DIV/0!", value => 0 };
348             }
349             } elsif ($fname eq "DSTDEVP") {
350 4 50       13 if ($count > 1) {
351 4         28 push @$operand,
352             { type => $resulttypesum, value => (sqrt($sk / $count)) };
353             } else {
354 0         0 push @$operand, { type => "e#DIV/0!", value => 0 };
355             }
356             } elsif ($fname eq "DVAR") {
357 2 50       10 if ($count > 1) {
358 2         13 push @$operand,
359             { type => $resulttypesum, value => ($sk / ($count - 1)) };
360             } else {
361 0         0 push @$operand, { type => "e#DIV/0!", value => 0 };
362             }
363             } elsif ($fname eq "DVARP") {
364 1 50       7 if ($count > 1) {
365 1         9 push @$operand, { type => $resulttypesum, value => ($sk / $count) };
366             } else {
367 0         0 push @$operand, { type => "e#DIV/0!", value => 0 };
368             }
369             } elsif ($fname eq "DGET") {
370 19 100       55 if ($count == 1) {
    50          
371 10         47 push @$operand, { type => $resulttypesum, value => $sum };
372             } elsif ($count == 0) {
373 0         0 push @$operand, { type => "e#VALUE!", value => 0 };
374             } else {
375 9         35 push @$operand, { type => "e#NUM!", value => 0 };
376             }
377             }
378              
379 182         904 return;
380             }
381              
382             =head2 index_function
383              
384             =over
385              
386             =item INDEX(range, rownum, colnum)
387              
388             =back
389              
390             =cut
391              
392             sub index_function {
393              
394 12     12 1 33 my ($fname, $operand, $foperand, $errortext, $typelookup, $sheetdata) = @_;
395              
396 12         57 my ($range, $rangetype) =
397             top_of_stack_value_and_type($sheetdata, $foperand, $errortext)
398             ; # get range
399 12 50       47 if ($rangetype ne "range") {
400 0         0 function_args_error($fname, $operand, $errortext);
401 0         0 return 0;
402             }
403 12         50 my ($indexsheetdata, $col1num, $ncols, $row1num, $nrows) =
404             decode_range_parts($sheetdata, $range, $rangetype);
405              
406 12         26 my $rowindex = 0;
407 12         12 my $colindex = 0;
408 12         14 my $tostype;
409              
410 12 50       31 if (scalar @$foperand) { # look for row number
411 12         39 $rowindex =
412             operand_as_number($sheetdata, $foperand, $errortext, \$tostype);
413 12 50 33     74 if (substr($tostype, 0, 1) ne "n" || $rowindex < 0) {
414 0         0 push @$operand, { type => "e#VALUE!", value => 0 };
415 0         0 return;
416             }
417 12 50       28 if (scalar @$foperand) { # look for col number
418 12         48 $colindex =
419             operand_as_number($sheetdata, $foperand, $errortext, \$tostype);
420 12 50 33     66 if (substr($tostype, 0, 1) ne "n" || $colindex < 0) {
421 0         0 push @$operand, { type => "e#VALUE!", value => 0 };
422 0         0 return;
423             }
424 12 50       38 if (scalar @$foperand) {
425 0         0 function_args_error($fname, $operand, $errortext);
426 0         0 return 0;
427             }
428             } else { # col number missing
429 0 0       0 if ($nrows == 1) { # if only one row, then rowindex is really colindex
430 0         0 $colindex = $rowindex;
431 0         0 $rowindex = 0;
432             }
433             }
434             }
435              
436 12 50 33     64 if ($rowindex > $nrows || $colindex > $ncols) {
437 0         0 push @$operand, { type => "e#REF!", value => 0 };
438 0         0 return;
439             }
440              
441 12         16 my ($result, $resulttype);
442              
443 12 50       30 if ($rowindex == 0) {
444 0 0       0 if ($colindex == 0) {
445 0 0 0     0 if ($nrows == 1 && $ncols == 1) {
446 0         0 $result = cr_to_coord($col1num, $row1num);
447 0         0 $resulttype = "coord";
448             } else {
449 0         0 $result =
450             cr_to_coord($col1num, $row1num) . "|"
451             . cr_to_coord($col1num + $ncols - 1, $row1num + $nrows - 1) . "|";
452 0         0 $resulttype = "range";
453             }
454             } else {
455 0 0       0 if ($nrows == 1) {
456 0         0 $result = cr_to_coord($col1num + $colindex - 1, $row1num);
457 0         0 $resulttype = "coord";
458             } else {
459 0         0 $result =
460             cr_to_coord($col1num + $colindex - 1, $row1num) . "|"
461             . cr_to_coord($col1num + $colindex - 1, $row1num + $nrows - 1)
462             . "|";
463 0         0 $resulttype = "range";
464             }
465             }
466             } else {
467 12 50       35 if ($colindex == 0) {
468 0 0       0 if ($ncols == 1) {
469 0         0 $result = cr_to_coord($col1num, $row1num + $rowindex - 1);
470 0         0 $resulttype = "coord";
471             } else {
472 0         0 $result =
473             cr_to_coord($col1num, $row1num + $rowindex - 1) . "|"
474             . cr_to_coord($col1num + $ncols - 1, $row1num + $rowindex - 1)
475             . "|";
476 0         0 $resulttype = "range";
477             }
478             } else {
479 12         49 $result =
480             cr_to_coord($col1num + $colindex - 1, $row1num + $rowindex - 1);
481 12         32 $resulttype = "coord";
482             }
483             }
484              
485 12         42 push @$operand, { type => $resulttype, value => $result };
486 12         34 return;
487              
488             }
489              
490             =head2 countif_sumif_functions
491              
492             =over
493              
494             =item COUNTIF(c1:c2,"criteria")
495              
496             =item SUMIF(c1:c2,"criteria")
497              
498             =back
499              
500             =cut
501              
502             sub countif_sumif_functions {
503              
504 29     29 1 64 my ($fname, $operand, $foperand, $errortext, $typelookup, $sheetdata) = @_;
505              
506 29         57 my ($tostype, $tostype2, $sumrangevalue, $sumrangetype);
507              
508 29         114 my ($rangevalue, $rangetype) =
509             top_of_stack_value_and_type($sheetdata, $foperand, $errortext)
510             ; # get range or coord
511 29         144 my ($criteriavalue, $criteriatype) =
512             operand_as_text($sheetdata, $foperand, $errortext, \$tostype)
513             ; # get criteria
514 29 50       95 if ($fname eq "SUMIF") {
515 29 100       118 if ((scalar @$foperand) == 1) { # three arg form of SUMIF
    50          
516 5         19 ($sumrangevalue, $sumrangetype) =
517             top_of_stack_value_and_type($sheetdata, $foperand, $errortext);
518             } elsif ((scalar @$foperand) == 0) { # two arg form
519 24         408 $sumrangevalue = $rangevalue;
520 24         47 $sumrangetype = $rangetype;
521             } else {
522 0         0 function_args_error($fname, $operand, $errortext);
523 0         0 return 0;
524             }
525             } else {
526 0         0 $sumrangevalue = $rangevalue;
527 0         0 $sumrangetype = $rangetype;
528             }
529              
530 29   50     224 my $ct = substr($criteriatype || '', 0, 1) || '';
531 29 50       124 if ($ct eq "n") {
    50          
    50          
532 0         0 $criteriavalue = "$criteriavalue";
533             } elsif ($ct eq "e") { # error
534 0         0 undef $criteriavalue;
535             } elsif ($ct eq "b") { # blank here is undefined
536 0         0 undef $criteriavalue;
537             }
538              
539 29 100 66     154 if ($rangetype ne "coord" && $rangetype ne "range") {
540 5         26 function_args_error($fname, $operand, $errortext);
541 5         15 return 0;
542             }
543              
544 24 50 33     196 if ( $fname eq "SUMIF"
      33        
545             && $sumrangetype ne "coord"
546             && $sumrangetype ne "range") {
547 0         0 function_args_error($fname, $operand, $errortext);
548 0         0 return 0;
549             }
550              
551 24         100 push @$foperand, { type => $rangetype, value => $rangevalue };
552 24         38 my @f2operand; # to allow for 3 arg form
553 24         106 push @f2operand, { type => $sumrangetype, value => $sumrangevalue };
554              
555 24         50 my $sum = 0;
556 24         38 my $resulttypesum = "";
557 24         35 my $count = 0;
558              
559 24         64 while (@$foperand) {
560 68         200 my $value1 =
561             operand_value_and_type($sheetdata, $foperand, $errortext, \$tostype);
562 68         403 my $value2 =
563             operand_value_and_type($sheetdata, \@f2operand, $errortext, \$tostype2);
564              
565 68 100       242 next unless test_criteria($value1, $tostype, $criteriavalue);
566              
567 26         42 $count += 1;
568              
569 26 100 33     122 if (substr($tostype2, 0, 1) eq "n") {
    50          
570 22         32 $sum += $value2;
571 22   66     153 $resulttypesum =
572             lookup_result_type($tostype2, $resulttypesum || $tostype2,
573             $typelookup->{plus});
574             } elsif (substr($tostype2, 0, 1) eq "e"
575             && substr($resulttypesum, 0, 1) ne "e") {
576 0         0 $resulttypesum = $tostype2;
577             }
578             }
579              
580 24   100     78 $resulttypesum ||= "n";
581              
582 24 50       88 if ($fname eq "SUMIF") {
    0          
583 24         128 push @$operand, { type => $resulttypesum, value => $sum };
584             } elsif ($fname eq "COUNTIF") {
585 0         0 push @$operand, { type => "n", value => $count };
586             }
587              
588 24         102 return;
589              
590             }
591              
592             =head2 columns_rows_function
593              
594             =over
595              
596             =item COLUMNS(c1:c2)
597              
598             =item ROWS(c1:c2)
599              
600             =back
601              
602             =cut
603              
604             sub columns_rows_function {
605              
606 21     21 1 46 my ($fname, $operand, $foperand, $errortext, $typelookup, $sheetdata) = @_;
607              
608 21         26 my ($value1, $tostype, $resultvalue, $resulttype);
609              
610 21         73 ($value1, $tostype) =
611             top_of_stack_value_and_type($sheetdata, $foperand, $errortext);
612              
613 21 100       95 if ($tostype eq "coord") {
    50          
614 9         14 $resultvalue = 1;
615 9         16 $resulttype = "n";
616             } elsif ($tostype eq "range") {
617 12         61 my ($v1, $v2, $sequence) = split (/\|/, $value1);
618 12         19 my ($sheet1, $sheet2);
619 12         41 ($v1, $sheet1) = split (/!/, $v1);
620 12         41 ($v2, $sheet2) = split (/!/, $v2);
621 12         45 my ($c1, $r1) = coord_to_cr($v1);
622 12         36 my ($c2, $r2) = coord_to_cr($v2);
623 12 50       31 ($c2, $c1) = ($c1, $c2) if ($c1 > $c2);
624 12 50       38 ($r2, $r1) = ($r1, $r2) if ($r1 > $r2);
625              
626 12 100       39 if ($fname eq "COLUMNS") {
    50          
627 9         16 $resultvalue = $c2 - $c1 + 1;
628             } elsif ($fname eq "ROWS") {
629 3         10 $resultvalue = $r2 - $r1 + 1;
630             }
631 12         27 $resulttype = "n";
632             } else {
633 0         0 $resultvalue = 0;
634 0         0 $resulttype = "e#VALUE!";
635             }
636              
637 21         88 push @$operand, { type => $resulttype, value => $resultvalue };
638              
639 21         61 return;
640              
641             }
642              
643             =head2 text_function
644              
645             =over
646              
647             =item PLAINTEXT
648              
649             =back
650              
651             =cut
652              
653             sub text_function {
654              
655 0     0 1 0 my ($fname, $operand, $foperand, $errortext, $typelookup, $sheetdata) = @_;
656              
657 0         0 my ($value1, $tostype, $resulttype);
658              
659 0         0 my $textstr = "";
660 0         0 $resulttype = "";
661 0         0 while (@$foperand) {
662 0         0 $value1 = operand_as_text($sheetdata, $foperand, $errortext, \$tostype);
663 0 0 0     0 if (substr($tostype, 0, 1) eq "t") {
    0          
664 0         0 $textstr .= $value1;
665 0   0     0 $resulttype = lookup_result_type($tostype, $resulttype || $tostype,
666             $typelookup->{concat});
667             } elsif (substr($tostype, 0, 1) eq "e"
668             && substr($resulttype, 0, 1) ne "e") {
669 0         0 $resulttype = $tostype;
670             }
671             }
672 0 0       0 $resulttype = substr($resulttype, 0, 1) eq "t" ? "t" : $resulttype;
673 0         0 push @$operand, { type => $resulttype, value => $textstr };
674              
675 0         0 return;
676              
677             }
678              
679             =head2 html_function
680              
681             =over
682              
683             =item HTML
684              
685             =back
686              
687             =cut
688              
689             sub html_function {
690              
691 0     0 1 0 my ($fname, $operand, $foperand, $errortext, $typelookup, $sheetdata) = @_;
692              
693 0         0 my ($value1, $tostype, $resulttype);
694              
695 0         0 my $textstr = "";
696 0         0 $resulttype = "";
697 0         0 while (@$foperand) {
698 0         0 $value1 = operand_as_text($sheetdata, $foperand, $errortext, \$tostype);
699 0 0 0     0 if (substr($tostype, 0, 1) eq "t") {
    0          
700 0         0 $textstr .= $value1;
701 0   0     0 $resulttype = lookup_result_type($tostype, $resulttype || $tostype,
702             $typelookup->{concat});
703             } elsif (substr($tostype, 0, 1) eq "e"
704             && substr($resulttype, 0, 1) ne "e") {
705 0         0 $resulttype = $tostype;
706             }
707             }
708 0 0       0 $resulttype = substr($resulttype, 0, 1) eq "t" ? "th" : $resulttype;
709 0         0 push @$operand, { type => $resulttype, value => $textstr };
710              
711 0         0 return;
712              
713             }
714              
715             =head1 HELPERS
716              
717             =head2 field_to_colnum
718              
719             $colnum = field_to_colnum(\@sheetdata, $col1num, $ncols, $row1num, $fieldname, $fieldtype)
720              
721             If fieldname is a number, uses it, otherwise looks up string in cells in row to find field number
722              
723             If not found, returns 0.
724              
725             =cut
726              
727             sub field_to_colnum {
728              
729 400     400 1 696 my ($sheetdata, $col1num, $ncols, $row1num, $fieldname, $fieldtype) = @_;
730              
731 400 50       975 if (substr($fieldtype, 0, 1) eq "n") { # number - return it if legal
732 0 0 0     0 if ($fieldname <= 0 || $fieldname > $ncols) {
733 0         0 return 0;
734             }
735 0         0 return int($fieldname);
736             }
737              
738 400 50       923 if (substr($fieldtype, 0, 1) ne "t") { # must be text otherwise
739 0         0 return 0;
740             }
741              
742 400         1302 $fieldname = decode('utf8', $fieldname); # change UTF-8 bytes to chars
743 400         12655 $fieldname = lc $fieldname;
744              
745 400         561 my ($cr, $value);
746              
747 400         1029 for (my $i = 0 ; $i < $ncols ; $i++)
748             { # look through column headers for a match
749 951         2338 $cr = cr_to_coord($col1num + $i, $row1num);
750 951         2386 $value = $sheetdata->{datavalues}->{$cr};
751 951         2126 $value = decode('utf8', $value);
752 951         20746 $value = lc $value; #ignore case
753 951 100       2692 next if $value ne $fieldname; # no match
754 400         939 return $i + 1; # match
755             }
756 0           return 0; # looked at all and no match
757             }
758              
759             1;
760              
761             __END__