Перейти к основному содержимому

Генератор advanced_regex

Используйте его, когда нужно всё, что умеет regex, но сама форма строки несёт статистическое распределение вариантов — например, ровно 70% кодов начинаются с RU, 20% — с US, 10% — с DE.

Обычный regex выбирает альтернативу (RU|US|DE) случайно, поэтому разбивка сходится только в среднем. advanced_regex добавляет взвешенный выбор (weighted choice), который раскладывает варианты на точные количества.

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

advanced_regex — надмножество regex: обычный генератор type="regex" остаётся стабильным и TDC-нейтральным, а advanced_regex добавляет TDC-специфичную мощь поверх того же конечного, переносимого движка. Сегодня эта дополнительная мощь — ровно одна конструкция, взвешенный выбор, а всё остальное на этой странице унаследовано без изменений из Regex.

Зачем это нужно​

Без взвешенного выбора задачу «70% RU, 20% US, 10% DE, у каждого после префикса ещё шесть случайных цифр» пришлось бы описывать несколькими последовательностями или через <mix>. С advanced_regex это сворачивается в один генератор. Он естественно подходит для:

  • кодов стран, филиалов, регионов или типов клиентов;
  • тестовых идентификаторов с разной структурой, но фиксированными пропорциями;
  • номеров документов, где одна часть строки должна повторять другую;
  • синтетических данных, где доли важны не меньше самих значений;
  • AI-генерации .tdc, когда агенту проще выдать один компактный паттерн, чем дерево последовательностей.

Примеры вывода ниже иллюстративны — конкретные строки зависят от сида и могут слегка меняться между версиями ядра, — но количества, которые обещает распределение, точны.

Weighted-choice​

Конструкция такая:

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

Разбор по частям:

(?%{ 70:RU ; 20:US ; 10:DE })
│ │ │ │ │ │ │ │
│ │ ветка │ ветка │ ветка │
│ процент процент процент │
└── начало weighted-choice ┘

Правила:

  • проценты должны быть числами и неотрицательными;
  • сумма процентов должна быть равна 100;
  • ветка может быть пустой;
  • ветка сама является полноценным выражением advanced_regex (значит, они вкладываются);
  • ;, } и : — управляющие символы; экранируйте их (см. раздел «Экранирование внутри weighted-choice»), если они нужны в ветке буквально.

Точные проценты по умолчанию​

Это главное: взвешенный выбор обещает точные проценты (ровно 70 из 100, а не «примерно 70»), и TDC выдаёт их из коробки — специально ничего включать не нужно.

Чтобы отмерить разбивку точно, движок строит весь столбец разом и раздаёт ветки по алгоритму Гамильтона (наибольшего остатка). Он делает это автоматически, как только в паттерне встречается (?%{…}) — даже в режиме по умолчанию disk. От вас нужен только паттерн, доли получатся ровными.

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

Считаем префиксы на всех 100 строках — ровно 70/20/10:

./run countries.tdc (100 строк, по префиксу)
RU   70
US   20
DE   10
Цена точности — память

Чтобы отмерить разбивку точно, столбец с взвешенным выбором строится целиком в оперативной памяти. Для небольших и средних наборов это норма. Если нужны точные доли при потоковой генерации (память O(1), выгрузка любого размера) — их так же дают <mix percent> и <gen type="text" percent="…">: они отдают точные проценты потоком, не держа весь столбец в памяти. А если вы вручную форсируете чисто потоковый движок (mode="stream" на <env> — легаси-алиас, который существует только как атрибут, — или --engine 2 в командной строке; --mode stream не бывает), TDC не станет молча портить проценты — посчитать их построчно нельзя, поэтому он честно откажется с понятной ошибкой. Уберите форс — и всё снова точно.

Веса действительно сдвигают распределение​

Проценты — не украшение, а реальный состав столбца. Возьмём один паттерн (?%{…})-[0-9]{2} при count="1000", поменяем только веса и посчитаем префиксы:

