Blog

All Blog Posts  |  Next Post  |  Previous Post



Filtering in Delphi: Generating, Parsing and Matching Filters

Today

TMS Software Delphi Components


In the previous blog post, we looked at why filtering can become difficult when everything is handled as a string.

A filter like this is simple enough:

Age > 18

But once you start combining multiple conditions, adding brackets, switching between AND and OR, validating field names, and converting values to the right type, it becomes harder to keep everything under control.

That is where the TTMSFNCFilterBuilder comes in.

From Filter Text to Filter Structure

The idea behind the FilterBuilder is simple:
Instead of building one long filter string manually, we build the filter as a structure.

The starting point for this structure is the Filter property.

FilterBuilder.Filter

This filter contains the expressions and groups that define the filtering logic.

Every filter expression consists of three parts:

  • The field or column name
  • The comparison operator
  • The value to compare against

For example:

Age > 18

Can be represented as:

  • Field: Age
  • Operator: feoLargerThan
  • Value: 18

By storing these parts separately, the filter becomes easier to build, inspect, validate, and eventually convert into filter text. It also avoids a lot of fragile string concatenation. Instead of manually writing operators such as > or =, you work with predefined operators such as feoLargerThan and feoEqual.

Building Expressions

The smallest building block of a filter is an expression.

An expression describes a single condition.

FilterBuilder.Filter.AddExpression('Age', feoLargerThan, 18);

Notice that we are not building a string here. We are simply describing the filter logic.

At this point, the FilterBuilder doesn't care whether the final output will be used for a TDataSet, OData, a universal filter expression, or a completely custom format.

Combining Expressions with Groups

Most filters contain multiple conditions.

For example:

Age > 18 AND Country = 'BE'

The FilterBuilder combines expressions through groups.

The root filter itself is already a group and has its own group operator.

FilterBuilder.Filter.GroupOperator := fgoAND;

Every expression that is added to this filter is automatically combined using AND.

If needed, you can simply change the operator to OR.

FilterBuilder.Filter.GroupOperator := fgoOR;


Nested Groups

The real strength of this approach becomes apparent when more complex filters are required.

Consider the following example:

(Age > 18 AND Country = 'BE') OR (Age > 21 AND Country = 'US')

Writing this manually isn't too difficult, but maintaining it quickly becomes harder as more conditions are added.

With the FilterBuilder, you simply create two groups.

var
  BelgiumGroup,
  USGroup: TTMSFNCFilterBuilderGroup;
begin
  FilterBuilder.Filter.GroupOperator := fgoOR;

  BelgiumGroup := FilterBuilder.Filter.AddGroup;
  BelgiumGroup.GroupOperator := fgoAND;
  BelgiumGroup.AddExpression('Age', feoLargerThan, 18);
  BelgiumGroup.AddExpression('Country', feoEqual, 'BE');

  USGroup := FilterBuilder.Filter.AddGroup;
  USGroup.GroupOperator := fgoAND;
  USGroup.AddExpression('Age', feoLargerThan, 21);
  USGroup.AddExpression('Country', feoEqual, 'US');
end;

Every group can contain:

  • Expressions
  • Other groups
  • Its own AND or OR operator

Because groups can contain other groups, filters can be nested indefinitely without manually keeping track of brackets.

Generating Filter Text

Once the filter structure has been created, generating the filter text becomes trivial.

ShowMessage(FilterBuilder.FilterText);

For the nested example above, the generated filter text would be similar to:

(Age > 18 AND Country = 'BE') OR (Age > 21 AND Country = 'US')

The FilterBuilder generates the complete expression, including operators and brackets.

When the selected format matches the syntax expected by a TDataSet, the generated text can immediately be assigned to the dataset.

FilterBuilder.FormatType := fftDelphiDataSet;

DataSet.Filter := FilterBuilder.FilterText;
DataSet.Filtered := DataSet.Filter <> '';

In other words, the FilterBuilder doesn't replace Delphi's filtering mechanism—it simply provides a much safer and easier way to generate the filter text.

Parsing Existing Filter Text

The process also works in reverse.

