Difference between revisions of "How To Use TFPExpressionParser"

From Lazarus wiki
Line 1: Line 1:
 +
 +
 
{{How To Use TFPExpressionParser}}
 
{{How To Use TFPExpressionParser}}
  
  
TFPExpressionParser allows to analyze and calculate expressions such as <code>sin(x)*cos(2*x)</code> for any value of the [[Variable|variable]] <code>x</code>. Besides mathematical expressions it can also handle [[Boolean|boolean]], string formulas, [[TDateTime|date/time]] values etc. Even user-provided functions can be linked in.  
+
TFPExpressionParser allows to analyze and calculate [[expression|expressions]] such as <syntaxhighlight lang="pascal" enclose="none">sin(x)*cos(2*x)</syntaxhighlight> for any value of the [[Variable|variable]] <syntaxhighlight lang="pascal" enclose="none">x</syntaxhighlight>. Besides mathematical expressions it can also handle [[Boolean|boolean]], string formulas, [[TDateTime|date/time]] values etc. Even user-provided functions can be linked in.  
  
It belongs to [[FPC]] [[FCL|Free Component Library (FCL)]] and is implemented in the [[Unit|unit]] <code>fpexprpars.pp</code>, folder <code>(fpc_source_dir)/packages/fcl-base/src</code>. Just add <code>fpexprpars</code> to the [[Uses|uses]] clauses to get access to its functionality. See the file "fpexprpars.txt" (in <code>(fpc_source_dir)/packages/fcl-base/examples</code>) for a short documenation.
+
It belongs to [[FPC]] [[FCL|Free Component Library (FCL)]] and is implemented in the [[Unit|unit]] <syntaxhighlight lang="pascal" enclose="none">fpexprpars.pp</syntaxhighlight>, folder <syntaxhighlight lang="pascal" enclose="none">(fpc_source_dir)/packages/fcl-base/src</syntaxhighlight>. Just add <syntaxhighlight lang="pascal" enclose="none">fpexprpars</syntaxhighlight> to the [[Uses|uses]] clauses to get access to its functionality. See the file "fpexprpars.txt" (in <syntaxhighlight lang="pascal" enclose="none">(fpc_source_dir)/packages/fcl-base/examples</syntaxhighlight>) for a short documenation.
  
 
== Creating the parser ==
 
== Creating the parser ==
 
You apply the parser by creating an instance like this:
 
You apply the parser by creating an instance like this:
<source>
+
<syntaxhighlight lang="pascal">
 
uses
 
uses
 
   fpexprpars;
 
   fpexprpars;
Line 17: Line 19:
 
   FParser := TFpExpressionParser.Create(self);
 
   FParser := TFpExpressionParser.Create(self);
 
   // ... do something (see below)
 
   // ... do something (see below)
</source>
+
</syntaxhighlight>
  
If this is called from a [[Method|method]] of a [[TForm|form]], "[[Self|self]]" points to the form. Since the parser inherits from <code>TComponent</code>, there is no need to destroy it explicitly since its owner, the form, will do it. On the other hand, it is also possible to create the parser from anywhere in a [[Program|program]] without a form or even [[Class|class]] being involved; in this case use <code>[[Nil|nil]]</code> as the owner of the parser, but don't forget to <code>.Free</code> the parser after its usage:
+
If this is called from a [[Method|method]] of a [[TForm|form]], "[[Self|self]]" points to the form. Since the parser inherits from <syntaxhighlight lang="pascal" enclose="none">TComponent</syntaxhighlight>, there is no need to destroy it explicitly since its owner, the form, will do it. On the other hand, it is also possible to create the parser from anywhere in a [[Program|program]] without a form or even [[Class|class]] being involved; in this case use <syntaxhighlight lang="pascal" enclose="none">[[Nil|nil]]</syntaxhighlight> as the owner of the parser, but don't forget to <syntaxhighlight lang="pascal" enclose="none">.Free</syntaxhighlight> the parser after its usage:
  
<source>
+
<syntaxhighlight lang="pascal">
 
uses
 
uses
 
   fpexprpars;
 
   fpexprpars;
Line 35: Line 37:
 
   end;
 
   end;
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
 
