Examples¶
Six worked examples: five drawn from the benchmark test suite — tasks 052, 053, 046,
116, and 051 — and one from the conformance corpus (join_product, the record product).
For each task the ATP pattern and its RTL equivalent are shown side by side.
Example 1 — Task 052: cross-table unpivot with compound cells and an injected constant¶
A two-dimensional cross-tabulation is unpivoted into a flat recordset. Each body cell is a
compound "ND MON" value, and a constant YEAR=2025 pair is injected into every record.
Input table (task 052, variant 1):
- Row 0: header with airline codes (
CA,HU). - Rows 1–2: airport code in column 0, then compound
"ND MON"body cells.
Schema: ⟨ND, AIRLINE, AIRPORT, MON, YEAR⟩ — four records:
ND | AIRLINE | AIRPORT | MON | YEAR
0 | CA | IKT | Jan | 2025
8 | HU | IKT | Feb | 2025
31 | CA | SVO | Jan | 2025
40 | HU | SVO | Feb | 2025
Item roles¶
Which item each cell yields (YEAR is injected as a constant, not read from a cell):
| col 0 | col 1 | col 2 | |
|---|---|---|---|
| row 0 | — (skip) | VAL CA → AIRLINE |
VAL HU → AIRLINE |
| row 1 | VAL IKT → AIRPORT |
VAL 0→ND, VAL Jan→MON |
VAL 8→ND, VAL Feb→MON |
| row 2 | VAL SVO → AIRPORT |
VAL 31→ND, VAL Jan→MON |
VAL 40→ND, VAL Feb→MON |
ATP pattern¶
import ru.icc.regtab.atp.spec.*;
ItemFilterConditionSpec SAME_COL = ItemFilterConditionSpec.sameCol();
ItemFilterConditionSpec SAME_ROW = ItemFilterConditionSpec.sameRow();
ItemFilterConditionSpec SAME_CELL = ItemFilterConditionSpec.sameCell();
// Compound body cell: "0 Jan" → ND ("0") + MON ("Jan")
var dataCell = CompoundContentSpec.of(
AtomicContentSpec.val(
ActionSpec.rec(
ProviderSpec.val(1, SAME_COL), // AIRLINE (column header)
ProviderSpec.val(1, SAME_ROW), // AIRPORT (leftmost cell)
ProviderSpec.val(1, SAME_CELL), // MON (same compound cell)
ProviderSpec.ctxAvp("YEAR", "2025") // constant YEAR=2025
),
ActionSpec.avp("ND")
),
CompoundContentSpec.Segment.of(" ",
AtomicContentSpec.val(ActionSpec.avp("MON"))
)
);
TablePattern pattern = TablePattern.of(
// Header subtable: skip the empty corner + one-or-more airline cells
SubtablePattern.of(Quantifier.one(),
RowPattern.of(
CellPattern.skip(),
CellPattern.of(Quantifier.oneOrMore(),
AtomicContentSpec.val(ActionSpec.avp("AIRLINE")))
)
),
// Data subtable: one-or-more rows of airport cell + one-or-more body cells
SubtablePattern.of(Quantifier.one(),
RowPattern.of(Quantifier.oneOrMore(),
CellPattern.of(AtomicContentSpec.val(ActionSpec.avp("AIRPORT"))),
CellPattern.of(Quantifier.oneOrMore(), dataCell)
)
)
);
RTL equivalent¶
[ [] [VAL : 'AIRLINE'->AVP]+ ]
[ [VAL : 'AIRPORT'->AVP]
[VAL : (COL, ROW, CL, @'YEAR'='2025')->REC, 'ND'->AVP " " VAL : 'MON'->AVP]+ ]+
How it works¶
- Header subtable
[ [] [VAL : 'AIRLINE'->AVP]+ ]: skip the empty corner cell[], then derive oneAIRLINEattribute-value per airline column (CA,HU). - Data subtable
[ … ]+: one-or-more rows. The first cell[VAL : 'AIRPORT'->AVP]yields theAIRPORTvalue (IKT,SVO). - Each body cell is compound:
[VAL : … " " VAL : 'MON'->AVP]splits"0 Jan"at the space intoND("0") andMON("Jan"). - The
NDitem'sRECaction uses four providers: COL(sameCol) — theAIRLINEvalue in the column header.ROW(sameRow) — theAIRPORTvalue in the leftmost cell of the row.CL(sameCell) — theMONvalue inside the same compound cell.@'YEAR'='2025'— a context provider that injects the constant pairYEAR=2025into every record.
Derivation¶
- AVP (headers):
AIRLINE=CA,AIRLINE=HU;AIRPORT=IKT,AIRPORT=SVO. - AVP (compound split): each body cell yields
ND=…andMON=…— e.g.ND=0,MON=Jan. - Context AVP:
YEAR=2025(the injected constant). - REC anchored on each
ND, pullingAIRLINE(COL),AIRPORT(ROW),MON(CL),YEAR(ctx): ⟨0, CA, IKT, Jan, 2025⟩⟨8, HU, IKT, Feb, 2025⟩⟨31, CA, SVO, Jan, 2025⟩⟨40, HU, SVO, Feb, 2025⟩
Running the test¶
Example 2 — Task 053: compound attribute names and paired-row CONCAT¶
Two physical rows describe one logical record. Attribute names are composed from a group header
(REF, SPECS) and a per-row qualifier (TP, HV, …), and the paired rows are concatenated
into one record by CONCAT — the counterpart of pandas.concat(axis=1) aligned on the ID key.
Input table (task 053, variant 1):
| REF | REF | SPECS | SPECS
T-1 | TP | D16 | HV | 750
T-1 | SN | 001 | LV | 110
T-2 | TP | D24 | HV | 110
T-2 | SN | 002 | LV | 10
- Row 0: group-name header cells (
REF,SPECS) — auxiliary, used only as name prefixes. - Data rows come in pairs sharing the same
ID(T-1,T-1).
Schema: ⟨ID, REF_TP, SPECS_HV, REF_SN, SPECS_LV⟩ — one record per ID:
Item roles¶
| col 0 | col 1 | col 2 | col 3 | col 4 | |
|---|---|---|---|---|---|
| row 0 | — | AUX REF |
— | AUX SPECS |
— |
| row 1 | VAL T-1 → ID |
ATTR TP |
VAL D16 |
ATTR HV |
VAL 750 |
| row 2 | VAL T-1 → ID |
ATTR SN |
VAL 001 |
ATTR LV |
VAL 110 |
| row 3 | VAL T-2 → ID |
ATTR TP |
VAL D24 |
ATTR HV |
VAL 110 |
| row 4 | VAL T-2 → ID |
ATTR SN |
VAL 002 |
ATTR LV |
VAL 10 |
The AUX group headers feed the PREFIX action; each ATTR qualifier gets the header above it
prepended to form a compound attribute name.
ATP pattern¶
import ru.icc.regtab.atp.spec.*;
ItemFilterConditionSpec SAME_ROW = ItemFilterConditionSpec.sameRow();
ItemFilterConditionSpec BELOW_STR = ItemFilterConditionSpec.and(FilterTerm.Below.INSTANCE,
FilterTerm.SameStr.INSTANCE);
ItemFilterConditionSpec ABOVE = ItemFilterConditionSpec.above();
ItemFilterConditionSpec SAME_SUBROW = ItemFilterConditionSpec.sameSubrow();
TablePattern pattern = TablePattern.of(
SubtablePattern.of(Quantifier.one(),
// Header row: skip the corner, mark group names as AUX
RowPattern.of(
CellPattern.skip(),
CellPattern.of(Quantifier.oneOrMore(), AtomicContentSpec.aux())
),
// Data rows (one-or-more)
RowPattern.of(Quantifier.oneOrMore(),
// Anchor subrow: the ID cell
SubrowPattern.of(
CellPattern.of(AtomicContentSpec.val(
ActionSpec.rec(ProviderSpec.val(ProviderSpec.UNBOUNDED, SAME_ROW)),
ActionSpec.concat(0, ProviderSpec.val(1, BELOW_STR)),
ActionSpec.avp("ID")
))
),
// Repeated qualifier/value subrows
SubrowPattern.of(Quantifier.oneOrMore(),
CellPattern.of(AtomicContentSpec.attr(
ActionSpec.prefix("_", ProviderSpec.any(1, ABOVE))
)),
CellPattern.of(AtomicContentSpec.val(
ActionSpec.avp(ProviderSpec.attr(SAME_SUBROW))
))
)
)
)
);
RTL equivalent¶
[ [] [AUX]+ ]
[ [VAL : ROW*->REC, BW&STR->CONCAT(0), 'ID'->AVP]
{[ATTR : AV->PREFIX('_')] [VAL : SR->AVP]}+ ]+
How it works¶
- Header subtable
[ [] [AUX]+ ]: skip the corner[], then mark the group-name cells (REF,REF,SPECS,SPECS) asAUX— they are not values, they only supply name prefixes. - Data subtable
[ … ]+: one-or-more rows. The anchor cell[VAL : ROW*->REC, BW&STR->CONCAT(0), 'ID'->AVP]is theIDvalue (T-1): ROW*->RECcollects allVALitems in the same row into one record.'ID'->AVPnames the anchor's attributeID.BW&STR->CONCAT(0)(Below&SameStr) concatenates the record of the next row whoseIDstring is identical below to the anchor's record, soT-1's two physical rows fold into a single wider record; key position0(theID) is not repeated.- The rest of each row is an explicit subrow
{[ATTR] [VAL]}+repeated per qualifier/value pair: [ATTR : AV->PREFIX('_')]— the qualifier cell (TP,HV, …) becomes anATTR;AV(Above) prepends the group header from the cell above with'_', formingREF_TP,SPECS_HV, ….[VAL : SR->AVP]— the value cell (D16,750) takes its attribute name from theATTRin the same subrow.
Derivation¶
- PREFIX builds the compound attribute names:
REF+TP→REF_TP,SPECS+HV→SPECS_HV,REF+SN→REF_SN,SPECS+LV→SPECS_LV. - AVP:
ID=T-1(rows 1–2),ID=T-2(rows 3–4); thenREF_TP=D16,SPECS_HV=750(row 1);REF_SN=001,SPECS_LV=110(row 2);REF_TP=D24,SPECS_HV=110(row 3);REF_SN=002,SPECS_LV=10(row 4). - REC (one per row):
⟨T-1, D16, 750⟩,⟨T-1, 001, 110⟩,⟨T-2, D24, 110⟩,⟨T-2, 002, 10⟩. - CONCAT(0) concatenates the record of the row below sharing the same
ID(its position 0, the secondID, is dropped): ⟨T-1, D16, 750, 001, 110⟩⟨T-2, D24, 110, 002, 10⟩
CONCAT vs JOIN
CONCAT folds records: two records become one wider record. JOIN multiplies them:
every record of the anchor is combined with every provided record, and the width stays fixed.
Here each ID has exactly one row below, so JOIN(0) would give the same two records — the
two operations coincide whenever a single record is provided. They diverge from two records
on: with three rows per ID, CONCAT(0) yields one record of width 7, JOIN(0) yields two
records of width 5 (see Example 6). A named attribute shared by the concatenated records
(apart from the key) is a conflict: CONCAT leaves both records as they are and reports a
diagnostic (TableInterpreter.diagnostics()); for JOIN the same situation is a natural-join
condition. Up to jRegTab 0.5.x the folding operation was spelled JOIN(K).
Running the test¶
Example 3 — Task 046: pivoting a flat list into a wide recordset¶
The inverse of Example 1: a flat (name, subject, score) list is pivoted so that each distinct subject becomes a schema attribute and the rows of each student collapse into one record.
Input table (task 046, variant 1):
Anna | Math | 43
Anna | French | 78
Bob | English | 96
Bob | French | 54
Joan | English | 79
Tom | Math | 90
Tom | French | 85
Rob | English | 87
Rob | French | 92
Every row has three non-blank cells: a name, a subject, and a score. Consecutive rows with the same name belong to the same student.
Schema: ⟨"", Math, French, English⟩ — the blank-named first attribute holds the student name,
the rest are derived from the subject column:
| Math | French | English
Anna | 43 | 78 |
Bob | | 54 | 96
Joan | | | 79
Tom | 90 | 85 |
Rob | | 92 | 87
Item roles¶
Every row plays the same three roles (shown for the first rows):
| col 0 | col 1 | col 2 | |
|---|---|---|---|
| row 1 | VAL Anna → "" |
ATTR Math |
VAL 43 |
| row 2 | VAL Anna → "" |
ATTR French |
VAL 78 |
| row 3 | VAL Bob → "" |
ATTR English |
VAL 96 |
| … | … | … | … |
The blank-named "" attribute holds the student name (the anchor); each ATTR subject names its
adjacent score VAL.
ATP pattern¶
import ru.icc.regtab.atp.spec.*;
CellMatchCondition NOT_BLANK = new CellMatchCondition(CellPredicate.NotBlank.INSTANCE);
ItemFilterConditionSpec SAME_SUBROW = ItemFilterConditionSpec.sameSubrow();
ItemFilterConditionSpec BELOW_STR = ItemFilterConditionSpec.and(FilterTerm.Below.INSTANCE,
FilterTerm.SameStr.INSTANCE);
TablePattern pattern = TablePattern.of(
SubtablePattern.of(Quantifier.oneOrMore(),
RowPattern.of(Quantifier.oneOrMore(),
// Anchor: non-blank name cell
CellPattern.of(NOT_BLANK, Quantifier.one(), AtomicContentSpec.val(
ActionSpec.avp(""), // blank-named attribute
ActionSpec.rec(ProviderSpec.val(ProviderSpec.UNBOUNDED, SAME_SUBROW)),
ActionSpec.concat(0, ProviderSpec.val(ProviderSpec.UNBOUNDED, BELOW_STR))
)),
// Subject cell → ATTR
CellPattern.of(NOT_BLANK, Quantifier.one(), AtomicContentSpec.attr()),
// Score cell → VAL named by the same-subrow ATTR
CellPattern.of(NOT_BLANK, Quantifier.one(), AtomicContentSpec.val(
ActionSpec.avp(ProviderSpec.attr(SAME_SUBROW))
))
)
)
);
RTL equivalent¶
{ [ [!BLANK? VAL : ''->AVP, SR*->REC, BW&STR*->CONCAT(0)] [!BLANK? ATTR] [!BLANK? VAL : SR->AVP] ]+ }+
How it works¶
- The whole list is matched by one-or-more subtables
{ … }+of one-or-more three-cell rows[ … ]+; every cell is guarded!BLANK?(must be non-blank). - Anchor cell
[!BLANK? VAL : ''->AVP, SR*->REC, BW&STR*->CONCAT(0)]— the name (Anna): ''->AVPbinds the name to the empty-named attribute (the blank-header name column).SR*->RECcollects all same-subrowVALitems into the record.BW&STR*->CONCAT(0)concatenates the records of every following row whose name string is identical below —Anna's two rows fold into one record,Bob's two, and so on. This is a fold, not a join: three rows per student would still give one record (see Example 6 for the record product).[!BLANK? ATTR]— the subject cell (Math) becomes anATTR(a schema attribute name).[!BLANK? VAL : SR->AVP]— the score cell (43) takes its attribute name from theATTRin the same subrow →Math=43.- The result is one record per student with a column per distinct subject; subject/student combinations that never appear stay blank.
Derivation¶
- AVP (score ← same-subrow subject):
Math=43,French=78(Anna);English=96,French=54(Bob);English=79(Joan);Math=90,French=85(Tom);English=87,French=92(Rob). - REC (one per row, anchored on the name):
⟨Anna,43⟩,⟨Anna,78⟩,⟨Bob,96⟩,⟨Bob,54⟩,⟨Joan,79⟩,⟨Tom,90⟩,⟨Tom,85⟩,⟨Rob,87⟩,⟨Rob,92⟩. - CONCAT(0) concatenates the records of every row whose name repeats directly below, folding
each student into one record:
⟨Anna, 43, 78⟩,⟨Bob, 96, 54⟩,⟨Joan, 79⟩,⟨Tom, 90, 85⟩,⟨Rob, 87, 92⟩.
Schema-flexible result. Different students list different subjects, so the records are ragged:
the schema is the union ⟨"", Math, French, English⟩, but a subject/student combination that never
occurs has no value item at all. For example Joan's record is just ⟨Joan, 79⟩ (English), so its
Math and French values are ⊥ (the manuscript's term for a missing value). In the API
record.get("Math") returns null for Joan, and the CSV renders it as an empty field.
Running the test¶
Example 4 — Task 116: named fragments (de-duplicating repeated sub-patterns)¶
When the same sub-pattern appears in multiple non-adjacent positions, named fragment
definitions eliminate the repetition. Fragments are declared in the RTL preamble
(before the first [ or {) and referenced by [$N] or {$N} at the appropriate level.
Task 116 — environmental monitoring table with repeating column groups.
Without fragments, [VAL: -AV->PREFIX(', ')] appears five times and
[VAL: 'VALUE'->AVP, (ROW, COL&R1..3*, -AV&#'IND')->REC] appears eight times.
$V1=[VAL: -AV->PREFIX(', ')]
$V2=[VAL: 'VALUE'->AVP, (ROW, COL&R1..3*, -AV&#'IND')->REC]
[ []+ ]
[ [] [VAL: 'TERRITORY'->AVP]+ ]
[ [AUX]+ ]
[ 'LOCATION'->AVP [] [$V1]{4} [VAL] []
[VAL] [$V1] [VAL]
[$V1] [VAL] []
{ [VAL] [$V1] [VAL] [] }? ]
{ [ [VAL#'IND': 'INDICATOR'->AVP ',' VAL: 'UNIT'->AVP]+ ]
[ ['20\\d\\d' ? VAL: 'YEAR'->AVP]
{ [$V2]{5} [] }{2}
{ [$V2]{3} [] }?
]+
}+
$V1= cell fragment;[$V1]{4}— four cells with PREFIX action,[$V1]— single cell.$V2= cell fragment;[$V2]{5}— five VALUE cells with REC,[$V2]{3}— three.- Quantifiers on references are independent of the definition.
This example reproduces task 116 — the full environmental-monitoring pattern, here written with named fragments to avoid repeating the two recurring cell sub-patterns; the same recordset is produced by the benchmark tests:
Example 5 — Task 051: low-level ITM construction (without a pattern)¶
The ATP and RTL paths populate the semantic layer automatically by matching a pattern. For full
control — or for use cases the pattern language does not cover — an InterpretableTable can also
be assembled by hand: you create the cell-derived items, the context-derived items, and the
interpretation actions yourself, then interpret the result directly. No AtpMatcher is involved.
This example reproduces task 051 — the cross-table unpivot from the
Getting started guide — entirely by hand (schema ⟨ND, AIRLINE, AIRPORT, MON⟩).
Each compound body cell yields two cell-derived items, which is the key difference from a
one-item-per-cell table:
import ru.icc.regtab.itm.InterpretableTable;
import ru.icc.regtab.interpret.TableInterpreter;
import ru.icc.regtab.itm.semantics.TableSemantics;
import ru.icc.regtab.itm.semantics.action.InterpretationAction;
import ru.icc.regtab.itm.semantics.item.*;
import ru.icc.regtab.itm.semantics.operation.*;
import ru.icc.regtab.itm.semantics.provider.*;
import ru.icc.regtab.itm.syntax.TableSyntax;
import ru.icc.regtab.recordset.Recordset;
import java.util.List;
import java.util.Set;
// 1. Build the syntactic layer (the empty corner cell defaults to "")
TableSyntax syntax = new TableSyntax(3, 3);
syntax.getCell(0, 1).setText("CA");
syntax.getCell(0, 2).setText("HU");
syntax.getCell(1, 0).setText("IKT");
syntax.getCell(1, 1).setText("0 Jan");
syntax.getCell(1, 2).setText("8 Feb");
syntax.getCell(2, 0).setText("SVO");
syntax.getCell(2, 1).setText("31 Jan");
syntax.getCell(2, 2).setText("40 Feb");
// 2a. Cell-derived items (ι).
// Header and row-header cells yield one VALUE item each (index 0).
CellDerivedItem iotaCA = new CellDerivedItem("CA", 0, syntax.getCell(0, 1), ItemType.VALUE);
CellDerivedItem iotaHU = new CellDerivedItem("HU", 0, syntax.getCell(0, 2), ItemType.VALUE);
CellDerivedItem iotaIKT = new CellDerivedItem("IKT", 0, syntax.getCell(1, 0), ItemType.VALUE);
CellDerivedItem iotaSVO = new CellDerivedItem("SVO", 0, syntax.getCell(2, 0), ItemType.VALUE);
// Each compound body cell "ND MON" yields TWO items: ND at index 0, MON at index 1.
CellDerivedItem nd11 = new CellDerivedItem("0", 0, syntax.getCell(1, 1), ItemType.VALUE);
CellDerivedItem mo11 = new CellDerivedItem("Jan", 1, syntax.getCell(1, 1), ItemType.VALUE);
CellDerivedItem nd12 = new CellDerivedItem("8", 0, syntax.getCell(1, 2), ItemType.VALUE);
CellDerivedItem mo12 = new CellDerivedItem("Feb", 1, syntax.getCell(1, 2), ItemType.VALUE);
CellDerivedItem nd21 = new CellDerivedItem("31", 0, syntax.getCell(2, 1), ItemType.VALUE);
CellDerivedItem mo21 = new CellDerivedItem("Jan", 1, syntax.getCell(2, 1), ItemType.VALUE);
CellDerivedItem nd22 = new CellDerivedItem("40", 0, syntax.getCell(2, 2), ItemType.VALUE);
CellDerivedItem mo22 = new CellDerivedItem("Feb", 1, syntax.getCell(2, 2), ItemType.VALUE);
Set<CellDerivedItem> allCdi = Set.of(
iotaCA, iotaHU, iotaIKT, iotaSVO,
nd11, mo11, nd12, mo12, nd21, mo21, nd22, mo22);
// 2b. Context-derived items (β): named ATTRIBUTE constants that define the schema fields
ContextDerivedItem betaND = new ContextDerivedItem("ND", ItemType.ATTRIBUTE);
ContextDerivedItem betaAIRLINE = new ContextDerivedItem("AIRLINE", ItemType.ATTRIBUTE);
ContextDerivedItem betaAIRPORT = new ContextDerivedItem("AIRPORT", ItemType.ATTRIBUTE);
ContextDerivedItem betaMON = new ContextDerivedItem("MON", ItemType.ATTRIBUTE);
Set<ContextDerivedItem> allCtx = Set.of(betaND, betaAIRLINE, betaAIRPORT, betaMON);
// 2c. Interpretation actions
// AVP: pair each VALUE item with its named ATTRIBUTE (establishes the schema field name).
// REC: anchor on each ND item; providers select the same-column airline, same-row airport,
// and the same-cell MON sibling. The provider excludes the anchor itself, and !sameCell
// keeps sameCol/sameRow from picking the ND item's own MON sibling.
ItemFilterCondition sameCol = (a, c) -> c.sameCol(a) && !c.sameCell(a);
ItemFilterCondition sameRow = (a, c) -> c.sameRow(a) && !c.sameCell(a);
ItemFilterCondition sameCell = (a, c) -> c.sameCell(a);
List<InterpretationAction> actions = List.of(
// AVP actions: bind header / row-header items to named attributes
new InterpretationAction(iotaCA,
List.of(new ContextDerivedItemProvider(List.of(betaAIRLINE))), new AvpOperation()),
new InterpretationAction(iotaHU,
List.of(new ContextDerivedItemProvider(List.of(betaAIRLINE))), new AvpOperation()),
new InterpretationAction(iotaIKT,
List.of(new ContextDerivedItemProvider(List.of(betaAIRPORT))), new AvpOperation()),
new InterpretationAction(iotaSVO,
List.of(new ContextDerivedItemProvider(List.of(betaAIRPORT))), new AvpOperation()),
// AVP actions: bind each ND segment to ND and each MON segment to MON
new InterpretationAction(nd11,
List.of(new ContextDerivedItemProvider(List.of(betaND))), new AvpOperation()),
new InterpretationAction(nd12,
List.of(new ContextDerivedItemProvider(List.of(betaND))), new AvpOperation()),
new InterpretationAction(nd21,
List.of(new ContextDerivedItemProvider(List.of(betaND))), new AvpOperation()),
new InterpretationAction(nd22,
List.of(new ContextDerivedItemProvider(List.of(betaND))), new AvpOperation()),
new InterpretationAction(mo11,
List.of(new ContextDerivedItemProvider(List.of(betaMON))), new AvpOperation()),
new InterpretationAction(mo12,
List.of(new ContextDerivedItemProvider(List.of(betaMON))), new AvpOperation()),
new InterpretationAction(mo21,
List.of(new ContextDerivedItemProvider(List.of(betaMON))), new AvpOperation()),
new InterpretationAction(mo22,
List.of(new ContextDerivedItemProvider(List.of(betaMON))), new AvpOperation()),
// REC actions: anchor on each ND item → ⟨ND, AIRLINE, AIRPORT, MON⟩
new InterpretationAction(nd11, List.of(
new CellDerivedItemProvider(sameCol, allCdi, 1), // → iotaCA (AIRLINE)
new CellDerivedItemProvider(sameRow, allCdi, 1), // → iotaIKT (AIRPORT)
new CellDerivedItemProvider(sameCell, allCdi, 1)), // → mo11 (MON)
new RecOperation()),
new InterpretationAction(nd12, List.of(
new CellDerivedItemProvider(sameCol, allCdi, 1), // → iotaHU
new CellDerivedItemProvider(sameRow, allCdi, 1), // → iotaIKT
new CellDerivedItemProvider(sameCell, allCdi, 1)), // → mo12
new RecOperation()),
new InterpretationAction(nd21, List.of(
new CellDerivedItemProvider(sameCol, allCdi, 1), // → iotaCA
new CellDerivedItemProvider(sameRow, allCdi, 1), // → iotaSVO
new CellDerivedItemProvider(sameCell, allCdi, 1)), // → mo21
new RecOperation()),
new InterpretationAction(nd22, List.of(
new CellDerivedItemProvider(sameCol, allCdi, 1), // → iotaHU
new CellDerivedItemProvider(sameRow, allCdi, 1), // → iotaSVO
new CellDerivedItemProvider(sameCell, allCdi, 1)), // → mo22
new RecOperation())
);
// 3. Build the semantic layer and interpret
TableSemantics semantics = new TableSemantics(allCdi, allCtx, actions);
InterpretableTable itm = new InterpretableTable(syntax, semantics);
Recordset result = new TableInterpreter().interpret(itm);
// schema ⟨ND, AIRLINE, AIRPORT, MON⟩; four records:
// ⟨0, CA, IKT, Jan⟩ ⟨8, HU, IKT, Feb⟩ ⟨31, CA, SVO, Jan⟩ ⟨40, HU, SVO, Feb⟩
This is exactly the recordset that task 051 produces from the two-line RTL pattern — here every
cell-derived item, attribute, provider, and action is spelled out by hand. The compound cells show
the general rule: a cell that yields multiple items gets one CellDerivedItem per item with a
distinct index. See CrosstabMinMaxTest for another worked example.
This is the lowest-level entry point. In practice the ATP and RTL paths express the same result far more compactly — the entire block above collapses to the two-line RTL pattern shown in the Getting started guide.
This example reproduces task 051 by hand; the same recordset is produced by the pattern-based tests:
Example 6 — Record join (product): exploding a delimited key against stacked columns¶
Examples 2 and 3 fold records with CONCAT. This example multiplies them with JOIN — the
record product. A key cell lists several identifiers separated by ;, and the numbers in the row
apply to each of them; every (identifier, column) combination must become a record of its own.
Input table (conformance case join_product):
Schema: ⟨id, value, var⟩ — six records, |tokens| × |columns| per row:
Item roles¶
| col 0 | col 1 | col 2 | |
|---|---|---|---|
| row 0 | ATTR id |
VAL x → var |
VAL y → var |
| row 1 | VAL a → id, VAL b → id (one cell) |
VAL 1 → value |
VAL 2 → value |
| row 2 | VAL c → id |
VAL 3 → value |
VAL 4 → value |
The delimited key cell a;b yields two cell-derived items (a at index 0, b at index 1);
each is an anchor of its own.
RTL pattern¶
[ [ATTR] [VAL: 'var'->AVP]+ ]
[ [(VAL: COL->AVP, ()->REC, RT*->JOIN){';'}] [VAL: 'value'->AVP, COL->REC]+ ]+
How it works¶
- Header row
[ [ATTR] [VAL: 'var'->AVP]+ ]:idis the attribute of the key column; the column namesx,yare values namedvar— they will travel into the records. - Data rows
[ … ]+: the key cell is delimited(VAL: …){';'}— one item per token: COL->AVPnames each tokenid(theATTRin the same column);()->RECgives each token a record of its own,⟨id:a⟩,⟨id:b⟩,⟨id:c⟩—JOINmultiplies records, so without it the token has nothing to multiply, drops out of the recordset, and the interpreter reportsJOIN skipped at …: anchor has no record — REC missing?throughTableInterpreter.diagnostics();RT*->JOINmultiplies the token's record by the records of all cells to its right.- Each number cell
[VAL: 'value'->AVP, COL->REC]is namedvalueand builds the record⟨value:1, var:x⟩with the column name above it (COL, cardinality 1, row-major → the header).
Derivation¶
- AVP:
var=x,var=y;id=a,id=b,id=c;value=1,value=2,value=3,value=4. - REC:
rec(a) = ⟨a⟩,rec(b) = ⟨b⟩,rec(c) = ⟨c⟩;rec(1) = ⟨1, x⟩,rec(2) = ⟨2, y⟩,rec(3) = ⟨3, x⟩,rec(4) = ⟨4, y⟩. - JOIN at
a(providers: cells1,2):rec(a) = ⟨⟨a, 1, x⟩, ⟨a, 2, y⟩⟩— one record per provided record, in nested-loop order; the cells1and2become joined-away anchors. - JOIN at
b: the records of1and2are still available (joined-away anchors are retired lazily, not removed), sorec(b) = ⟨⟨b, 1, x⟩, ⟨b, 2, y⟩⟩. With immediate removal,bwould have found nothing to join. - JOIN at
c:rec(c) = ⟨⟨c, 3, x⟩, ⟨c, 4, y⟩⟩. - Recordset extraction visits the live anchors
a,b,conly — six records.
CONCAT vs JOIN¶
CONCAT(K) |
JOIN(K) |
|
|---|---|---|
| direction | n records → 1 | 1 record → n |
| what grows | the width of the record | the number of records |
| SQL counterpart | GROUP BY + collect into columns, pandas.concat(axis=1) |
CROSS JOIN, LATERAL, pandas.merge |
key K (positions and/or attribute names) |
must agree in all records; not repeated | a record pair is combined only if it agrees there; not repeated |
| shared named attribute | a conflict: no effect + diagnostic | a natural-join condition: kept once if the values agree, pair dropped otherwise |
| one provided record | one wider record | the same record — the two coincide |
| two or more provided records | still one record | one record each — the two diverge |
Running the test¶
mvn test -Dtest="RtlSemanticConformanceTest" # conformance/semantic/join_product
mvn test -Dtest="TableInterpreterMultiRecordTest" # the same table, both schema strategies
mvn test -Dtest="WorkingStateJoinTest" # the operation on the working state
Running all examples¶
Examples 1–5 above are benchmark tasks (052, 053, 046, 116, 051). The *Task<NN>Test
wildcard runs both the ATP and the RTL test for each; Example 6 is a conformance case:
# ATP + RTL tests for the five benchmark examples on this page
mvn test -Dtest="*Task052Test,*Task053Test,*Task046Test,*Task116Test,*Task051Test"
# Example 6
mvn test -Dtest="RtlSemanticConformanceTest,TableInterpreterMultiRecordTest"
To run the whole benchmark suite instead, use the AtpTask*Test / RtlTask*Test globs.