Recent

Author Topic: About TFPExpressionParser  (Read 22678 times)

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #30 on: November 27, 2019, 09:35:41 am »
mod is not yet implemented in fpc 3.0.4, fpc trunk has it.

min seems to be buggy. Please file a bug report (with example code). Incluse the other aggregates (max, sum, avg, count) as well, they seem to have the same issue in the scanner.

Thaddy

  • Hero Member
  • *****
  • Posts: 14377
  • Sensorship about opinions does not belong here.
Re: About TFPExpressionParser
« Reply #31 on: November 27, 2019, 10:30:41 am »
Will do (based on my example, because that does not have the mod issue)
[edit]
reported on Mantis as issue 0036366
« Last Edit: November 27, 2019, 10:38:21 am by Thaddy »
Object Pascal programmers should get rid of their "component fetish" especially with the non-visuals.

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #32 on: November 27, 2019, 01:02:14 pm »
According to Michael Van Canneyt's response to Thaddy's bug report, we do not use the aggregate  functions correctly. In its correct usage it is cumulatively applied to a variable to which repeatedly a new value is assigned. Call parser.InitAggregate to initialize the aggregation process and parser.UpdateAggregate when the variable value has been changed.

Here is a small example which calculates the sum of 1 to 5:
Code: Pascal  [Select][+][-]
  1. program Project1;
  2.  
  3. uses
  4.   fpexprpars;
  5.  
  6. var
  7.   parser: TFPExpressionParser;
  8.   x: TFPExprIdentifierDef;
  9.   i: Integer;
  10.  
  11. begin
  12.   parser := TFPExpressionParser.Create(nil);
  13.   try
  14.     parser.BuiltIns := [bcMath, bcAggregate];
  15.     x := parser.Identifiers.AddFloatVariable('x', 0);
  16.     parser.Expression := 'sum(x)';
  17.     parser.InitAggregate;
  18.     WriteLn('Sum of');
  19.     for i := 1 to 5 do
  20.     begin
  21.       x.AsFloat := i*1.0;
  22.       WriteLn(x.AsFloat:10:3);
  23.       parser.UpdateAggregate;
  24.     end;
  25.     WriteLn('----------');
  26.     WriteLn(parser.Evaluate.ResFloat:10:3);
  27.   finally
  28.     parser.Free;
  29.   end;
  30.  
  31.   ReadLn;
  32. end.

I updated the wiki article (https://wiki.lazarus.freepascal.org/How_To_Use_TFPExpressionParser#Aggregate_functions).
« Last Edit: November 27, 2019, 03:17:14 pm by wp »

CM630

  • Hero Member
  • *****
  • Posts: 1091
  • Не съм сигурен, че те разбирам.
    • http://sourceforge.net/u/cm630/profile/
Re: About TFPExpressionParser
« Reply #33 on: November 29, 2019, 08:34:50 am »

Thanks! Mod works in ver 2.0.6; 2019.10.27; FPC 3.2.0; SVN: 62129.
But I tried adding a custom Min, but I got
Quote
Expected ) bracket at position 7, but got ,
I think there should be no conflict with the min of fpexprpars, since I have disabled it with Parser.Builtins :=[bcMath]; but just in case I renamed min to mAn (as shown in the code below). Still the same.
I also tried FParser.Expression := 'mAn([3,5])'; but I got an error that parser cannot understand '['.
So how am I to pass a second parameter?

Code: Pascal  [Select][+][-]
  1. uses ...fpexprpars,math...
  2. ...
  3.  
  4.  
  5. procedure ExprMin(var Result: TFPExpressionResult; Const Args: TExprParameterArray);
  6. begin
  7.   result.ResFloat:=  math.Min(ArgToFloat(Args[0]),ArgToFloat(Args[1]));
  8. end;
  9. ...
  10. procedure TForm1.Button1Click(Sender: TObject);
  11. var
  12.   FParser: TFPExpressionParser;
  13. begin
  14.   FParser := TFPExpressionParser.Create(nil);
  15.   FParser.Builtins :=[bcMath];
  16.   FParser.Identifiers.AddFunction('mAn', 'F', 'F', @ExprMin);
  17.  
  18.  
  19.   try
  20.     writeln(min(3,5));
  21.     FParser.Expression := 'mAn(3,5)';
  22.     FParser.Evaluate;
  23.     writeln(FParser.AsInteger);
  24.   except
  25.     on E:EExprParser do
  26.       writeln(E.Message)
  27.     else Raise;
  28.   end;
  29.   FParser.Free;
  30. end;
  31. ...
  32.  
