0077

Dynamic Filters from Configuration Rows

Filtering
Medium
recordref
fieldref
setfilter
configuration

Dynamic Filters from Configuration Rows

Your customer's cleanup job must be configurable: an administrator decides which records the job touches by entering condition rows — a table, a field, an operator and a value — without anyone redeploying code. Those rows are plain data; your engine has to turn them into real filters at runtime, for any table, without knowing at compile time which table or which field a row points at.

Requirements

The starter ships two finished objects — keep them in your submission exactly as they are, because the tests use them to feed your engine:

  • Enum "Dynamic Filter Operator" with the values Equals, "At Least" and "At Most".
  • Table "Dynamic Filter Line" with the fields "Entry No." (Integer, primary key), "Table ID" (Integer), "Field No." (Integer), Operator (the enum above) and "Filter Value" (Text[250]).

Complete the codeunit named "Dynamic Filter Engine" with this public procedure:

procedure CountWithConfiguredFilters(TableId: Integer; var AppliedFilters: Text): Integer

Rules:

  1. The procedure opens the table whose ID is TableId and applies every "Dynamic Filter Line" row whose "Table ID" equals TableId, in "Entry No." order. Rows configured for other tables are ignored entirely.
  2. All applied rows combine: a record is counted only if it satisfies every condition. The return value is the number of records left after all conditions are applied; with no matching configuration rows it is the table's unfiltered record count.
  3. Equals keeps records whose field is exactly equal to "Filter Value"; "At Least" keeps records whose field is greater than or equal to it; "At Most" keeps records whose field is less than or equal to it.
  4. The configured fields are only ever of type Text, Code, Integer, Decimal or Date, and "At Least" / "At Most" are only ever configured on Integer, Decimal or Date fields. For those three types, "Filter Value" holds the value the way Format renders it and must be turned back into the field's own type before it can filter anything.
  5. Every character of "Filter Value" is data, never filter syntax: a value containing |, *, ?, .., &, ( or ) must match records literally — a | must not become either/or, a * must not become a wildcard, .. must not become a range.
  6. If a row's "Field No." does not exist in the target table, raise an error with exactly this message: Field %1 does not exist in table %2. — %1 is the configured field number, %2 is the table's name (for the customer table: Customer).
  7. If a row's "Filter Value" cannot be turned into the type of an Integer, Decimal or Date field, raise an error with exactly this message: Value '%1' is not valid for field %2. — %1 is the raw "Filter Value" text, %2 is the field's name (for example Credit Limit (LCY)).
  8. AppliedFilters returns the filter description the record itself produces once all conditions are applied — the text GetFilters yields; it must name the filtered fields and carry the filter values.

Pick object IDs in the 50100–50199 range (the starter already does) and reference other objects by name, never by ID.

What the tests check

The tests insert "Dynamic Filter Line" rows and run your engine against two structurally different tables — Customer and Item Ledger Entry — so an implementation hardwired to one table cannot pass. They seed records with hostile names like Import|Export, Star* Retail, 10..20 Storage and Smith & Jones next to near-miss decoys, and assert exact counts for Equals; they check the "At Least" / "At Most" boundaries as inclusive on Decimal and Date fields (the record exactly at the boundary counts); they combine several rows and expect all of them to apply; they configure a row for a different table and expect it to be ignored; with no rows they expect the unfiltered count. One test asserts AppliedFilters names the filtered fields and values. Two tests trigger the named errors from rules 6 and 7 and match the messages — note the wording, the quotes and the final period.

Learn More

Hint 1
You never know the table at compile time, so a Record variable of a fixed table cannot work. RecordRef.Open takes any table number, RecordRef.Field hands you a FieldRef for a field number, and FieldRef.Type tells you what kind of value that field holds.
Hint 2
Text pasted into a filter string is parsed as a little query language — that is exactly how a | becomes either/or. FieldRef has one filtering method that takes the value as plain data, and SetFilter accepts replacement fields (%1) whose values stay out of the expression. Either way, the value you hand over must already have the FieldRef's own type — Evaluate converts the configured text, and its Boolean return tells you when the text does not fit. A Variant can carry whichever type came out.
Hint 3
Equals is FieldRef.SetRange(TypedValue); At Least and At Most are FieldRef.SetFilter('>=%1', TypedValue) and FieldRef.SetFilter('<=%1', TypedValue). Check RecordRef.FieldExist before RecordRef.Field for the unknown-field error, guard each Evaluate for the invalid-value error, and finish with RecordRef.GetFilters and RecordRef.Count.
ALBusiness Central 28.5
Press Compile to check your code compiles — Submit runs the tests.
The code editor is desktop-only
Open this problem on a computer to write and run code. Reading the description, tests and discussion works fine here.