Skip to main content

The advanced_regex generator

Use it when you need everything regex does, but the shape of the string itself carries a statistical distribution of variants — say, exactly 70% of the codes start with RU, 20% with US, 10% with DE.

Plain regex picks an alternation (RU|US|DE) randomly, so the split only comes out right on average. advanced_regex adds weighted choice, which lays the variants out in exact counts.

<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{6}"/>

advanced_regex is a superset of regex: the ordinary type="regex" generator stays stable and TDC-neutral, while advanced_regex layers TDC-specific power on top of the same finite, portable engine. Today that extra power is exactly one construct — weighted choice — and everything else on this page is inherited unchanged from Regex.

Why you need it​

Without weighted choice, "70% RU, 20% US, 10% DE, each followed by six random digits" would take several sequences or a <mix> to express. With advanced_regex it collapses into a single generator. It's a natural fit for:

  • codes for countries, branches, regions, or customer tiers
  • test identifiers that must vary in structure but hold fixed proportions
  • document numbers where one part of the string has to repeat another
  • synthetic data where the shares matter as much as the values
  • AI-generated .tdc files, where an agent finds it easier to emit one compact pattern than a tree of sequences

Example outputs below are illustrative — exact rows depend on the seed and can shift slightly between core versions — but the counts a distribution promises are exact.

Weighted choice​

The construct is:

(?%{PERCENT:BRANCH;PERCENT:BRANCH;...})

Piece by piece:

(?%{ 70:RU ; 20:US ; 10:DE })
│ │ │ │ │ │ │ │
│ │ branch │ branch │ branch│
│ percent percent percent │
└── start of weighted choice ┘

The rules:

  • percentages must be numbers, and they must be non-negative
  • the percentages must sum to 100
  • a branch may be empty
  • a branch is itself a full advanced_regex expression, so branches nest
  • ;, }, and : are the control characters — escape them if a branch needs them literally

Exact proportions, by default​

This is the headline: weighted choice promises exact percentages (exactly 70 of 100, not "about 70"), and TDC delivers them out of the box — you switch nothing on.

To measure the split exactly, the engine builds the whole column at once and hands out branches with the Hamilton (largest-remainder) method. It does this automatically the moment a (?%{…}) appears in the pattern — even under the default disk mode. All you supply is the pattern; the shares come out exact.

<env count="100" seed="countries">
<sequence name="CountryCode">
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/>
</sequence>
</env>

Count the prefixes across all 100 rows — exactly 70/20/10:

./run countries.tdc (100 rows, by prefix)
RU   70
US   20
DE   10
The cost of exactness is memory

To measure the split exactly, a weighted-choice column is built entirely in RAM. Fine for small and medium sets. If you need exact shares while streaming (O(1) memory, any output size), <mix percent> and <gen type="text" percent="…"> give exact proportions in a stream, without holding the whole column. And if you manually force the pure streaming engine (mode="stream" on <env>, an attribute-only legacy alias, or --engine 2 on the command line — --mode stream is not a thing), TDC won't silently ruin the percentages — it can't count them one row at a time, so it refuses with a clear error. Drop the override and it's exact again.

The weights really move the distribution​

The percentages are not decoration — they are the actual makeup of the column. Take one pattern, (?%{…})-[0-9]{2} at count="1000", change only the weights, and count the prefixes:

<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/> <!-- variant 1 -->
<gen type="advanced_regex" value="(?%{34:RU;33:US;33:DE})-[0-9]{2}"/> <!-- variant 2 -->
<gen type="advanced_regex" value="(?%{10:RU;10:US;80:DE})-[0-9]{2}"/> <!-- variant 3 -->
./run weights.tdc (1000 rows, by prefix)
weights          RU     US     DE
70 / 20 / 10     700    200    100
34 / 33 / 33     340    330    330
10 / 10 / 80     100    100    800

Over 1000 rows the counts reproduce the weights exactly. Change the weights and the makeup of the column changes with them.

regex vs advanced_regex​

Capabilityregexadvanced_regex
Generate a string from a patternyesyes
Character classesyesyes
Named Unicode alphabetsyesyes
Groups and backreferencesyesyes
Length cap via regex_max_lengthyesyes
Exact percentages inside the patternnoyes
Nest weighted variantsnoyes
Weighted choice in the output blocknono

The difference is easiest to see side by side. Plain regex picks each character freely — the shape is fixed, the shares are not:

<gen type="regex" value="[A-Z]{2}[0-9]{6}"/>
./run plain.tdc
FZ399441
YH481897
LR586083
YA900972
WT831899

advanced_regex keeps the same kind of code but pins the prefix shares — 70% RU, 20% US, 10% DE over the full run:

<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{6}"/>
./run coded.tdc (first 8 of 100 rows)
RU-441627
RU-476822
RU-948319
US-450875
RU-398584
RU-131212
RU-418648
RU-830959

In short: use regex for a string of a given form; use advanced_regex when the form itself carries a statistical distribution.

It inherits the whole regex language​

Every finite construct from regex works here too — literals, escapes, character classes, BMP ranges, named alphabets \a{…}, \d/\w/\s and their inverses, ., alternation, groups, backreferences, bounded quantifiers, and the regex_max_length cap. A simple weighted split over plain Latin codes:

<gen type="advanced_regex" value="(?%{70:[A-Z]{2};30:[A-Z]{3}})-[0-9]{4}"/>
./run mixed.tdc
QY-3500
ZT-3381
GSK-1914
VO-5921
DW-7570
SO-1660
MSE-2247

Unicode demo. Because the branches accept named alphabets, you can hold exact shares across scripts. Here 7 of 10 codes take a Cyrillic prefix and 3 take a Latin one — a deliberate Unicode/localization example, showing that the exact-percentage machinery is script-agnostic:

<gen type="advanced_regex" value="(?%{70:\a{cyrillic.ru.upper}{2};30:\a{latin.upper}{2}})-[0-9]{4}"/>
./run unicode.tdc (count=10)
ЭЗ-2477
WJ-0170
ЧП-8026
СЦ-1020
ЫЦ-2747
FJ-7879
РЛ-6827
ЩЕ-4485
ПВ-0297
UD-1550

Nested weighted choice​

Because branches are full expressions, weighted choices nest — and the inner split is computed inside the subset that reached the outer branch:

<gen type="advanced_regex" value="(?%{50:A(?%{80:X;20:Y});50:B})"/>

At count="100":

./run nested.tdc (100 rows)
AX   40
AY   10
B    50

80% of the 50 A rows gives 40 AX; the other 20% gives 10 AY. This "percentages within a subset" behavior matches TDC's sequence-hierarchy philosophy exactly.

Several weighted choices in one pattern​

<gen type="advanced_regex" value="(?%{60:M;40:F})-(?%{25:00;75:99})"/>

Each distribution is worked out exactly, over the same set of rows. At count="100":

./run two.tdc (100 rows)
M-99   44
F-99   31
M-00   16
F-00    9

Add up each part and both are exact: M = 44 + 16 = 60 and F = 31 + 9 = 40 (60/40); 99 = 44 + 31 = 75 and 00 = 16 + 9 = 25 (75/25). Two independent picks, each laid out by the same exact-percentage method.

With a parent filter​

advanced_regex lives inside the normal sequence-dependency model. If the sequence is filtered by parent, the percentages count only within the filtered subset:

<sequence name="Gender">
<gen type="text" value="M,F" percent="50,50"/>
</sequence>

<sequence name="MaleCode" parent="Gender.M">
<gen type="advanced_regex" value="M-(?%{40:A;60:B})-[0-9]{2}"/>
</sequence>

At count="100" (50 men):

./run parent.tdc (100 rows)
F      50    (MaleCode empty)
M-A    20
M-B    30

The 40/60 split is measured against the 50 filtered rows, not the full 100.

Captures and backreferences​

A backreference repeats a group that was already generated. This works exactly as in plain regex — the first three digits are echoed at the end:

<gen type="advanced_regex" value="([0-9]{3})-[A-Z]{2}-\1"/>
./run backref.tdc
299-YZ-299
929-UE-929
462-VR-462
905-BC-905
876-JF-876

A weighted branch can be captured and repeated with \1 — the captured part is echoed verbatim and the percentages still hold. At count="40":

<gen type="advanced_regex" value="((?%{25:AB;75:CD}))-\1"/>
./run branch-capture.tdc (40 rows)
AB-AB   10
CD-CD   30

The captured pair is repeated literally, and the 25/75 split survives.

A capture made before a weighted choice can be used inside a branch. Here half the rows repeat the captured two letters, half print the fixed XX (count="8"):

<gen type="advanced_regex" value="([A-W]{2})-(?%{50:\1;50:XX})"/>
./run capture-in-branch.tdc
TV-XX
GR-GR
RN-XX
OU-OU
WM-WM
SS-XX
CL-XX
QG-QG