Лазар 3,2 32 bit (sometimes 64 bit); FPC3,2,2; rev: Lazarus_3_0 on Win10 64bit.

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #34 on: November 29, 2019, 10:22:33 am »
This is not all. You must convince the parser to accept multiple arguments for a function. In the source code of TFPExpressionParser.Primitive you can find a hint on it:

Code: Pascal  [Select][+][-]
  1.     // Determine number of arguments
  2.     if Iff then
  3.       ACount:=3
  4.     else if IfC then
  5.       ACount:=-4
  6.     else if (ID.IdentifierType in [itFunctionCallBack,itFunctionHandler,itFunctionNode]) then
  7.       ACount:=ID.ArgumentCount
  8.     else
  9.       ACount:=0;
  10.     // Parse arguments.
  11.     // Negative is for variable number of arguments, where Abs(value) is the minimum number of arguments
  12.     If (ACount<>0) then
  13.       ...

This means that you must somehow make ACount, the count of arguments, a negative value, then everything should work. Unfortunately, the method ArgumentCount returns a positive value:

Code: Pascal  [Select][+][-]
  1. function TFPExprIdentifierDef.ArgumentCount: Integer;
  2. begin
  3.   Result:=Length(FArgumentTypes);
  4. end;

Therefore, you must modify the fpexprpars sources to allow for negative values. I did this for the formula parser of fpspreadsheet. It defines variable parameter count by appending a '+' to the last input type specifier symbol in the AddFunction call for registering external functions to the parser, i.e. in your example:

Code: Pascal  [Select][+][-]
  1. FParser.Identifiers.AddFunction('mAn', 'F', 'F+', @ExprMin);

The TExprIdentiferDef must be extended by a boolean field FVariableArgumentCount which is set when the '+' is found as last symbol in the AddFunction call and is evaluated when the ArgumentCount is determined:

Code: Pascal  [Select][+][-]
  1. function TsExprIdentifierDefs.AddFunction(const AName: ShortString;
  2.   const AResultType: Char; const AParamTypes: String; const AExcelCode: Integer;
  3.   ACallBack: TsExprFunctionEvent): TsExprIdentifierDef;
  4. begin
  5.   Result := Add as TsExprIdentifierDef;
  6.   Result.Name := AName;
  7.   Result.IdentifierType := itFunctionHandler;
  8.   Result.ResultType := CharToResultType(AResultType);
  9.   //Result.ExcelCode := AExcelCode;
  10.   Result.FOnGetValue := ACallBack;
  11.   if (Length(AParamTypes) > 0) and (AParamTypes[Length(AParamTypes)]='+') then
  12.   begin
  13.     Result.ParameterTypes := Copy(AParamTypes, 1, Length(AParamTypes)-1);
  14.     Result.VariableArgumentCount := true;
  15.   end else
  16.     Result.ParameterTypes := AParamTypes;
  17. end;
  18.  
  19. function TsExprIdentifierDef.ArgumentCount: Integer;
  20. begin
  21.   if FVariableArgumentCount then
  22.     Result := -Length(FArgumentTypes)
  23.   else
  24.     Result := Length(FArgumentTypes);
  25. end;  

Not sure whether some more changes are required. Please study the source code of fpspreadsheet's fpsexprpars (https://sourceforge.net/p/lazarus-ccr/svn/HEAD/tree/components/fpspreadsheet/source/common/fpsexprparser.pas).

P.S.
Please give your "min" function a more speaking name, e.g. "minvalue" or "minof" instead of "mAn"

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #35 on: November 29, 2019, 05:37:48 pm »
In the attachment you can find a modified version of the fpexprpars unit of FPC trunk. It implements a variable count of function arguments. As described, a '+' must be added the the parameter list of a function definition in order to repeat this parameter in an unlimited way.

