Skip to main content

Lookup tables — <switch>

Use it when one field must be derived from another, not drawn on its own. You have a Country column and you need a Currency beside it that is always consistent: USUSD, JPJPY. A second random generator is exactly wrong here — it would hand you a currency that doesn't match the country. What you want is a lookup table: read one field, return the value keyed to it.

That's <switch>. On every row it reads the value of one subject sequence (named by on) and returns the value stored under that key. Unlike <mix>, which splits a field randomly by percentage, <switch> is deterministic — the subject's value fixes the result.

<tdc>
<env count="8" seed="demo" local="en">
<sequence name="Country">
<gen type="text" value="US,FR,DE,JP" percent="40,25,20,15"/>
</sequence>

<switch name="Currency" on="Country">
<map>US:USD, FR:EUR, DE:EUR, JP:JPY</map>
</switch>
</env>
<block><line><data>${{Country}} -> ${{Currency}}</data></line></block>
</tdc>
./run currency.tdc
US -> USD
US -> USD
FR -> EUR
DE -> EUR
US -> USD
JP -> JPY
FR -> EUR
DE -> EUR
Outputs are illustrative

The values below come from a fixed seed, so they're reproducible, but exact strings can differ between core versions. Treat them as examples of shape, not guarantees.

On every row, TDC reads Country and returns the currency keyed to it. This is the "derive one field from another" job that <mix> (random) and if chains (verbose) both handle awkwardly.

A three-row lookup table and 24 rows generated through it.
  • Athe table: each key carries one value
  • Bthe generated rows — the same key always produces the same number, every time

At a glance

<switch> lives directly in <env>, next to <sequence> and <mix>. It takes two attributes and holds one or more branches.

AttributeRequiredWhat it does
nameyesThe name you interpolate with ${{name}}
onyesThe subject sequence whose value is looked up
commentnoFree-text note
Child tagWhat it is
<map>Compact KEY:VALUE table of literals
<case is="…">A branch whose value is a generator or composite
<default>The "else" branch — used when no key matches

You need at least one branch (a <map> row or a <case>).

The subject — on

on names the subject: the sequence whose value is looked up on each row. It's required on <switch>, and it must point at a sequence declared earlier in the same <env> (a plain sequence, a composite field Parent.Field, or a built-in like _count). An undeclared subject is error TDC134.

<sequence name="Country">
<gen type="text" value="US,CA,MX,FR,DE,JP"/>
</sequence>

<switch name="Currency" on="Country">
<case is="US|CA|MX"><data>USD</data></case>
<case is="FR|DE"><data>EUR</data></case>
<case is="JP"><data>JPY</data></case>
</switch>
./run currency.tdc
CA -> USD
MX -> USD
FR -> EUR
JP -> JPY
US -> USD
JP -> JPY
FR -> EUR
DE -> EUR

Whatever value Country drew on a row decides the result: JP always yields JPY, and any of US/CA/MX yields USD. Why it matters: the subject is the single input. Point on at a different sequence and the same table starts reading its values instead — the lookup logic is reusable.

<map> — a compact literal table

When every value is a plain literal, don't write a stack of near-identical branches — collapse them into one <map>. Its body is plain text (like <data>): not markup, but KEY:VALUE records.

<switch name="Currency" on="Country">
<map>US:USD, FR:EUR, DE:EUR, JP:JPY</map>
</switch>

This is byte-for-byte equivalent to four separate <case is="US"><data>USD</data></case> branches — just shorter. The format rules:

  • Records are separated by a comma ,.
  • Key and value are split by the first colon : — so colons inside a value are kept (US:Down : LeftDown : Left).
  • Multiple keys → one value with |: CA|MX:USD matches both CA and MX.
  • Whitespace and line breaks around records are ignored, so you can wrap the table across lines for readability.
  • The value is always a literal. A generator belongs in a <case>, not in a <map> row.
<switch name="Currency" on="Country">
<map>
US:USD, FR:EUR, DE:EUR, JP:JPY,
CA|MX:USD
</map>
</switch>
./run currency.tdc
FR -> EUR
DE -> EUR
JP -> JPY
MX -> USD
US -> USD
MX -> USD
JP -> JPY
CA -> USD

The multi-key row CA|MX:USD matches both CA and MX — both print USD.

One limit: commas

A value that contains a comma won't fit in a <map> (the comma is the record separator). For those, use a <case is="…"> instead. A line with no colon isn't a record at all, and the validator warns (TDC136).

<case> — a branch with a generator

A <map> row can only hold a literal. When a branch needs to generate its value — a random amount, a counter, a prefix plus a generator — reach for <case>. Its content is assembled left-to-right from <data> literals and <gen> generators, exactly like a sequence branch.

Inside <switch>, a <case> needs is — the key(s) it matches. Here a customer tier drives a discount range, something a flat table can't express:

<sequence name="Tier">
<gen type="text" value="gold,silver,bronze" percent="20,30,50"/>
</sequence>