<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/> <!-- вариант 1 -->
<gen type="advanced_regex" value="(?%{34:RU;33:US;33:DE})-[0-9]{2}"/> <!-- вариант 2 -->
<gen type="advanced_regex" value="(?%{10:RU;10:US;80:DE})-[0-9]{2}"/> <!-- вариант 3 -->
./run weights.tdc (1000 строк, по префиксу)
веса             RU     US     DE
70 / 20 / 10     700    200    100
34 / 33 / 33     340    330    330
10 / 10 / 80     100    100    800

На 1000 строк количества повторяют веса один в один. Поменяли веса — поменялся состав столбца.

regex против advanced_regex​

Возможностьregexadvanced_regex
Сгенерировать строку по паттернудада
Character classesдада
Именованные Unicode-алфавитыдада
Groups и backreferencesдада
Ограничение длины через regex_max_lengthдада
Точные проценты внутри паттернанетда
Вкладывать процентные вариантынетда
weighted-choice в блоке выводанетнет

Разницу проще всего увидеть рядом. Обычный regex выбирает каждый символ свободно — форма фиксирована, доли нет:

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

advanced_regex сохраняет ту же природу кода, но закрепляет доли префиксов — 70% RU, 20% US, 10% DE на всём прогоне:

<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{6}"/>
./run coded.tdc (первые 8 из 100 строк)
RU-441627
RU-476822
RU-948319
US-450875
RU-398584
RU-131212
RU-418648
RU-830959

Коротко: используйте regex для строки заданной формы; используйте advanced_regex, когда сама форма несёт статистическое распределение.

Наследует весь regex-язык​

Каждая конечная конструкция из regex работает и здесь — литералы, экранирование, character classes, BMP-диапазоны, именованные алфавиты \a{…}, \d/\w/\s и их инверсии, ., alternation, groups, backreferences, ограниченные квантификаторы и ограничение regex_max_length. Простая взвешенная разбивка над обычными латинскими кодами:

<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-демо. Поскольку ветки принимают именованные алфавиты, можно держать точные доли между письменностями. Здесь 7 из 10 кодов получают кириллический префикс, а 3 — латинский; это намеренный пример по Unicode/локализации, показывающий, что механика точных процентов не зависит от письменности:

<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

Вложенные weighted-choice​

Поскольку ветки — это полноценные выражения, взвешенные выборы вкладываются, и внутренняя разбивка считается внутри подмножества, которое попало во внешнюю ветку:

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

При count="100":

./run nested.tdc (100 строк)
AX   40
AY   10
B    50

80% от 50 строк A — это 40 строк AX; 20% — это 10 строк AY. Это поведение «проценты внутри подмножества» в точности совпадает с философией иерархии последовательностей в TDC.

Несколько weighted-choice в одном паттерне​

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

Каждое распределение отмеряется точно на текущих строках. При count="100":

./run two.tdc (100 строк)
M-99   44
F-99   31
M-00   16
F-00    9

Сложите по частям — и оба точны: M = 44 + 16 = 60 и F = 31 + 9 = 40 (60/40); 99 = 44 + 31 = 75 и 00 = 16 + 9 = 25 (75/25). Два независимых выбора, каждый разложен тем же методом точных процентов.

С parent-фильтром​

advanced_regex живёт внутри обычной модели зависимостей последовательностей. Если последовательность отфильтрована через parent, проценты считаются только внутри отфильтрованного подмножества:

<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>

При count="100" (50 мужских строк):

./run parent.tdc (100 строк)
F      50    (MaleCode пустой)
M-A    20
M-B    30

Разбивка 40/60 отмерена от 50 отфильтрованных строк, а не от всех 100.

Захваты и backreferences​

Backreference повторяет уже сгенерированную группу. Это работает точно так же, как в обычном regex — первые три цифры повторяются в конце:

<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

Взвешенную ветку можно захватить и повторить через \1 — захваченная часть воспроизводится дословно, и проценты сохраняются. При count="40":

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

