Difference between revisions of "How To Use TFPExpressionParser"

From Lazarus wiki
Jump to navigationJump to search
(→‎Calculating the expression: tryed your example -> now it work for me -> sorry are you actually work here?!)
m (Add category tutorials)
Line 215: Line 215:
  
 
The result of the calculation is returned as parameter "Result" which is a TFPExpressionResult as we met above. The arguments for the calculation are passed by <code>Args</code> which is just an array of TFPExpressionResult values - again because parameters can be of several data types. The term "Results" is maybe a bit misleading here, because this array holds all the *input* parameters as specified by the input types of the AddFunction method.
 
The result of the calculation is returned as parameter "Result" which is a TFPExpressionResult as we met above. The arguments for the calculation are passed by <code>Args</code> which is just an array of TFPExpressionResult values - again because parameters can be of several data types. The term "Results" is maybe a bit misleading here, because this array holds all the *input* parameters as specified by the input types of the AddFunction method.
 +
 +
[[Category:Tutorials]]

Revision as of 20:29, 26 June 2014

TFPExpressionParser allows to analyze and calculate expressions such as sin(x)*cos(2*x) for any value of the variable x. Besides mathematical expressions it can also handle boolean, string formulas, date/time values etc. Even user-provided functions can be linked in.

It belongs to fpc and is implemented in the unit fpexprpars.pp, folder (fpc_source_dir)/packages/fcl-base/src. Just add fpexprpars to the uses clauses to get access to its functionality.

Creating the parser

You apply the parser by creating an instance like that:

uses
  fpexprpars;

var
  FParser: TFPExpressionParser;
begin
  FParser := TFpExpressionParser.Create(self);
  // ... do something (see below)

If this is called from a method of a form, "self" points to the form. Since the parser inherits from TComponent there is no need to destroy it explicitly since its owner, the form, will do it this way. On the other hand it is also possible to create the parser from anywhere in a program; in this case use nil as the owner of the form, and don't forget to destroy the parser after its usage:

uses
  fpexprpars;

var
  FParser: TFPExpressionParser;
begin
  FParser := TFPExpressionParser.Create(nil);
  try
    // ... do something (see below)
  finally
    FParser.Free;
  end;
end;

Built-in categories

The parser is designed in a very flexible way, but the default parser is quite dumb. You have to specify which kind of expressions it will accept. This is done by adding the corresponding identifier to the set of built-in categories. They are accessible by the parser's property BuildIns:

type
  TBuiltInCategory = (bcStrings, bcDateTime, bcMath, bcBoolean, bcConversion, bcData, bcVaria, bcUser);
  TBuiltInCategories = set of TBuiltInCategory;

