Table of Contents

Filter Builder

TTMSFNCFilterBuilder builds and evaluates complex multi-rule filter expressions. It is the shared filtering engine used by TTMSFNCDataGrid, TTMSFNCDataSetFilterDialog, and other FNC components.

Key class: TTMSFNCFilterBuilder

Tip

Before using TTMSFNCFilterBuilder directly, prefer the built-in filtering UI of controls that integrate it (e.g. TTMSFNCDataGrid's column filter). Direct use is for programmatic filter evaluation or sharing a single filter across multiple components.

Quick Start

Create a local instance or use the global TMSFNCFilterBuilder:

uses
  TMS.TMSFNCFilterBuilder;

var
  fb: TTMSFNCFilterBuilder;
begin
  fb := TTMSFNCFilterBuilder.Create;
  try
    fb.ParseExpression('[Name] LIKE ' + QuotedStr('C%'));
  finally
    fb.Free;
  end;

  // Or use the global instance:
  TMSFNCFilterBuilder.ParseExpression('[Name] LIKE ' + QuotedStr('C%'));
end;

Parsing Filter Text

ParseExpression converts a filter string into structured Groups and Expressions. It returns True on success, or fires parse-error events for malformed input.

Default format (fftDelphiDataSet):

[Name] LIKE 'K*' AND [Status] LIKE 'Abroad') OR ([Name] LIKE 'S%' AND [Progress] > 50) OR [Available] = True

Universal expression format (fftUniversalFilterExpressions):

[Name] = "K*" & [Status] = "Abroad") | ([Name] = "S*" & [Progress] > 50) | [Available] = True

Building Filter Expressions Programmatically

uses
  TMS.TMSFNCFilterBuilder, TMS.TMSFNCValue;

var
  g1, g2: TTMSFNCFilterBuilderGroup;
begin
  TMSFNCFilterBuilder.ClearFilter;
  TMSFNCFilterBuilder.Filter.GroupOperator := fgoOR;

  g1 := TMSFNCFilterBuilder.Filter.AddANDGroup;
  g1.AddExpression('Name', feoStartsWith, 'K');
  g1.AddExpression('Status', feoEqual, 'Abroad');

  g2 := TMSFNCFilterBuilder.Filter.AddANDGroup;
  g2.AddExpression('Name', feoStartsWith, 'S');
  g2.AddExpression('Progress', feoLargerThan, 50);

  TMSFNCFilterBuilder.Filter.AddExpression('Available', feoEqual, True);

  ShowMessage(TMSFNCFilterBuilder.FilterText);
  // ([Name] LIKE 'K%' AND [Status] = Abroad) OR ([Name] LIKE 'S%' AND [Progress] > 50) OR [Available] = True
end;

Validating Data Against a Filter

Validation compares a row of values against the parsed filter. Declare the data type of every column before the first call: without it the builder infers a type from whatever value it receives, so the same row can compare differently depending on how it was supplied. The values in a row are matched to the columns by position, in DataColumns order.

uses
  TMS.TMSFNCFilterBuilder;

