Детерминизм и пропорции
Данным TDC можно доверять по двум причинам: один и тот же сид воспроизводит те
же данные байт в байт, а доли ложатся в точные пропорции. Обещание точно называет,
что должно совпасть: тот же конфиг, тот же сид, та же версия ядра и тот же режим
вывода. Изменился любой из этих четырёх — байты вправе стать другими; язык, из которого
вы запускаете, в этот список не входит, поэтому пять реализаций сходятся. У конфига,
который спрашивает сегодняшнюю дату, есть пятое условие —
часы. Эта страница
разбирает сразу три атрибута — seed, count и percent, — потому что они работают
вместе: count решает, сколько записей вы получите, seed — какие именно, а
percent фиксирует их пропорции.
Примеры вывода ниже иллюстративны — конкретные имена и числа могут отличаться от версии к версии ядра, но их свойства (воспроизводимость, префиксы, точные количества) остаются в силе.
- 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) выглядят случайно. Запустите документ
дважды подряд — вывод совпадёт байт в байт:
запуск 1 запуск 2 Braylen #2004 Braylen #2004 Amiri #2900 Amiri #2900 Andre #2771 Andre #2771 Izaiah #5951 Izaiah #5951
Ничего не «поплыло». Тот же сид, тот же документ, тот же результат — это и есть детерминизм.
Смените сид → другой, но столь же стабильный набор
Замените сид другим словом — получите другой набор, столь же воспроизводимый.
Тот же документ, только 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, которому не задали
границ, смотрят на часы прямо во время прогона. Сид закрепляет, какие строки вы
получите, но не закрепляет, какое сегодня число. Конфиг, где есть хоть одно из этого,
воспроизводится в пределах суток, а дальше уезжает:
--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":
count=3 count=6
Braylen Braylen
Amiri Amiri
Andre Andre
Izaiah
Zachariah
Saulcount не «сдвигает» данные, а просто продолжает тот же ряд. Поэтому можно
отлаживать на 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" не имеют общего префикса:
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}}:
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>
Первые строки идут вперемешку (порядок зависит от сида):
Женщина Мужчина Женщина Мужчина Женщина Мужчина
А посчитайте все 100 строк — распределение окажется ровным до записи:
Мужчина 60 Женщина 40
Ровно 60 и 40 — не «около 60 %». Это и есть метод Хэмилтона: он раскладывает точно, а случайность оставляет только в порядке строк.
Короткие маски — список процентов может быть короче списка значений
Маска выше — просто percent="60", хотя значений два. Маска короче списка
value разворачивается: заполненные позиции фиксируют
свой процент, а пустые делят остаток до 100 поровну. Это покрывает большинство
реальных случаев минимумом набора:
| Маска | Для 2 / 4 / 5 значений разворачивается как |
|---|---|
60 | 60,40 |
,40 | 60,40 |
,10,10 | 40,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>
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>
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 не указан, кейсы распределяются
равномерно.
См. также
- Text —
percentцеликом, включая короткие маски. - Иерархические зависимости — пропорции внутри подмножества.
- Уникальные значения — другая раскладка «на весь набор», пересчитываемая с
count. - Большие объёмы вывода — как точные пропорции работают при потоковой генерации.