A backreference can live inside a branch. Where the (A[0-9]) branch was taken, \1 repeats its capture; where the B branch was taken, there is no capture, so \1 comes out empty (count="20"):

<gen type="advanced_regex" value="(?%{40:(A[0-9]);60:B})-\1"/>
./run optional-capture.tdc
A2-A2
B-
B-
A1-A1
B-
A8-A8
B-

It isn't a full if, but it's already a useful logical link: one part of a string can depend on an already-generated group.

Escaping inside a weighted choice​

;, } and : are the control characters of a weighted choice. To use them as literal text in a branch, escape them (count="6"):

<gen type="advanced_regex" value="(?%{50:A\;\}\:;50:B})"/>
./run escape.tdc
A;}:
A;}:
B
B
A;}:
B

The branch A\;\}\: prints the literal A;}:, and the branch B prints just B.

Where weighted choice works​

Weighted choice sets an exact percentage split, so the runtime has to know how many rows fall into each branch. That count is known everywhere a <gen> can appear:

  • inside a <sequence> — from count, or from a parent subset
  • inside a <case> of a <mix> — from the size of that case

Both are valid:

<sequence name="CountryCode">
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/>
</sequence>

<mix name="Country" percent="50,50">
<case><gen type="advanced_regex" value="(?%{70:RU;30:US})"/></case>
<case><data>-</data></case>
</mix>

There are no generators in the output block — a <line> only formats — so weighted choice never lands there. To print one, declare it as a sequence and interpolate it with ${{Name}}:

<tdc>
<env count="100" seed="demo" inject="${{%}}">
<sequence name="Code">
<gen type="advanced_regex" value="(?%{70:A;30:B})-[0-9]{4}"/>
</sequence>
</env>
<block>
<line><data>code=${{Code}}</data></line>
</block>
</tdc>

The first few rows, with the run splitting exactly 70 A / 30 B:

./run code.tdc (first rows of 100)
code=A-8870
code=B-2495
code=B-1961
code=A-8865
code=A-9221
code=A-3234

The row order is shuffled deterministically by seed; the totals stay exact.

Practical examples​

Customer code by segment — count="1000", count the prefixes:

<gen type="advanced_regex" value="(?%{80:REG;15:VIP;5:TEST})-[A-Z]{2}[0-9]{4}"/>
./run segment.tdc (1000 rows, by prefix)
REG    800
VIP    150
TEST    50

Document with a repeating block — the first three digits and the last three digits always match, while the middle splits 60% A / 40% B (count="100"):

<gen type="advanced_regex" value="([0-9]{3})-(?%{60:A;40:B})-\1"/>
./run doc.tdc (first rows of 100)
924-B-924
419-B-419
788-A-788
692-B-692

Short vs long technical codes — 85% short, 15% long (count="100"):

<gen type="advanced_regex" value="(?%{85:[A-Z]{2}[0-9]{2};15:[A-Z]{4}[0-9]{8}})"/>
./run codes.tdc (100 rows, by length)
length  4    85    (AB42)
length 12    15    (ABCD12345678)

<mix> or advanced_regex?​

Both do exact percentages, for different jobs.

Use <mix> when the branches have different structure — each with its own generators and its own literal text (and <mix percent> gives exact shares while streaming, without holding the column in memory):

<mix name="Kind" percent="70,30">
<case><data>{"type":"regular"}</data></case>
<case><data>{"type":"vip","bonus":true}</data></case>
</mix>

Use advanced_regex when the whole thing fits in one pattern (exact by default; the column is built in RAM — for very large outputs prefer <mix percent>):

<gen type="advanced_regex" value="(?%{70:REG;30:VIP})-[0-9]{6}"/>

Invalid patterns​

<gen type="advanced_regex" value="(?%{70:A;20:B})"/> <!-- sums to 90, needs 100 -->
<gen type="advanced_regex" value="[a-z]+"/> <!-- unbounded, as in plain regex -->

Both are rejected before generation. Here's the error for the second one:

./run bad.tdc
error: invalid advanced_regex generator pattern: unbounded "+"
quantifier is not allowed; use "{1,n}"

Naming a group — (?<name>…)​

A group can carry a name as well as a number:

(?<sex>(?%{50:male;50:female}))

That is an ordinary capturing group — \1 still reads it — with a label. The label exists so that something LATER in the pattern can ask what this group produced, which is what the next section is for.