{ Inside your form's OnCreate: }
begin
  TMSFNCFilterBuilder.ClearFilter;
  TMSFNCFilterBuilder.DataColumns.Clear;

  // Declare the column types up front. Without them the builder infers a type
  // from the incoming value, which makes numeric and boolean comparisons
  // depend on how the caller happens to pass the value.
  TMSFNCFilterBuilder.AddDataColumn('Name', fdtText);
  TMSFNCFilterBuilder.AddDataColumn('Status', fdtText);
  TMSFNCFilterBuilder.AddDataColumn('Progress', fdtNumber);
  TMSFNCFilterBuilder.AddDataColumn('Available', fdtBoolean);

  // ParseExpression returns False (and fires the OnParseError* events) when the
  // text is malformed, so never validate against an unchecked filter.
  if not TMSFNCFilterBuilder.ParseExpression(
    '[Status] LIKE ' + QuotedStr('Abroad') + ' AND [Progress] > 50') then
    raise Exception.Create('Invalid filter expression');

  // The value order of every row passed to ValidateFilterRow or
  // ValidateFilterArray must match the DataColumns order declared above.
end;

Single Row

TMSFNCFilterBuilder.DataColumn['Name'].DataType := fdtText;
TMSFNCFilterBuilder.DataColumn['Status'].DataType := fdtText;
TMSFNCFilterBuilder.DataColumn['Progress'].DataType := fdtNumber;
TMSFNCFilterBuilder.DataColumn['Available'].DataType := fdtBoolean;

InputRow := ['Sarah', 'Abroad', 60, False];
if TMSFNCFilterBuilder.ValidateFilterRow(InputRow) then
  ShowMessage('Matches Filter');

Multiple Rows

// Data organized as columns (default, AByRow = False):
InputArray := [
  ['Sarah', 'Sarah', 'Kurt', 'Alex', 'Alex'],
  ['Abroad', 'Abroad', 'Abroad', 'Abroad', 'Abroad'],
  [60, 40, 40, 40, 40],
  [False, False, False, False, True]
];
OutputArray := TMSFNCFilterBuilder.ValidateFilterArray(InputArray, False);

// Data organized as rows (AByRow = True):
InputArray := [
  ['Sarah', 'Abroad', 60, False],
  ['Kurt', 'Abroad', 40, False],
  ['Alex', 'Abroad', 40, True]
];
OutputArray := TMSFNCFilterBuilder.ValidateFilterArray(InputArray, True);
// [True, False, True, False, True]

Properties

Property Description
DataColumn['Name'] Returns (or creates) a column by name
DataColumnDisplayName['Name'] User-friendly display name for a column
DataColumnType['Name'] Data type of the column
ParseFormat Format settings for parsing filter text
FilterText Raw filter expression text
DisplayFilterText FilterText with column display names substituted
Filter Root TTMSFNCFilterBuilderGroup of the filter structure
DataColumns Collection of all defined columns
FormatType Output format type (default: fftDelphiDataSet)

Methods

Method Description
ParseExpression(AFilterText) Parse text into groups/expressions
ClearFilter Reset all groups and expressions
ValidateFilterRow(AInputData) Validate one row
ValidateFilterArray(AInputArray, AByRow) Validate multiple rows
AddDataColumn(AName, ADataType, ADisplayName) Add a column definition
AddDataColumnsFromFilter Create the missing column definitions for every column referenced by the current filter
GetColumnByName(AColumnName) / GetColumnByDisplayName(AColumnDisplayName) Look up an existing column definition
isExpressionValid(AExpression) Check one expression string without parsing it into the filter
DeleteGroup(AGroup) / DeleteExpression(AExpression) Remove a group or expression
BeginUpdate / EndUpdate Suspend and resume change notifications while building a filter

Events

Event Description
OnParseError General parse error
OnParseErrorParenthesis Mismatched parentheses
OnParseErrorOperatorMismatch Operator mismatch within a group
OnParseErrorOperatorPosition Operator in wrong position
OnParseErrorInvalidExpression Invalid expression syntax
OnFilterTextParsed Full filter text successfully parsed
OnExpressionTextParsed Single expression successfully parsed
OnValidateFilter / OnValidateGroup / OnValidateExpression Custom validation hooks
OnGetFilterText / OnGetGroupText / OnGetExpressionText Customize text generation
OnExpressionAdded A new expression was added

Wire the parse-error events whenever filter text can come from a user: a failed parse leaves the previous filter in place, so without a handler a typo silently keeps filtering on the old expression. OnValidateExpression and its group and filter counterparts are the hook for comparisons the built-in operators cannot express - set AExitValidation there to keep the default evaluation from overwriting your result.

uses
  TMS.TMSFNCFilterBuilder, TMS.TMSFNCValue;

procedure TForm1.FormCreate(Sender: TObject);
begin
  TMSFNCFilterBuilder.OnParseError := HandleParseError;
  TMSFNCFilterBuilder.OnFilterTextParsed := HandleFilterTextParsed;
  TMSFNCFilterBuilder.OnExpressionAdded := HandleExpressionAdded;
  TMSFNCFilterBuilder.OnValidateExpression := HandleValidateExpression;
end;

procedure TForm1.HandleParseError(Sender: TObject; AFilterText: string);
begin
  // Fires for text the parser cannot turn into groups and expressions. The more
  // specific OnParseErrorParenthesis, OnParseErrorOperatorPosition,
  // OnParseErrorOperatorMismatch and OnParseErrorInvalidExpression events also
  // carry the character position of the problem.
  ShowMessage('Could not parse: ' + AFilterText);
end;

procedure TForm1.HandleFilterTextParsed(Sender: TObject;
  AFilter: TTMSFNCFilterBuilderGroup; AText: string);
begin
  // AFilter is the root group AText was parsed into.
  Memo1.Lines.Add(Format('%s -> %d expression(s), %d group(s)',
    [AText, AFilter.Expressions.Count, AFilter.Groups.Count]));
end;

procedure TForm1.HandleExpressionAdded(Sender: TObject;
  AExpression: TTMSFNCFilterBuilderExpression; AText: string);
begin
  // Fires for every expression added, whether parsed from text or added in
  // code. Use it to apply a house rule to new expressions.
  if AExpression.GetValueDataType = fdtText then
    AExpression.CaseSensitive := True;
end;

procedure TForm1.HandleValidateExpression(Sender: TObject;
  AExpression: TTMSFNCFilterBuilderExpression; AInputValue: TTMSFNCValue;
  var AValidationResult: Boolean; var AExitValidation: Boolean);
begin
  // Override the built-in comparison for one column: an empty Status counts as
  // a match. Setting AExitValidation stops the default evaluation from
  // overwriting AValidationResult afterwards.
  if (AExpression.GetColumnName = 'Status') and ValueIsEmpty(AInputValue) then
  begin
    AValidationResult := True;
    AExitValidation := True;
  end;
end;

TTMSFNCFilterBuilderColumn

Property Description
Name Internal column name used in filter expressions
DisplayName User-visible name shown in the UI
DataType fdtAutomatic, fdtText, fdtBoolean, fdtDateTime, fdtDate, fdtTime, fdtFloat, fdtNumber, fdtOther

A column definition is what ties a name inside an expression to a value type and a caption. AddDataColumn creates one; the indexed DataColumn, DataColumnType and DataColumnDisplayName properties reach an existing one by name (and create it on demand).

uses
  TMS.TMSFNCFilterBuilder;

var
  LColumn: TTMSFNCFilterBuilderColumn;
begin
  TMSFNCFilterBuilder.ClearFilter;
  TMSFNCFilterBuilder.DataColumns.Clear;

  // Name is what expressions refer to, DataType decides how values compare,
  // DisplayName is what an end user reads.
  LColumn := TMSFNCFilterBuilder.AddDataColumn('Progress', fdtNumber, 'Completion');

  // The same column stays reachable by name through the indexed properties.
  TMSFNCFilterBuilder.DataColumnDisplayName['Progress'] := 'Completion (%)';
  TMSFNCFilterBuilder.DataColumnType['Progress'] := fdtFloat;

  TMSFNCFilterBuilder.Filter.AddExpression(LColumn, feoLargerThan, 50);

  // FilterText writes Name, DisplayFilterText writes DisplayName.
  ShowMessage(TMSFNCFilterBuilder.FilterText);        // [Progress] > 50
  ShowMessage(TMSFNCFilterBuilder.DisplayFilterText); // [Completion (%)] > 50
end;

TTMSFNCFilterBuilderGroup

Groups combine expressions using AND or OR. The Filter property of TTMSFNCFilterBuilder is the root group.

Property Description
GroupOperator fgoAND or fgoOR
IsInverted Negate the group's combined result (NOT (...))
Groups Nested subgroups
Expressions Filter expressions in this group
FilterText Textual representation of this group

Nesting is how precedence is expressed: a sub-group is emitted as a bracketed term, so mixing AND and OR in one filter means adding a sub-group rather than relying on operator precedence.

uses
  TMS.TMSFNCFilterBuilder;

var
  LGroup: TTMSFNCFilterBuilderGroup;
begin
  TMSFNCFilterBuilder.ClearFilter;

  // The root group combines its own expressions with its sub-groups.
  TMSFNCFilterBuilder.Filter.GroupOperator := fgoAND;
  TMSFNCFilterBuilder.Filter.AddExpression('Status', feoEqual, 'Abroad');

  // A sub-group brackets an alternative: (Progress > 50 OR Available = True)
  LGroup := TMSFNCFilterBuilder.Filter.AddORGroup;
  LGroup.AddExpression('Progress', feoLargerThan, 50);
  LGroup.AddExpression('Available', feoEqual, True);

  // IsInverted puts a NOT in front of the whole sub-group instead of in front
  // of each expression, so the group's combined result is negated.
  LGroup.IsInverted := True;

  ShowMessage(TMSFNCFilterBuilder.Filter.FilterText);
  ShowMessage(Format('%d expression(s), %d sub-group(s)',
    [TMSFNCFilterBuilder.Filter.Expressions.Count,
     TMSFNCFilterBuilder.Filter.Groups.Count]));
end;

TTMSFNCFilterBuilderExpression

Property Description
Column The column being tested
ExpressionOperator feoStartsWith, feoEndsWith, feoContains, feoEqual, feoNotEqual, feoEmpty, feoNotEmpty, feoLargerThan, feoSmallerThan, feoLargerThanOrEqual, feoSmallerThanOrEqual
Value The comparison value
Expression Read-only text of this single expression
IsInverted Invert the condition (e.g. "not contains")
CaseSensitive Case-sensitive text comparison (default: False)

An expression stays editable after it is added, which is what a filter editor UI binds to. ValidateExpression evaluates a single value against one expression without walking the rest of the filter, so it is the cheap way to preview the effect of a change.

uses
  TMS.TMSFNCFilterBuilder;

var
  LExpression: TTMSFNCFilterBuilderExpression;
begin
  TMSFNCFilterBuilder.ClearFilter;
  TMSFNCFilterBuilder.DataColumns.Clear;
  TMSFNCFilterBuilder.AddDataColumn('Name', fdtText, 'Employee');

  LExpression := TMSFNCFilterBuilder.Filter.AddExpression('Name', feoContains, 'ar');

  // A text comparison is case insensitive by default.
  LExpression.CaseSensitive := True;

  // Every part of an expression can be retargeted after it was added.
  LExpression.Column := TMSFNCFilterBuilder.GetColumnByName('Name');
  LExpression.ExpressionOperator := feoStartsWith;
  LExpression.Value := 'Sar';

  // IsInverted negates this one expression ("does not start with").
  LExpression.IsInverted := False;

  ShowMessage(LExpression.Expression);           // [Name] LIKE 'Sar%'
  ShowMessage(LExpression.GetColumnDisplayName); // Employee

  // ValidateExpression evaluates a single value against this expression only,
  // without walking the rest of the filter.
  if LExpression.ValidateExpression('Sarah') then
    ShowMessage('Sarah matches');
end;

Parse Format Reference

Property Delphi DataSet Universal Expression
StringDelimiter ' "
AndOperator AND &
OrOperator OR \|
EqualStrOperator LIKE '#VALUE' = "#VALUE"
ContainsOperator LIKE '%#VALUE%' = "*#VALUE*"
StartsWithOperator LIKE '#VALUE%' = "#VALUE*"
MultiCharWildCard % *

Set FormatType to fftDelphiDataSet (default), fftUniversalFilterExpressions, fftOData, or fftCustom, or configure ParseFormat properties individually.

See Also