== Built-in categories ==
 
== 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|identifier]] to the set of built-in categories. They are accessible by the parser's [[Property|property]] <code>BuiltIns</code>:
+
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|identifier]] to the set of built-in categories. They are accessible by the parser's [[Property|property]] <syntaxhighlight lang="pascal" enclose="none">BuiltIns</syntaxhighlight>:
  
<source>
+
<syntaxhighlight lang="pascal">
 
type
 
type
 
   TBuiltInCategory = (bcStrings, bcDateTime, bcMath, bcBoolean, bcConversion, bcData, bcVaria, bcUser);
 
   TBuiltInCategory = (bcStrings, bcDateTime, bcMath, bcBoolean, bcConversion, bcData, bcVaria, bcUser);
 
   TBuiltInCategories = set of TBuiltInCategory;
 
   TBuiltInCategories = set of TBuiltInCategory;
</source>
+
</syntaxhighlight>
  
Here is a collection of the built-in symbols which can be used by adding categories to the parser's <code>BuiltIns</code> - it should be clear to anybody who "speaks" [[Pascal]] what these symbols mean...
+
Here is a collection of the built-in symbols which can be used by adding categories to the parser's <syntaxhighlight lang="pascal" enclose="none">BuiltIns</syntaxhighlight> - it should be clear to anybody who "speaks" [[Pascal]] what these symbols mean...
* '''bcStrings''': <code>Length</code>, <code>Copy</code>, <code>Delete</code>, <code>Pos</code>, <code>Lowercase</code>, <code>Uppercase</code>, <code>StringReplace</code>, <code>CompareText</code>
+
* '''bcStrings''': <syntaxhighlight lang="pascal" enclose="none">Length</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Copy</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Delete</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Pos</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Lowercase</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Uppercase</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StringReplace</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">CompareText</syntaxhighlight>
* '''bcDateTime''': <code>Date</code>, <code>Time</code>, <code>Now</code>, <code>DayOfWeek</code>, <code>ExtractYear</code>, <code>ExtractMonth</code>, <code>ExtractDay</code>, <code>ExtractHour</code>, <code>ExtractMin</code>, <code>ExtractSec</code>, <code>Extractmsec</code>, <code>EncodeDate</code>, <code>EncodeTime</code>, <code>ShortDayName</code>, <code>ShortMonthName</code>, <code>LongDayName</code>, <code>LongMonthName</code>
+
* '''bcDateTime''': <syntaxhighlight lang="pascal" enclose="none">Date</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Time</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Now</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">DayOfWeek</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ExtractYear</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ExtractMonth</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ExtractDay</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ExtractHour</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ExtractMin</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ExtractSec</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">Extractmsec</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">EncodeDate</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">EncodeTime</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ShortDayName</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ShortMonthName</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">LongDayName</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">LongMonthName</syntaxhighlight>
* '''bcMath''': <code>cos</code>, <code>sin</code>, <code>arctan</code>, <code>abs</code>, <code>sqr</code>, <code>sqrt</code>, <code>exp</code>, <code>ln</code>, <code>log</code>, <code>frac</code>, <code>int</code>, <code>round</code>, <code>trunc</code>,
+
* '''bcMath''': <syntaxhighlight lang="pascal" enclose="none">cos</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">sin</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">arctan</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">abs</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">sqr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">sqrt</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">exp</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">ln</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">log</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">frac</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">int</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">round</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">trunc</syntaxhighlight>,
* '''bcBoolean''': <code>shl</code>, <code>shr</code>, <code>IFS</code>, <code>IFF</code>, <code>IFD</code>, <code>IFI</code> (The <code>IFxxx</code> symbols have the same effect as fpc's <code>IfThen</code> for string (<code>IFS</code>), floating point (<code>IFF</code>), date/time (<code>IFD</code>), or integer (<code>IFI</code>) variables)
+
* '''bcBoolean''': <syntaxhighlight lang="pascal" enclose="none">shl</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">shr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">IFS</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">IFF</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">IFD</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">IFI</syntaxhighlight> (The <syntaxhighlight lang="pascal" enclose="none">IFxxx</syntaxhighlight> symbols have the same effect as fpc's <syntaxhighlight lang="pascal" enclose="none">IfThen</syntaxhighlight> for string (<syntaxhighlight lang="pascal" enclose="none">IFS</syntaxhighlight>), floating point (<syntaxhighlight lang="pascal" enclose="none">IFF</syntaxhighlight>), date/time (<syntaxhighlight lang="pascal" enclose="none">IFD</syntaxhighlight>), or integer (<syntaxhighlight lang="pascal" enclose="none">IFI</syntaxhighlight>) variables)
* '''bcConversion''': <code>IntToStr</code>, <code>StrToInt</code>, <code>StrToIntDef</code>, <code>FloatToStr</code>, <code>StrToFloat</code>, <code>StrToFloatDef</code>, <code>BoolToStr</code>, <code>StrToBool</code>, <code>StrToBoolDef</code>, <code>DateToStr</code>, <code>TimeToStr</code>, <code>StrToDate</code>, <code>StrToDateDef</code>, <code>StrToTime</code>, <code>StrToTimeDef</code>, <code>StrToDateTime</code>, <code>StrToDateTimeDef</code>
+
* '''bcConversion''': <syntaxhighlight lang="pascal" enclose="none">IntToStr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToInt</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToIntDef</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">FloatToStr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToFloat</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToFloatDef</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">BoolToStr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToBool</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToBoolDef</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">DateToStr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">TimeToStr</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToDate</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToDateDef</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToTime</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToTimeDef</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToDateTime</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">StrToDateTimeDef</syntaxhighlight>
  
<code>bcData</code>, <code>bcVaria</code>, and <code>bcUser</code> are not used anywhere within fpexprpars. The [[#Adding user-defined functions|last section]] gives instructions how to extend the parser with more functions.
+
<syntaxhighlight lang="pascal" enclose="none">bcData</syntaxhighlight>, <syntaxhighlight lang="pascal" enclose="none">bcVaria</syntaxhighlight>, and <syntaxhighlight lang="pascal" enclose="none">bcUser</syntaxhighlight> are not used anywhere within fpexprpars. The [[#Adding user-defined functions|last section]] gives instructions how to extend the parser with more functions.
  
 
{{Note|These symbols are not [[case-sensitive]].}}
 
{{Note|These symbols are not [[case-sensitive]].}}
  
In order to use a mathematical expression the option <code>bcMath</code> has to be added to the parser's <code>Builtins</code>:
+
In order to use a mathematical expression the option <syntaxhighlight lang="pascal" enclose="none">bcMath</syntaxhighlight> has to be added to the parser's <syntaxhighlight lang="pascal" enclose="none">Builtins</syntaxhighlight>:
  
<source>
+
<syntaxhighlight lang="pascal">
 
   FParser.Builtins := [bcMath];  // or FParser.Builtins := FParser.Builtins + [bcMath];
 
   FParser.Builtins := [bcMath];  // or FParser.Builtins := FParser.Builtins + [bcMath];
</source>
+
</syntaxhighlight>
  
 
== Expressions ==
 
== Expressions ==
 
=== An expression with constants ===
 
=== An expression with constants ===
As a first example we have the parser calculate a very simple expression <code>1+1</code>.
+
As a first example we have the parser calculate a very simple expression <syntaxhighlight lang="pascal" enclose="none">1+1</syntaxhighlight>.
  
The first step is to tell the parser which expression is to be calculated. There is a property <code>Expression</code> for this purpose:
+
The first step is to tell the parser which expression is to be calculated. There is a property <syntaxhighlight lang="pascal" enclose="none">Expression</syntaxhighlight> for this purpose:
  
<source>
+
<syntaxhighlight lang="pascal">
 
   FParser.Expression := '1+1';
 
   FParser.Expression := '1+1';
</source>
+
</syntaxhighlight>
  
The next step is to calculate the expression: just call <code>Evaluate</code> or <code>EvaluateExpression</code> - the former is is a [[Function|function]] while the latter one is a [[Procedure|procedure]] which passes the result as a parameter.  
+
The next step is to calculate the expression: just call <syntaxhighlight lang="pascal" enclose="none">Evaluate</syntaxhighlight> or <syntaxhighlight lang="pascal" enclose="none">EvaluateExpression</syntaxhighlight> - the former is is a [[Function|function]] while the latter one is a [[Procedure|procedure]] which passes the result as a parameter.  
  
<source>
+
<syntaxhighlight lang="pascal">
 
var
 
var
 
   parserResult: TFPExpressionResult;
 
   parserResult: TFPExpressionResult;
Line 81: Line 83:
 
  ....
 
  ....
 
   parserResult := FParser.Evaluate;  // or: FParser.EvaluateExpression(parserResult);
 
   parserResult := FParser.Evaluate;  // or: FParser.EvaluateExpression(parserResult);
</source>
+
</syntaxhighlight>
  
What is that mysterious <code>TFPExpressionResult</code>? Since the parser is very flexible and can deal with numbers, strings, date/times or booleans there must be a more complex data type which returns a calculation result:
+
What is that mysterious <syntaxhighlight lang="pascal" enclose="none">TFPExpressionResult</syntaxhighlight>? Since the parser is very flexible and can deal with numbers, strings, date/times or booleans there must be a more complex data type which returns a calculation result:
  
<source>
+
<syntaxhighlight lang="pascal">
 
type
 
type
 
   TResultType = (rtBoolean, rtInteger, rtFloat, tDateTime, rtString);
 
   TResultType = (rtBoolean, rtInteger, rtFloat, tDateTime, rtString);
Line 98: Line 100:
 
       rtString  : ();
 
       rtString  : ();
 
   end;
 
   end;
</source>
+
</syntaxhighlight>
  
The member <code>ResultType</code> signals which one of the data fields is valid. It is important to understand this since the expression parser is very strict on [[Data type|data types]].
+
The member <syntaxhighlight lang="pascal" enclose="none">ResultType</syntaxhighlight> signals which one of the data fields is valid. It is important to understand this since the expression parser is very strict on [[Data type|data types]].
  
In our example, we are adding two [[Integer|integers]], therefore the result is an integer as well. If, on the other had, we had used the expression <code>"1.0 + 1"</code>, 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 <code>ResultType</code> of the <code>TFPExpressionResult</code> before picking the result. To simplify the usage of the expression result data type, <code>fpexprpars</code> exposes a function <code>ArgToFloat</code> which gets the entire expression result [[Record|record]] as a parameter and selects the right component if a floating point result is expected:
+
In our example, we are adding two [[Integer|integers]], therefore the result is an integer as well. If, on the other had, we had used the expression <syntaxhighlight lang="pascal" enclose="none">"1.0 + 1"</syntaxhighlight>, 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 <syntaxhighlight lang="pascal" enclose="none">ResultType</syntaxhighlight> of the <syntaxhighlight lang="pascal" enclose="none">TFPExpressionResult</syntaxhighlight> before picking the result. To simplify the usage of the expression result data type, <syntaxhighlight lang="pascal" enclose="none">fpexprpars</syntaxhighlight> exposes a function <syntaxhighlight lang="pascal" enclose="none">ArgToFloat</syntaxhighlight> which gets the entire expression result [[Record|record]] as a parameter and selects the right component if a floating point result is expected:
  
<source>
+
<syntaxhighlight lang="pascal">
 
var
 
var
 
   parserResult: TFPExpressionResult;
 
   parserResult: TFPExpressionResult;
Line 111: Line 113:
 
   parserResult := FParser.Evaluate;  // or: FParser.EvaluateExpression(parserResult);
 
   parserResult := FParser.Evaluate;  // or: FParser.EvaluateExpression(parserResult);
 
   resultValue := ArgToFloat(parserResult);
 
   resultValue := ArgToFloat(parserResult);
</source>
+
</syntaxhighlight>
  
{{Note|Floating point constants in expressions must have a [[period|point]] as decimal separator, not a [[Comma|comma]] as used 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 <code>Expression</code>. }}
+
{{Note|Floating point constants in expressions must have a [[period|point]] as decimal separator, not a [[Comma|comma]] as used 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 <syntaxhighlight lang="pascal" enclose="none">Expression</syntaxhighlight>. }}
  
 
=== An expression with a variable ===
 
=== An expression with a variable ===
In this example, we calculate the value of <code>sin(x)*cos(2*x)</code> for <code>x = 0.5</code>.
+
In this example, we calculate the value of <syntaxhighlight lang="pascal" enclose="none">sin(x)*cos(2*x)</syntaxhighlight> for <syntaxhighlight lang="pascal" enclose="none">x = 0.5</syntaxhighlight>.
  
 
==== Defining variables ====
 
==== Defining variables ====
At first we have to define the variables. We have only one, <code>x</code>. The parser has a [[Method|method]] <code>AddFloatVariable</code> to declare a floating point variable; there are also methods
+
At first we have to define the variables. We have only one, <syntaxhighlight lang="pascal" enclose="none">x</syntaxhighlight>. The parser has a [[Method|method]] <syntaxhighlight lang="pascal" enclose="none">AddFloatVariable</syntaxhighlight> to declare a floating point variable; there are also methods
 
   
 
   
* <code>AddBooleanVariable</code>
+
* <syntaxhighlight lang="pascal" enclose="none">AddBooleanVariable</syntaxhighlight>
* <code>AddStringVariable</code>
+
* <syntaxhighlight lang="pascal" enclose="none">AddStringVariable</syntaxhighlight>
* <code>AddDateTimeVariable</code>
+
* <syntaxhighlight lang="pascal" enclose="none">AddDateTimeVariable</syntaxhighlight>
  
 
for boolean, string and date/time variables, respectively.
 
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 <code>sin(x)*cos(2*x)</code> we just call:
+
Each one of these methods expects the name of the variable along with its default value. For the sample function <syntaxhighlight lang="pascal" enclose="none">sin(x)*cos(2*x)</syntaxhighlight> we just call:
<source>
+
<syntaxhighlight lang="pascal">
 
   FParser.Identifiers.AddFloatVariable('x', 0.5);
 
   FParser.Identifiers.AddFloatVariable('x', 0.5);
</source>
+
</syntaxhighlight>
  
<code>0.5</code> is entered here as a default value because that is the argument at which we want to calculate the expression (it will be shown [[#Changing_variables|below]] how to modify a variable). From now on, the parser will use this number whenever it finds the variable <code>x</code> in the expression.
+
<syntaxhighlight lang="pascal" enclose="none">0.5</syntaxhighlight> is entered here as a default value because that is the argument at which we want to calculate the expression (it will be shown [[#Changing_variables|below]] how to modify a variable). From now on, the parser will use this number whenever it finds the variable <syntaxhighlight lang="pascal" enclose="none">x</syntaxhighlight> in the expression.
  
Of course, you can add other names, e.g. [[Constant|constants]] like <code>e</code>, etc. (The number [[Pi|<code>pi</code>]] is already built-in).
+
Of course, you can add other names, e.g. [[Constant|constants]] like <syntaxhighlight lang="pascal" enclose="none">e</syntaxhighlight>, etc. (The number [[Pi|<syntaxhighlight lang="pascal" enclose="none">pi</syntaxhighlight>]] is already built-in).
  
 
==== Defining the expression ====
 
==== Defining the expression ====
 
In the next step, the expression string has to be passed to the parser:  
 
In the next step, the expression string has to be passed to the parser:  
  
<source>
+
<syntaxhighlight lang="pascal">
 
   FParser.Expression := 'sin(x)*cos(2*x)';
 
   FParser.Expression := 'sin(x)*cos(2*x)';
</source>
+
</syntaxhighlight>
  
 
It is essential to call this after setting up of the variables because the parser needs to know the variables for analyzing the expression.
 
It is essential to call this after setting up of the variables because the parser needs to know the variables for analyzing the expression.
Line 148: Line 150:
 
This is done as before with the constant expression - here is a complete procedure which shows the equation and its result in a [[Dialog_Examples#ShowMessage|message box]]:
 
This is done as before with the constant expression - here is a complete procedure which shows the equation and its result in a [[Dialog_Examples#ShowMessage|message box]]:
  
<source>
+
<syntaxhighlight lang="pascal">
 
var
 
var
 
   FParser: TFPExpressionParser;
 
   FParser: TFPExpressionParser;
Line 164: Line 166:
 
   end;
 
   end;
 
end;   
 
end;   
</source>
+
</syntaxhighlight>
  
 
==== Changing variables ====
 
==== Changing variables ====
So far, <code>x</code> always has the value 0.5 - it behaves like a constant, we could have used the expression <code>"sin(0.5)*cos(2*0.5)"</code> as well.
+
So far, <syntaxhighlight lang="pascal" enclose="none">x</syntaxhighlight> always has the value 0.5 - it behaves like a constant, we could have used the expression <syntaxhighlight lang="pascal" enclose="none">"sin(0.5)*cos(2*0.5)"</syntaxhighlight> as well.
  
To make it behave more like a "variable", we now calculate the test function for the <code>x</code> values between -10 and 10 at integer steps.
+
To make it behave more like a "variable", we now calculate the test function for the <syntaxhighlight lang="pascal" enclose="none">x</syntaxhighlight> 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 require the internal variable <code>Identifier</code> (type <code>TFPExprIdentifierDef</code>) which exposes various ways to access variables and their properties:
+
The main question is: How to replace the value assigned to a variable? There are several possibilities - all of them require the internal variable <syntaxhighlight lang="pascal" enclose="none">Identifier</syntaxhighlight> (type <syntaxhighlight lang="pascal" enclose="none">TFPExprIdentifierDef</syntaxhighlight>) which exposes various ways to access variables and their properties:
* Use the return value of the <code>AddFloatVariable</code> function.
+
* Use the return value of the <syntaxhighlight lang="pascal" enclose="none">AddFloatVariable</syntaxhighlight> function.
* Seek an identifier by calling <code>FindIdentifierByName</code> with the variable name as a parameter.
+
* Seek an identifier by calling <syntaxhighlight lang="pascal" enclose="none">FindIdentifierByName</syntaxhighlight> with the variable name as a parameter.
* Access the identifier from the <code>Identifiers</code> collection of the parser by using the known index of the variable: We had added <code>x</code> as the only variable, therefore, it must be at index 0.
+
* Access the identifier from the <syntaxhighlight lang="pascal" enclose="none">Identifiers</syntaxhighlight> collection of the parser by using the known index of the variable: We had added <syntaxhighlight lang="pascal" enclose="none">x</syntaxhighlight> as the only variable, therefore, it must be at index 0.
  
Once the <code>Identifier</code> is known, the value of the variable can be changed by accessing the property <code>AsFloat</code> (or <code>AsDateTime</code> etc. accordingly):
+
Once the <syntaxhighlight lang="pascal" enclose="none">Identifier</syntaxhighlight> is known, the value of the variable can be changed by accessing the property <syntaxhighlight lang="pascal" enclose="none">AsFloat</syntaxhighlight> (or <syntaxhighlight lang="pascal" enclose="none">AsDateTime</syntaxhighlight> etc. accordingly):
  
<source>
+
<syntaxhighlight lang="pascal">
 
var
 
var
 
   FParser: TFPExpressionParser;
 
   FParser: TFPExpressionParser;
Line 213: Line 215:
 
   end;
 
   end;
 
end;  
 
end;  
</source>
+
</syntaxhighlight>
  
 
== Adding user-defined functions ==
 
== Adding user-defined functions ==
The default parser only knows the built-in functions mentioned above. One of the strengths of of the expression parser is that it is very easy to extend to include other functions. This can be done by calling the method <code>Identifiers.AddFunction</code>, e.g.
+
The default parser only knows the built-in functions mentioned above. One of the strengths of of the expression parser is that it is very easy to extend to include other functions. This can be done by calling the method <syntaxhighlight lang="pascal" enclose="none">Identifiers.AddFunction</syntaxhighlight>, e.g.
<source>
+
<syntaxhighlight lang="pascal">
 
   FParser.Identifiers.AddFunction('tan', 'F', 'F', @ExprTan);
 
   FParser.Identifiers.AddFunction('tan', 'F', 'F', @ExprTan);
</source>
+
</syntaxhighlight>
  
 
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, "I" = integer, "D" = date/time, "S" = string, or "B" = boolean) and the type of the input values (third parameter, same logic). 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 found in the expression string. Since this function has a particular syntax we have to implement it in our own source code:
 
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, "I" = integer, "D" = date/time, "S" = string, or "B" = boolean) and the type of the input values (third parameter, same logic). 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 found in the expression string. Since this function has a particular syntax we have to implement it in our own source code:
  
<source>
+
<syntaxhighlight lang="pascal">
 
procedure ExprTan(var Result: TFPExpressionResult; Const Args: TExprParameterArray);
 
procedure ExprTan(var Result: TFPExpressionResult; Const Args: TExprParameterArray);
 
var
 
var
Line 231: Line 233:
 
   Result.resFloat := tan(x);
 
   Result.resFloat := tan(x);
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
The result of the calculation is returned as parameter <code>"Result"</code> which is a <code>TFPExpressionResult</code> that 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 have several data types. The term TFPExpression''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 <syntaxhighlight lang="pascal" enclose="none">"Result"</syntaxhighlight> which is a <syntaxhighlight lang="pascal" enclose="none">TFPExpressionResult</syntaxhighlight> that we met above. The arguments for the calculation are passed by <syntaxhighlight lang="pascal" enclose="none">Args</syntaxhighlight> which is just an array of TFPExpressionResult values - again because parameters can have several data types. The term TFPExpression''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.

Revision as of 19:03, 11 July 2019


English (en) suomi (fi)


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 Free Component Library (FCL) 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. See the file "fpexprpars.txt" (in (fpc_source_dir)/packages/fcl-base/examples) for a short documenation.

Creating the parser

You apply the parser by creating an instance like this:

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. On the other hand, it is also possible to create the parser from anywhere in a program without a form or even class being involved; in this case use [[Nil|nil]] as the owner of the parser, but don't forget to .Free 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 BuiltIns:

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

Here is a collection of the built-in symbols which can be used by adding categories 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 as 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. The last section gives instructions how to extend the parser with more functions.

Note-icon.png

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 booleans 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);
Note-icon.png

Note: Floating point constants in expressions must have a point as decimal separator, not a comma as used 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.Identifiers.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 (it will be shown below how to modify a variable). 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)';

It is essential to call this after setting up of the variables because the parser needs to know the variables for analyzing the expression.

Calculating the expression

This is done as before with the constant expression - here is 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;  // or: resultValue := ArgToFloat(FParser.Evaluate);
    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 now calculate 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 require the internal variable Identifier (type TFPExprIdentifierDef) which exposes various ways to access variables and their properties:

  • Use the return value of the AddFloatVariable function.
  • Seek an identifier by calling FindIdentifierByName with the variable name as a parameter.
  • Access the identifier from the 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 can be changed by accessing the property AsFloat (or AsDateTime etc. accordingly):

var
  FParser: TFPExpressionParser;
  argument: TFPExprIdentifierDef;
  s: string;
  x: integer;
  f: double;
begin
  s:='';
  FParser := TFPExpressionParser.Create(nil);
  try
    // Enable the use of mathematical expressions
    FParser.BuiltIns := [bcMath];

    // Add the function argument
    argument := FParser.Identifiers.AddFloatVariable('x', 0.0);

    // Define the function, using the argument name x as defined above
    FParser.Expression := 'sin(x)*cos(2*x)';

    // Calculate some function values
    for x := -10 to 10 do
    begin
      argument.AsFloat := x;                             // Set the function argument value
      f := FParser.Evaluate.ResFloat;                    // Calculate the function value
      s := s + format('x[%d]:[%g]' + LineEnding, [x,f]); // Demo output to display the result
    end;

    // Show the result
    ShowMessage(s);

  finally
    FParser.Free;
  end;
end;

Adding user-defined functions

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

  FParser.Identifiers.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, "I" = integer, "D" = date/time, "S" = string, or "B" = boolean) and the type of the input values (third parameter, same logic). 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 found in the expression string. Since this function has a particular syntax we have to implement it in our own source code:

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 that we met above. The arguments for the calculation are passed by Args which is just an array of TFPExpressionResult values - again because parameters can have several data types. The term TFPExpressionResults is maybe a bit misleading here, because this array holds all the *input* parameters as specified by the input types of the AddFunction method.