Захваченная пара повторяется буквально, а разбивка 25/75 сохраняется.

Захват, сделанный до взвешенного выбора, можно использовать внутри ветки. Здесь половина строк повторяет захваченные две буквы, половина печатает фиксированное 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

Backreference может жить внутри ветки. Где взята ветка (A[0-9]), \1 повторяет её захват; где взята ветка B, захвата нет, поэтому \1 пустой (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-

Это ещё не полноценный if, но уже полезная логическая связь: одна часть строки может зависеть от уже сгенерированной группы.

Экранирование внутри weighted-choice​

;, } и : — управляющие символы взвешенного выбора. Чтобы использовать их как буквальный текст в ветке, экранируйте их (count="6"):

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

Ветка A\;\}\: печатает буквальные A;}:, а ветка B печатает просто B.

Где работает взвешенный выбор​

Взвешенный выбор задаёт точную процентную разбивку, поэтому среда выполнения должна знать, сколько строк придётся на каждую ветку. Это известно везде, где вообще может стоять <gen>:

  • внутри <sequence> — из count или подмножества parent;
  • внутри <case> у <mix> — из размера этого кейса.

Оба варианта валидны:

<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>

В блоке вывода генераторов нет вовсе — <line> только форматирует, — поэтому взвешенный выбор туда не попадает. Чтобы вывести его в строке, объявите его как последовательность и вставьте через интерполяцию ${{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>

Первые строки, где прогон делится ровно 70 A / 30 B:

./run code.tdc (первые строки из 100)
code=A-8870
code=B-2495
code=B-1961
code=A-8865
code=A-9221
code=A-3234

Порядок строк детерминированно перемешан по seed; итоги остаются точными.

Практические примеры​

Код клиента по сегменту — count="1000", считаем префиксы:

<gen type="advanced_regex" value="(?%{80:REG;15:VIP;5:TEST})-[A-Z]{2}[0-9]{4}"/>
./run segment.tdc (1000 строк, по префиксу)
REG    800
VIP    150
TEST    50

Документ с повторяющимся блоком — первые три цифры и последние три цифры всегда совпадают, а середина делится 60% A / 40% B (count="100"):

<gen type="advanced_regex" value="([0-9]{3})-(?%{60:A;40:B})-\1"/>
./run doc.tdc (первые строки из 100)
924-B-924
419-B-419
788-A-788
692-B-692

Короткие и длинные технические коды — 85% коротких, 15% длинных (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 строк, по длине)
длина  4    85    (AB42)
длина 12    15    (ABCD12345678)

<mix> или advanced_regex?​

Оба умеют точные проценты, но для разных задач.

Используйте <mix>, когда у веток разная структура — свой набор генераторов и литерального текста в каждой (а ещё <mix percent> даёт точные доли потоком, не держа столбец в памяти):

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

Используйте advanced_regex, когда всё укладывается в один паттерн (точно по умолчанию; столбец строится в памяти — для очень больших выгрузок берите <mix percent>):

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

Невалидные паттерны​

<gen type="advanced_regex" value="(?%{70:A;20:B})"/> <!-- сумма 90, нужна 100 -->
<gen type="advanced_regex" value="[a-z]+"/> <!-- бесконечная длина, как и в обычном regex -->

Обе отклоняются ещё до генерации — например, вторая:

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

Имя для группы — (?<name>…)​

У группы кроме номера может быть имя:

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

Это обычная захватывающая группа — \1 по-прежнему её читает — только с ярлыком. Ярлык нужен, чтобы что-то ДАЛЬШЕ по шаблону могло спросить, что эта группа выдала; об этом следующий раздел.

\k<name> повторяет группу по этому имени — так же, как в regex, — и это та же самая группа, которую повторяет \1:

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

Имя должно принадлежать группе, которая уже закрылась: это то же правило, которому следуют \1 и (?if{…}) — группа дальше по шаблону ещё ничего не выдала, чтобы это повторять.

Имя начинается с буквы или _ и состоит из букв, цифр и _. Двум группам одно имя нельзя: тогда (?if{sex=…}) стало бы броском монетки между ними. Это верно и когда одна из двух групп находится внутри другой, а не рядом с ней.

Чтобы одна часть следовала за другой — (?if{…})​

Когда нужно: две части одного значения обязаны согласовываться. Всё остальное в этом генераторе решает значение из одной только случайности — поэтому шаблон мог описать индекс или идентификатор, но не обращение, которое совпадает с полом, выбранным двумя символами раньше. Раньше это означало бросить advanced_regex и пересобирать колонку через <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

Каждая строка согласована сама с собой. За male никогда не идёт Ms.

Как это читается​

(?if{имя=значение:ветка;имя=значение:ветка}). Ветки разделяются ;, каждая — это что проверяем, потом :, потом что выдаём. Они пробуются в написанном порядке, побеждает первая совпавшая.

Ветка — полноценное выражение advanced_regex, поэтому внутрь вкладываются и взвешенные выборы, и другие условия:

<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

* — ветка «во всех остальных случаях», здесь это строки DE. Без неё строка, не подошедшая ни под одну ветку, выдаёт на этом месте вообще ничего:

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

Так задумано. Шаблон ничего не сказал про то, что должна выдать строка a, и тихий откат к первой ветке склеил бы неправильные пары в файле, который в остальном выглядит нормально. Напишите ветку *, чтобы сказать, что вы имели в виду.

Чего оно не делает​

  • Не читает группу, объявленную позже. Шаблон собирается слева направо, поэтому группа дальше по строке ещё ничего не выдала, и ветку нельзя было бы взять никогда. Это отклоняется, а не оставляется выдавать пустые половинки.
  • Не читает ничего, кроме именованной группы — ни другую колонку, ни ${{…}}. Для логики между колонками есть <switch> и фильтр parent; это же — про части ОДНОГО значения.
  • Не сравнивает ничего, кроме текста. Проверка — это имя=значение, точное совпадение с тем, что выдала группа. Значение не может содержать : — там начинается ветка.

Квоты остаются точными​

Условие читает взвешенный выбор, но не трогает его. На 200 строках (?<c>(?%{70:RU;20:US;10:DE})) по-прежнему даёт ровно 140, 40 и 20 — и вторая половина каждой строки следует из её собственной первой.

Уникальные значения — uniq="true"​

uniq="true" даёт каждой строке своё значение, а точные доли остаются точными:

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

Шестьдесят процентов номеров начинаются с 77 и сорок — с 78, ровно как без uniq, и двух одинаковых номеров нет. Сначала колонка раскладывается по долям; номер, который повторился, тянется заново по тем же веткам, так что строка с 77 остаётся строкой с 77, и ни одна доля не сдвигается ни на строку.

Поэтому вопрос «хватит ли места» задаётся каждой доле, а не только образцу. Каждая доля считается по своим веткам: (?%{70:RU;30:US})-[0-9]{3} даёт 2 000 строк, и на 2 000 строк доле RU нужно 1 400 из той тысячи, что начинается с RU-. Прогон откажет раньше, чем что-то вытянет заново, и назовёт долю по её проценту и оба числа. Доля не может занять у другой и остаться точной.

Условие, которое читает группу с долями, по-прежнему с ней согласно: группа разложена заранее, и ветка, которая её читает, после перетяжки та же. Если выбор с долями стоит внутри повтора, | или условия, ветки строки зависят и от её свободных выборов, и перетяжка, которая забрела на другие ветки, отбрасывается, а не сдвигает долю. Такую долю не сосчитать отдельно, поэтому за неё отвечает общий счёт образца, и нехватка в ней выясняется после долгой череды повторов, а не заранее.

См. также​

  • Regex — конечные конструкции, которые наследует эта страница.
  • Symbol — именованные алфавиты (\a{name}).
  • regex_max_length в справочнике атрибутов.
  • <mix> — точные проценты для структурно разных веток.