\k<name> repeats the group by that name, exactly as in regex, and it is the same group \1 repeats:

<gen type="advanced_regex" value="(?<c>(?%{50:AB;50:CD}))-\k<c>"/>

The name must belong to a group that has already closed — the rule \1 and (?if{…}) both follow, since a group further along has produced nothing to repeat.

Names start with a letter or _ and hold letters, digits and _. Two groups cannot share one name: (?if{sex=…}) would then be a coin toss between them. That holds when one of the two sits inside the other, as well as side by side.

Making one part follow another — (?if{…})​

Use it when two parts of the same value have to agree. Everything else in this generator decides a value from randomness alone, which is why a pattern could describe a postcode or an identifier but never a title that matches a sex chosen two characters earlier. That used to mean abandoning advanced_regex and rebuilding the column as a <switch>.

<gen type="advanced_regex" value="(?<sex>(?%{50:male;50:female}))/(?if{sex=male:Mr;sex=female:Ms})"/>
./run titles.tdc (count=8, seed=titles)
female/Ms
male/Mr
male/Mr
female/Ms
male/Mr
female/Ms
female/Ms
male/Mr

Every row agrees with itself. male is never followed by Ms.

How it reads​

(?if{name=value:branch;name=value:branch}). Branches are separated by ;, and each is what to test then : then what to produce. They are tried in the order written and the first match wins.

A branch is a full advanced_regex expression, so weighted choices and further conditionals nest inside it:

<gen type="advanced_regex" value="(?<country>(?%{60:RU;30:US;10:DE}))-(?if{country=RU:[0-9]{3};country=US:[A-Z]{3};*:[A-Z]{2}[0-9]})"/>
./run plates.tdc (count=10, seed=plates)
RU-683
US-NGS
DE-ZQ5
US-VNS
RU-867
RU-722
RU-372
RU-890
US-SEA
RU-589

* is the branch that matches everything else — the DE rows here. Without one, a row that matches no branch produces nothing at all for that part:

(?<c>(?%{50:a;50:b}))-(?if{c=zzz:NEVER}) → a- b- a- b- …

That is deliberate. The pattern said nothing about what an a row should produce, and quietly falling back to the first branch would pair the wrong things in a file that otherwise looks right. Write a * branch to say what you meant.

What it will not do​

  • Read a group declared later. The pattern is built left to right, so a group further along has produced nothing to compare against and the branch could never be taken. Refused, rather than left to produce empty halves.
  • Read anything but a named group — not another column, not ${{…}}. For cross-column logic use <switch> or a parent filter; this is for parts of ONE value.
  • Compare anything but plain text. The test is name=value, an exact match against what the group produced. A value cannot contain : — that is where the branch begins.

It leaves the quota exact​

The conditional reads the weighted choice; it does not disturb it. Over 200 rows (?<c>(?%{70:RU;20:US;10:DE})) still gives exactly 140, 40 and 20 — and each row's second half follows from its own first half.

Unique values — uniq="true"​

uniq="true" gives every row a different string, and the exact shares stay exact:

<sequence name="Plate" uniq="true">
<gen type="advanced_regex" value="(?%{60:77;40:78})[A-Z]{2}[0-9]{3}"/>
</sequence>

Sixty percent of the plates start 77 and forty percent 78, exactly as without uniq, and no two plates are the same. The column is dealt first, share by share; a plate that repeats is then drawn again along the same branches, so a 77 row stays a 77 row and no share moves by a single row.

That makes "is there room?" a question for every share, not only for the pattern. Each share is counted along its own branches — (?%{70:RU;30:US})-[0-9]{3} can make 2 000 strings, and at 2 000 rows its RU share needs 1 400 of the 1 000 that start RU-. The run is refused before anything is redrawn, naming the share by its percentage and both numbers. A share cannot borrow from another without stopping being exact.

A conditional that reads a weighted group keeps agreeing with it: the group is dealt, so the branch that reads it is the same after a redraw. Where a weighted choice stands under a repeat, an | or a conditional, a row's branches depend on its free draws too, and a redraw that wanders onto different branches is thrown away rather than moving a share. Its share then cannot be counted on its own, so the pattern's whole count stands in, and a share that runs dry is reported after a long run of repeats instead of up front.

See also​

  • Regex — the finite constructs this page inherits.
  • Symbol — the named alphabets (\a{name}).
  • regex_max_length in the attribute reference.
  • <mix> — exact percentages across structurally different branches.