<switch name="Discount" on="Tier">
<case is="gold"><gen type="number" value="15..25"/></case>
<case is="silver"><gen type="number" value="5..10"/></case>
<default><data>0</data></default>
</switch>
./run discount.tdc
silver -> 7
gold   -> 22
bronze -> 0
gold   -> 18
silver -> 5
bronze -> 0

gold draws a fresh number in 15..25 on every matching row, silver in 5..10, and bronze falls through to the <default> literal 0. Why it matters: the value is computed per row, not looked up from a fixed string — that's the whole reason <case> exists alongside <map>.

Matching keys — is

is gives a <case> its key(s): the subject values that make the branch fire.

  • Required on a <case> inside <switch> — without it the branch can never match (error TDC137).
  • Multiple keys via |: is="US|CA|MX" matches if the subject equals any of them, folding a whole group of values into one branch.
  • Comparison is string equality against the subject's value.
<switch name="Currency" on="Country">
<case is="US|CA|MX"><data>USD</data></case>
<case is="FR|DE"><data>EUR</data></case>
<case is="JP"><data>JPY</data></case>
</switch>
./run currency.tdc
CA -> USD
MX -> USD
FR -> EUR
JP -> JPY
US -> USD
DE -> EUR
Inside <mix> there is no is

The same <case> tag is also used inside <mix>, but there the branches are chosen randomly by percentage and carry no is. <case> never supports if or default in either parent — for arbitrary conditions, use a sequence with <gen if="…"> branches.

<default> — the fallback

<default> is the "else" branch: its value is used when the subject matches no key. It's a tag, not an attribute, precisely so it can hold a generator — the same <data> / <gen> content as a <case>.

Without a <default>, an unmatched key produces an empty value (the brackets below are only there to make the hole visible):

<switch name="Currency" on="Country">
<map>US:USD, FR:EUR</map>
</switch>
./run currency.tdc
FR -> [EUR]
FR -> [EUR]
JP -> []
GB -> []
US -> [USD]
GB -> []
DE -> []
JP -> []

Five rows out of eight are blank — a silent defect in a real export. Add a <default> to cover everything the table missed:

<switch name="Currency" on="Country">
<map>US:USD, FR:EUR</map>
<default><gen type="text" value="XXX"/></default>
</switch>
./run currency.tdc
FR -> EUR
FR -> EUR
JP -> XXX
GB -> XXX
US -> USD
GB -> XXX
DE -> XXX
JP -> XXX

The same unmatched keys (DE, JP, GB) now yield XXX — no holes left. If the fallback is just a literal, put it in <data>: <default><data>Other</data></default>.

  • Put it last, where the eye looks for it — the same place it goes in a switch statement in code.
  • It's optional. With no <default> and no matching key, the value is empty on that row.

A share inside a branch

A <case> can hold a <mix>, and its percentages are a quota over that branch's rows — not over the run. Twenty percent of the male records is twenty percent of the male records, whatever share of the run turns out to be male.

<tdc>
<env count="10" seed="clinic" local="en">
<sequence name="Sex">
<gen type="text" value="male,female" percent="50,50"/>
</sequence>

<switch name="Diagnosis" on="Sex">
<case is="male">
<mix percent="20,80">
<case><data>prostatitis</data></case>
<case><data>influenza</data></case>
</mix>
</case>
<case is="female"><data>influenza</data></case>
</switch>
</env>
<block><line><data>${{Sex}} -> ${{Diagnosis}}</data></line></block>
</tdc>
./run clinic.tdc
male -> influenza
female -> influenza
male -> influenza
male -> influenza
male -> influenza
female -> influenza
male -> prostatitis
female -> influenza
female -> influenza
female -> influenza

Five records are male, and one of them carries the male-specific diagnosis. Raise the count and the ratio holds — 20% of the male records, whatever the run size:

./run clinic.tdc (count=100, tallied)
female -> influenza    50
male -> influenza      40
male -> prostatitis    10

Not "about ten" — the count is the same on every seed.

Why it matters: the denominator is the branch. A share measured against the whole run would hand out 20 prostatitis values over 100 records, then drop the ones that landed on a female record. You would get somewhere near ten, and never reliably ten.

Multi-key branches and <default>

The rule is the same for a share inside a branch that matches several keys, and inside <default>. Here 30% of the North American records ship express:

<sequence name="Country">
<gen type="text" value="US,CA,MX,DE" percent="25,25,25,25"/>
</sequence>

<switch name="Shipping" on="Country">
<case is="US|CA|MX">
<mix percent="30,70">
<case><data>express</data></case>
<case><data>standard</data></case>
</mix>
</case>
<default><data>international</data></default>
</switch>
./run shipping.tdc (count=120, tallied)
express          27
standard         63
international    30

Ninety records match US|CA|MX, and 27 + 63 = 90 — 30% of the branch, to the record. These two shapes cost you the streaming engine; see Deterministic across engines.

A <switch> inside a <case>

A branch can hold a whole second lookup. Use it when a value depends on two fields and the second one only matters for some values of the first — a national id whose shape depends on the sex, and for one sex only, on the region.