If you already have filter text that follows one of the supported formats, the FilterBuilder can parse it back into its internal structure.

if FilterBuilder.ParseExpression('Age > 18 AND Country = ''BE''') then
begin
  // The filter text was parsed successfully
end;

This allows existing filters to be visualized, modified, validated, or reused without having to recreate them manually.

Of course, parsing depends on the selected formatting rules. The expression text needs to follow the configured syntax and format type in order to be parsed successfully. When the expression is not valid, ParseExpression returns False. And you have an event available to have a calculated quess where the error might be.

Supporting Different Filter Formats

One of the biggest advantages of separating the filter structure from the generated text is that the output format can easily be changed.

The same filter logic can be used for multiple targets.

For example, a Delphi DataSet filter doesn't use the same syntax as an OData filter.

Changing the output is simply a matter of selecting another format.

FilterBuilder.FormatType := fftOData;
FilterText ==> (Age gt 18 and Country eq 'BE') or (Age gt 21 and Country eq 'US')

The available format types are:

  • fftDelphiDataSet for Delphi DataSet filter syntax
  • fftUniversalFilterExpressions used in the TMS FNC DataGrid
  • fftOData for OData query syntax
  • fftCustom for a fully customized parse format

This means you only build the filter once, while the FilterBuilder generates the appropriate output for the destination.

Matching Data Without Filter Text

Generating filter text is only one way of using the FilterBuilder.

Sometimes you don't want to apply a filter string at all.

For example, when working with custom controls, collections of objects, or any data that isn't connected to a TDataSet, you can validate the data directly against the filter structure.

Before validating rows, the builder needs to know which data columns are available and in which order values will be supplied.

FilterBuilder.AddDataColumn('Age', fdtNumber);
FilterBuilder.AddDataColumn('Country', fdtText);

This can then be used with ValidateFilterRow.

var
  Values: TTMSFNCFilterValidateInputRow;
  Match: Boolean;
begin
  SetLength(Values, 2);

  Values[0] := 25;
  Values[1] := 'BE';

  Match := FilterBuilder.ValidateFilterRow(Values);
end;

The values are matched against the configured data columns. In this example, the first value is compared with Age, while the second value is compared with Country.

If the current filter is:

Age > 18 AND Country = 'BE'

The row above will match because 25 is larger than 18 and the country value is BE.

This allows you to use exactly the same filtering logic regardless of where your data is stored.

You decide what should happen when the data matches.

  • Show or hide rows
  • Highlight matching records
  • Filter object collections
  • Implement your own filtering logic

If you're working with multiple rows, you can validate the complete dataset in a single call.

ResultArray := FilterBuilder.ValidateFilterArray(InputArray, True);

This returns an array of Boolean values indicating which rows satisfy the filter. The second parameter controls how the input array is organized. When it is set to True, the input array is treated as row-based data.

Several FNC controls use this functionality internally, but it is equally useful in your own applications where no built-in filtering mechanism exists.

Why This Matters

By separating the filter logic from the generated filter text, the FilterBuilder offers several advantages:

  • You no longer build complex filter strings manually.
  • The generated filter text can be assigned directly to a TDataSet.
  • Existing filter text can be parsed back into a structured model.
  • Nested groups become much easier to create and maintain.
  • The same filter can generate different output formats.
  • You can validate data directly without relying on a specific component.

The FilterBuilder forms the foundation for all filtering components in the FNC framework.

Whether you want to generate filter text, parse existing expressions, validate data, or build a visual filtering interface, everything starts with this structured filter model.

Next Step

Now that we've built a structured filter model, it's time to make it easier to use.

In the next blog post, we'll take a look at the TTMSFNCFilterDialog, which uses the FilterBuilder internally to let users create complex filters through a visual interface—without ever having to write a filter expression themselves.



Gjalt Vanhouwaert


  1. Filtering in Delphi: From Strings to Structured Logic

  2. Filtering in Delphi: Generating, Parsing and Matching Filters



This blog post has not received any comments yet. Add a comment.



All Blog Posts  |  Next Post  |  Previous Post