Here is a short collection of the built-in symbols which can be used when these elements are added to the parser's BuiltIns - it should be clear to anybody who "speaks" Pascal what these symbols mean...:

  • bcStrings: Length, Copy, Delete, Pos, Lowercase, Uppercase, StringReplace, CompareText
  • bcDateTime: Date, Time, Now, DayOfWeek, ExtractYear, ExtractMonth, ExtractDay, ExtractHour, ExtractMin, ExtractSec, Extractmsec, EncodeDate, EncodeTime, ShortDayName, ShortMonthName, LongDayName, LongMonthName
  • bcMath: cos, sin, arctan, abs, sqr, sqrt, exp, ln, log, frac, int, round, trunc,
  • bcBoolean: shl, shr, IFS, IFF, IFD, IFI (The IFxxx symbols have the same effect like fpc's IfThen for string (IFS), floating point (IFF), date/time (IFD), or integer (IFI) variables)
  • bcConversion: IntToStr, StrToInt, StrToIntDef, FloatToStr, StrToFloat, StrToFloatDef, BoolToStr, StrToBool, StrToBoolDef, DateToStr, TimeToStr, StrToDate, StrToDateDef, StrToTime, StrToTimeDef, StrToDateTime, StrToDateTimeDef

bcData, bcVaria, and bcUser are not used anywhere within fpexprpars.

Light bulb  Note: These symbols are not case-sensitive.

In order to use a mathematical expression the option bcMath has to be added to the parser's Builtins:

  FParser.Builtins := [bcMath];   // or FParser.Builtins := FParser.Builtins + [bcMath];

Expressions

An expression with constants

As a first example we have the parser calculate a very simple expression 1+1.

The first step is to tell the parser which expression is to be calculated. There is a property Expression for this purpose:

  FParser.Expression := '1+1';

The next step is to calculate the expression: just call Evaluate or EvaluateExpression - the former is is a function while the latter one is a procedure which passes the result as a parameter.

var
  parserResult: TFPExpressionResult;
begin
 ....
  parserResult := FParser.Evaluate;  // or: FParser.EvaluateExpression(parserResult);

What is that mysterious TFPExpressionResult? Since the parser is very flexible and can deal with numbers, strings, date/times or boolean there must be a more complex data type which returns a calculation result:

type
  TResultType = (rtBoolean, rtInteger, rtFloat, tDateTime, rtString);

  TFPExpressionResult = record
    ResString   : String;
    Case ResultType : TResultType of
      rtBoolean  : (ResBoolean  : Boolean);
      rtInteger  : (ResInteger  : Int64);
      rtFloat    : (ResFloat    : TExprFloat);
      rtDateTime : (ResDateTime : TDatetime);
      rtString   : ();
  end;

The member ResultType signals which one of the data fields is valid. It is important to understand this since the expression parser is very strict on data types. In our example, we are adding two integers, therefore the result is an integer as well; if, on the other had, we had used the expression "1.0 + 1", the first number would have been a floating point value, and the result would have been a float! Therefore, always have a look at the member ResultType of the TFPExpressionResult before picking the result. To simplify the usage of the expression result data type, fpexprpars exposes a function ArgToFloat which gets the entire expression result record as a parameter and selects the right component if a floating point result is expected:

var
  parserResult: TFPExpressionResult;
  resultValue: Double;
...
  parserResult := FParser.Evaluate;   // or: FParser.EvaluateExpression(parserResult);
  resultValue := ArgToFloat(parserResult);
Light bulb  Note: Floating point constants in expressions must have a point as decimal separator, not a comma as it is usual in some European countries. If your expression string comes from user input and contains decimal commas you have to replace the commas by points first before assigning it to the parsers's Expression.

An expression with a variable

In this example, we calculate the value of sin(x)*cos(2*x) for x = 0.5.

Defining variables

At first we have to define the variables. We have only one, x. The parser has a method AddFloatVariable to declare a floating point variable; there are also methods

  • AddBooleanVariable
  • AddStringVariable
  • AddDateTimeVariable

for boolean, string and date/time variables, respectively.

Each one of these methods expects the name of the variable along with its default value. For the sample function sin(x)*cos(2*x) we just call:

  FParser.AddFloatVariable('x', 0.5);

0.5 is entered here as a default value because that is the argument at which we want to calculate the expression. From now on, the parser will use this number whenever it finds the variable x in the expression.

Of course, you can add other names, e.g. constants like e, etc. (The number pi is already built-in).

Defining the expression

In the next step, the expression string has to be passed to the parser:

  FParser.Expression := 'sin(x)*cos(2*x)';

Calculating the expression

This is done as before with the constant expression - we show here a complete procedure which shows the equation and its result in a message box:

var
  FParser: TFPExpressionParser;
  resultValue: Double;
begin
  FParser := TFPExpressionParser.Create(nil);
  try
    FParser.BuiltIns := [bcMath];
    FParser.Identifiers.AddFloatVariable('x', 0.5);
    FParser.Expression := 'sin(x)*cos(2*x)';
    resultValue := FParser.Evaluate.ResFloat;
    ShowMessage(FParser.Expression + ' = ' + FloatToStr(resultValue));
  finally
    FParser.Free;
  end;
end;

Changing variables

So far, x always has the value 0.5 - it behaves like a constant, we could have used the expression "sin(0.5)*cos(2*0.5)" as well.

To make it behave more like a "variable", we calculate now the test function for the x values between -10 and 10 at integer steps.

The main question is: How to replace the value assigned to a variable? There are several possibilities - all of them seek the internal Identifier data:

  • Use the return value of the AddFloatVariable function.
  • Seek an identifer by calling FindIdentifierByName with the variable name as a parameter
  • Access the identifier from Identifiers collection of the parser by using the known index of the variable: We had added x as the only variable, therefore, it must be at index 0.

Once the Identifier is known, the value of the variable assigned to it can be changed by accessing the property AsFloat (or AsDateTime etc. accordingly):

var
  FParser: TFPExpressionParser;
  data_array[0..20]: array of Double;  // there are 21 integers from -10 to 10
  x: double;
  identifier: TFPExprIdentifierDef;
begin
  FParser := TFPExpressionParser.Create(nil);
  try
    identifier := FParser.AddFloatVariable('x', 0.0);
    // or: identifier := FParser.FindIdentifierByName('x');
    // or: identifier .= FParser.Identifiers[0];   // because 'x' is the first variable
    //It is advantageous to seek the identifier outside of the look to save computation time.
    FParser.Expression := 'sin(x)*cos(2*x)';
    for i := 0 to 20 do begin
      x := -10 + i;
      identifier.AsFloat := x;
      data_array[i] := ArgAsFloat(FParser.Evaluate);
    end;
  finally
    FParser.Free;
  end;
end;

Adding user-defined functions

The default parser only knows the built-in functions mentioned above. It is one of the strengths of the expression parser that it is very easy to extend it to include other functions. This can be done by calling the method Identifiers.AddFunction, e.g.

  FParser.AddFunction('tan', 'F', 'F', @ExprTan);

In this example, we add the function tan(x) by specifying its name as it will be called in the function expressions (first parameter), the type of the result values (second parameter, "F" = float, or "I" = integer, or "D" = date/time, or "S" = string, or "B" = boolean) and the type of the input values (third parameter, same logics). If a function accepts several input parameters the type of each one must be specified, e.g. by 'FF' for two floating point values, or 'FI' for a first float and a second integer parameter. The last parameter points to the function which is called whenever "tan" is met in the expression string. Since this function has a particular syntax you have to implement it in your source code, in our case like that:

procedure ExprTan(var Result: TFPExpressionResult; Const Args: TExprParameterArray);
var
  x: Double;
begin
  x := ArgToFloat(Args[0]);
  Result.resFloat := tan(x);
end;

The result of the calculation is returned as parameter "Result" which is a TFPExpressionResult as we met above. The arguments for the calculation are passed by Args which is just an array of TFPExpressionResult values - again because parameters can be of several data types. The term "Results" is maybe a bit misleading here, because this array holds all the *input* parameters as specified by the input types of the AddFunction method.