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

Тег <distinct>

Когда применять — когда два поля в одной строке тянут из одного пула и не должны совпасть по значению: два симптома одного пациента — ни у кого не бывает температуры и температуры; страна рождения и страна проживания, которые не должны оказаться одинаковыми. <distinct> говорит: его прямые дети в одной строке должны получиться разными между собой.

Это горизонтальный механизм — он смотрит поперёк полей одной строки. Его вертикальный близнец — uniq: он не даёт всей строке повториться где-либо в датасете. Эти два механизма полностью независимы — используйте любой из них или оба сразу.

Примеры вывода иллюстративны

Показанные ниже вытяжки — то, что даёт типичный запуск; они могут измениться в зависимости от версии ядра и seed. Неизменной остаётся структура, которую гарантирует <distinct>: никакие два ребёнка группы никогда не равны друг другу в пределах строки.

Сколько раз выпало каждое сочетание двух полей. По горизонтали первое поле, по вертикали второе.
  • Aс distinct на 60 строках: диагональ пуста, потому что внутри строки поля не могут совпасть
  • Bс uniq на 6 строках: ни в одной клетке не больше единицы, потому что сочетание не может повториться между строками — а пустые клетки это сочетания, до которых прогон просто не дошёл

Проблема: два поля сталкиваются

Возьмём два симптома одного пациента из одного списка. Два поля <gen> работают независимо, так что рано или поздно на какой-то строке выпадает одно и то же слово дважды — а «лихорадка, лихорадка» это не карточка пациента, это дефект. Здесь оба поля используют короткий список из четырёх, поэтому совпадения видны сразу:

<sequence name="Case">
<gen name="S1" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
<gen name="S2" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
</sequence>
...
<data>${{Case.S1}}, ${{Case.S2}}</data>
./run case.tdc (8 строк)
Тошнота, Головная боль
Головная боль, Головная боль
Лихорадка, Кашель
Тошнота, Тошнота
Головная боль, Лихорадка
Кашель, Тошнота
Кашель, Лихорадка
Лихорадка, Кашель

Строки 2 и 4 — Головная боль, Головная боль и Тошнота, Тошнота: это не пациент с двумя жалобами, а пациент с одной жалобой, записанной дважды.

Решение: обернуть поля

Оборачиваем оба поля в <distinct>. Всё остальное как было — те же генераторы, тот же список, даже тот же seed:

<sequence name="Case">
<distinct>
<gen name="S1" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
<gen name="S2" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
</distinct>
</sequence>
./run case.tdc (8 строк)
Тошнота, Головная боль
Головная боль, Тошнота
Лихорадка, Кашель
Тошнота, Кашель
Головная боль, Лихорадка
Кашель, Тошнота
Кашель, Лихорадка
Лихорадка, Кашель

Что мы получаем. Строки, где совпадения не было, остались байт-в-байт теми же — движок их не трогал. Починены только два столкновения: Головная боль, Головная боль стало Головная боль, Тошнота, а Тошнота, ТошнотаТошнота, Кашель. Перетянуто только второе поле и только там, где было нужно. Порядок и seed сохранены.

Два уровня

<distinct> работает в двух местах, и правило на обоих одно: прямые дети <distinct> дают разные значения в каждой строке. Меняется лишь то, что считается «ребёнком».

1. Внутри <sequence> — оборачивает поля <gen>

Здесь <distinct> стоит внутри <sequence> и оборачивает поля <gen name="…">. Читается как «A ≠ B». Здесь данные берутся из настоящего списка симптомов в пакете, а не из написанной вручную четвёрки:

<sequence name="Case">
<distinct>
<gen name="A" type="template" value="medical.symptom"/>
<gen name="B" type="template" value="medical.symptom"/>
</distinct>
</sequence>
./run case.tdc (6 строк) — A + B
Рвота + Головокружение
Потеря аппетита + Рвота
Затуманенное зрение + Насморк
Отёк + Лихорадка
Учащённое сердцебиение + Онемение
Затуманенное зрение + Одышка

Почему здесь: оба поля тянут из одного пула template, но два значения в каждой строке всегда разные. Это самый частый случай — два атрибута одной сущности с общим источником, которые не должны совпадать. Заметьте, что значение всё ещё может повторяться по столбцу (Затуманенное зрение в строках 3 и 6): <distinct> смотрит поперёк строки, а не вдоль набора.

2. Внутри <env> — оборачивает целые блоки <sequence>

На верхнем уровне <distinct> стоит внутри <env> и оборачивает целые блоки <sequence>. Классический пример — «страна рождения» против «страны проживания»: две независимые вытяжки из одного списка стран иногда совпадают в одной строке.

