From 0aaff39316004cfd89a4bfdf42e5a6e36da311f3 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Tue, 1 Jul 2014 12:55:02 +0000 Subject: [PATCH] fpspreadsheet: Implement calculation of rnp date/time and info formulas. Partial implementation of SUBSTITUTE calculation is back. Add corresponding unit tests, passed. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3262 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- components/fpspreadsheet/fpsfunc.pas | 362 +++++++++++++++++- components/fpspreadsheet/fpspreadsheet.pas | 78 ++-- .../fpspreadsheet/tests/formulatests.pas | 7 + .../fpspreadsheet/tests/spreadtestgui.lpi | 6 + .../tests/testcases_calcrpnformula.inc | 299 +++++++++++++++ 5 files changed, 712 insertions(+), 40 deletions(-) diff --git a/components/fpspreadsheet/fpsfunc.pas b/components/fpspreadsheet/fpsfunc.pas index 241add651..bd4692828 100644 --- a/components/fpspreadsheet/fpsfunc.pas +++ b/components/fpspreadsheet/fpsfunc.pas @@ -86,6 +86,21 @@ function fpsSINH (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsSQRT (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsTAN (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsTANH (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +{ Date/time functions } +function fpsDATE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsDATEDIF (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsDATEVALUE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsDAY (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsHOUR (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsMINUTE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsMONTH (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsNOW (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsSECOND (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsTIME (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsTIMEVALUE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsTODAY (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsWEEKDAY (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsYEAR (Args: TsArgumentStack; NumArgs: Integer): TsArgument; { Statistical functions } function fpsAVEDEV (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsAVERAGE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; @@ -114,13 +129,23 @@ function fpsLOWER (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsMID (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsREPLACE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsRIGHT (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsSUBSTITUTE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsTRIM (Args: TsArgumentStack; NumArgs: Integer): TsArgument; function fpsUPPER (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +{ info functions } +function fpsISERR (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsISERROR (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsISLOGICAL (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsISNA (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsISNONTEXT (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsISNUMBER (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsISTEXT (Args: TsArgumentStack; NumArgs: Integer): TsArgument; +function fpsVALUE (Args: TsArgumentStack; NumArgs: Integer): TsArgument; implementation uses - Math, lazutf8, StrUtils, fpsUtils; + Math, lazutf8, StrUtils, DateUtils, fpsUtils; type TBoolArray = array of boolean; @@ -222,6 +247,7 @@ begin AValue := Arg.BoolValue; Result := errOK; end; + atError: Result := Arg.ErrorValue; else Result := errWrongType; end; end; @@ -233,6 +259,7 @@ begin atNumber : ANumber := Arg.NumberValue; atString : if not TryStrToFloat(arg.StringValue, ANumber) then Result := errWrongType; atBool : if Arg.BoolValue then ANumber := 1.0 else ANumber := 0.0; + atError : Result := Arg.ErrorValue; end; end; @@ -243,6 +270,7 @@ begin AString := Arg.StringValue; Result := errOK; end; + atError : Result := Arg.ErrorValue; else Result := errWrongType; end; end; @@ -332,6 +360,31 @@ begin end; end; +function PopDateValue(Args: TsArgumentStack; out ADate: TDate; + out AErrArg: TsArgument): Boolean; +var + arg: TsArgument; +begin + arg := Args.Pop; + case arg.ArgumentType of + atError, atBool, atEmpty: + begin + Result := false; + AErrArg := CreateError(errWrongType); + end; + atNumber: + begin + Result := true; + ADate := arg.NumberValue; + end; + atString: + begin + Result := TryStrToDate(arg.StringValue, ADate); + if not Result then AErrArg := CreateError(errWrongType); + end + end; +end; + {@@ Pops floating point values from the argument stack. Is called when calculating rpn formulas. @@ -435,6 +488,32 @@ begin end; +function PopTimeValue(Args: TsArgumentStack; out ATime: TTime; + out AErrArg: TsArgument): Boolean; +var + arg: TsArgument; +begin + arg := Args.Pop; + case arg.ArgumentType of + atError, atBool, atEmpty: + begin + Result := false; + AErrArg := CreateError(errWrongType); + end; + atNumber: + begin + Result := true; + ATime := frac(arg.NumberValue); + end; + atString: + begin + Result := TryStrToTime(arg.StringValue, ATime); + if not Result then AErrArg := CreateError(errWrongType); + end + end; +end; + + { Operations } function fpsAdd(Args: TsArgumentStack; NumArgs: Integer): TsArgument; @@ -759,13 +838,13 @@ begin base := arg_base.NumberValue; end; end; + arg_number := Args.Pop; if base < 0 then begin Result := CreateError(errOverflow); exit; end; - arg_number := Args.Pop; if arg_number.ArgumentType <> atNumber then begin Result := CreateError(errWrongType); exit; @@ -872,6 +951,159 @@ begin end; +{ Date/time functions } + +function fpsDATE(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// DATE( year, month, day ) +var + data: TFloatArray; + d: TDate; +begin + if PopFloatValues(Args, 3, data, Result) then begin + d := EncodeDate(round(data[0]), round(data[1]), round(data[2])); + Result := CreateNumber(d); + end; +end; + +function fpsDATEDIF(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +{ DATEDIF( start_date, end_date, interval ) + start_date <= end_date ! + interval = Y - The number of complete years. + = M - The number of complete months. + = D - The number of days. + = MD - The difference between the days (months and years are ignored). + = YM - The difference between the months (days and years are ignored). + = YD - The difference between the days (years and dates are ignored). } +var + data: TStrArray; + start_date, end_date: TDate; +begin + if not PopStringValues(Args, 1, data, Result) then exit; + if not PopDateValue(Args, end_date, Result) then exit; + if not PopDateValue(Args, start_date, Result) then exit; + + if end_date > start_date then + Result := CreateError(errOverflow) + else if data[0] = 'Y' then + Result := CreateNumber(YearsBetween(end_date, start_date)) + else if data[0] = 'M' then + Result := CreateNumber(MonthsBetween(end_date, start_date)) + else if data[0] = 'D' then + Result := CreateNumber(DaysBetween(end_date, start_date)) + else + Result := CreateError(errFormulaNotSupported); +end; + +function fpsDATEVALUE(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// DATEVALUE( date ) -- date can be a string or a date/time +var + d: TDate; +begin + if PopDateValue(Args, d, Result) then + Result := CreateNumber(d); +end; + +function fpsDAY(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +var + d: TDate; +begin + if PopDateValue(Args, d, Result) then + Result := CreateNumber(DayOf(d)); +end; + +function fpsHOUR(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +var + t: TTime; +begin + if PopTimeValue(Args, t, Result) then + Result := CreateNumber(HourOf(t)); +end; + +function fpsMINUTE(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +var + t: TTime; +begin + if PopTimeValue(Args, t, Result) then + Result := CreateNumber(MinuteOf(t)); +end; + +function fpsMONTH(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +var + d: TDate; +begin + if PopDateValue(Args, d, Result) then + Result := CreateNumber(MonthOf(d)); +end; + +function fpsNOW(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// NOW() +begin + Result := CreateNumber(now); +end; + +function fpsSECOND(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +var + t: TTime; +begin + if PopTimeValue(Args, t, Result) then + Result := CreateNumber(SecondOf(t)); +end; + +function fpsTIME(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// TIME( hour, minute, second ) +var + data: TFloatArray; + t: TTime; +begin + if PopFloatValues(Args, 3, data, Result) then begin + t := EncodeTime(round(data[0]), round(data[1]), round(data[2]), 0); + Result := CreateNumber(t); + end; +end; + +function fpsTIMEVALUE(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// TIMEVALUE( time_value ) +var + t: TTime; +begin + if PopTimeValue(Args, t, Result) then + Result := CreateNumber(t); +end; + +function fpsToday(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// TODAY() +begin + Result := CreateNumber(Date()); +end; + +function fpsWEEKDAY(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +{ WEEKDAY( serial_number, [return_value] + return_value = 1 - Returns a number from 1 (Sunday) to 7 (Saturday). + This is the default if parameter is omitted. + = 2 - Returns a number from 1 (Monday) to 7 (Sunday). + = 3 - Returns a number from 0 (Monday) to 6 (Sunday). } +var + d: TDate; + data: TFloatArray; + n: Integer; +begin + n := 1; + if NumArgs = 2 then + if PopFloatValues(Args, 1, data, Result) then n := round(data[0]) + else exit; + if PopDateValue(Args, d, Result) then + Result := CreateNumber(DayOfWeek(d)); +end; + +function fpsYEAR(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +var + d: TDate; +begin + if PopDateValue(Args, d, Result) then + Result := CreateNumber(YearOf(d)); +end; + + { Statistical functions } function fpsAVEDEV(Args: TsArgumentStack; NumArgs: Integer): TsArgument; @@ -901,15 +1133,19 @@ begin end; function fpsCOUNT(Args: TsArgumentStack; NumArgs: Integer): TsArgument; -{ Count non-missing arguments +{ counts the number of cells that contain numbers as well as the number of + arguments that contain numbers. COUNT( argument1, [argument2, ... argument_n] ) } var n, i: Integer; + arg: TsArgument; begin n := 0; - for i:=1 to NumArgs do - if not Args.Pop.IsMissing then inc(n); + for i:=1 to NumArgs do begin + arg := Args.Pop; + if (not arg.IsMissing) and (arg.ArgumentType = atNumber) then inc(n); + end; Result := CreateNumber(n); end; @@ -1221,6 +1457,34 @@ begin Result := CreateString(UTF8RightStr(s, count)); end; +function fpsSUBSTITUTE(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// SUBSTITUTE( text, old_text, new_text, [nth_appearance] ) +var + n: Integer; + arg: TsArgument; + data: TStrArray; + s, s_old, s_new: String; +begin + Result := CreateError(errWrongType); + n := -1; + if (NumArgs = 4) then begin + arg := Args.Pop; + if not arg.IsMissing and (arg.ArgumentType <> atNumber) then + exit; + n := round(arg.NumberValue); + end; + + if PopStringValues(Args, 3, data, Result) then begin + s := data[0]; + s_old := data[1]; + s_new := data[2]; + if n = -1 then + Result := CreateString(UTF8StringReplace(s, s_old, s_new, [rfReplaceAll])) + else + Result := CreateError(errFormulaNotSupported); + end; +end; + function fpsTRIM(Args: TsArgumentStack; NumArgs: Integer): TsArgument; // TRIM( text ) var @@ -1239,4 +1503,92 @@ begin Result := CreateString(UTF8UpperCase(data[0])); end; + +{ Info functions } + +function fpsISERR(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISERR( value ) +// If value is an error value (except #N/A), this function will return TRUE. +// Otherwise, it will return FALSE. +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool((arg.ArgumentType = atError) and (arg.ErrorValue <> errArgError)); +end; + +function fpsISERROR(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISERROR( value ) +// If value is an error value (#N/A, #VALUE!, #REF!, #DIV/0!, #NUM!, #NAME? +// or #NULL), this function will return TRUE. Otherwise, it will return FALSE. +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool((arg.ArgumentType = atError)); +end; + +function fpsISLOGICAL(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISLOGICAL( value ) +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool(arg.ArgumentType = atBool); +end; + +function fpsISNA(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISNA( value ) +// If value is a #N/A error value , this function will return TRUE. +// Otherwise, it will return FALSE. +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool((arg.ArgumentType = atError) and (arg.ErrorValue = errArgError)); +end; + +function fpsISNONTEXT(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISNONTEXT( value ) +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool(arg.ArgumentType <> atString); +end; + +function fpsISNUMBER(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISNUMBER( value ) +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool(arg.ArgumentType = atNumber); +end; + +function fpsISTEXT(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// ISTEXT( value ) +var + arg: TsArgument; +begin + arg := Args.Pop; + Result := CreateBool(arg.ArgumentType = atString); +end; + +function fpsVALUE(Args: TsArgumentStack; NumArgs: Integer): TsArgument; +// VALUE( text ) +// text is the text value to convert to a number. If text is not a number, the +// VALUE function will return #VALUE!. +var + data: TStrArray; + x: Double; +begin + if PopStringValues(Args, 1, data, Result) then + if TryStrToFloat(data[0], x) then + Result := CreateNumber(x) + else + Result := CreateError(errWrongType); +end; + + end. diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index dd6b64284..8a2cf7a99 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -1094,8 +1094,8 @@ type end; const - FEProps: array[TFEKind] of TFEProp = ( - { Operands } + FEProps: array[TFEKind] of TFEProp = ( // functions marked by (*) + { Operands } // are only partially supported (Symbol:''; MinParams:Byte(-1); MaxParams:Byte(-1); Func:nil), // fekCell (Symbol:''; MinParams:Byte(-1); MaxParams:Byte(-1); Func:nil), // fekCellRef (Symbol:''; MinParams:Byte(-1); MaxParams:Byte(-1); Func:nil), // fekCellRange @@ -1149,37 +1149,37 @@ const (Symbol:'TAN'; MinParams:1; MaxParams:1; Func:fpsTAN), // fekTAN (Symbol:'TANH'; MinParams:1; MaxParams:1; Func:fpsTANH), // fekTANH, { date/time } - (Symbol:'DATE'; MinParams:3; MaxParams:3; Func:nil), // fekDATE - (Symbol:'DATEDIF'; MinParams:3; MaxParams:3; Func:nil), // fekDATEDIF - (Symbol:'DATEVALUE'; MinParams:1; MaxParams:1; Func:nil), // fekDATEVALUE - (Symbol:'DAY'; MinParams:1; MaxParams:1; Func:nil), // fekDAY - (Symbol:'HOUR'; MinParams:1; MaxParams:1; Func:nil), // fekHOUR - (Symbol:'MINUTE'; MinParams:1; MaxParams:1; Func:nil), // fekMINUTE - (Symbol:'MONTH'; MinParams:1; MaxParams:1; Func:nil), // fekMONTH - (Symbol:'NOW'; MinParams:0; MaxParams:0; Func:nil), // fekNOW - (Symbol:'SECOND'; MinParams:1; MaxParams:1; Func:nil), // fekSECOND - (Symbol:'TIME'; MinParams:3; MaxParams:3; Func:nil), // fekTIME - (Symbol:'TIMEVALUE'; MinParams:1; MaxParams:1; Func:nil), // fekTIMEVALUE - (Symbol:'TODAY'; MinParams:0; MaxParams:0; Func:nil), // fekTODAY - (Symbol:'WEEKDAY'; MinParams:1; MaxParams:2; Func:nil), // fekWEEKDAY - (Symbol:'YEAR'; MinParams:1; MaxParams:1; Func:nil), // fekYEAR + (Symbol:'DATE'; MinParams:3; MaxParams:3; Func:fpsDATE), // fekDATE + (Symbol:'DATEDIF'; MinParams:3; MaxParams:3; Func:fpsDATEDIF), // fekDATEDIF (*) + (Symbol:'DATEVALUE'; MinParams:1; MaxParams:1; Func:fpsDATEVALUE), // fekDATEVALUE + (Symbol:'DAY'; MinParams:1; MaxParams:1; Func:fpsDAY), // fekDAY + (Symbol:'HOUR'; MinParams:1; MaxParams:1; Func:fpsHOUR), // fekHOUR + (Symbol:'MINUTE'; MinParams:1; MaxParams:1; Func:fpsMINUTE), // fekMINUTE + (Symbol:'MONTH'; MinParams:1; MaxParams:1; Func:fpsMONTH), // fekMONTH + (Symbol:'NOW'; MinParams:0; MaxParams:0; Func:fpsNOW), // fekNOW + (Symbol:'SECOND'; MinParams:1; MaxParams:1; Func:fpsSECOND), // fekSECOND + (Symbol:'TIME'; MinParams:3; MaxParams:3; Func:fpsTIME), // fekTIME + (Symbol:'TIMEVALUE'; MinParams:1; MaxParams:1; Func:fpsTIMEVALUE), // fekTIMEVALUE + (Symbol:'TODAY'; MinParams:0; MaxParams:0; Func:fpsTODAY), // fekTODAY + (Symbol:'WEEKDAY'; MinParams:1; MaxParams:2; Func:fpsWEEKDAY), // fekWEEKDAY + (Symbol:'YEAR'; MinParams:1; MaxParams:1; Func:fpsYEAR), // fekYEAR { statistical } (Symbol:'AVEDEV'; MinParams:1; MaxParams:30; Func:fpsAVEDEV), // fekAVEDEV (Symbol:'AVERAGE'; MinParams:1; MaxParams:30; Func:fpsAVERAGE), // fekAVERAGE - (Symbol:'BETADIST'; MinParams:3; MaxParams:5; Func:nil), // fekBETADIST - (Symbol:'BETAINV'; MinParams:3; MaxParams:5; Func:nil), // fekBETAINV - (Symbol:'BINOMDIST'; MinParams:4; MaxParams:4; Func:nil), // fekBINOMDIST - (Symbol:'CHIDIST'; MinParams:2; MaxParams:2; Func:nil), // fekCHIDIST - (Symbol:'CHIINV'; MinParams:2; MaxParams:2; Func:nil), // fekCHIINV + (Symbol:'BETADIST'; MinParams:3; MaxParams:5; Func:nil), // fekBETADIST + (Symbol:'BETAINV'; MinParams:3; MaxParams:5; Func:nil), // fekBETAINV + (Symbol:'BINOMDIST'; MinParams:4; MaxParams:4; Func:nil), // fekBINOMDIST + (Symbol:'CHIDIST'; MinParams:2; MaxParams:2; Func:nil), // fekCHIDIST + (Symbol:'CHIINV'; MinParams:2; MaxParams:2; Func:nil), // fekCHIINV (Symbol:'COUNT'; MinParams:0; MaxParams:30; Func:fpsCOUNT), // fekCOUNT (Symbol:'COUNTA'; MinParams:0; MaxParams:30; Func:nil), // fekCOUNTA - (Symbol:'COUNTBLANK';MinParams:1; MaxParams:1; Func:nil), // fekCOUNTBLANK - (Symbol:'COUNTIF'; MinParams:2; MaxParams:2; Func:nil), // fekCOUNTIF + (Symbol:'COUNTBLANK';MinParams:1; MaxParams:1; Func:nil), // fekCOUNTBLANK + (Symbol:'COUNTIF'; MinParams:2; MaxParams:2; Func:nil), // fekCOUNTIF (Symbol:'MAX'; MinParams:1; MaxParams:30; Func:fpsMAX), // fekMAX (Symbol:'MEDIAN'; MinParams:1; MaxParams:30; Func:nil), // fekMEDIAN (Symbol:'MIN'; MinParams:1; MaxParams:30; Func:fpsMIN), // fekMIN - (Symbol:'PERMUT'; MinParams:2; MaxParams:2; Func:nil), // fekPERMUT - (Symbol:'POISSON'; MinParams:3; MaxParams:3; Func:nil), // fekPOISSON + (Symbol:'PERMUT'; MinParams:2; MaxParams:2; Func:nil), // fekPERMUT + (Symbol:'POISSON'; MinParams:3; MaxParams:3; Func:nil), // fekPOISSON (Symbol:'PRODUCT'; MinParams:0; MaxParams:30; Func:fpsPRODUCT), // fekPRODUCT (Symbol:'STDEV'; MinParams:1; MaxParams:30; Func:fpsSTDEV), // fekSTDEV (Symbol:'STDEVP'; MinParams:1; MaxParams:30; Func:fpsSTDEVP), // fekSTDEVP @@ -1210,7 +1210,7 @@ const (Symbol:'PROPER'; MinParams:1; MaxParams:1; Func:nil), // fekPROPER (Symbol:'REPLACE'; MinParams:4; MaxParams:4; Func:fpsREPLACE), // fekREPLACE (Symbol:'RIGHT'; MinParams:1; MaxParams:2; Func:fpsRIGHT), // fekRIGHT - (Symbol:'SUBSTITUTE';MinParams:3; MaxParams:4; Func:nil), // fekSUBSTITUTE + (Symbol:'SUBSTITUTE';MinParams:3; MaxParams:4; Func:fpsSUBSTITUTE), // fekSUBSTITUTE (*) (Symbol:'TRIM'; MinParams:1; MaxParams:1; Func:fpsTRIM), // fekTRIM (Symbol:'UPPER'; MinParams:1; MaxParams:1; Func:fpsUPPER), // fekUPPER { lookup/reference } @@ -1222,15 +1222,15 @@ const (Symbol:'CELL'; MinParams:1; MaxParams:2; Func:nil), // fekCELLINFO (Symbol:'INFO'; MinParams:1; MaxParams:1; Func:nil), // fekINFO (Symbol:'ISBLANK'; MinParams:1; MaxParams:1; Func:nil), // fekIsBLANK - (Symbol:'ISERR'; MinParams:1; MaxParams:1; Func:nil), // fekIsERR - (Symbol:'ISERROR'; MinParams:1; MaxParams:1; Func:nil), // fekIsERROR - (Symbol:'ISLOGICAL'; MinParams:1; MaxParams:1; Func:nil), // fekIsLOGICAL - (Symbol:'ISNA'; MinParams:1; MaxParams:1; Func:nil), // fekIsNA - (Symbol:'ISNONTEXT'; MinParams:1; MaxParams:1; Func:nil), // fekIsNONTEXT - (Symbol:'ISNUMBER'; MinParams:1; MaxParams:1; Func:nil), // fekIsNUMBER + (Symbol:'ISERR'; MinParams:1; MaxParams:1; Func:fpsISERR), // fekIsERR + (Symbol:'ISERROR'; MinParams:1; MaxParams:1; Func:fpsISERROR), // fekIsERROR + (Symbol:'ISLOGICAL'; MinParams:1; MaxParams:1; Func:fpsISLOGICAL), // fekIsLOGICAL + (Symbol:'ISNA'; MinParams:1; MaxParams:1; Func:fpsISNA), // fekIsNA + (Symbol:'ISNONTEXT'; MinParams:1; MaxParams:1; Func:fpsISNONTEXT), // fekIsNONTEXT + (Symbol:'ISNUMBER'; MinParams:1; MaxParams:1; Func:fpsISNUMBER), // fekIsNUMBER (Symbol:'ISREF'; MinParams:1; MaxParams:1; Func:nil), // fekIsRef - (Symbol:'ISTEXT'; MinParams:1; MaxParams:1; Func:nil), // fekIsTEXT - (Symbol:'VALUE'; MinParams:1; MaxParams:1; Func:nil), // fekValue + (Symbol:'ISTEXT'; MinParams:1; MaxParams:1; Func:fpsISTEXT), // fekIsTEXT + (Symbol:'VALUE'; MinParams:1; MaxParams:1; Func:fpsVALUE), // fekValue { Other operations } (Symbol:'SUM'; MinParams:1; MaxParams:1; Func:nil) // fekOpSUM (Unary sum operation). Note: CANNOT be used for summing sell contents; use fekSUM} ); @@ -1460,14 +1460,18 @@ begin func := FEProps[fe.ElementKind].Func; if not Assigned(func) then begin // calculation of function not implemented + WriteErrorValue(ACell, errFormulaNotSupported); exit; end; if args.Count < fe.ParamsNum then begin // not enough parameters + WriteErrorValue(ACell, errArgError); exit; end; // Result of function val := func(args, fe.ParamsNum); + args.Push(val); + { // Push valid result on stack, exit in case of error case val.ArgumentType of atNumber, atString, atBool, atEmpty: @@ -1478,6 +1482,7 @@ begin exit; end; end; + } end; // case end; // for if args.Count = 1 then begin @@ -1486,19 +1491,22 @@ begin atNumber: WriteNumber(ACell, val.NumberValue); atBool : WriteBoolValue(ACell, val.BoolValue); atString: WriteUTF8Text(ACell, val.StringValue); + atError : WriteErrorValue(ACell, val.ErrorValue); atEmpty : WriteBlank(ACell); end; end else + WriteErrorValue(ACell, errArgError); + { // This case is a program error --> raise an exception raise Exception.CreateFmt('Incorrect argument count of the formula in cell %s', [ GetCellString(ACell^.Row, ACell^.Col, []) ]); + } finally args.Free; end; end; - {@@ Converts a FPSpreadsheet cell position, which is Row, Col in numbers and zero based, to a textual representation which is [Col][Row], diff --git a/components/fpspreadsheet/tests/formulatests.pas b/components/fpspreadsheet/tests/formulatests.pas index d21c8c317..e48500d4f 100644 --- a/components/fpspreadsheet/tests/formulatests.pas +++ b/components/fpspreadsheet/tests/formulatests.pas @@ -6,6 +6,10 @@ unit formulatests; when the corresponding rpn formula is calculated. } {.$DEFINE ENABLE_CALC_RPN_EXCEPTIONS} +{ Deactivate this define to include errors in the structure of the rpn formulas. + Note that Excel report a corrupted file when trying to read this file } +{.DEFINE ENABLE_DEFECTIVE_FORMULAS } + interface @@ -143,6 +147,9 @@ var cell: PCell; sollValues: array of TsArgument; formula: String; + s: String; + t: TTime; + hr,min,sec,msec: Word; begin TempFile := GetTempFileName; diff --git a/components/fpspreadsheet/tests/spreadtestgui.lpi b/components/fpspreadsheet/tests/spreadtestgui.lpi index 86230b550..b35028661 100644 --- a/components/fpspreadsheet/tests/spreadtestgui.lpi +++ b/components/fpspreadsheet/tests/spreadtestgui.lpi @@ -47,6 +47,9 @@ + + + @@ -154,6 +157,9 @@ + + + diff --git a/components/fpspreadsheet/tests/testcases_calcrpnformula.inc b/components/fpspreadsheet/tests/testcases_calcrpnformula.inc index 6cc8eb3bb..04883ba45 100644 --- a/components/fpspreadsheet/tests/testcases_calcrpnformula.inc +++ b/components/fpspreadsheet/tests/testcases_calcrpnformula.inc @@ -680,6 +680,230 @@ SetLength(sollValues, Row+1); sollValues[Row] := CreateNumber(tanh(0.1)); + +{------------------------------------------------------------------------------} +{ Date/time functions } +{------------------------------------------------------------------------------} + + // DATE + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=DATE(2014, 7, 1)'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(2014, + RPNNumber(7, + RPNNumber(1, + RPNFunc(fekDATE, nil)))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(EncodeDate(2014,7,1)); + + // DATEVALUE + inc(Row); + s := DateToStr(EncodeDate(2014, 7, 1)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=DATEVALUE("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekDATEVALUE, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(EncodeDate(2014,7,1)); + + // DAY / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=DAY(DATE(2014,7,1))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(2014, + RPNNumber(7, + RPNNumber(1, + RPNFunc(fekDATE, + RPNFunc(fekDAY, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(1); + + // DAY / argument string + inc(Row); + s := DateToStr(EncodeDate(2014, 7, 1)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=DAY("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekDAY, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(1); + + // HOUR / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=HOUR(TIME(9, 59, 20))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(9, + RPNNumber(59, + RPNNumber(20, + RPNFunc(fekTIME, + RPNFunc(fekHOUR, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(9); + + // HOUR / argument string + inc(Row); + s := TimeToStr(EncodeTime(9, 59, 20, 0)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=HOUR("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekHOUR, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(9); + + // MINUTE / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=MINUTE(TIME(9, 59, 20))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(9, + RPNNumber(59, + RPNNumber(20, + RPNFunc(fekTIME, + RPNFunc(fekMINUTE, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(59); + + // MINUTE / argument string + inc(Row); + s := TimeToStr(EncodeTime(9, 59, 20, 0)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=MINUTE("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekMINUTE, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(59); + + // MONTH / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=MONTH(DATE(2014,7,1))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(2014, + RPNNumber(7, + RPNNumber(1, + RPNFunc(fekDATE, + RPNFunc(fekMONTH, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(7); + + // MONTH / argument string + inc(Row); + s := DateToStr(EncodeDate(2014, 7, 1)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=MONTH("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekMONTH, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(7); + + // NOW + inc(Row); + // Make sure that, when the file is read we still have the same second + // Otherwise there would be a mismatch. + repeat + t := now(); + DecodeTime(t, hr, min, sec, msec); + until msec < 500; + MyWorksheet.WriteUTF8Text(Row, 0, '=NOW()'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNFunc(fekNOW, nil))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(t); + + // SECOND / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=SECOND(TIME(9, 59, 20))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(9, + RPNNumber(59, + RPNNumber(20, + RPNFunc(fekTIME, + RPNFunc(fekSECOND, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(20); + + // SECOND / argument string + inc(Row); + s := TimeToStr(EncodeTime(9, 59, 20, 0)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=SECOND("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekSECOND, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(20); + + // TIME + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=TIME(9, 59, 20)'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(9, + RPNNumber(59, + RPNNumber(20, + RPNFunc(fekTIME, nil)))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(EncodeTime(9, 59, 20, 0)); + + // TIMEVALUE + inc(Row); + s := TimeToStr(EncodeTime(9, 59, 20, 0)); // Localization! + MyWorksheet.WriteUTF8Text(Row, 0, '=TIMEVALUE("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekTIMEVALUE, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(EncodeTime(9, 59, 20, 0)); + + // TODAY + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=TODAY()'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNFunc(fekTODAY, nil))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(Date()); + + // WEEKDAY / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=WEEKDAY(DATE(2014,7,1))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(2014, + RPNNumber(7, + RPNNumber(1, + RPNFunc(fekDATE, + RPNFunc(fekWEEKDAY, 1, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(DayOfWeek(EncodeDate(2014,7,1))); + + // WEEKDAY / argument string + inc(Row); + s := DateToStr(EncodeDate(2014, 7, 1)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=WEEKDAY("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekWEEKDAY, 1, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(DayOfWeek(EncodeDate(2014,7,1))); + + // YEAR / argument number + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=YEAR(DATE(2014,7,1))'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(2014, + RPNNumber(7, + RPNNumber(1, + RPNFunc(fekDATE, + RPNFunc(fekYEAR, nil))))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(2014); + + // YEAR / argument string + inc(Row); + s := DateToStr(EncodeDate(2014, 7, 1)); // Localization of the test + MyWorksheet.WriteUTF8Text(Row, 0, '=YEAR("'+s+'")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString(s, + RPNFunc(fekYEAR, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(2014); + + {------------------------------------------------------------------------------} { Statistical functions } {------------------------------------------------------------------------------} @@ -1184,6 +1408,17 @@ SetLength(sollValues, Row+1); sollValues[Row] := CreateString('d'); + // SUBSTITUTE + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=SUBSTITUTE("lAzArus", "A", "a")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString('lAzArus', + RPNString('A', + RPNString('a', + RPNFunc(fekSUBSTITUTE, 3, nil)))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateString('lazarus'); + // Trim inc(Row); MyWorksheet.WriteUTF8Text(Row, 0, '=TRIM(" Hallo word ")'); @@ -1211,3 +1446,67 @@ SetLength(sollValues, Row+1); sollValues[Row] := CreateString(UTF8UpperCase('Viele Grüße')); + +{------------------------------------------------------------------------------} +{ Information functions } +{------------------------------------------------------------------------------} + + // IsError + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=ISERROR(1/0)'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(1, + RPNNumber(0, + RPNFunc(fekDiv, + RPNFunc(fekISERROR, nil)))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateBool(true); + + // IsError + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=ISERROR(0/1)'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNNumber(0, + RPNNumber(1, + RPNFunc(fekDiv, + RPNFunc(fekISERROR, nil)))))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateBool(false); + + // VALUE + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=VALUE("100")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNString('100', + RPNFunc(fekVALUE, nil)))); + SetLength(sollValues, Row+1); + sollValues[Row] := CreateNumber(100); + +{------------------------------------------------------------------------------} +{ Error cases } +{------------------------------------------------------------------------------} +{$IFDEF ENABLE_DEFECTIVE_FORMULAS } + // Using less parameters than specified + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=IF(true,1)'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNBool(true, + RPNNumber(1.0, + RPNFunc(fekIF,3, nil))))); // <-- we have only 2 parameters! + SetLength(sollValues, Row+1); + sollValues[Row] := CreateError(errArgError); + + // Using more parameters than specified + inc(Row); + MyWorksheet.WriteUTF8Text(Row, 0, '=IF(true,1,"A")'); + MyWorksheet.WriteRPNFormula(Row, 1, CreateRPNFormula( + RPNBool(true, + RPNNumber(1.0, + RPNString('A', + RPNFunc(fekIF,2, nil)))))); // <-- we have 3 parameters, not 2 + SetLength(sollValues, Row+1); + sollValues[Row] := CreateError(errWrongType); + { The first idea was that this should report an ArgError, but in fact it is + a WrongType error because popping two values from the stack finds a number, + but a bool is expected } +{$ENDIF}