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

Детерминизм и пропорции

Данным TDC можно доверять по двум причинам: один и тот же сид воспроизводит те же данные байт в байт, а доли ложатся в точные пропорции. Обещание точно называет, что должно совпасть: тот же конфиг, тот же сид, та же версия ядра и тот же режим вывода. Изменился любой из этих четырёх — байты вправе стать другими; язык, из которого вы запускаете, в этот список не входит, поэтому пять реализаций сходятся. У конфига, который спрашивает сегодняшнюю дату, есть пятое условие — часы. Эта страница разбирает сразу три атрибута — seed, count и percent, — потому что они работают вместе: count решает, сколько записей вы получите, seedкакие именно, а percent фиксирует их пропорции.

примечание

Примеры вывода ниже иллюстративны — конкретные имена и числа могут отличаться от версии к версии ядра, но их свойства (воспроизводимость, префиксы, точные количества) остаются в силе.

Три прогона одного конфига, по 60 строк.
  • Aпервый прогон с одним сидом
  • Bвторой прогон с тем же сидом — совпадает значение в значение
  • Cдругой сид: данные той же природы, но ни одного совпадающего числа

seed — воспроизводимая случайность

Данные для теста должны выглядеть «случайными», но при этом быть воспроизводимыми: если завтра прогнать тот же документ, должны получиться те же самые записи — иначе не с чем сравнивать баг-репорт, не на что опереться в снапшот-тесте. Обычный «random» этого не даёт: каждый запуск — новый набор. seed даёт: один и тот же сид при одном и том же документе всегда выдаёт ровно тот же вывод.

Тот же вывод — на любом движке

Движков три, и TDC выбирает один по конфигу: по умолчанию быстрый потоковый, точный дисковый — для уникальности, маленький оперативный — под mode="memory" и за объектным API. Все три дают одни и те же значения при одном сиде. Значение строки выводится из (seed, имя колонки, номер строки), поэтому оно не зависит ни от того, какой движок его посчитал, ни от того, что вытянули соседние колонки, ни от числа потоков записи.

Об этом стоит сказать прямо, потому что раньше было не так: движки тянули значения в разном порядке, и один объект отвечал по-разному в зависимости от того, вызвали вы toString() или iterate(). Теперь они совпадают, и каждый общий фикстур проверяется на всех трёх. Как выбирается движок, см. в Больших выводах — это про скорость и память, а не про то, какими будут ваши данные.

seed задаётся на <env>. Значение — любая строка: хеш, слово, число в виде текста; внутри TDC она нормализуется в 128-битный ключ алгоритмом cyrb128. Опция CLI --seed и параметр API { seed } имеют приоритет над атрибутом.

<env count="4" seed="demo" local="en">
<sequence name="Name"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Code"><gen type="number" value="1000..9999"/></sequence>
</env>

И имя (из template), и код (из number) выглядят случайно. Запустите документ дважды подряд — вывод совпадёт байт в байт:

./run demo.tdc — два запуска подряд
запуск 1           запуск 2
Braylen #2004      Braylen #2004
Amiri #2900        Amiri #2900
Andre #2771        Andre #2771
Izaiah #5951       Izaiah #5951

Ничего не «поплыло». Тот же сид, тот же документ, тот же результат — это и есть детерминизм.

Смените сид → другой, но столь же стабильный набор

Замените сид другим словом — получите другой набор, столь же воспроизводимый. Тот же документ, только seed="alpha":

./run demo.tdc (seed=alpha)
Ryland #1695
Leonidas #8152
Jakobe #8337
Jase #3363

Так удобно держать рядом несколько независимых, но воспроизводимых наборов: seed="demo" для одного теста, seed="alpha" для другого — каждый стабилен от запуска к запуску.

Уберите сид → каждый раз заново

Если seed вообще не задан, TDC берёт случайный на каждый запуск, и вывод всякий раз новый. Это полезно, когда нужны свежие тестовые данные и не требуется воспроизвести конкретный вывод, — но возможность позже указать на определённый результат при этом теряется.

