# Plugin for TWiki Enterprise Collaboration Platform, http://TWiki.org/
#
# Copyright (C) 2001-2018 Peter Thoeny, peter[at]thoeny.org and
# TWiki Contributors.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version. For
# more details read LICENSE in the root of this distribution.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details, published at
# http://www.gnu.org/copyleft/gpl.html
#
# As per the GPL, removal of this notice is prohibited.
#
# =========================
#
# This is part of TWiki's Spreadsheet Plugin.
#
# The code below is kept out of the main plugin module for
# performance reasons, so it doesn't get compiled until it
# is actually used.
package TWiki::Plugins::SpreadSheetPlugin::Calc;
use strict;
use Time::Local;
use Time::Local qw( timegm_nocheck timelocal_nocheck ); # Necessary for DOY
# =========================
my $web;
my $topic;
my $debug;
my $renderingWeb;
my @tableMatrix;
my $insideTABLE = 0;
my $cPos = -1;
my $rPos = -1;
my $escToken = "\0";
my $escComma = "\1"; # Single char escapes so that size functions work as expected
my $escOpenP = "\2";
my $escCloseP = "\3";
my $escNewLn = "\4";
my %varStore = ();
my %listStore = ();
my $hashStore = {};
my $dontSpaceRE = '';
my $currencySymbol = '';
my @monArr = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" );
my @wdayArr = ( "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" );
my %mon2num;
{
my $count = 0;
%mon2num = map { $_ => $count++ } @monArr;
}
my $funcRef;
my $recurseFunc = \&_recurseFunc;
# =========================
sub init
{
( $web, $topic, $debug ) = @_;
# initialize variables, once per page view
@tableMatrix = ();
$insideTABLE = 0;
$cPos = -1;
$rPos = -1;
%varStore = ();
%listStore = ();
$hashStore = {};
$dontSpaceRE = '';
$currencySymbol = '';
# Module initialized
TWiki::Func::writeDebug( "- TWiki::Plugins::SpreadSheetPlugin::Calc::init( $web.$topic )" ) if $debug;
return 1;
}
# =========================
sub handleVarCALC
{
### my ( $text, $topic, $web ) = @_; # do not uncomment, use $_[0], $_[1]... instead
TWiki::Func::writeDebug( "- SpreadSheetPlugin::Calc::CALC( $_[2].$_[1] )" ) if $debug;
@tableMatrix = ();
$insideTABLE = 0;
$cPos = -1;
$rPos = -1;
$web = $_[2];
my @result = ();
my $insidePRE = 0;
my $line = "";
my $before = "";
my $cell = "";
my @row = ();
$_[0] =~ s/\r//go;
$_[0] =~ s/\\\n//go; # Join lines ending in "\"
foreach( split( /\n/, $_[0] ) ) {
# change state:
m|
|i && ( $insidePRE = 1 );
m||i && ( $insidePRE = 1 );
m|
|i && ( $insidePRE = 0 );
m||i && ( $insidePRE = 0 );
if( ! ( $insidePRE ) ) {
if( /^\s*\|.*\|\s*$/ ) {
# inside | table |
if( ! $insideTABLE ) {
$insideTABLE = 1;
@tableMatrix = (); # reset table matrix
$cPos = -1;
$rPos = -1;
}
$line = $_;
$line =~ s/^(\s*\|)(.*)\|\s*$/$2/o;
$before = $1;
@row = split( /\|/o, $line, -1 );
$row[0] = '' unless( @row );
push( @tableMatrix, [ @row ] );
$rPos++;
$line = "$before";
for( $cPos = 0; $cPos < @row; $cPos++ ) {
$cell = $row[$cPos];
$cell =~ s/%CALC\{(.*?)\}%/doCalc($1)/geo;
$line .= "$cell|";
}
$cPos--; # Decrement to indicate proper last column number
s/.*/$line/o;
} else {
# outside | table |
if( $insideTABLE ) {
$insideTABLE = 0;
}
s/%CALC\{(.*?)\}%/doCalc($1)/geo;
}
}
push( @result, $_ );
}
$_[0] = join( "\n", @result );
}
# =========================
sub doCalc
{
my( $theAttributes ) = @_;
my $text = &TWiki::Func::extractNameValuePair( $theAttributes );
# Escape commas, parenthesis and newlines in tripple quoted strings
$text =~ s/'''(.*?)'''/_escapeString($1)/geos;
# For better performance, use a function reference when calling the recurse
# functions, instead of an "if" statement within the _recurseFunc function
if ( $text =~ /\n/ ) {
# recursively evaluate functions, and remove white space around functions and parameters
$recurseFunc = \&_recurseFuncCutWhitespace;
} else {
# recursively evaluate functions without removing white space (compatible with old spec)
$recurseFunc = \&_recurseFunc;
}
# Add nesting level to parenthesis,
# e.g. "A(B())" gets "A-esc-1(B-esc-2(-esc-2)-esc-1)"
my $level = 0;
$text =~ s/([\(\)])/_addNestingLevel($1, \$level)/geo;
$text = _doFunc( "MAIN", $text );
if( $insideTABLE && ( $rPos >= 0 ) && ( $cPos >= 0 ) ) {
# update cell in table matrix
$tableMatrix[$rPos][$cPos] = $text;
}
# Restore escaped strings
$text =~ s/$escComma/,/go;
$text =~ s/$escOpenP/\(/go;
$text =~ s/$escCloseP/\)/go;
$text =~ s/$escNewLn/\n/go;
return $text;
}
# =========================
sub _escapeString
{
my( $text ) = @_;
$text =~ s/,/$escComma/go;
$text =~ s/\(/$escOpenP/go;
$text =~ s/\)/$escCloseP/go;
$text =~ s/\n/$escNewLn/go;
return $text;
}
# =========================
sub _addNestingLevel
{
my( $theParen, $theLevelRef ) = @_;
my $result = "";
if( $theParen eq "(" ) {
$$theLevelRef++;
$result = "$escToken$$theLevelRef$theParen";
} else {
$result = "$escToken$$theLevelRef$theParen";
$$theLevelRef-- if( $$theLevelRef > 0 );
}
return $result;
}
# =========================
sub _recurseFunc
{
# Handle functions recursively
$_[0] =~ s/\$([A-Z]+[A-Z0-9]*)$escToken([0-9]+)\((.*?)$escToken\2\)/_doFunc($1,$3)/geos;
# Clean up unbalanced mess
$_[0] =~ s/$escToken\-*[0-9]+([\(\)])/$1/go;
}
# =========================
sub _recurseFuncCutWhitespace
{
# Handle functions recursively
$_[0] =~ s/\s*\$([A-Z]+[A-Z0-9]*)$escToken([0-9]+)\(\s*(.*?)\s*$escToken\2\)\s*/_doFunc($1,$3)/geos;
# Clean up unbalanced mess
$_[0] =~ s/$escToken\-*[0-9]+([\(\)])/$1/go;
}
# =========================
sub _doFunc
{
my( $theFunc, $theAttr ) = @_;
$theAttr = "" unless( defined $theAttr );
TWiki::Func::writeDebug( "- SpreadSheetPlugin::Calc::_doFunc: $theFunc( $theAttr ) start" ) if $debug;
unless( $theFunc =~ /^(HASHEACH|IF|LISTEACH|LISTIF|LISTMAP|NOEXEC|WHILE)$/ ) {
&$recurseFunc( $theAttr );
}
# else: delay the function handler to after parsing the parameters,
# in which case handling functions and cleaning up needs to be done later
my $result = "";
my $f = $funcRef->{$theFunc};
if( $f ) {
$result = &$f( $theAttr );
}
else {
$result = "func $theFunc not found. ";
}
TWiki::Func::writeDebug( "- SpreadSheetPlugin::Calc::_doFunc: $theFunc( $theAttr ) returns: $result" ) if $debug;
return $result;
}
# =========================
$funcRef->{ABOVE} = \&_funcABOVE;
sub _funcABOVE
{
my( $theAttr ) = @_;
my $i = $cPos + 1;
return "R0:C$i..R$rPos:C$i";
}
# =========================
$funcRef->{ABS} = \&_funcABS;
sub _funcABS
{
my( $theAttr ) = @_;
return abs( _getNumber( $theAttr ) );
}
# =========================
$funcRef->{ADDLIST} = \&_funcADDLIST;
sub _funcADDLIST
{
my( $theAttr ) = @_;
my( $name, $value ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$value = '' unless( defined $value );
$value = _listToDelimitedString( _getList( $value ) );
$name =~ s/[^a-zA-Z0-9\_]//go;
if( $name ) {
my $old = $listStore{ $name };
if( defined( $old ) && $old ne '' ) {
$listStore{ $name } = "$old, $value";
} else {
$listStore{ $name } = $value;
}
}
return '';
}
# =========================
$funcRef->{AND} = \&_funcAND;
sub _funcAND
{
my( $theAttr ) = @_;
my $result = 0;
my @arr = _getListAsInteger( $theAttr );
foreach my $i( @arr ) {
unless( $i ) {
$result = 0;
last;
}
$result = 1;
}
return $result;
}
# =========================
$funcRef->{AVERAGE} = \&_funcAVERAGE;
$funcRef->{MEAN} = \&_funcAVERAGE; # undocumented - do not remove
sub _funcAVERAGE
{
my( $theAttr ) = @_;
my $result = 0;
my $items = 0;
my @arr = grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
foreach my $i ( @arr ) {
$result += $i;
$items++;
}
if( $items > 0 ) {
$result = $result / $items;
}
return $result;
}
# =========================
$funcRef->{BIN2DEC} = \&_funcBIN2DEC;
sub _funcBIN2DEC
{
my( $theAttr ) = @_;
$theAttr =~ s/[^0-1]//g; # only binary digits
$theAttr ||= 0;
my $result = oct( '0b' . $theAttr );
return $result;
}
# =========================
$funcRef->{BITXOR} = \&_funcBITXOR;
sub _funcBITXOR
{
my( $theAttr ) = @_;
my $ff = chr(255) x length( $theAttr );
return $theAttr ^ $ff;
}
# =========================
$funcRef->{CEILING} = \&_funcCEILING;
sub _funcCEILING
{
my( $theAttr ) = @_;
my $i = _getNumber( $theAttr );
my $result = int( $i );
if( $i > 0 && $i != $result ) {
$result += 1;
}
return $result;
}
# =========================
$funcRef->{CHAR} = \&_funcCHAR;
sub _funcCHAR
{
my( $theAttr ) = @_;
my $result = '';
my $i = 0;
if( $theAttr =~ /([0-9]+)/ ) {
$i = $1;
}
$i = 255 if $i > 255;
$i = 0 if $i < 0;
return chr( $i );
}
# =========================
$funcRef->{CODE} = \&_funcCODE;
sub _funcCODE
{
my( $theAttr ) = @_;
return ord( $theAttr );
}
# =========================
$funcRef->{COLUMN} = \&_funcCOLUMN;
sub _funcCOLUMN
{
my( $theAttr ) = @_;
my $i = $theAttr || 0;
return $cPos + $i + 1;
}
# =========================
$funcRef->{COUNTITEMS} = \&_funcCOUNTITEMS;
sub _funcCOUNTITEMS
{
my( $theAttr ) = @_;
my $result = '';
my @arr = _getList( $theAttr );
my %items = ();
my $key = "";
foreach $key ( @arr ) {
$key =~ s/^\s*(.*?)\s*$/$1/o if( $key );
if( $key ) {
if( exists( $items{ $key } ) ) {
$items{ $key }++;
} else {
$items{ $key } = 1;
}
}
}
foreach $key ( sort keys %items ) {
$result .= "$key: $items{ $key }
";
}
$result =~ s|
$||o;
return $result;
}
# =========================
$funcRef->{COUNTSTR} = \&_funcCOUNTSTR;
sub _funcCOUNTSTR
{
my( $theAttr ) = @_;
my $result = 0; # count any string
my $i = 0; # count string equal second attr
my $list = $theAttr;
my $str = "";
if( $theAttr =~ /^(.*),\s*(.*?)$/ ) { # greedy match for last comma
$list = $1;
$str = $2;
}
$str =~ s/\s*$//o;
my @arr = _getList( $list );
foreach my $cell ( @arr ) {
if( defined $cell ) {
$cell =~ s/^\s*(.*?)\s*$/$1/o;
$result++ if( $cell );
$i++ if( $cell eq $str );
}
}
$result = $i if( $str );
return $result;
}
# =========================
$funcRef->{DEC2BIN} = \&_funcDEC2BIN;
sub _funcDEC2BIN
{
my( $theAttr ) = @_;
my ( $num, $size ) = _getListAsInteger( $theAttr );
$num ||= 0;
my $format = '%';
$format .= '0' . $size if( $size );
$format .= 'b';
my $result = sprintf( $format, $num );
return $result;
}
# =========================
$funcRef->{DEC2HEX} = \&_funcDEC2HEX;
sub _funcDEC2HEX
{
my( $theAttr ) = @_;
my ( $num, $size ) = _getListAsInteger( $theAttr );
$num ||= 0;
my $format = '%';
$format .= '0' . $size if( $size );
$format .= 'X';
my $result = sprintf( $format, $num );
return $result;
}
# =========================
$funcRef->{DEC2OCT} = \&_funcDEC2OCT;
sub _funcDEC2OCT
{
my( $theAttr ) = @_;
my ( $num, $size ) = _getListAsInteger( $theAttr );
$num ||= 0;
my $format = '%';
$format .= '0' . $size if( $size );
$format .= 'o';
my $result = sprintf( $format, $num );
return $result;
}
# =========================
$funcRef->{DEF} = \&_funcDEF;
sub _funcDEF
{
my( $theAttr ) = @_;
# Format DEF(list) returns first defined cell
# Added by MF 26/3/2002, fixed by PeterThoeny
my @arr = _getList( $theAttr );
my $result = '';
foreach my $cell ( @arr ) {
if( $cell ) {
$cell =~ s/^\s*(.*?)\s*$/$1/o;
if( $cell ) {
$result = $cell;
last;
}
}
}
return $result;
}
# =========================
$funcRef->{EMPTY} = \&_funcEMPTY;
sub _funcEMPTY
{
my( $theAttr ) = @_;
my $result = 1;
$result = 0 if( length( $theAttr ) > 0 );
return $result;
}
# =========================
$funcRef->{EQUAL} = \&_funcEQUAL;
sub _funcEQUAL
{
my( $theAttr ) = @_;
my $result = 0;
my( $str1, $str2 ) = split( /,\s*/, $theAttr, 2 );
$str1 = '' unless( $str1 );
$str2 = '' unless( $str2 );
$str1 =~ s/^\s+//os; # cut leading and trailing spaces
$str1 =~ s/\s+$//os;
$str2 =~ s/^\s+//os;
$str2 =~ s/\s+$//os;
$result = 1 if( lc( $str1 ) eq lc( $str2 ) );
return $result;
}
# =========================
$funcRef->{EVAL} = \&_funcEVAL;
sub _funcEVAL
{
my( $theAttr ) = @_;
return _safeEvalPerl( $theAttr );
}
# =========================
$funcRef->{EVEN} = \&_funcEVEN;
sub _funcEVEN
{
my( $theAttr ) = @_;
return ( _getNumber( $theAttr ) + 1 ) % 2;
}
# =========================
$funcRef->{EXACT} = \&_funcEXACT;
sub _funcEXACT
{
my( $theAttr ) = @_;
my $result = 0;
my( $str1, $str2 ) = split( /,\s*/, $theAttr, 2 );
$str1 = '' unless( $str1 );
$str2 = '' unless( $str2 );
$str1 =~ s/^\s+//os; # cut leading and trailing spaces
$str1 =~ s/\s+$//os;
$str2 =~ s/^\s+//os;
$str2 =~ s/\s+$//os;
$result = 1 if( $str1 eq $str2 );
return $result;
}
# =========================
$funcRef->{EXEC} = \&_funcEXEC;
sub _funcEXEC
{
my( $theAttr ) = @_;
# add nesting level escapes
my $level = 0;
my $result = $theAttr;
$result =~ s/([\(\)])/_addNestingLevel($1, \$level)/geo;
# execute functions in attribute recursively and clean up unbalanced parenthesis
&$recurseFunc( $result );
return $result;
}
# =========================
$funcRef->{EXISTS} = \&_funcEXISTS;
sub _funcEXISTS
{
my( $theAttr ) = @_;
my $result = TWiki::Func::topicExists( $web, $theAttr );
$result = 0 unless( $result );
return $result;
}
# =========================
$funcRef->{EXP} = \&_funcEXP;
sub _funcEXP
{
my( $theAttr ) = @_;
return exp( _getNumber( $theAttr ) );
}
# =========================
$funcRef->{FILTER} = \&_funcFILTER;
sub _funcFILTER
{
my( $theAttr ) = @_;
my $result = '';
my( $filter, $string ) = split ( /,\s*/, $theAttr, 2 );
if( defined $string ) {
$filter =~ s/\$comma/,/g;
$filter =~ s/\$sp/ /g;
eval '$string =~ s/$filter//go';
$result = $string;
}
return $result;
}
# =========================
$funcRef->{FIND} = \&_funcFIND;
sub _funcFIND
{
my( $theAttr ) = @_;
my( $searchString, $string, $pos ) = split( /,\s*/, $theAttr, 3 );
my $result = 0;
$searchString = '' unless( defined $searchString );
$string = '' unless( defined $string );
$pos--;
$pos = 0 if( $pos < 0 );
pos( $string ) = $pos if( $pos );
$searchString = quotemeta( $searchString );
# using zero width lookahead '(?=...)' to keep pos at the beginning of match
if( eval '$string =~ m/(?=$searchString)/g' && $string ) {
$result = pos( $string ) + 1;
}
return $result;
}
# =========================
$funcRef->{FLOOR} = \&_funcFLOOR;
sub _funcFLOOR
{
my( $theAttr ) = @_;
my $i = _getNumber( $theAttr );
my $result = int( $i );
if( $i < 0 && $i != $result ) {
$result -= 1;
}
return $result;
}
# =========================
$funcRef->{FORMAT} = \&_funcFORMAT;
sub _funcFORMAT
{
my( $theAttr ) = @_;
# Format FORMAT(TYPE, precision, value) returns formatted value -- JimStraus - 05 Jan 2003
my( $format, $res, $value ) = split( /,\s*/, $theAttr );
$format =~ s/^\s*(.*?)\s*$/$1/os; #Strip leading and trailing spaces
$res =~ s/^\s*(.*?)\s*$/$1/os;
$value =~ s/^\s*(.*?)\s*$/$1/os;
my $result = '';
if( $format =~ /^(DOLLAR|CURRENCY)$/ ) {
unless( $currencySymbol ) {
$currencySymbol = TWiki::Func::getPreferencesValue( "CURRENCYSYMBOL" )
|| TWiki::Func::getPreferencesValue( "SPREADSHEETPLUGIN_CURRENCYSYMBOL" )
|| '$';
}
my $symbol = '$';
$symbol = $currencySymbol if( $format eq "CURRENCY" );
my $neg = 1 if $value < 0;
$value = abs($value);
$result = sprintf("%0.${res}f", $value);
my $temp = reverse $result;
$temp =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g;
$result = $symbol . (scalar reverse $temp);
$result = "(".$result.")" if $neg;
} elsif( $format eq "COMMA" ) {
$result = sprintf("%0.${res}f", $value);
my $temp = reverse $result;
$temp =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g;
$result = scalar reverse $temp;
} elsif( $format eq "PERCENT" ) {
$result = sprintf("%0.${res}f%%", $value * 100);
} elsif( $format eq "NUMBER" ) {
$result = sprintf("%0.${res}f", $value);
} elsif( $format eq "K" ) {
$result = sprintf("%0.${res}f K", $value / 1024);
} elsif( $format eq "KB" ) {
$result = sprintf("%0.${res}f KB", $value / 1024);
} elsif ($format eq "MB") {
$result = sprintf("%0.${res}f MB", $value / (1024 * 1024));
} elsif( $format =~ /^KBMB/ ) {
$value /= 1024;
my @lbls = ( "MB", "GB", "TB", "PB", "EB", "ZB" );
my $lbl = "KB";
while( $value >= 1024 && @lbls ) {
$value /= 1024;
$lbl = shift @lbls;
}
$result = sprintf("%0.${res}f", $value) . " $lbl";
} else {
# FORMAT not recognized, just return value
$result = $value;
}
return $result;
}
# =========================
$funcRef->{FORMATGMTIME} = \&_funcFORMATGMTIME;
sub _funcFORMATGMTIME
{
my( $theAttr ) = @_;
my $result = '';
my( $time, $str ) = split( /,\s*/, $theAttr, 2 );
if( $time =~ /([0-9]+)/ ) {
$time = $1;
} else {
$time = time();
}
return _serial2date( $time, $str, 1 );
}
# =========================
$funcRef->{FORMATTIME} = \&_funcFORMATTIME;
sub _funcFORMATTIME
{
my( $theAttr ) = @_;
my( $time, $str ) = split( /,\s*/, $theAttr, 2 );
if( $time =~ /([0-9]+)/ ) {
$time = $1;
} else {
$time = time();
}
my $isGmt = 0;
$isGmt = 1 if( $str =~ m/ gmt/i );
return _serial2date( $time, $str, $isGmt );
}
# =========================
$funcRef->{FORMATTIMEDIFF} = \&_funcFORMATTIMEDIFF;
sub _funcFORMATTIMEDIFF
{
my( $theAttr ) = @_;
my( $scale, $prec, $time, $option ) = split( /,\s*/, $theAttr, 4 );
$scale |= '';
$prec = int( _getNumber( $prec ) - 1 );
$prec = 0 if( $prec < 0 );
$time = _getNumber( $time );
$time = 0 if( $time < 0 );
$option |= '';
my @unit = ( 0, 0, 0, 0, 0, 0 ); # sec, min, hours, days, month, years
my @factor = ( 1, 60, 60, 24, 30.4166, 12 ); # sec, min, hours, days, month, years
my @singular = ( 'second', 'minute', 'hour', 'day', 'month', 'year' );
my @plural = ( 'seconds', 'minutes', 'hours', 'days', 'months', 'years' );
my $min = 0;
my $max = $prec;
if( $scale =~ /^min/i ) {
$min = 1;
$unit[1] = $time;
} elsif( $scale =~ /^hou/i ) {
$min = 2;
$unit[2] = $time;
} elsif( $scale =~ /^day/i ) {
$min = 3;
$unit[3] = $time;
} elsif( $scale =~ /^mon/i ) {
$min = 4;
$unit[4] = $time;
} elsif( $scale =~ /^yea/i ) {
$min = 5;
$unit[5] = $time;
} else {
$unit[0] = $time;
}
my @arr = ();
my $i = 0;
my $val1 = 0;
my $val2 = 0;
for( $i = $min; $i < 5; $i++ ) {
$val1 = int($unit[$i]);
$val2 = $unit[$i+1] = int($val1 / $factor[$i+1]);
$val1 = $unit[$i] = $val1 - int($val2 * $factor[$i+1]);
push( @arr, "$val1 $singular[$i]" ) if( $val1 == 1 );
push( @arr, "$val1 $plural[$i]" ) if( $val1 > 1 );
}
push( @arr, "$val2 $singular[$i]" ) if( $val2 == 1 );
push( @arr, "$val2 $plural[$i]" ) if( $val2 > 1 );
push( @arr, "0 $plural[$min]" ) unless( @arr );
my @reverse = reverse( @arr );
$#reverse = $prec if( @reverse > $prec );
my $result = join( ', ', @reverse );
if( $option eq 's' ) {
# short format: 1 y, 2 mon, 3 d, 4 h, 5 min, 6 sec
$result =~ s/([0-9]) (sec|min|mon)[a-z]*/$1~$2/g;
$result =~ s/([0-9]) ([a-z])[a-z]*/$1 $2/g;
$result =~ s/~/ /g;
} elsif( $option eq 'c' ) {
# compact format: 1y 2mon 3d 4h 5m 6s
$result =~ s/([0-9]) (mon)[a-z]*/$1$2/g;
$result =~ s/([0-9]) ([a-z])[a-z]*/$1$2/g;
$result =~ s/,//g;
} else {
$result =~ s/(.+)\, /$1 and /;
}
return $result;
}
# =========================
$funcRef->{GET} = \&_funcGET;
sub _funcGET
{
my( $theAttr ) = @_;
my $name = $theAttr;
$name =~ s/[^a-zA-Z0-9\_]//go;
my $result = $varStore{ $name } if( $name );
$result = "" unless( defined( $result ) );
return $result;
}
# =========================
$funcRef->{GETHASH} = \&_funcGETHASH;
sub _funcGETHASH
{
my( $theAttr ) = @_;
my( $name, $key ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
$key = '' unless( defined $key );
my $result = '';
if( $name ne '' && $key ne '' ) {
if( exists $hashStore->{$name} && exists $hashStore->{$name}{$key} ) {
$result = $hashStore->{$name}{$key};
}
} elsif( $name ne '' ) {
if( exists $hashStore->{$name} ) {
$result = join( ', ', sort keys %{ $hashStore->{$name} } );
}
} else {
$result = join( ', ', sort keys %$hashStore );
}
return $result;
}
# =========================
$funcRef->{GETLIST} = \&_funcGETLIST;
sub _funcGETLIST
{
my( $theAttr ) = @_;
my $name = $theAttr;
$name =~ s/[^a-zA-Z0-9\_]//go;
my $result = $listStore{ $name } if( $name );
$result = "" unless( defined( $result ) );
return $result;
}
# =========================
$funcRef->{HASH2LIST} = \&_funcHASH2LIST;
sub _funcHASH2LIST
{
my( $theAttr ) = @_;
my( $name, $format ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
return '' if( $name eq '' || ! exists $hashStore->{$name} );
$format = '$key, $value' unless( defined $format );
my $result = join( ', ',
map {
my $item = $format;
$item =~ s/\$key/$_/g;
$item =~ s/\$value/$hashStore->{$name}{$_}/g;
$item =~ s/\$comma/, /g;
$item;
}
sort
keys %{ $hashStore->{$name} }
);
return $result;
}
# =========================
$funcRef->{HASHCOPY} = \&_funcHASHCOPY;
sub _funcHASHCOPY
{
my( $theAttr ) = @_;
my( $from, $to ) = split( /,\s*/, $theAttr, 2 );
$from = '' unless( defined $from );
$from =~ s/[^\w\.\/]//go;
return '' if( $from eq '' || ! exists $hashStore->{$from} );
$to = '' unless( defined $to );
$to =~ s/[^\w\.\/]//go;
return '' if( $to eq '' );
%{ $hashStore->{$to} } = %{ $hashStore->{$from} };
return '';
}
# =========================
$funcRef->{HASHEACH} = \&_funcHASHEACH;
sub _funcHASHEACH
{
my( $theAttr ) = @_;
my( $action, $name ) = _properSplit( $theAttr, 2 );
$action = '' unless( defined $action );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
&$recurseFunc( $name );
return '' if( $name eq '' || ! exists $hashStore->{$name} );
# with delay, handle functions recursively and clean up unbalanced parenthesis
my $i = 0;
for my $key ( sort keys %{ $hashStore->{$name} } ) {
$i++;
my $value = $hashStore->{$name}{$key};
my $item = $action;
$item =~ s/\$index/$i/go;
$item =~ s/\$key/$key/go;
$item .= $value unless( $item =~ s/\$(item|value)/$value/go );
&$recurseFunc( $item );
$hashStore->{$name}{$key} = $item;
}
return '';
}
# =========================
$funcRef->{HASHEXISTS} = \&_funcHASHEXISTS;
sub _funcHASHEXISTS
{
my( $theAttr ) = @_;
my( $name, $key ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
$key = '' unless( defined $key );
my $result = 0;
if( $name ne '' && $key ne '' ) {
$result = 1 if( exists $hashStore->{$name} && exists $hashStore->{$name}{$key} );
} elsif( $name ne '' ) {
$result = 1 if( exists $hashStore->{$name} );
}
return $result;
}
# =========================
$funcRef->{HASHREVERSE} = \&_funcHASHREVERSE;
sub _funcHASHREVERSE
{
my( $theAttr ) = @_;
my( $name, $key ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
return '' if( $name eq '' || ! exists $hashStore->{$name} );
%{ $hashStore->{$name} } = reverse %{ $hashStore->{$name} };
return '';
}
# =========================
$funcRef->{HEX2DEC} = \&_funcHEX2DEC;
sub _funcHEX2DEC
{
my( $theAttr ) = @_;
$theAttr =~ s/[^0-9A-Fa-f]//g; # only hex numbers
$theAttr ||= 0;
my $result = hex( $theAttr );
return $result;
}
# =========================
$funcRef->{HEXDECODE} = \&_funcHEXDECODE;
sub _funcHEXDECODE
{
my( $theAttr ) = @_;
$theAttr =~ s/[^0-9A-Fa-f]//g; # only hex numbers
$theAttr =~ s/.$// if( length( $theAttr ) % 2 ); # must be set of two
return pack( "H*", $theAttr );
}
# =========================
$funcRef->{HEXENCODE} = \&_funcHEXENCODE;
sub _funcHEXENCODE
{
my( $theAttr ) = @_;
return uc( unpack( "H*", $theAttr ) );
}
# =========================
$funcRef->{IF} = \&_funcIF;
sub _funcIF
{
my( $theAttr ) = @_;
# IF(condition, value if true, value if false)
my( $condition, $str1, $str2 ) = _properSplit( $theAttr, 3 );
# with delay, handle functions in condition recursively and clean up unbalanced parenthesis
&$recurseFunc( $condition );
$condition =~ s/^\s*(.*?)\s*$/$1/os;
my $result = _safeEvalPerl( $condition );
unless( $result =~ /^ERROR/ ) {
if( $result ) {
$result = $str1;
} else {
$result = $str2;
}
$result = "" unless( defined( $result ) );
# with delay, handle functions in result recursively and clean up unbalanced parenthesis
&$recurseFunc( $result );
} # else return error message
return $result;
}
# =========================
$funcRef->{INSERTSTRING} = \&_funcINSERTSTRING;
sub _funcINSERTSTRING
{
my( $theAttr ) = @_;
my( $string, $start, $new ) = split ( /,\s*/, $theAttr, 3 );
$start = _getNumber( $start );
eval 'substr( $string, $start, 0, $new )';
return $string;
}
# =========================
$funcRef->{INT} = \&_funcINT;
sub _funcINT
{
my( $theAttr ) = @_;
my $result = _safeEvalPerl( $theAttr );
unless( $result =~ /^ERROR/ ) {
$result = int( _getNumber( $result ) );
}
return $result;
}
# =========================
$funcRef->{ISDIGIT} = \&_funcISDIGIT;
sub _funcISDIGIT
{
my( $theAttr ) = @_;
my $regex = ($TWiki::regex{numeric}) ? qr/[$TWiki::regex{numeric}]+/o : '[[:digit:]]+';
return ( $theAttr =~ m/^$regex$/o ) ? 1 : 0;
}
# =========================
$funcRef->{ISLOWER} = \&_funcISLOWER;
sub _funcISLOWER
{
my( $theAttr ) = @_;
my $regex = ($TWiki::regex{lowerAlpha}) ? qr/[$TWiki::regex{lowerAlpha}]+/o : '[[:lower:]]+';
return ( $theAttr =~ m/^$regex$/o ) ? 1 : 0;
}
# =========================
$funcRef->{ISUPPER} = \&_funcISUPPER;
sub _funcISUPPER
{
my( $theAttr ) = @_;
my $regex = ($TWiki::regex{upperAlpha}) ? qr/[$TWiki::regex{upperAlpha}]+/o : '[[:upper:]]+';
return ( $theAttr =~ m/^$regex$/o ) ? 1 : 0;
}
# =========================
$funcRef->{ISWIKIWORD} = \&_funcISWIKIWORD;
sub _funcISWIKIWORD
{
my( $theAttr ) = @_;
my $regex = ($TWiki::regex{wikiWordRegex}) ? $TWiki::regex{wikiWordRegex} :
'[[:upper:]]+[[:lower:][:digit:]]+[[:upper:]]+[[:alpha:][:digit:]]*';
return ( $theAttr =~ m/^$regex$/o ) ? 1 : 0;
}
# =========================
$funcRef->{LEFT} = \&_funcLEFT;
sub _funcLEFT
{
my( $theAttr ) = @_;
my $i = $rPos + 1;
return "R$i:C0..R$i:C$cPos";
}
# =========================
$funcRef->{LEFTSTRING} = \&_funcLEFTSTRING;
sub _funcLEFTSTRING
{
my( $theAttr ) = @_;
my $result = '';
my( $string, $num ) = split ( /,\s*/, $theAttr, 2 );
$string = '' unless( defined $string );
$num = 1 unless( $num );
my $start = 0;
eval '$result = substr( $string, $start, $num )';
return $result;
}
# =========================
$funcRef->{LENGTH} = \&_funcLENGTH;
sub _funcLENGTH
{
my( $theAttr ) = @_;
return length( $theAttr );
}
# =========================
$funcRef->{LIST} = \&_funcLIST;
sub _funcLIST
{
my( $theAttr ) = @_;
my @arr = _getList( $theAttr );
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LIST2HASH} = \&_funcLIST2HASH;
sub _funcLIST2HASH
{
my( $theAttr ) = @_;
my( $name, $str ) = _properSplit( $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
return '' if( $name eq '' );
$str = "" unless( defined $str );
my @arr = _getList( $str );
while( my $key = shift @arr ) {
my $value = shift @arr;
last unless( defined $value );
$key = '' unless( defined $key );
unless( $key eq '' ) {
$hashStore->{$name}{$key} = $value;
}
}
return '';
}
# =========================
$funcRef->{LISTIF} = \&_funcLISTIF;
sub _funcLISTIF
{
my( $theAttr ) = @_;
# LISTIF(cmd, item 1, item 2, ...)
my( $cmd, $str ) = _properSplit( $theAttr, 2 );
$cmd = "" unless( defined( $cmd ) );
$cmd =~ s/^\s*(.*?)\s*$/$1/os;
$str = "" unless( defined( $str ) );
# with delay, handle functions in result recursively and clean up unbalanced parenthesis
&$recurseFunc( $str );
my $item = "";
my $eval = "";
my $i = 0;
my @arr =
grep { ! /^TWIKI_GREP_REMOVE$/ }
map {
$item = $_;
$_ = $cmd;
$i++;
s/\$index/$i/go;
s/\$item/$item/go;
&$recurseFunc( $_ );
$eval = _safeEvalPerl( $_ );
if( $eval =~ /^ERROR/ ) {
$_ = $eval;
} elsif( $eval ) {
$_ = $item;
} else {
$_ = "TWIKI_GREP_REMOVE";
}
} _getList( $str );
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LISTITEM} = \&_funcLISTITEM;
sub _funcLISTITEM
{
my( $theAttr ) = @_;
my $result = '';
my( $index, $str ) = _properSplit( $theAttr, 2 );
$index = _getNumber( $index );
$str = "" unless( defined( $str ) );
my @arr = _getList( $str );
my $size = scalar @arr;
if( $index && $size ) {
$index-- if( $index > 0 ); # documented index starts at 1
$index = $size + $index if( $index < 0 ); # start from back if negative
$result = $arr[$index] if( ( $index >= 0 ) && ( $index < $size ) );
}
return $result;
}
# =========================
$funcRef->{LISTJOIN} = \&_funcLISTJOIN;
sub _funcLISTJOIN
{
my( $theAttr ) = @_;
my( $sep, $str ) = _properSplit( $theAttr, 2 );
$str = "" unless( defined( $str ) );
my $result = _listToDelimitedString( _getList( $str ) );
$sep = ", " if( !defined $sep || $sep eq '' );
$sep =~ s/\$comma/,/go;
$sep =~ s/\$sp/ /go;
$sep =~ s/\$(nop|empty)//go;
$sep =~ s/\$n/\n/go;
$result =~ s/, */$sep/go;
return $result;
}
# =========================
$funcRef->{LISTEACH} = \&_funcLISTEACH;
$funcRef->{LISTMAP} = \&_funcLISTEACH;
sub _funcLISTEACH
{
my( $theAttr ) = @_;
# LISTEACH(action, item 1, item 2, ...)
my( $action, $str ) = _properSplit( $theAttr, 2 );
$action = "" unless( defined( $action ) );
$str = "" unless( defined( $str ) );
# with delay, handle functions in result recursively and clean up unbalanced parenthesis
&$recurseFunc( $str );
my $item = "";
my $i = 0;
my @arr =
map {
$item = $_;
$_ = $action;
$i++;
s/\$index/$i/go;
$_ .= $item unless( s/\$item/$item/go );
&$recurseFunc( $_ );
$_
} _getList( $str );
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LISTNONEMPTY} = \&_funcLISTNONEMPTY;
sub _funcLISTNONEMPTY
{
my( $theAttr ) = @_;
my @arr = grep { /./ } _getList( $theAttr );
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LISTRAND} = \&_funcLISTRAND;
sub _funcLISTRAND
{
my( $theAttr ) = @_;
my $result = '';
my @arr = _getList( $theAttr );
my $size = scalar @arr;
if( $size > 0 ) {
my $i = int( rand( $size ) );
$result = $arr[$i];
}
return $result;
}
# =========================
$funcRef->{LISTREVERSE} = \&_funcLISTREVERSE;
sub _funcLISTREVERSE
{
my( $theAttr ) = @_;
my @arr = reverse _getList( $theAttr );
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LISTSHUFFLE} = \&_funcLISTSHUFFLE;
sub _funcLISTSHUFFLE
{
my( $theAttr ) = @_;
my @arr = _getList( $theAttr );
my $size = scalar @arr;
if( $size > 1 ) {
for( my $i = $size; $i--; ) {
my $j = int( rand( $i + 1 ) );
next if( $i == $j );
@arr[$i, $j] = @arr[$j, $i];
}
}
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LISTSIZE} = \&_funcLISTSIZE;
sub _funcLISTSIZE
{
my( $theAttr ) = @_;
my @arr = _getList( $theAttr );
return scalar @arr;
}
# =========================
$funcRef->{LISTSORT} = \&_funcLISTSORT;
sub _funcLISTSORT
{
my( $theAttr ) = @_;
my $isNumeric = 1;
my @arr = map {
s/^\s*//o;
s/\s*$//o;
$isNumeric = 0 unless( $_ =~ /^[\+\-]?[0-9\.]+$/ );
$_
} _getList( $theAttr );
if( $isNumeric ) {
@arr = sort { $a <=> $b } @arr;
} else {
@arr = sort @arr;
}
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LISTTRUNCATE} = \&_funcLISTTRUNCATE;
sub _funcLISTTRUNCATE
{
my( $theAttr ) = @_;
my $result = '';
my( $index, $str ) = _properSplit( $theAttr, 2 );
$index = int( _getNumber( $index ) );
$str = "" unless( defined( $str ) );
my @arr = _getList( $str );
my $size = scalar @arr;
if( $index > 0 ) {
$index = $size if( $index > $size );
$#arr = $index - 1;
$result = _listToDelimitedString( @arr );
} elsif( $index < 0 ) {
$index = - $size if( $index < - $size );
splice( @arr, 0, $size + $index );
$result = _listToDelimitedString( @arr );
} #else result = '';
return $result;
}
# =========================
$funcRef->{LISTUNIQUE} = \&_funcLISTUNIQUE;
sub _funcLISTUNIQUE
{
my( $theAttr ) = @_;
my %seen = ();
my @arr = grep { ! $seen{$_} ++ } _getList( $theAttr );
return _listToDelimitedString( @arr );
}
# =========================
$funcRef->{LN} = \&_funcLN;
sub _funcLN
{
my( $theAttr ) = @_;
return log(_getNumber( $theAttr ) );
}
# =========================
$funcRef->{LOG} = \&_funcLOG;
sub _funcLOG
{
my( $theAttr ) = @_;
my( $num, $base ) = split( /,\s*/, $theAttr, 2 );
$num = _getNumber( $num );
$base = _getNumber( $base );
$base = 10 if( $base <= 0 );
return log( $num ) / log( $base );
}
# =========================
$funcRef->{LOWER} = \&_funcLOWER;
sub _funcLOWER
{
my( $theAttr ) = @_;
return lc( $theAttr );
}
# =========================
$funcRef->{MAIN} = \&_funcMAIN;
sub _funcMAIN
{
my( $theAttr ) = @_;
return $theAttr;
}
# =========================
$funcRef->{MAX} = \&_funcMAX;
sub _funcMAX
{
my( $theAttr ) = @_;
my @arr = sort { $a <=> $b }
grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
return $arr[$#arr] if( scalar @arr );
return '';
}
# =========================
$funcRef->{MEDIAN} = \&_funcMEDIAN;
sub _funcMEDIAN
{
my( $theAttr ) = @_;
my $result = '';
my @arr = sort { $a <=> $b } grep { defined $_ } _getListAsFloat( $theAttr );
my $i = @arr;
if( ( $i % 2 ) > 0 ) {
$result = $arr[$i/2];
} elsif( $i ) {
$i /= 2;
$result = ( $arr[$i] + $arr[$i-1] ) / 2;
}
return $result;
}
# =========================
$funcRef->{MIN} = \&_funcMIN;
sub _funcMIN
{
my( $theAttr ) = @_;
my @arr = sort { $a <=> $b }
grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
return $arr[0] if( scalar @arr );
return '';
}
# =========================
$funcRef->{MOD} = \&_funcMOD;
sub _funcMOD
{
my( $theAttr ) = @_;
my $result = 0;
my( $num1, $num2 ) = split( /,\s*/, $theAttr, 2 );
$num1 = _getNumber( $num1 );
$num2 = _getNumber( $num2 );
if( $num1 && $num2 ) {
$result = $num1 % $num2;
}
return $result;
}
# =========================
$funcRef->{NOEXEC} = \&_funcNOEXEC;
sub _funcNOEXEC
{
my( $theAttr ) = @_;
return $theAttr;
}
# =========================
$funcRef->{NOP} = \&_funcNOP;
sub _funcNOP
{
my( $theAttr ) = @_;
# pass everything through, this will allow plugins to defy plugin order
# for example the %SEARCH{}% variable
$theAttr =~ s/\$per(cnt)?/%/g;
$theAttr =~ s/\$quot/"/g;
return $theAttr;
}
# =========================
$funcRef->{NOT} = \&_funcNOT;
sub _funcNOT
{
my( $theAttr ) = @_;
my $result = 1;
$result = 0 if( _getNumber( $theAttr ) );
return $result;
}
# =========================
$funcRef->{NOTE} = \&_funcNOTE;
sub _funcNOTE
{
return '';
}
# =========================
$funcRef->{OCT2DEC} = \&_funcOCT2DEC;
sub _funcOCT2DEC
{
my( $theAttr ) = @_;
$theAttr =~ s/[^0-7]//g; # only octal digits
$theAttr ||= 0;
my $result = oct( $theAttr );
return $result;
}
# =========================
$funcRef->{ODD} = \&_funcODD;
sub _funcODD
{
my( $theAttr ) = @_;
return _getNumber( $theAttr ) % 2;
}
# =========================
$funcRef->{OR} = \&_funcOR;
sub _funcOR
{
my( $theAttr ) = @_;
my $result = 0;
my @arr = _getListAsInteger( $theAttr );
foreach my $i( @arr ) {
if( $i ) {
$result = 1;
last;
}
}
return $result;
}
# =========================
$funcRef->{PERCENTILE} = \&_funcPERCENTILE;
sub _funcPERCENTILE
{
my( $theAttr ) = @_;
my( $percentile, $set ) = split( /,\s*/, $theAttr, 2 );
$set = '' unless( defined $set );
my @arr = sort { $a <=> $b } grep { defined $_ } _getListAsFloat( $set );
my $result = 0;
my $size = scalar( @arr );
if( $size > 0 ) {
my $i = $percentile / 100 * ( $size + 1 );
my $iInt = int( $i );
if( $i <= 1 ) {
$result = $arr[0];
} elsif( $i >= $size ) {
$result = $arr[$size-1];
} elsif( $i == $iInt ) {
$result = $arr[$i-1];
} else {
# interpolate beween neighbors # Example: $i = 7.25
my $r1 = $iInt + 1 - $i; # 0.75 = 7 + 1 - 7.25
my $r2 = 1 - $r1; # 0.25 = 1 - 0.75
my $x1 = $arr[$iInt-1];
my $x2 = $arr[$iInt];
$result = ($r1 * $x1) + ($r2 * $x2);
}
}
return $result;
}
# =========================
$funcRef->{PI} = \&_funcPI;
sub _funcPI
{
return 3.1415926535897932384;
}
# =========================
$funcRef->{MULT} = \&_funcPRODUCT; # MULT is deprecated (no not remove)
$funcRef->{PRODUCT} = \&_funcPRODUCT;
sub _funcPRODUCT
{
my( $theAttr ) = @_;
my @arr = _getListAsFloat( $theAttr );
my $result = 1;
foreach my $i ( @arr ) {
$result *= $i if defined $i;
}
return $result;
}
# =========================
$funcRef->{PROPER} = \&_funcPROPER;
sub _funcPROPER
{
my( $theAttr ) = @_;
# FIXME: I18N
my $result = lc( $theAttr );
$result =~ s/(^|[^a-z])([a-z])/$1 . uc($2)/geo;
return $result;
}
# =========================
$funcRef->{PROPERSPACE} = \&_funcPROPERSPACE;
sub _funcPROPERSPACE
{
my( $theAttr ) = @_;
return _properSpace( $theAttr );
}
# =========================
$funcRef->{RAND} = \&_funcRAND;
sub _funcRAND
{
my( $theAttr ) = @_;
my $max = _getNumber( $theAttr );
$max = 1 if( $max <= 0 );
return rand( $max );
}
# =========================
$funcRef->{RANDSTRING} = \&_funcRANDSTRING;
sub _funcRANDSTRING
{
my( $theAttr ) = @_;
my( $chars, $format ) = split( /,\s*/, $theAttr, 2 );
$chars = '' unless defined( $chars );
$chars =~ s/(.)\.\.(.)/_expandRange($1, $2)/ge;
my @pool = split( //, $chars );
@pool = ( 'a'..'z', 'A'..'Z', '0'..'9', '_' ) unless( scalar @pool );
$format = 'xxxxxxxx' unless( $format );
if( $format =~ m/^([0-9]*)$/ ) {
my $num = _getNumber( $format );
$num = 8 if( $num < 1 );
$num = 1024 if( $num > 1024 );
$format = 'x' x $num;
}
my $result = '';
foreach my $ch ( split( //, $format ) ) {
if( $ch eq 'x' ) {
$result .= $pool[rand @pool];
} else {
$result .= $ch;
}
}
return $result;
}
# =========================
sub _expandRange
{
my( $lowCh, $highCh ) = @_;
my $text = "$1$2"; # in case out of range, return just low char and high char
if( ord $highCh > ord $lowCh ) {
$text = join( '', ( $lowCh..$highCh ) );
}
return $text;
}
# =========================
$funcRef->{REPEAT} = \&_funcREPEAT;
sub _funcREPEAT
{
my( $theAttr ) = @_;
my( $str, $num ) = split( /,\s*/, $theAttr, 2 );
$str = "" unless( defined( $str ) );
$num = _getNumber( $num );
return "$str" x $num;
}
# =========================
$funcRef->{REPLACE} = \&_funcREPLACE;
sub _funcREPLACE
{
my( $theAttr ) = @_;
my( $string, $start, $num, $replace ) = split ( /,\s*/, $theAttr, 4 );
$string = '' unless( defined $string );
$start = 0 unless( $start );
$start-- if( $start > 0 );
$num = 0 unless( $num );
$replace = "" unless( defined $replace );
eval 'substr( $string, $start, $num, $replace )';
return $string;
}
# =========================
$funcRef->{RIGHT} = \&_funcRIGHT;
sub _funcRIGHT
{
my( $theAttr ) = @_;
my $i = $rPos + 1;
my $cStart = $cPos + 2;
return "R$i:C$cStart..R$i:C32000";
}
# =========================
$funcRef->{RIGHTSTRING} = \&_funcRIGHTSTRING;
sub _funcRIGHTSTRING
{
my( $theAttr ) = @_;
my $result = '';
my( $string, $num ) = split ( /,\s*/, $theAttr, 2 );
$string = '' unless( defined $string );
$num = 1 unless( $num );
my $start = 0;
$start = length( $string ) - $num;
eval '$result = substr( $string, $start, $num )';
return $result;
}
# =========================
$funcRef->{ROUND} = \&_funcROUND;
sub _funcROUND
{
my( $theAttr ) = @_;
# ROUND(num, digits)
my( $num, $digits ) = split( /,\s*/, $theAttr, 2 );
my $result = _safeEvalPerl( $num );
unless( $result =~ /^ERROR/ ) {
$result = _getNumber( $result );
if( ( $digits ) && ( $digits =~ s/^.*?(\-?[0-9]+).*$/$1/os ) && ( $digits ) ) {
my $factor = 10**$digits;
$result *= $factor;
( $result >= 0 ) ? ( $result += 0.5 ) : ( $result -= 0.5 );
$result = int( $result );
$result /= $factor;
} else {
( $result >= 0 ) ? ( $result += 0.5 ) : ( $result -= 0.5 );
$result = int( $result );
}
}
return $result;
}
# =========================
$funcRef->{ROW} = \&_funcROW;
sub _funcROW
{
my( $theAttr ) = @_;
my $i = $theAttr || 0;
return $rPos + $i + 1;
}
# =========================
$funcRef->{SEARCH} = \&_funcSEARCH;
sub _funcSEARCH
{
my( $theAttr ) = @_;
my( $searchString, $string, $pos ) = split( /,\s*/, $theAttr, 3 );
my $result = 0;
$searchString = '' unless( defined $searchString );
$string = '' unless( defined $string );
$pos--;
$pos = 0 if( $pos < 0 );
pos( $string ) = $pos if( $pos );
# using zero width lookahead '(?=...)' to keep pos at the beginning of match
if( eval '$string =~ m/(?=$searchString)/g' && $string ) {
$result = pos( $string ) + 1;
}
return $result;
}
# =========================
$funcRef->{SET} = \&_funcSET;
sub _funcSET
{
my( $theAttr ) = @_;
my( $name, $value ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^a-zA-Z0-9\_]//go;
if( $name ) {
if( defined( $value ) ) {
$varStore{ $name } = $value;
} else {
delete $varStore{ $name };
}
}
return '';
}
# =========================
$funcRef->{SETHASH} = \&_funcSETHASH;
sub _funcSETHASH
{
my( $theAttr ) = @_;
my( $name, $key, $value ) = split( /,\s*/, $theAttr, 3 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
$key = '' unless( defined $key );
if( $name ne '' && $key ne '' ) {
if( defined $value ) {
$hashStore->{$name}{$key} = $value;
} else {
delete $hashStore->{$name}{$key};
}
} elsif( $name ne '' ) {
delete $hashStore->{$name};
} else {
$hashStore = {};
}
return '';
}
# =========================
$funcRef->{SETIFEMPTY} = \&_funcSETIFEMPTY;
sub _funcSETIFEMPTY
{
my( $theAttr ) = @_;
my( $name, $value ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$value = '' unless( defined $value );
$name =~ s/[^a-zA-Z0-9\_]//go;
if( $name && defined( $value ) && ! $varStore{ $name } ) {
$varStore{ $name } = $value;
}
return '';
}
# =========================
$funcRef->{SETLIST} = \&_funcSETLIST;
sub _funcSETLIST
{
my( $theAttr ) = @_;
my( $name, $value ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$name =~ s/[^a-zA-Z0-9\_]//go;
if( $name ) {
if( defined( $value ) ) {
$listStore{ $name } = _listToDelimitedString( _getList( $value ) );
} else {
delete $listStore{ $name };
}
}
return '';
}
# =========================
$funcRef->{SETM} = \&_funcSETM;
sub _funcSETM
{
my( $theAttr ) = @_;
my( $name, $value ) = split( /,\s*/, $theAttr, 2 );
$name = '' unless( defined $name );
$value = '' unless( defined $value );
$name =~ s/[^a-zA-Z0-9\_]//go;
if( $name ) {
my $old = $varStore{ $name };
$old = '' unless( defined( $old ) );
$value = _safeEvalPerl( "$old $value" );
$varStore{ $name } = $value;
}
return '';
}
# =========================
$funcRef->{SETMHASH} = \&_funcSETMHASH;
sub _funcSETMHASH
{
my( $theAttr ) = @_;
my( $name, $key, $value ) = split( /,\s*/, $theAttr, 3 );
$name = '' unless( defined $name );
$name =~ s/[^\w\.\/]//go;
$key = '' unless( defined $key );
$value = '' unless( defined $value );
if( $name ne '' && $key ne '' ) {
my $old = $hashStore->{$name}{$key};
$old = '' unless( defined( $old ) );
$value = _safeEvalPerl( "$old $value" );
$hashStore->{$name}{$key} = $value;
}
return '';
}
# =========================
$funcRef->{SIGN} = \&_funcSIGN;
sub _funcSIGN
{
my( $theAttr ) = @_;
my $i = _getNumber( $theAttr );
my $result = 0;
$result = 1 if( $i > 0 );
$result = -1 if( $i < 0 );
return $result;
}
# =========================
$funcRef->{SPLIT} = \&_funcSPLIT;
sub _funcSPLIT
{
my( $theAttr ) = @_;
my( $sep, $str ) = _properSplit( $theAttr, 2 );
$str = '' unless( defined $str );
$sep = " *" if( !defined $sep || $sep eq '' );
$sep =~ s/\$comma/,/g;
$sep =~ s/\$sp/ /g;
$sep =~ s/\$(nop|empty)//g;
return _listToDelimitedString( split( $sep, $str ) );
}
# =========================
$funcRef->{SQRT} = \&_funcSQRT;
sub _funcSQRT
{
my( $theAttr ) = @_;
return sqrt( _getNumber( $theAttr ) );
}
# =========================
$funcRef->{STDEV} = \&_funcSTDEV;
sub _funcSTDEV
{
my( $theAttr ) = @_;
my @arr = grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
my $result = 0;
my $mean = 0;
my $size = scalar @arr;
return $result unless( $size > 1 );
# calculate mean
foreach my $i ( @arr ) {
$mean += $i;
}
$mean = $mean / $size;
foreach my $i ( @arr ) {
$result += ($i - $mean) ** 2;
}
return sqrt( $result / ($size - 1) );
}
# =========================
$funcRef->{STDEVP} = \&_funcSTDEVP;
sub _funcSTDEVP
{
my( $theAttr ) = @_;
my @arr = grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
my $result = 0;
my $mean = 0;
my $size = scalar @arr;
return $result unless( $size > 1 );
# calculate mean
foreach my $i ( @arr ) {
$mean += $i;
}
$mean = $mean / $size;
foreach my $i ( @arr ) {
$result += ($i - $mean) ** 2;
}
return sqrt( $result / $size );
}
# =========================
$funcRef->{SUBSTITUTE} = \&_funcSUBSTITUTE;
sub _funcSUBSTITUTE
{
my( $theAttr ) = @_;
my( $string, $from, $to, $inst, $options ) = split( /,\s*/, $theAttr );
$string = '' unless( defined $string );
$from = '' unless( defined $from );
$to = '' unless( defined $to );
my $result = $string;
$from = quotemeta( $from ) unless( $options && $options =~ /r/i);
if( $inst ) {
# replace Nth instance
my $count = 0;
if( eval '$string =~ s/($from)/if( ++$count == $inst ) { $to; } else { $1; }/gex;' && $string ) {
$result = $string;
}
} else {
# global replace
if( eval '$string =~ s/$from/$to/g' ) {
$result = $string;
}
}
return $result;
}
# =========================
$funcRef->{SUBSTRING} = \&_funcSUBSTRING;
$funcRef->{MIDSTRING} = \&_funcSUBSTRING; # undocumented - do not remove
sub _funcSUBSTRING
{
my( $theAttr ) = @_;
my $result = '';
# greedy match for comma separated parameters (in case first parameter has embedded commas)
if( $theAttr =~ /^(.*)\,\s*(.+)\,\s*(.+)$/os ) {
my( $string, $start, $num ) = ( $1, _getNumber( $2 ), _getNumber( $3 ) );
if( $start && abs( $start ) <= length( $string ) && $num ) {
$start-- unless ($start < 1);
eval '$result = substr( $string, $start, $num )';
}
}
return $result;
}
# =========================
$funcRef->{SUM} = \&_funcSUM;
sub _funcSUM
{
my( $theAttr ) = @_;
my $result = 0;
my @arr = _getListAsFloat( $theAttr );
foreach my $i ( @arr ) {
$result += $i if defined $i;
}
return $result;
}
# =========================
$funcRef->{SUMDAYS} = \&_funcSUMDAYS;
$funcRef->{DURATION} = \&_funcSUMDAYS; # undocumented - do not remove
sub _funcSUMDAYS
{
my( $theAttr ) = @_;
# contributed by SvenDowideit - 07 Mar 2003; modified by PTh
my $result = 0;
my @arr = _getListAsDays( $theAttr );
foreach my $i ( @arr ) {
$result += $i if defined $i;
}
return $result;
}
# =========================
$funcRef->{SUMPRODUCT} = \&_funcSUMPRODUCT;
sub _funcSUMPRODUCT
{
my( $theAttr ) = @_;
my $result = 0;
my @arr;
my @lol = split( /,\s*/, $theAttr );
my $size = 32000;
my $i;
for $i (0 .. $#lol ) {
@arr = _getListAsFloat( $lol[$i] );
$lol[$i] = [ @arr ]; # store reference to array
$size = @arr if( @arr < $size ); # remember smallest array
}
if( ( $size > 0 ) && ( $size < 32000 ) ) {
my $y; my $prod; my $val;
$size--;
for $y (0 .. $size ) {
$prod = 1;
for $i (0 .. $#lol ) {
$val = $lol[$i][$y];
if( defined $val ) {
$prod *= $val;
} else {
$prod = 0; # don't count empty cells
}
}
$result += $prod;
}
}
return $result;
}
# =========================
$funcRef->{T} = \&_funcT;
sub _funcT
{
my( $theAttr ) = @_;
my $result = '';
my @arr = _getTableRange( "$theAttr..$theAttr" );
if( @arr ) {
$result = $arr[0];
}
return $result;
}
# =========================
$funcRef->{TIME} = \&_funcTIME;
sub _funcTIME
{
my( $result ) = @_;
$result =~ s/^\s+//o;
$result =~ s/\s+$//o;
if( $result ) {
$result = _date2serial( $result );
} else {
$result = time();
}
return $result;
}
# =========================
$funcRef->{TIMEADD} = \&_funcTIMEADD;
sub _funcTIMEADD
{
my( $theAttr ) = @_;
my( $time, $value, $scale ) = split( /,\s*/, $theAttr, 3 );
$time = 0 unless( $time );
$value = 0 unless( $value );
$scale = "" unless( $scale );
$time =~ s/.*?([0-9]+).*/$1/o || 0;
$value =~ s/.*?(\-?[0-9\.]+).*/$1/o || 0;
$value *= 60 if( $scale =~ /^min/i );
$value *= 3600 if( $scale =~ /^hou/i );
$value *= 3600*24 if( $scale =~ /^day/i );
$value *= 3600*24*7 if( $scale =~ /^week/i );
$value *= 3600*24*30.42 if( $scale =~ /^mon/i ); # FIXME: exact calc
$value *= 3600*24*365 if( $scale =~ /^year/i ); # FIXME: exact calc
return int( $time + $value );
}
# =========================
$funcRef->{TIMEDIFF} = \&_funcTIMEDIFF;
sub _funcTIMEDIFF
{
my( $theAttr ) = @_;
my( $time1, $time2, $scale ) = split( /,\s*/, $theAttr, 3 );
$scale ||= '';
$time1 = 0 unless( $time1 );
$time2 = 0 unless( $time2 );
$time1 =~ s/.*?([0-9]+).*/$1/o || 0;
$time2 =~ s/.*?([0-9]+).*/$1/o || 0;
my $result = $time2 - $time1;
$result /= 60 if( $scale =~ /^min/i );
$result /= 3600 if( $scale =~ /^hou/i );
$result /= 3600*24 if( $scale =~ /^day/i );
$result /= 3600*24*7 if( $scale =~ /^week/i );
$result /= 3600*24*30.42 if( $scale =~ /^mon/i ); # FIXME: exact calc
$result /= 3600*24*365 if( $scale =~ /^year/i ); # FIXME: exact calc
return $result;
}
# =========================
$funcRef->{TODAY} = \&_funcTODAY;
sub _funcTODAY
{
return _date2serial( _serial2date( time(), '$year/$month/$day GMT', 1 ) );
}
# =========================
$funcRef->{TRANSLATE} = \&_funcTRANSLATE;
sub _funcTRANSLATE
{
my( $theAttr ) = @_;
my $result = $theAttr;
# greedy match for comma separated parameters (in case first parameter has embedded commas)
if( $theAttr =~ /^(.*)\,\s*(.+)\,\s*(.+)$/os ) {
my $string = $1;
my $from = $2;
my $to = $3;
$from =~ s/\$comma/,/g; $to =~ s/\$comma/,/g;
$from =~ s/\$sp/ /g; $to =~ s/\$sp/ /g;
$from =~ s/\$n/\n/g; $to =~ s/\$n/\n/g; # the $from is silly, CALC can't be multi-line, yet
$from =~ s/\$quot/"/g; $to =~ s/\$quot/"/g;
$from =~ s/\$aquot/'/g; $to =~ s/\$aquot/'/g;
$from = quotemeta( $from ); $to = quotemeta( $to );
$from =~ s/([a-zA-Z0-9])\\\-([a-zA-Z0-9])/$1\-$2/g; # fix quotemeta (allow only ranges)
$to =~ s/([a-zA-Z0-9])\\\-([a-zA-Z0-9])/$1\-$2/g;
$result = $string;
if( $string && eval "\$string =~ tr/$from/$to/" ) {
$result = $string;
}
}
return $result;
}
# =========================
$funcRef->{TRIM} = \&_funcTRIM;
sub _funcTRIM
{
my( $theAttr ) = @_;
my $result = $theAttr || '';
$result =~ s/^\s*//o;
$result =~ s/\s*$//o;
$result =~ s/\s+/ /go;
return $result;
}
# =========================
$funcRef->{UPPER} = \&_funcUPPER;
sub _funcUPPER
{
my( $theAttr ) = @_;
return uc( $theAttr );
}
# =========================
$funcRef->{VALUE} = \&_funcVALUE;
sub _funcVALUE
{
my( $theAttr ) = @_;
return _getNumber( $theAttr );
}
# =========================
$funcRef->{VAR} = \&_funcVAR;
sub _funcVAR
{
my( $theAttr ) = @_;
my @arr = grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
my $result = 0;
my $mean = 0;
my $size = scalar @arr;
return $result unless( $size > 1 );
# calculate mean
foreach my $i ( @arr ) {
$mean += $i;
}
$mean = $mean / $size;
foreach my $i ( @arr ) {
$result += ( abs($i - $mean) ) ** 2;
}
return $result / ($size - 1);
}
# =========================
$funcRef->{VARP} = \&_funcVARP;
sub _funcVARP
{
my( $theAttr ) = @_;
my @arr = grep { /./ }
grep { defined $_ }
_getListAsFloat( $theAttr );
my $result = 0;
my $mean = 0;
my $size = scalar @arr;
return $result unless( $size > 1 );
# calculate mean
foreach my $i ( @arr ) {
$mean += $i;
}
$mean = $mean / $size;
foreach my $i ( @arr ) {
$result += ( abs($i - $mean) ) ** 2;
}
return $result / $size;
}
# =========================
$funcRef->{WHILE} = \&_funcWHILE;
sub _funcWHILE
{
my( $theAttr ) = @_;
# WHILE(condition, do something)
my( $condition, $str ) = _properSplit( $theAttr, 2 );
$condition = '' unless( defined $condition );
$str = '' unless( defined $str );
my $i = 0;
my $result = '';
while( 1 ) {
if( $i++ >= 32767 ) {
$result .= 'ERROR: Infinite loop (32767 cycles)';
last; # prevent infinite loop
}
# with delay, handle functions in condition recursively and clean up unbalanced parenthesis
my $cond = $condition;
$cond =~ s/\$counter/$i/go;
&$recurseFunc( $cond );
$cond =~ s/^\s*(.*?)\s*$/$1/os;
my $res = _safeEvalPerl( $cond );
if( $res =~ /^ERROR/ ) {
$result .= $res;
last; # exit loop and return error
}
last unless( $res ); # proper loop exit
$res = $str;
$res = "" unless( defined( $res ) );
# with delay, handle functions in result recursively and clean up unbalanced parenthesis
$res =~ s/\$counter/$i/go;
&$recurseFunc( $res );
$result .= $res;
}
return $result;
}
# =========================
$funcRef->{WORKINGDAYS} = \&_funcWORKINGDAYS;
sub _funcWORKINGDAYS
{
my( $theAttr ) = @_;
my( $num1, $num2 ) = split( /,\s*/, $theAttr, 2 );
return _workingDays( _getNumber( $num1 ), _getNumber( $num2 ) );
}
# =========================
$funcRef->{XOR} = \&_funcXOR;
sub _funcXOR
{
my( $theAttr ) = @_;
my @arr = _getListAsInteger( $theAttr );
my $result = shift( @arr );
if( scalar( @arr ) > 0 ) {
foreach my $i ( @arr ) {
$result = ( $result xor $i );
}
} else {
$result = 0;
}
$result = $result ? 1 : 0;
return $result;
}
# =========================
sub _listToDelimitedString
{
my @arr = map { s/^\s*//o; s/\s*$//o; $_ } @_;
my $text = join( ", ", @arr );
return $text;
}
# =========================
sub _properSplit
{
my( $theAttr, $theLevel ) = @_;
# escape commas inside functions
$theAttr =~ s/(\$[A-Z]+[A-Z0-9]*$escToken([0-9]+)\(.*?$escToken\2\))/_escapeCommas($1)/geos;
# split at commas and restore commas inside functions
my @arr = map{ s/<$escToken>/\,/go; $_ } split( /,\s*/, $theAttr, $theLevel );
return @arr;
}
# =========================
sub _escapeCommas
{
my( $theText ) = @_;
$theText =~ s/\,/<$escToken>/go;
return $theText;
}
# =========================
sub _getNumber
{
my( $theText ) = @_;
return 0 unless( $theText );
$theText =~ s/([0-9])\,(?=[0-9]{3})/$1/go; # "1,234,567" ==> "1234567"
if( $theText =~ s/^.*?(\-?[0-9\.]+e[\-\+]?[0-9]+).*$/$1/is ) { # "1.5e-3" ==> "0.0015"
$theText = sprintf "%.20f", $theText;
$theText =~ s/0+$//;
}
unless( $theText =~ s/^.*?(\-?[0-9\.]+).*$/$1/os ) { # "xy-1.23zz" ==> "-1.23"
$theText = 0;
}
$theText =~ s/^(\-?)0+([0-9])/$1$2/o; # "-0009.12" ==> "-9.12"
$theText =~ s/^(\-?)\./${1}0\./o; # "-.25" ==> "-0.25"
$theText =~ s/^\-0$/0/o; # "-0" ==> "0"
$theText =~ s/\.$//o; # "123." ==> "123"
return $theText;
}
# =========================
sub _safeEvalPerl
{
my( $theText ) = @_;
$theText = '' unless( defined $theText );
# Allow only simple math with operators - + * / % ( )
$theText =~ s/\%\s*[^\-\+\*\/0-9\.\(\)]+//gos; # defuse %hash but keep modulus
# keep only numbers and operators (shh... don't tell anyone, we support comparison operators)
$theText =~ s/[^\!\<\=\>\-\+\*\/\%0-9e\.\(\)]*//go;
$theText =~ s/(^|[^\.])\b0+(?=[0-9])/$1/go; # remove leading 0s to defuse interpretation of numbers as octals
$theText =~ s/(^|[^0-9])e/$1/go; # remove "e"-s unless in expression such as "123e-4"
$theText =~ /(.*)/s;
$theText = $1; # untainted variable
return "" unless( $theText );
local $SIG{__DIE__} = sub { TWiki::Func::writeDebug($_[0]); warn $_[0] };
my $result = eval $theText;
if( $@ ) {
$result = $@;
$result =~ s/[\n\r]//go;
$result =~ s/\[[^\]]+.*view.*?\:\s?//o; # Cut "[Mon Mar 15 23:31:39 2004] view: "
$result =~ s/\s?at \(eval.*?\)\sline\s?[0-9]*\.?\s?//go; # Cut "at (eval 51) line 2."
$result =~ s/at end of line\.?//go; # Cut "at end of line"
$result =~ s/,?\s*$//o;
$result = "ERROR: $result";
} else {
$result = 0 unless( $result ); # logical false is "0"
}
return $result;
}
# =========================
sub _getListAsInteger
{
my( $theAttr ) = @_;
my $val = 0;
my @list = _getList( $theAttr );
(my $baz = "foo") =~ s/foo//; # reset search vars. defensive coding
for my $i (0 .. $#list ) {
$val = $list[$i];
# search first integer pattern, skip over HTML tags
if( $val =~ /^\s*(?:<[^>]*>)*([\-\+]*[0-9]+).*/os ) {
$list[$i] = $1; # untainted variable, possibly undef
} else {
$list[$i] = undef;
}
}
return @list;
}
# =========================
sub _getListAsFloat
{
my( $theAttr ) = @_;
my $val = 0;
my @list = _getList( $theAttr );
(my $baz = "foo") =~ s/foo//; # reset search vars. defensive coding
for my $i (0 .. $#list ) {
$val = $list[$i];
# search first float pattern, skip over HTML tags
if( $val =~ /^\s*(?:<[^>]*>)*\$?([\-\+]*[0-9\.]+).*/os ) {
$list[$i] = $1; # untainted variable, possibly undef
} else {
$list[$i] = undef;
}
}
return @list;
}
# =========================
sub _getListAsDays
{
my( $theAttr ) = @_;
# contributed by by SvenDowideit - 07 Mar 2003; modified by PTh
my $val = 0;
my @arr = _getList( $theAttr );
(my $baz = "foo") =~ s/foo//; # reset search vars. defensive coding
for my $i (0 .. $#arr ) {
$val = $arr[$i];
# search first float pattern
if( $val =~ /^\s*([\-\+]*[0-9\.]+)\s*d/oi ) {
$arr[$i] = $1; # untainted variable, possibly undef
} elsif( $val =~ /^\s*([\-\+]*[0-9\.]+)\s*w/oi ) {
$arr[$i] = 5 * $1; # untainted variable, possibly undef
} elsif( $val =~ /^\s*([\-\+]*[0-9\.]+)\s*h/oi ) {
$arr[$i] = $1 / 8; # untainted variable, possibly undef
} elsif( $val =~ /^\s*([\-\+]*[0-9\.]+)/o ) {
$arr[$i] = $1; # untainted variable, possibly undef
} else {
$arr[$i] = undef;
}
}
return @arr;
}
# =========================
sub _getList
{
my( $theAttr ) = @_;
my @list = ();
foreach( split( /,\s*/, $theAttr ) ) {
if( m/\s*R([0-9]+)\:C([0-9]+)\s*\.\.+\s*R([0-9]+)\:C([0-9]+)/ ) {
# table range
push( @list, _getTableRange( $_ ) );
} else {
# list item
push( @list, $_ );
}
}
return @list;
}
# =========================
sub _getTableRange
{
my( $theAttr ) = @_;
my @arr = ();
if( $rPos < 0 ) {
return @arr;
}
TWiki::Func::writeDebug( "- SpreadSheetPlugin::Calc::_getTableRange( $theAttr )" ) if $debug;
unless( $theAttr =~ /\s*R([0-9]+)\:C([0-9]+)\s*\.\.+\s*R([0-9]+)\:C([0-9]+)/ ) {
return @arr;
}
my $r1 = $1 - 1;
my $c1 = $2 - 1;
my $r2 = $3 - 1;
my $c2 = $4 - 1;
my $r = 0;
my $c = 0;
if( $c1 < 0 ) { $c1 = 0; }
if( $c2 < 0 ) { $c2 = 0; }
if( $c2 < $c1 ) { $c = $c1; $c1 = $c2; $c2 = $c; }
if( $r1 > $rPos ) { $r1 = $rPos; }
if( $r1 < 0 ) { $r1 = 0; }
if( $r2 > $rPos ) { $r2 = $rPos; }
if( $r2 < 0 ) { $r2 = 0; }
if( $r2 < $r1 ) { $r = $r1; $r1 = $r2; $r2 = $r; }
my $pRow = ();
for $r ( $r1 .. $r2 ) {
$pRow = $tableMatrix[$r];
for $c ( $c1 .. $c2 ) {
if( $c < @$pRow ) {
push( @arr, $$pRow[$c] );
}
}
}
TWiki::Func::writeDebug( "- SpreadSheetPlugin::Calc::_getTableRange() returns @arr" ) if $debug;
return @arr;
}
# =========================
sub _date2serial
{
my ( $theText ) = @_;
my $sec = 0; my $min = 0; my $hour = 0; my $day = 1; my $mon = 0; my $year = 0;
# Handle DOY (Day of Year)
if( $theText =~ m|([Dd][Oo][Yy])\s*([0-9]{4})[\.]([0-9]{1,3})[\.]([0-9]{1,2})[\.]([0-9]{1,2})[\.]([0-9]{1,2})| ) {
# "DOY2003.122.23.15.59", "DOY2003.2.9.3.5.9" i.e. year.ddd.hh.mm.ss
$year = $2 - 1900; $day = $3; $hour = $4; $min = $5; $sec = $6; # Note: $day is in fact doy
} elsif( $theText =~ m|([Dd][Oo][Yy])\s*([0-9]{4})[\.]([0-9]{1,3})[\.]([0-9]{1,2})[\.]([0-9]{1,2})| ) {
# "DOY2003.122.23.15", "DOY2003.2.9.3" i.e. year.ddd.hh.mm
$year = $2 - 1900; $day = $3; $hour = $4; $min = $5;
} elsif( $theText =~ m|([Dd][Oo][Yy])\s*([0-9]{4})[\.]([0-9]{1,3})[\.]([0-9]{1,2})| ) {
# "DOY2003.122.23", "DOY2003.2.9" i.e. year.ddd.hh
$year = $2 - 1900; $day = $3; $hour = $4;
} elsif( $theText =~ m|([Dd][Oo][Yy])\s*([0-9]{4})[\.]([0-9]{1,3})| ) {
# "DOY2003.122", "DOY2003.2" i.e. year.ddd
$year = $2 - 1900; $day = $3;
} elsif ($theText =~ m|([0-9]{1,2})[-\s/]+([A-Z][a-z][a-z])[-\s/]+([0-9]{4})[-\s/]+([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})| ) {
# "31 Dec 2003 - 23:59:59", "31-Dec-2003 - 23:59:59", "31 Dec 2003 - 23:59:59 - any suffix"
$day = $1; $mon = $mon2num{$2} || 0; $year = $3 - 1900; $hour = $4; $min = $5; $sec = $6;
} elsif ($theText =~ m|([0-9]{1,2})[-\s/]+([A-Z][a-z][a-z])[-\s/]+([0-9]{4})[-\s/]+([0-9]{1,2}):([0-9]{1,2})| ) {
# "31 Dec 2003 - 23:59", "31-Dec-2003 - 23:59", "31 Dec 2003 - 23:59 - any suffix"
$day = $1; $mon = $mon2num{$2} || 0; $year = $3 - 1900; $hour = $4; $min = $5;
} elsif( $theText =~ m|([0-9]{1,2})[-\s/]+([A-Z][a-z][a-z])[-\s/]+([0-9]{2,4})| ) {
# "31 Dec 2003", "31 Dec 03", "31-Dec-2003", "31/Dec/2003"
$day = $1; $mon = $mon2num{$2} || 0; $year = $3;
$year += 100 if( $year < 80 ); # "05" --> "105" (leave "99" as is)
$year -= 1900 if( $year >= 1900 ); # "2005" --> "105"
} elsif( $theText =~ m|([0-9]{4})[-/\.]([0-9]{1,2})[-/\.]([0-9]{1,2})[-/\.\,\s]+([0-9]{1,2})[-\:/\.]([0-9]{1,2})[-\:/\.]([0-9]{1,2})| ) {
# "2003/12/31 23:59:59", "2003-12-31-23-59-59", "2003.12.31.23.59.59"
$year = $1 - 1900; $mon = $2 - 1; $day = $3; $hour = $4; $min = $5; $sec = $6;
} elsif( $theText =~ m|([0-9]{4})[-/\.]([0-9]{1,2})[-/\.]([0-9]{1,2})[-/\.\,\s]+([0-9]{1,2})[-\:/\.]([0-9]{1,2})| ) {
# "2003/12/31 23:59", "2003-12-31-23-59", "2003.12.31.23.59"
$year = $1 - 1900; $mon = $2 - 1; $day = $3; $hour = $4; $min = $5;
} elsif( $theText =~ m|([0-9]{4})[-/]([0-9]{1,2})[-/]([0-9]{1,2})| ) {
# "2003/12/31", "2003-12-31"
$year = $1 - 1900; $mon = $2 - 1; $day = $3;
} elsif( $theText =~ m|([0-9]{1,2})[-/]([0-9]{1,2})[-/]([0-9]{2,4})| ) {
# "12/31/2003", "12/31/03", "12-31-2003"
# (shh, don't tell anyone that we support ambiguous American dates, my boss asked me to)
$year = $3; $mon = $1 - 1; $day = $2;
$year += 100 if( $year < 80 ); # "05" --> "105" (leave "99" as is)
$year -= 1900 if( $year >= 1900 ); # "2005" --> "105"
} else {
# unsupported format
return 0;
}
if( ( $sec > 60 ) || ( $min > 59 ) || ( $hour > 23 ) || ( $day < 1 ) || ( $day > 365 ) || ( $mon > 11 )) {
# unsupported, out of range
return 0;
}
# To handle DOY, use timegm_nocheck or timelocal_nocheck that won't check input data range.
# This is necessary because with DOY, $day must be able to be greater than 31 and timegm
# and timelocal won't allow it. Keep using timegm or timelocal for non-DOY stuff.
if( $theText =~ /gmt/i ) {
if( $theText =~ /DOY/i ) {
return timegm_nocheck( $sec, $min, $hour, $day, $mon, $year);
} else {
return timegm( $sec, $min, $hour, $day, $mon, $year );
}
} else {
if( $theText =~ /DOY/i ) {
return timelocal_nocheck( $sec, $min, $hour, $day, $mon, $year);
} else {
return timelocal( $sec, $min, $hour, $day, $mon, $year );
}
}
}
# =========================
sub _serial2date
{
my ( $theTime, $theStr, $isGmt ) = @_;
my( $sec, $min, $hour, $day, $mon, $year, $wday, $yday ) = ( $isGmt ? gmtime( $theTime ) : localtime( $theTime ) );
$theStr =~ s/\$isoweek\(([^\)]*)\)/_isoWeek( $1, $day, $mon, $year, $wday, $theTime )/geoi;
$theStr =~ s/\$isoweek/_isoWeek( '$week', $day, $mon, $year, $wday, $theTime )/geoi;
$theStr =~ s/\$sec[o]?[n]?[d]?[s]?/sprintf("%.2u",$sec)/geoi;
$theStr =~ s/\$min[u]?[t]?[e]?[s]?/sprintf("%.2u",$min)/geoi;
$theStr =~ s/\$hou[r]?[s]?/sprintf("%.2u",$hour)/geoi;
$theStr =~ s/\$day/sprintf("%.2u",$day)/geoi;
$theStr =~ s/\$mon(?!t)/$monArr[$mon]/goi;
$theStr =~ s/\$mo[n]?[t]?[h]?/sprintf("%.2u",$mon+1)/geoi;
$theStr =~ s/\$yearday/$yday+1/geoi;
$theStr =~ s/\$yea[r]?/sprintf("%.4u",$year+1900)/geoi;
$theStr =~ s/\$ye/sprintf("%.2u",$year%100)/geoi;
$theStr =~ s/\$wday/substr($wdayArr[$wday],0,3)/geoi;
$theStr =~ s/\$wd/$wday+1/geoi;
$theStr =~ s/\$weekday/$wdayArr[$wday]/goi;
return $theStr;
}
# =========================
sub _isoWeek
{
my ( $format, $day, $mon, $year, $wday, $serial ) = @_;
# Contributed by PeterPayne - 22 Oct 2007
# Enhanced by PeterThoeny 2010-08-27
# Calculate the ISO8601 week number from the serial.
my $isoyear = $year + 1900;
my $yearserial = _year2isoweek1serial( $year + 1900, 1 );
if ( $mon >= 11 ) { # check if date is in next year's first week
my $yearnextserial = _year2isoweek1serial( $year + 1900 + 1, 1 );
if ( $serial >= $yearnextserial ) {
$yearserial = $yearnextserial;
$isoyear += 1;
}
} elsif ( $serial < $yearserial ) {
$yearserial = _year2isoweek1serial( $year + 1900 - 1, 1 );
$isoyear -= 1;
}
# calculate GMT of just past midnight today
my $today_gmt = timegm( 0, 0, 0, $day, $mon, $year );
my $isoweek = int( ( $today_gmt - $yearserial ) / ( 7 * 24 * 3600 ) ) + 1 ;
my $isowk = sprintf("%.2u", $isoweek );
my $isoday = $wday;
$isoday = 7 unless( $isoday );
$format =~ s/\$iso/$isoyear-W$isoweek/go;
$format =~ s/\$year/$isoyear/go;
$format =~ s/\$week/$isoweek/go;
$format =~ s/\$wk/$isowk/go;
$format =~ s/\$day/$isoday/go;
return $format;
}
# =========================
sub _year2isoweek1serial
{
my ( $year, $isGmt ) = @_;
# Contributed by PeterPayne - 22 Oct 2007
# Calculate the serial of the beginning of week 1 for specified year.
# Year is 4 digit year (e.g. "2000")
$year -= 1900;
# get Jan 4
my @param = ( 0, 0, 0, 4, 0, $year );
my $jan4epoch = ( $isGmt ? timegm( @param ) : timelocal( @param ) );
# what day does Jan 4 fall on?
my $jan4day = ( $isGmt ? (gmtime($jan4epoch))[6] : (localtime($jan4epoch))[6] );
$jan4day += 7 if ( $jan4day < 1 );
return( $jan4epoch - ( 24 * 3600 * ( $jan4day - 1 ) ) );
}
# =========================
sub _properSpace
{
my ( $theStr ) = @_;
# FIXME: I18N
unless( $dontSpaceRE ) {
$dontSpaceRE = &TWiki::Func::getPreferencesValue( "DONTSPACE" ) ||
&TWiki::Func::getPreferencesValue( "SPREADSHEETPLUGIN_DONTSPACE" ) ||
"UnlikelyGibberishWikiWord";
$dontSpaceRE =~ s/[^a-zA-Z0-9\,\s]//go;
$dontSpaceRE = "(" . join( "|", split( /[\,\s]+/, $dontSpaceRE ) ) . ")";
# Example: "(RedHat|McIntosh)"
}
$theStr =~ s/$dontSpaceRE/_spaceWikiWord( $1, "" )/geo; # e.g. "McIntosh"
$theStr =~ s/(^|[\s\(]|\]\[)([a-zA-Z0-9]+)/$1 . _spaceWikiWord( $2, " " )/geo;
$theStr =~ s///go; # remove "" marker
return $theStr;
}
# =========================
sub _spaceWikiWord
{
my ( $theStr, $theSpacer ) = @_;
$theStr =~ s/([a-z])([A-Z0-9])/$1$theSpacer$2/go;
$theStr =~ s/([0-9])([a-zA-Z])/$1$theSpacer$2/go;
$theStr =~ s/([A-Z])([0-9])/$1$theSpacer$2/go;
return $theStr;
}
# =========================
sub _workingDays
{
my ( $start, $end ) = @_;
# Rewritten by PeterThoeny - 2009-05-03 (previous implementation was buggy)
# Calculate working days between two times. Times are standard system times (secs since 1970).
# Working days are Monday through Friday (sorry, Israel!)
# A day has 60 * 60 * 24 sec
# Adding 3601 sec to account for daylight saving change in March in Northern Hemisphere
my $days = int( ( abs( $end - $start ) + 3601 ) / 86400 );
my $weeks = int( $days / 7 );
my $fullWeekWorkingDays = 5 * $weeks;
my $extra = $days % 7;
if( $extra > 0 ) {
$start = $end if( $start > $end );
my @tm = gmtime( $start );
my $wday = $tm[6]; # 0 is Sun, 6 is Sat
if( $wday == 0 ) {
$extra--;
} else {
my $sum = $wday + $extra;
$extra-- if( $sum > 6 );
$extra-- if( $sum > 7 );
}
}
return $fullWeekWorkingDays + $extra;
}
# =========================
1;