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

Тег <distinct>

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

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

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

Показанные ниже вытяжки — то, что даёт типичный запуск; они могут измениться в зависимости от версии ядра и 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> держите каждую сгруппированную последовательность с одним значением.

Две ссылки на один <pool> — сравнение по ЗАПИСИ, а не по значению

Последовательность с <gen type="pool"> — исключение, и как раз тот случай, ради которого группу чаще всего и пишут: двое из одного пула, которые не должны оказаться одним человеком.

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

<pool name="Doctors" count="6">
<sequence name="name" uniq="true"><gen type="text" value="Adams,Brooks,Chase,Dunn,Ellis,Frost"/></sequence>
</pool>
<distinct>
<sequence name="Duty"><gen type="pool" value="Doctors"/></sequence>
<sequence name="Seen"><gen type="pool" value="Doctors"/></sequence>
</distinct>
./run clinic.tdc (6 строк, seed=clinic)
on duty: Chase | seen by: Ellis
on duty: Brooks | seen by: Frost
on duty: Brooks | seen by: Chase
on duty: Dunn | seen by: Brooks
on duty: Ellis | seen by: Frost
on duty: Adams | seen by: Frost

Сравнение идёт по идентичности, а не по фамилии: две записи, у которых совпало какое-то поле, всё равно две разные записи, и группа разводит их именно на этом основании.

Работает вместе с filter=. Фильтр решает, какие записи доступны этой строке, а группа раздаёт каждой ссылке свою из доступных. Оба обещания держатся одновременно.

Три формы отклоняются с TDC302, потому что ни одна из них ничего не значит:

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

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

Своё обещание uniq держит перестановкой, а расстановке нужна вся колонка — по одной строке её не найти. Поэтому именно эта форма идёт на движок в памяти, как и running-сумма; <distinct> решается построчно и потоку не мешает.

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

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

Они решают разные задачи, и один конфиг может использовать оба — на разных полях. На одних и тех же check отклоняет это с TDC267: uniq перетасовывает готовые колонки, не зная, какие пары запретила починка, поэтому починка отменяется. О вертикальном правиле см. Уникальные значения.

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

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

См. также