Кросс-язычная гарантия

Алгоритм PRNG (cyrb128 + sfc32) выбран так, чтобы один и тот же seed и документ давали идентичные результаты в TypeScript-, Python- и Java-реализациях. Эта портабельность — одна из ключевых гарантий TDC.

Часы — пятый вход

value="today", value="now", person.b_day и генератор date, которому не задали границ, смотрят на часы прямо во время прогона. Сид закрепляет, какие строки вы получите, но не закрепляет, какое сегодня число. Конфиг, где есть хоть одно из этого, воспроизводится в пределах суток, а дальше уезжает:

./run people.tdc — тот же сид, с разницей в год
--now 2026-04-23      --now 2027-04-23
Robert 1988-08-21     Robert 1989-08-21
John 2005-06-13       John 2006-06-13
James 1977-06-16      James 1978-06-16

Имена совпали — имя приходит от одного лишь сида. Даты рождения сдвинулись, потому что возрастное окно отсчитывается назад от сегодняшнего дня.

Запишите часы — и уезжать перестанет:

./run people.tdc --seed demo --now 2026-04-23

Два прогона с одним и тем же --now совпадают байт в байт, а два прогона с разным --now не совпадают. API библиотеки принимает тот же момент в миллисекундах от эпохи: now в TdcOptions (TypeScript), now= у TDC (Python), Options.now(long) (Java), Options.NowMillis (C#), Options.now_millis (Rust). Допустимый синтаксис и остальное про флаг — на --now.

count — сколько записей

count — это сколько раз рендерится блок. Он задаётся на <env>, по умолчанию равен 10 и переопределяется опцией CLI --count или параметром API { count }. Значение — положительное целое число в виде строки.

<env count="1000" seed="demo" local="en">
...
</env>
# Переопределение из CLI:
./run config.tdc --count 50

Важное свойство: короткий прогон — это честный префикс длинного. Большинство генераторов — number, невзвешенный template, counter, regex — вычисляют значение каждой строки из её номера и сида, а не из общего количества. Поэтому первые три строки при count="3" — это ровно первые три при count="6":

./run demo.tdc --count 3 vs --count 6
count=3        count=6
Braylen        Braylen
Amiri          Amiri
Andre          Andre
             Izaiah
             Zachariah
             Saul

count не «сдвигает» данные, а просто продолжает тот же ряд. Поэтому можно отлаживать на count="3", зная, что первые записи будут теми же и на count="1000".

Одно исключение — раскладка «на весь набор»

Генераторы, которые распределяют значения по всему прогону, при смене count пересчитываются — их колонки уже не префикс. Так работают четыре возможности: точные доли (percent на text и на <mix>, метод Хэмилтона), уникальность (uniq), взвешенный пакет template и длины списков у repeat="min..max" — диапазон там задаёт квоту на весь прогон, а не кубик на строку, поэтому 200 строк с repeat="1..4" выходят 50 / 50 / 50 / 50, а 201 строка — 51 / 50 / 50 / 50. Пакеты имён и мест несут частоты по каждому значению, и TDC раскладывает их в точную квоту на весь count, той же механикой, что и percent. Поэтому person.male.firstName пересобирается при смене count, а невзвешенный список вроде location.country остаётся префиксом. Именно счёт «от всего count» и даёт ровные проценты и гарантию уникальности на любом объёме.

Пересчёт виден напрямую. С percent="34,33,33" на трёх значениях прогон count="4" и прогон count="8" не имеют общего префикса:

./run grade.tdc — раскладка percent пересчитана
count=4:   C A A B
count=8:   A C A B A B B C

Первые четыре строки различаются — раскладка перебалансирована под новый итог. Позиционные генераторы (число, невзвешенный шаблон, счётчик, regex) остались бы здесь префиксом; пересобирается механика долей, уникальности, взвешенных пакетов и длин repeat.

Практическое правило: маленький прогон показывает форму, а не строки. Отлаживайтесь на count="3", чтобы проверить формат, пропорции и согласованность полей, — но если в конфиге есть хоть одна из четырёх возможностей выше, не ждите, что третья строка маленького прогона окажется третьей строкой большого.

Встроенные последовательности, зависящие от итога

Встроенные последовательности, которым нужен размер всего прогона, тоже меняются с count_total (сколько строк всего) и _count (номер текущей строки). Тот же документ, отрендеренный как ${{_count}}/${{_total}}: ${{Name}}:

./run demo.tdc --count 3 vs --count 6
count=3            count=6
1/3: Braylen       1/6: Braylen
2/3: Amiri         2/6: Amiri
3/3: Andre         3/6: Andre
                 4/6: Izaiah
                 5/6: Zachariah
                 6/6: Saul

_total честно показывает 3 против 6 — он по определению про весь прогон.

percent — точные пропорции

Добавьте percent к генератору text (или к <mix>) — и доли лягут точно, разложенные методом Хэмилтона (наибольшего остатка): количество вхождений каждого значения гарантированно совпадёт с заданными процентами. Случайность остаётся только в порядке строк.

<sequence name="Gender">
<gen type="text" value="Мужчина,Женщина" percent="60"/>
</sequence>

Первые строки идут вперемешку (порядок зависит от сида):

./run gender.tdc — первые строки
Женщина
Мужчина
Женщина
Мужчина
Женщина
Мужчина

А посчитайте все 100 строк — распределение окажется ровным до записи:

./run gender.tdc (count=100)
Мужчина  60
Женщина  40

Ровно 60 и 40 — не «около 60 %». Это и есть метод Хэмилтона: он раскладывает точно, а случайность оставляет только в порядке строк.

Короткие маски — список процентов может быть короче списка значений

Маска выше — просто percent="60", хотя значений два. Маска короче списка value разворачивается: заполненные позиции фиксируют свой процент, а пустые делят остаток до 100 поровну. Это покрывает большинство реальных случаев минимумом набора:

МаскаДля 2 / 4 / 5 значений разворачивается как
6060,40
,4060,40
,10,1040,40,10,10
,,25,,18.75,18.75,25,18.75,18.75
46,46,13.5,13.5,13.5,13.5

Правила: если маска полная, её числа должны давать в сумме 100. Если в ней есть пустые позиции, сумма заполненных чисел должна быть не больше 100 (остаток делится по пустым местам).

Доли, не делящиеся нацело

Три равные оценки на 100 строк — это по 33.33 % на каждую, но «треть от 100» — не целое число. Хэмилтон отдаёт лишнюю запись той доле, у которой самый большой остаток, так что сумма по-прежнему ровно count:

<sequence name="Grade"><gen type="text" value="A,B,C" percent=",,"/></sequence>
./run grade.tdc (count=100)
A   34
B   33
C   33

34 + 33 + 33 = 100 — ни одна запись не потеряна и не задвоена. Явные доли ведут себя так же буквально:

<sequence name="Tier"><gen type="text" value="gold,silver,bronze" percent="50,30,20"/></sequence>
./run tier.tdc (count=100)
gold      50
silver    30
bronze    20

Малые количества — сумма всё равно держится

При малом count доли округляются, но их сумма всегда равна count. С percent="50,50" и count="3" вы получите либо 2 + 1, либо 1 + 2 (кому достанется лишняя запись — зависит от сида), но никогда 1 + 1 или 2 + 2. Пропорция приближается; количество не бывает неверным.

Внутри подмножества — percent с parent

Когда у последовательности есть parent, проценты считаются внутри отфильтрованного подмножества, а не по всему прогону. Деление активных пользователей 70/30 — это 70/30 от строк этого родителя, вычисленные независимо для каждой группы. На этом строятся иерархические зависимости.

На <mix>

percent управляет и <mix>, где длина маски сравнивается с числом вложенных веток <case>, а не со списком значений. Если percent не указан, кейсы распределяются равномерно.

См. также