<env count="100" seed="s">
<distinct>
<sequence name="Birth"><gen type="template" value="location.country"/></sequence>
<sequence name="Live"><gen type="template" value="location.country"/></sequence>
</distinct>
</env>
./run migration.tdc (6 строк) — Birth → Live
Франция     → Бутан
Панама      → Чили
Черногория  → Япония
Камерун     → Кения
Перу        → Намибия
Гватемала   → Гренада

Почему здесь: теперь два значения живут в разных последовательностях, поэтому ни одна отдельная последовательность не может их сравнить — группа должна стоять уровнем выше, в <env>. Страна рождения и страна проживания в любой строке не совпадут.

Как это работает

Движок генерирует поля как обычно. Если два значения внутри группы в строке столкнулись, он перетягивает одно из них следующим значением генератора, повторяя, пока они не станут разными. Детерминизм сохраняется: перетяжки идут в фиксированном порядке, поэтому вывод для заданного seed не меняется. Это работает и в движке памяти, и в стриминге — см. Большие объёмы.

Детали и подводные камни

Каждый из этих пунктов стоит держать в голове:

Сравниваются значения, а не источники

<distinct> смотрит на получившуюся строку, а не на то, откуда она взялась. Если два поля читают из разных файлов, но выдали одно и то же слово, это всё равно считается столкновением, и одно из них перетягивается.

Группы независимы

Групп <distinct> может быть несколько, и они не мешают друг другу. Группа для двух симптомов и отдельная группа, скажем, для двух телефонов — каждая обеспечивает своё правило без всякого пересечения.

<sequence name="Case">
<distinct>
<gen name="A" type="template" value="medical.symptom"/>
<gen name="B" type="template" value="medical.symptom"/>
</distinct>
<distinct>
<gen name="HomePhone" type="regex" value="\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{4}"/>
<gen name="CellPhone" type="regex" value="\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{4}"/>
</distinct>
</sequence>

Почему: каждая группа ограничивает только своё. Два симптома никогда не равны друг другу; домашний и мобильный телефон никогда не равны друг другу; но симптом свободно может совпасть с телефоном, который случайно отрендерился так же, — две группы не видят одна другую.

Поля вне <distinct> остаются без ограничений

Ограничены только прямые дети группы <distinct>. Любое поле, оставленное снаружи, генерируется как обычно и может свободно повторить значение, которое выдало distinct-поле.

Слишком мало значений — честная ошибка

Если в списке меньше разных значений, чем полей, которые должны различаться, — скажем, одно слово на два поля, — ограничение выполнить невозможно. Чтобы не крутиться в цикле вечно, TDC сдаётся после 1000 попыток на строке и говорит об этом. В отличие от uniq, который доказывает выполнимость до генерации, здесь отказ происходит уже во время прогона — быстро, но не до его начала:

./run person.tdc
tdc: stream mode: <distinct> in sequence "Person": could not find
a value for field "B" different from the others after 1000 attempts
— its source likely has too few distinct values.

Почему: невыполнимый запрос должен падать громко, а не зависать. От uniq отличается момент: uniq доказывает выполнимость для всего столбца до генерации, а <distinct> узнаёт об этом на первой же строке, которую не может удовлетворить.

На уровне <env> группа принимает только последовательности с одним значением

<distinct> внутри <env> может оборачивать только последовательности с одним значением — простой <gen>, <mix> или <switch>. У составной (многополевой) последовательности нет единственного значения для сравнения, поэтому положить её в группу нельзя — ошибка TDC129:

./run migration.tdc
error[TDC129]: <sequence name="Person"> inside a config-level
<distinct> must produce a single value
note: A <distinct> around sequences uses one value per sequence.
Use a simple <gen> or a <switch> sequence, not a compound
(multi-field) one.

Почему: горизонтальному правилу нужно по одному значению на каждого ребёнка для сравнения. Внутри последовательности оборачивайте поля <gen> напрямую (форма уровня 1 выше); на уровне <env> держите каждую сгруппированную последовательность с одним значением.

<distinct> и uniq — коротко о разнице

МеханизмОсьОбластьСмысл
<distinct>горизонтальнаяодна строкаполя не равны друг другу в пределах строки
uniqвертикальнаявсе строкисочетание полей никогда не повторяется

Они решают разные задачи и свободно сочетаются — строка может требовать, чтобы два её поля с именами различались, и чтобы вся пара (имя, фамилия) была уникальной по всему датасету. О вертикальном правиле см. Уникальные значения.

Может содержать

ТегГдеЧто содержит
<gen/>внутри <sequence>Поля, которые должны различаться
<sequence>внутри <env>Последовательности, которые должны различаться

См. также