<tdc>
<env count="8" seed="clinic-id" local="en">
<sequence name="Sex"><gen type="text" value="male,female" percent="50,50"/></sequence>
<sequence name="Region"><gen type="text" value="north,south" percent="50,50"/></sequence>

<switch name="Id" on="Sex">
<case is="male"><data>M-</data><gen type="number" value="100..199"/></case>
<case is="female">
<switch on="Region">
<case is="north"><data>FN-</data><gen type="number" value="200..299"/></case>
<case is="south"><data>FS-</data><gen type="number" value="300..399"/></case>
</switch>
</case>
</switch>
</env>
<block><line><data>${{Sex}}/${{Region}} -> ${{Id}}</data></line></block>
</tdc>
./run ids.tdc
male/south -> M-156
male/north -> M-118
male/north -> M-177
male/north -> M-129
female/south -> FS-371
female/north -> FN-212
female/south -> FS-358
female/south -> FS-353

A male record never reaches the inner switch. A female one reads Region and takes the branch keyed to it. Why it matters: the alternative is one sequence per combination plus an expression to choose between them — three declarations to say one thing, with nothing tying them together.

The nested form takes no name: it contributes its value to the branch around it and nothing can interpolate it, so a name would name nothing (error TDC245). Everything else is the same as at the top level — the subject must be a sequence declared earlier, <map> works, and <default> covers the rows of THAT BRANCH which matched no inner key.

The same <case> sits inside <mix>, so a nested <switch> works there too: the mix decides whether a record is an invoice, the switch decides which tax it names.

A share inside a nested branch does not stream

A branch of a nested switch covers an intersection of two partitions — the enclosing branch's rows and the inner subject's — which the streaming engines cannot number one row at a time. A share written there is exact, and TDC routes the config to the in-memory engine to keep it so.

Selection rules

  • The first matching branch wins. <map> rows are checked first (in written order), then <case> branches. If a key appears in both, the higher one — the map row — takes it.
  • Keys are compared as strings against the subject value.
  • A multi-key A|B|C matches when the subject equals any of A, B, or C.

A tiny precedence demo — the same key CA in both a map row and a case:

<switch name="Label" on="Country">
<map>CA:from-map</map>
<case is="CA"><data>from-case</data></case>
</switch>
./run precedence.tdc
CA -> from-map
CA -> from-map
CA -> from-map

The map row wins because map rows are checked before cases.

Putting it together

All three branch kinds in one <switch> — a literal table, a generated branch, and a fallback:

<switch name="Currency" on="Country">
<!-- 1. Literals — compact, multi-key via | -->
<map>
US:USD, FR:EUR, DE:EUR, JP:JPY,
CA|MX:USD
</map>

<!-- 2. A branch whose value is generated -->
<case is="TR|BR"><data>REG-</data><gen type="number" value="100..999"/></case>

<!-- 3. The fallback when nothing matched -->
<default><gen type="text" value="XXX"/></default>
</switch>
./run currency.tdc (subject in US,FR,CA,MX,TR,BR,GB)
GB -> XXX
CA -> USD
GB -> XXX
TR -> REG-473
US -> USD
BR -> REG-208
MX -> USD
TR -> REG-819
FR -> EUR
MX -> USD

CA and MX resolve through the map's CA|MX:USD row, TR/BR build a generated REG-### code, and GB, which matches no branch at all, falls through to <default>.

Deterministic across engines

A <switch> whose branches are literals is a pure function of its subject: the same subject value on a row gives the same result, every time. There's no per-row random draw to keep in sync.

A branch that generates its value draws like any other generator. The result is still fixed by the seed, and all three engines (memory / stream / disk) produce the same rows, but the value is no longer decided by the subject alone.

One shape gives up the streaming engine: a share inside a branch keyed on more than one value (is="US|CA|MX"), or inside <default>. Those rows are a union of subsets, or what is left after every other branch has taken its own, and the streaming engine cannot number them one row at a time — which is what an exact share needs. TDC routes such a config to the in-memory engine on its own: you get the exact share, and the memory cost of holding the run. A branch keyed on a single value streams normally.

Forcing the streaming engine on one of those configs says so rather than approximating:

./run shipping.tdc (engine="2" forced)
tdcv2: stream mode: a percentage inside <case is="US|CA|MX"> of <switch on="Country">
("Shipping") is not supported yet — run without mode="stream" (the in-memory engine
handles it), or remove it.

See Large outputs for the streaming path, and Determinism for the guarantee.

<switch> is generation, not formatting

Like <mix>, <switch> produces a value and lives only in <env>. It's not allowed inside the output block (<line>) — that's error TDC132. Declare it in <env>, give it a name, and interpolate ${{name}} where you want it.

The one-line contrast to keep in mind:

  • <mix> chooses randomly, by percentage — for realistic proportions.
  • <switch> chooses deterministically, by key — for a value derived from another field.

See also

  • Coherent & relational data — the other way to keep two fields consistent: a child sequence drawn from a per-parent file.
  • Sequences — declaring the subject and other fields.
  • Text and Number — the generators used inside <case> and <default>.
  • Tag reference<mix>, <case>, and the full tag list.