Иерархические зависимости
Это главная отличительная черта TDC. Обычные fake-генераторы заполняют каждое поле
независимо. TDC умеет связывать последовательности
отношением родитель → потомок: значения потомка вычисляются только на тех
строках, где родитель принял указанное значение, а любые проценты внутри потомка
считаются от размера этого отфильтрованного подмножества, а не от общего count.
Именно это позволяет моделировать реальные зависимые распределения — «мужские имена только у мужчин», «диагноз зависит от пола», «у детей нет супругов» — одним декларативным описанием.
Примеры вывода ниже — иллюстративные. Конкретные значения, которые выдаёт генератор, могут меняться между версиями ядра и сидами; гарантируются же количества и структурные правила (какие строки заполнены, а какие остаются пустыми).
- строки, где родитель выбрал A — потомок под ними всегда 1, 2 или 3
- строки, где родитель выбрал B — потомок всегда 7 или 8
Проблема: независимые поля дают невозможные пары
Чтобы понять, зачем нужен parent, посмотрим, что
происходит без него. Две текстовые последовательности —
страна и город — объявлены независимо:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="Russia,France" percent="50,50"/></sequence>
<sequence name="City"><gen type="text" value="Moscow,Paris" percent="50,50"/></sequence>
</env>
<block><line><data>${{Country}}: ${{City}}</data></line></block>
Russia: Moscow Russia: Moscow France: Paris France: Paris Russia: Moscow France: Paris Russia: Paris France: Moscow
Каждое поле «бросает свой кубик». Проблема — в последних двух строках: Russia: Paris и France: Moscow — Париж в России, Москва во Франции. Для теста, который проверяет, что «город принадлежит стране», такие данные — мусор, и чем больше строк, тем больше невозможных пар.
Решение: parent
Заведём по одному «городскому» генератору на каждую страну и повесим фильтр
parent="Country.Значение". Теперь город выбирается только на строках нужной
страны:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="Russia,France" percent="50,50"/></sequence>
<sequence name="CityRU" parent="Country.Russia"><gen type="text" value="Moscow,Kazan"/></sequence>
<sequence name="CityFR" parent="Country.France"><gen type="text" value="Paris,Lyon"/></sequence>
</env>
<block><line><data>${{Country}}: ${{CityRU}}${{CityFR}}</data></line></block>
Russia: Moscow Russia: Moscow France: Lyon France: Lyon Russia: Kazan France: Paris Russia: Kazan France: Paris
Столбец Country тот же самый (одинаковый seed),
но город теперь всегда согласован: у России только Moscow/Kazan, у Франции только
Paris/Lyon. Невозможные пары исчезли. На каждой строке активен ровно один из двух
городских генераторов, а второй пуст, поэтому ${{CityRU}}${{CityFR}} даёт один город.
Механика
- Объявляем родителя с распределением
(
textсpercent,templateили любой другой тип). - Объявляем потомка с атрибутом
parent="Parent.Value". - Потомок материализуется только на строках, где
Parent == Value. На остальных строках его значение не определено — в интерполяции это пустая строка. - Любые проценты внутри потомка считаются от числа активных строк — от отфильтрованного подмножества.
Имена по полу
Классический случай: мужчинам нужны мужские имена, женщинам — женские. Один общий
генератор имён так не умеет — он не знает про пол строки. Два именных генератора,
каждый под своим parent, умеют:
<env count="8" seed="demo" local="ru">
<sequence name="Gender"><gen type="text" value="Мужчина,Женщина" percent="60,40"/></sequence>
<sequence name="MaleName" parent="Gender.Мужчина"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="FemaleName" parent="Gender.Женщина"><gen type="template" value="person.female.firstName"/></sequence>
</env>
<block><line><data>${{Gender}}: ${{MaleName}}${{FemaleName}}</data></line></block>
Женщина: Анна Мужчина: Пётр Мужчина: Иван Мужчина: Алексей Мужчина: Дмитрий Мужчина: Сергей Женщина: Мария Женщина: Елена
Genderприcount="8"даёт 5Мужчина+ 3Женщина(60/40 от восьми, округление методом наибольшего остатка — см.percent). Приcount="100"вышло бы ровно 60 + 40.MaleNameзаполняется только на мужских строках (шаблонperson.male.firstNameберёт имя из мужского справочника); на женских строках он пуст.FemaleNameсимметрично заполняется только на женских строках.- Склейка
${{MaleName}}${{FemaleName}}оставляет ровно одно имя на строку, и оно всегда соответствует полу — никогда неМужчина: Мария.
Вероятность внутри подмножества
«30 % болельщиков» — 30 % от кого? Если болеть могут только россияне, а вы считаете
30 % от всех, реальная доля болельщиков-россиян уменьшится вдвое. Поставьте
percent на потомка — и он применится к отфильтрованным
строкам:
<env count="100" seed="demo">
<sequence name="Country"><gen type="text" value="RU,US" percent="50,50"/></sequence>
<sequence name="FootballFan" parent="Country.RU"><gen type="text" value="Yes,No" percent="30,70"/></sequence>
</env>
<block><line><data>${{Country}} -> [${{FootballFan}}]</data></line></block>
RU -> [Yes] RU -> [Yes] US -> [] RU -> [No] RU -> [No] US -> [] US -> [] RU -> [No] RU -> [No] RU -> [No]
На US-строках FootballFan пуст — генератор там не срабатывает. Подсчёт по всем 100
строкам, столбец за столбцом:
| Что считаем | Результат |
|---|---|
Country | 50 RU + 50 US |
FootballFan среди RU-строк | 15 Yes + 35 No |
FootballFan среди US-строк | 50 пустых |
Распределение 30/70 посчиталось от 50 RU-строк — «30 % россиян болеют за футбол»,
а не «30 из всех 100» (это дало бы 30 Yes).
Вариация — другой процент, то же подмножество
Меняем только маску потомка на percent="50,50":
<sequence name="FootballFan" parent="Country.RU"><gen type="text" value="Yes,No" percent="50,50"/></sequence>
FootballFan среди RU-строк: 25 Yes + 25 No FootballFan среди US-строк: 50 пустых
Теперь среди тех же 50 RU-строк получается ровно 25 Yes + 25 No — половина
подмножества, а не половина count. US-строки остаются пустыми. Вот почему процент
живёт на потомке: он всегда масштабируется к срезу, отфильтрованному родителем.
Порядок объявления
Родитель обязан быть объявлен раньше потомка в документе. Объявите их в обратном порядке — и рендер падает сразу:
<env count="8" seed="demo">
<sequence name="City" parent="Country.Russia"><gen type="text" value="Moscow,Kazan"/></sequence>
<sequence name="Country"><gen type="text" value="Russia,France" percent="50,50"/></sequence>
</env>
Error: parent sequence "Country" is not declared before this sequence
Ошибка называет строку и столбец. Циклические зависимости и опережающие ссылки не поддерживаются — граф зависимостей разрешается сверху вниз, так что родители всегда идут первыми.
parent без значения
parent="Parent" — без точки и значения — означает «любая строка, где у родителя
вообще есть значение», независимо от того, какое именно. На первом уровне это нужно
редко (у верхнего родителя значение есть всегда), но это инструмент для более глубоких
цепочек, где промежуточная последовательность сама отфильтрована и вам нужен внук
только там, где сработал этот промежуточный уровень.
Здесь Country выбирает US-строки, USCity заполняет только их, а USZip должен
появляться везде, где есть город США — любой из них, — поэтому используется форма
без значения:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="US,UK" percent="50,50"/></sequence>
<sequence name="USCity" parent="Country.US"><gen type="text" value="New York,Chicago"/></sequence>
<sequence name="USZip" parent="USCity"><gen type="regex" value="[0-9]{5}"/></sequence>
</env>
<block><line><data>${{Country}} | ${{USCity}} ${{USZip}}</data></line></block>
US | New York 10021 US | Chicago 60614 UK | UK | US | New York 10021 UK | US | Chicago 60614 UK |
USZip срабатывает на каждой US-строке и больше нигде — parent="USCity.New York"
ограничило бы его одним только Нью-Йорком. Форма без значения говорит «унаследуй фильтр
родителя, не добавляй свой», что как раз и нужно для поля, которое висит на том, что бы
ни выдал родитель.
Взаимодействие с if
Выражения if вычисляются по значениям текущей строки.
На отфильтрованной строке значение потомка не определено — трактуется как пустое, то
есть как ложь. Поэтому условие на столбце-потомке автоматически исключает строки, где
этот потомок не сработал, без явной проверки родителя:
<env count="8" seed="demo">
<sequence name="Country"><gen type="text" value="RU,US" percent="50,50"/></sequence>
<sequence name="FootballFan" parent="Country.RU"><gen type="text" value="Yes,No" percent="30,70"/></sequence>
</env>
<block><line>
<data>${{Country}} ${{FootballFan}}</data>
<data if="FootballFan == Yes"> BUY</data>
</line></block>
RU Yes BUY RU No US US RU No US RU No US
Ticket рендерится только там, где FootballFan == Yes. У US-строк FootballFan
пуст, и сравнение читает его как ложь, поэтому они проходят мимо без Ticket — и вам
ни разу не пришлось писать Country == RU в условии.
Дерево в данных, а не в конфиге
parent связывает одну последовательность с другой. Почти так же часто возникает
другая задача: запись, которая ссылается на другую запись того же вида. Сотрудник, чей
начальник — сотрудник. Комментарий, отвечающий на комментарий. Категория внутри
категории.
Это столбец, а не конструкция, и вся сложность в одном слове: циклы. Цепочка начальников, которая закольцевалась, повесит всё, что попробует её отрисовать, и перерозыгрышем это не лечится — проблема в форме, а не в отдельном значении.
Лечится арифметикой, и ничего нового не нужно. Пусть каждая запись ссылается на id меньше своего:
<env count="10" seed="tree" local="en">
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Back"><gen type="number" value="1..4"/></sequence>
<sequence name="ParentId"><compute><result>
<choose>
<when>
<test><less_than><subtract><to_number><field name="Id"/></to_number><to_number><field name="Back"/></to_number></subtract><int v="1"/></less_than></test>
<then>
<choose>
<when><test><equals><to_number><field name="Id"/></to_number><int v="1"/></equals></test><then><int v="0"/></then></when>
<otherwise><int v="1"/></otherwise>
</choose>
</then>
</when>
<otherwise><subtract><to_number><field name="Id"/></to_number><to_number><field name="Back"/></to_number></subtract></otherwise>
</choose>
</result></compute></sequence>
<sequence name="Author"><gen type="template" value="person.lastName"/></sequence>
</env>
<block>
<line><data>${{Id}},${{ParentId}},${{Author}}</data></line>
</block>
1,0,Smith 2,1,Jones 3,1,Miller 4,1,Garcia 5,3,Davis 6,5,Williams 7,5,Brown 8,7,Johnson 9,7,Martinez 10,7,Rodriguez
Читается как id, parent_id, автор. Back — на сколько строк вверх цепляется запись, от
одной до четырёх, а две ветки <choose> разбирают начало файла: запись 1 получает 0,
это корень, а всё, что дотянулось бы выше него, цепляется к корню.
Что это гарантирует по построению, а не по везению:
- Циклов нет. Каждая стрелка указывает на меньшее число, поэтому обход всегда заканчивается.
- Корень один. Без родителя только запись 1.
- Форма живая. У записи 1 здесь трое детей, а у записи 2 ни одного, потому что
Backразыгрывается на каждой строке. Расширьте до1..20— дерево станет плоским и широким; сузьте до1..2— глубоким и узким.
Тот же столбец годится на оргструктуру, дерево комментариев, спецификацию изделия или
вложенные категории. А что записи говорят — вопрос отдельный: текст комментария это
просто text.paragraph, и связного разговора он изображать
не обязан, чтобы дерево осталось деревом.
См. также
- Последовательности и
parent— механика, стоящая за фильтром. - Детерминизм и пропорции — как
seedиpercentостаются точными. - Связные и реляционные данные — родитель → потомок через поиск по имени.