I submitted a feature request for inclusion in fpc (https://bugs.freepascal.org/view.php?id=36380). So, hopefully, this feature will soon be available officially.

[EDIT]
The new feature now is available in fpc 3.3.1.
See also: https://wiki.lazarus.freepascal.org/How_To_Use_TFPExpressionParser#Adding_user-defined_functions
« Last Edit: December 04, 2019, 03:32:11 pm by wp »

CM630

  • Hero Member
  • *****
  • Posts: 1091
  • Не съм сигурен, че те разбирам.
    • http://sourceforge.net/u/cm630/profile/
Re: About TFPExpressionParser
« Reply #36 on: December 04, 2019, 09:36:56 am »
I have not tried your latest code yet, but IMHO there is a signifficant issue with trigonomic functions- they work in radians only...not very human.

Maybe some global switch can be used (i.e. AngleUnit):

Code: Pascal  [Select][+][-]
  1. Procedure BuiltInCos(Var Result : TFPExpressionResult; Const Args : TExprParameterArray);
  2. begin
  3.   Result.resFloat:=Cos(ArgToFloat(Args[0]));
  4. end;


could be changed to


Code: Pascal  [Select][+][-]
  1. Procedure BuiltInCos(Var Result : TFPExpressionResult; Const Args : TExprParameterArray);
  2. begin
  3.   Result.resFloat:=Cos(toRad(ArgToFloat(Args[0])),AngleUnit);
  4. end;


and toRad could be sth. like:
Code: Pascal  [Select][+][-]
  1. function toRad(Angle : double; AngleUnit: TAngleUnit);
  2. begin
  3.    case AngleUnit of
  4.       auRad: exit Angle;
  5.       auDegreee: exit {conversion result};
  6.      auGrad: exit {conversion result};
  7. end;
Лазар 3,2 32 bit (sometimes 64 bit); FPC3,2,2; rev: Lazarus_3_0 on Win10 64bit.

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #37 on: December 04, 2019, 11:59:46 am »
I don't think that it is a good idea to have this in the library -- I don't know of any application which calls trigonometric functions with degree arguments, it would be very confusing.

But I think you can implement it for your own application. You can search for the trigonometric functions in the "BuiltinIdentifiers", delete and re-add them with your own degree-aware trigonometric functions (tested):

Code: Pascal  [Select][+][-]
  1. program Project1;
  2.  
  3. uses
  4.   Math, fpExprPars, Classes;
  5.  
  6. type
  7.   TMyBuiltinManager = class(TExprBuiltinManager);
  8.  
  9. var
  10.   parser: TFPExpressionParser;
  11.   idx: Integer;
  12.  
  13.   Procedure ExprCos(Var Result : TFPExpressionResult; Const Args : TExprParameterArray);
  14.   begin
  15.     Result.resFloat:=Cos(DegToRad(ArgToFloat(Args[0])));
  16.   end;
  17.  
  18. begin
  19.   idx := BuiltinIdentifiers.IndexOfIdentifier('cos');
  20.   TMyBuiltinManager(BuiltinIdentifiers).Defs.Delete(idx);
  21.   BuiltinIdentifiers.AddFunction(bcMath, 'cos', 'F', 'F', @ExprCos);
  22.  
  23.   parser := TFPExpressionParser.Create(nil);
  24.   try
  25.     parser.BuiltIns := [bcMath];
  26.     parser.Expression := 'cos(45.0)';
  27.     WriteLn(parser.Evaluate.ResFloat:0:5);
  28.     parser.Expression := 'cos(90.0)';
  29.     WriteLn(parser.Evaluate.ResFloat:0:5);
  30.     parser.Expression := 'cos(180.0)';
  31.     WriteLn(parser.Evaluate.ResFloat:0:5);
  32.   finally
  33.     parser.Free;
  34.   end;
  35.                
  36.   WriteLn('Press ENTER to close.');
  37.   ReadLn;
  38.  
  39. end.

Remark:
The current TBuiltinManager does not provide a method to delete an existing built-in identifier. An "ugly" cast to a descendant of TBuiltinManager is used to access the protected element Defs. I posted a feature request for providing a direct Delete method to the bug tracker (https://bugs.freepascal.org/view.php?id=36396).

Update:
Michael already applied the patch 15 minutes later! Now there is also a method to "Remove" the built-in function directly by specifying its name
Code: Pascal  [Select][+][-]
  1. BuiltinIdentifiers.Remove('cos');
« Last Edit: December 04, 2019, 01:08:19 pm by wp »

CM630

  • Hero Member
  • *****
  • Posts: 1091
  • Не съм сигурен, че те разбирам.
    • http://sourceforge.net/u/cm630/profile/
Re: About TFPExpressionParser
« Reply #38 on: December 04, 2019, 02:18:39 pm »
Thanks, I will try it when a new snapshot of Lazarus is available!
Edit: I have found another way to try it, but I had no time to check it properly...
« Last Edit: December 04, 2019, 10:22:17 pm by CM630 »
Лазар 3,2 32 bit (sometimes 64 bit); FPC3,2,2; rev: Lazarus_3_0 on Win10 64bit.

CM630

  • Hero Member
  • *****
  • Posts: 1091
  • Не съм сигурен, че те разбирам.
    • http://sourceforge.net/u/cm630/profile/
Re: About TFPExpressionParser
« Reply #39 on: December 05, 2019, 12:18:18 pm »

I have downloaded latest source code of fpexprpars.pp from GitHub and renamed it to fpexprpars1.pas, using it instead of the .pp.
When I do


Quote
Tform1.OnCreate...
begin
   Test; //This line goes fine
   Test; //This line throws 'An identifier with name "asin" already exists.
end;
with the procedure below, everything is okay on the first executin, but on the second call I get "An identifier with name "asin" already exists.".
On one hand I know how to overcome this, but on the other hand- since there is a FParser.Free, I suppose that BuiltinIdentifiers.AddFunction shall be destroyed.
And I wonder what shall happen if I create two simultaneous instances of TFPExpressionParser. Shall the added functions be the same for both instances?
So maybe it is worth mentioning.


Quote
procedure Test;
const
  fmt = '#,##0.#########';
var
  FParser: TFPExpressionParser;
  parserResult: TFPExpressionResult;
  strOutput: string = 'Грешка';
begin
  FParser := TFPExpressionParser.Create(nil);
  FParser.Builtins := [bcMath,bcConversion,bcVaria,bcUser];
  BuiltinIdentifiers.Remove('cos');
  BuiltinIdentifiers.Remove('sin');
  BuiltinIdentifiers.Remove('arccos');
  BuiltinIdentifiers.Remove('arcsin');
  BuiltinIdentifiers.Remove('min');
  BuiltinIdentifiers.AddFunction(bcMath, 'sin',    'F', 'F', @ExprSin);
  BuiltinIdentifiers.AddFunction(bcMath, 'cos',    'F', 'F', @ExprCos);
  BuiltinIdentifiers.AddFunction(bcMath, 'asin',   'F', 'F', @ExprASin);
  BuiltinIdentifiers.AddFunction(bcMath, 'arcsin', 'F', 'F', @ExprASin);
  BuiltinIdentifiers.AddFunction(bcMath, 'acos',   'F', 'F', @ExprACos);
  BuiltinIdentifiers.AddFunction(bcMath, 'arccos', 'F', 'F', @ExprACos);


//  FParser.Identifiers.AddFunction('min', 'F', 'F+', @ExprMin); //This is not implemented yet


  DefaultFormatSettings.ThousandSeparator:=' ';
  try
    FParser.Expression := 'asin(0.5)';
    parserResult := FParser.Evaluate;
    Case parserResult.ResultType of
      rtInteger  : strOutput := FormatFloat(fmt, float(parserResult.ResInteger));
      rtFloat    : strOutput := FormatFloat(fmt, parserResult.ResFloat);
    end; //case
    ShowMessage(strOutput) ;
    FParser.Free;
  except
    ShowMessage('Error');
    FParser.Free;
  end;
end;
Лазар 3,2 32 bit (sometimes 64 bit); FPC3,2,2; rev: Lazarus_3_0 on Win10 64bit.

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #40 on: December 05, 2019, 12:32:33 pm »
BuiltInIdentifiers is a function which returns a singleton instance of the TExprBuiltInManager which is administrated by the unit, i.e. the underlying code is not accessibile to the user. There is an initialization and finalization section in the fpexprpars unit which takes care of creating and destroying the built-in identifiers list. So, you can safely create and destroy instances of TFPExpressionParser without affecting the builtins. Also, when you add functions directly to the BuiltinIdentifiers (instead of to the Parser.Identifiers) they should be available to all parser instances.

CM630

  • Hero Member
  • *****
  • Posts: 1091
  • Не съм сигурен, че те разбирам.
    • http://sourceforge.net/u/cm630/profile/
Re: About TFPExpressionParser
« Reply #41 on: December 11, 2019, 08:33:07 am »
Silly me, if I have read my own code... most AddFunctions do not start with FParser.
I have the following problem:
When I run the code below with the following string h2s(10:11:12) I get Unexpected character in number ::


Code: Pascal  [Select][+][-]
  1.   BuiltinIdentifiers.AddFunction(bcMath, 'h2s', 'F','S',@ExprHoursToSeconds);
  2. ...
  3. procedure ExprHoursToSeconds(var Result: TFPExpressionResult; Const Args: TExprParameterArray);
  4. var
  5.   Argument: string;
  6.   SplitVals: array of string;
  7. begin
  8.   Argument:= StringReplace(args[1].ResString,',','.',[rfReplaceAll]);
  9.   SplitVals := Split(Argument,':');
  10.   result.resfloat := StrToInt(SplitVals[0])*3600 + StrToInt(SplitVals[1])*60 + StrToFloat(SplitVals[2]); //TODO: StrToFloat might cause troubles with the decimal separator
  11. end;
  12.  


I have used this sample from fpexprpars.pp as a reference:

Code: Pascal  [Select][+][-]
  1. AddFunction(bcStrings,'length','I','S',@BuiltinLength);
  2. ...
  3. Procedure BuiltInLength(Var Result : TFPExpressionResult; Const Args : TExprParameterArray);
  4. begin
  5.   Result.resInteger:=Length(Args[0].resString);
  6. end;
  7.  
Лазар 3,2 32 bit (sometimes 64 bit); FPC3,2,2; rev: Lazarus_3_0 on Win10 64bit.

wp

  • Hero Member
  • *****
  • Posts: 11922
Re: About TFPExpressionParser
« Reply #42 on: December 11, 2019, 11:23:06 am »
This is how it works for me:
  • A string in the expression must be single-quoted.
  • Argument index is zero-based.

Code: Pascal  [Select][+][-]
  1. program Project1;
  2.  
  3. uses
  4.   SysUtils, StrUtils,
  5.   fpexprpars;
  6.  
  7. var
  8.   parser: TFPExpressionParser;
  9.  
  10. procedure ExprHoursToSeconds(var Result: TFPExpressionResult; Const Args: TExprParameterArray);
  11. var
  12.   Argument: string;
  13.   SplitVals: array of string;
  14. begin
  15.   Argument:= StringReplace(args[0].ResString,',','.',[rfReplaceAll]);
  16.   SplitVals := Argument.Split(':');
  17.   result.resfloat := StrToInt(SplitVals[0])*3600 + StrToInt(SplitVals[1])*60 + StrToFloat(SplitVals[2]); //TODO: StrToFloat might cause troubles with the decimal separator
  18. end;
  19.  
  20. var
  21.   res: TFPExpressionResult;
  22. begin
  23.   BuiltinIdentifiers.AddFunction(bcMath, 'h2s', 'F','S',@ExprHoursToSeconds);
  24.   parser := TFPExpressionParser.Create(nil);
  25.   try
  26.     parser.BuiltIns := [bcMath];
  27.     parser.Expression :=  'h2s(''10:11:12'')';
  28.     res := parser.Evaluate;
  29.     WriteLn(res.ResFloat:0:3);
  30.   finally
  31.     parser.Free;
  32.   end;
  33.  
  34.   ReadLn;
  35. end.

CM630

  • Hero Member
  • *****
  • Posts: 1091
  • Не съм сигурен, че те разбирам.
    • http://sourceforge.net/u/cm630/profile/
Re: About TFPExpressionParser
« Reply #43 on: December 12, 2019, 07:04:44 am »
Thanks once again, that fixed it:
parser.Expression :=  'h2s(''10:11:12'')';

I am making  a simple calculator for myself, I will find a way to add these quotes automatically, before they get sent to the parser.


BUT there is another issue with the *.pp file. As it can be seen from the code below, no care is taken for division by zero. That results in the crash of the app when for example parser.Expression :=  '8/0'; is done.

Code: Pascal  [Select][+][-]
  1.  
  2. Procedure TFPDivideOperation.GetNodeValue(var Result : TFPExpressionResult);
  3.  
  4.  
  5. Var
  6.   RRes : TFPExpressionResult;
  7.  
  8.  
  9. begin
  10.   Left.GetNodeValue(Result);
  11.   Right.GetNodeValue(RRes);
  12.   case Result.ResultType of
  13.     rtInteger  : Result.ResFloat:=Result.ResInteger/RRes.ResInteger;
  14.     rtFloat    : Result.ResFloat:=Result.ResFloat/RRes.ResFloat;
  15.     rtCurrency :
  16.         if NodeType=rtCurrency then
  17.           Result.ResCurrency:=Result.ResCurrency/RRes.ResCurrency
  18.         else
  19.           Result.ResFloat:=Result.ResFloat/RRes.ResFloat;
  20.   end;
  21.   Result.ResultType:=NodeType;
  22. end;
  23.  




EDIT: I have created a bug report: https://bugs.freepascal.org/view.php?id=36839
« Last Edit: March 28, 2020, 04:43:36 pm by CM630 »
Лазар 3,2 32 bit (sometimes 64 bit); FPC3,2,2; rev: Lazarus_3_0 on Win10 64bit.

 

TinyPortal © 2005-2018