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

Несколько значений в ячейке (repeat)

Когда пригодится — когда поле не всегда содержит одно значение. В заказе один товар или пять. У статьи один тег или четыре. Датчик за раз отдаёт три показания. repeat заставляет один <gen> выдать в ячейку несколько значений вместо одного — и количество может меняться от строки к строке.

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

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

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

Кратко

ЗаписьЧто означает
repeat="3"ровно три значения
repeat="1..5"от одного до пяти — каждой длине достаётся точная доля строк
repeat="0..3"может выпасть и ноль — ячейка останется пустой
separator=" "чем склеивать; по умолчанию запятая

Верхняя граница — 64. separator без repeat — ошибка: склеивать нечего.

Диапазон — это квота, а не подбрасывание монетки

repeat="1..4" не кидает кубик на каждую строку. TDC выдаёт каждой длине точную долю прогона — так же, как это делает percent: 200 строк выходят 50 / 50 / 50 / 50. Когда count не делится нацело, остаток достаётся одной из длин, а какой именно — решает сид, так что 201 строка даёт 51 / 50 / 50 / 50 в каком-то порядке. Сумма длин всегда равна count — именно поэтому и пропорции значений остаются точными — см. раздел «Списки слов и точные пропорции» ниже.

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

repeat="1..5" — количество, меняющееся от строки к строке

Это повседневный случай. Строка заказа должна содержать столько товаров, сколько в заказе оказалось. Здесь каждая корзина берёт от одного до четырёх артикулов, склеенных пробелами:

<env count="8" seed="basket-7">
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Items">
<gen type="regex" value="SKU-[0-9]{4}" repeat="1..4" separator=" "/>
</sequence>
</env>
<block>
<line><data>заказ ${{Id}}: ${{Items}}</data></line>
</block>
./run basket.tdc (count=8)
заказ 1: SKU-5365 SKU-2241 SKU-0758 SKU-9382
заказ 2: SKU-2033 SKU-3412 SKU-3799
заказ 3: SKU-3278
заказ 4: SKU-3984 SKU-4578
заказ 5: SKU-5351 SKU-5903
заказ 6: SKU-3412
заказ 7: SKU-0258 SKU-3326 SKU-1157
заказ 8: SKU-3205 SKU-4821 SKU-3618 SKU-2450

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

repeat="3" — фиксированное количество значений

Задайте одно целое число — и каждая ячейка будет содержать ровно столько значений. Удобно для токенов фиксированной формы: код из трёх групп, тройка (x, y, z), набор из трёх показаний:

<sequence name="Code">
<gen type="regex" value="[A-Z]{2}" repeat="3" separator="-"/>
</sequence>
./run code.tdc (count=6)
QR-LM-ZP
BX-TT-KD
WM-AE-RH
NP-CC-JU
FL-GO-VS
DK-HY-QN

Почему: когда форма фиксирована и меняются только значения, repeat="3" понятнее, чем три отдельных поля, склеенные в блоке вывода.

repeat="0..3" — можно и ноль

Диапазон, который начинается с 0, позволяет ячейке выйти пустой. Это как раз то, что нужно для необязательного списка: у части статей тегов нет вовсе.

<sequence name="Tags">
<gen type="text" value="news,tech,sport,food,travel" repeat="0..2" separator=", "/>
</sequence>
./run tags.tdc (count=6)
статья 1: tech, news
статья 2:
статья 3: sport
статья 4:
статья 5: travel, food
статья 6: news

Статьи 2 и 4 вытянули длину ноль, поэтому их ячейка пустая — не какое-то особое значение, просто пустой список. Почему: «иногда есть, иногда нет» — реальная форма данных, и 0..n описывает её без второй последовательности.

lengths= — какая доля строк получает какое число значений

Без него все длины равновероятны, и «равно» здесь точно, а не приблизительно: длины раскладываются квотой, поэтому repeat="0..5" даёт каждой из шести длин ровно шестую часть строк, вообще без случайного разброса.

Настоящие данные «один ко многим» так не выглядят никогда. Заказы на клиента, приёмы на пациента, транзакции на счёт: у большинства родителей один-два потомка, у немногих двадцать. lengths= задаёт долю строк для каждого числа значений, начиная с наименьшего:

<gen type="number" value="100..999" repeat="0..5" lengths="40,25,15,10,7,3" separator=";"/>
./run orders.tdc (8 строк)
[896;648;701;334]
[]
[765]
[342;706]
[447;991;569]
[]
[148]
[]

На 20 000 строках доли выходят до сотых, потому что это квота, а не жребий:

./run orders.tdc (20 000 строк, посчитано)
0: 40.00%  1: 25.00%  2: 15.00%  3: 10.00%  4: 7.00%  5: 3.00%

По одной доле на каждую возможную длину, и в сумме ровно 100 — та же арифметика, что у percent=. Пять долей на шесть длин получают отказ, а не починку: конфиг, написанный так, имел в виду какую-то форму, и угадывать, какую из шести забыли, — не дело движка.

Точно, в отличие от нарисованного распределения

lengths= — это квота: 40% значит 40.00%. Форма, нарисованная через <gen type="pattern" mode="density">, воспроизводит форму с точностью до долей процента — верный инструмент для «примерно так» и неверный для «ровно столько».

separator= — чем склеиваются значения

Без separator значения склеиваются запятой. Задайте любую строку под формат, который вы собираете: пробел, вертикальную черту, " | ", "; " — что нужно колонке.

<sequence name="Letters">
<gen type="text" value="a,b,c,d,e" repeat="3"/> <!-- по умолчанию запятая -->
</sequence>
<sequence name="Piped">
<gen type="text" value="a,b,c,d,e" repeat="3" separator=" | "/>
</sequence>
./run sep.tdc (count=3) — по умолчанию и через черту
d,a,c        d | a | c
b,e,b        b | e | b
a,c,e        a | c | e

Почему: склеенная ячейка — это просто текст, поэтому разделитель — это то, чем вы подгоняете её под колонку CSV, под встроенный JSON-подобный список или под читаемую человеком строку.

accumulate= — нарастающий итог по списку

Значения в ячейке часто оказываются шагами одного и того же, а не тремя независимыми розыгрышами: строки чека, отрезки маршрута, минуты сессии. accumulate= заменяет список его нарастающим итогом.

<gen type="number" value="150..900" decimals="2" repeat="3" separator=", " accumulate="sum"/>
./run cart.tdc
239.10, 568.84, 809.63
791.92, 1059.68, 1593.11
473.43, 785.34, 1006.51

Слева тот же генератор без accumulate=, справа — с ним. Последний элемент это итог, а каждый перед ним — промежуточная сумма на этом шаге.

accumulate=Каждый элемент становится
sumсуммой всего до него включительно
maxнаибольшим из встреченных — «рекорд на этот момент»
minнаименьшим из встреченных
./run peaks.tdc
пик 22,22,22,83,83
пик 11,48,54,54,54
пик 57,60,62,62,93

Арифметика точная, включая копейки. Сумма считается на целых числах, умноженных на самую широкую дробь списка, и ни разу не на плавающей точке — поэтому 19.99 + 0.01 это 20.00, а не 20.000000000000004, и это одно и то же 20.00 во всех пяти реализациях.

min и max возвращают уже существующий элемент, поэтому его написание сохраняется. Значение, разыгранное как 007, остаётся 007, а не превращается в 7.

Пустой элемент пропускается. missing= обнуляет часть ячеек, и пустая оставляет аккумулятор в покое, а не считается нулём: «в этот день показаний не снимали» не должно обнулять счётчик.

accumulate= нужен список, то есть нужен repeat= (TDC237), а операция — одна из этих трёх (TDC238).

А если нужно по колонке

Здесь накопление идёт внутри одной записи. Для итога, который переносится со строки на строку — остаток на счёте, счётчик, который только растёт, — это другая конструкция: <gen type="running">.

Пропуски, аномалии и форматирование работают поэлементно

Это главное, что стоит запомнить: раз repeat стоит на генераторе, всё, что генератор делает, происходит с каждым значением по отдельности, а не с ячейкой целиком. Пропуски-пустышки, впрыск аномалий и форматирование вывода работают элемент за элементом.

Практическая выгода видна на колонке-метке. Когда генератор помечает аномалии, его поле-флаг становится списком той же длины, что и показания — по одному флагу на показание, выровненные ровно:

./run sensors.tdc (показания -> флаги аномалий)
5,500,100     ->   false,true,true
800,200,1     ->   true,true,false
6,8,7         ->   false,false,false
100,300,900   ->   true,true,true

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

Если же нужно «испорчена вся карточка целиком», это другая задача для другого инструмента: ветка <mix> с флагом на уровне записи, а не repeat.

Списки слов и точные пропорции

Самый частый список — набор тегов, и repeat работает прямо на генераторе text:

<sequence name="Tags">
<gen type="text" value="news,tech,sport,food,travel" repeat="1..3" separator=", "/>
</sequence>
./run tags.tdc (count=6)
статья 1: [food]
статья 2: [tech, news]
статья 3: [news, tech, travel]
статья 4: [sport, tech]
статья 5: [travel, travel, news]
статья 6: [sport]

Вот тонкий момент. text раскладывает значения по точным пропорциям через маску percent, а не по приблизительным — и эта гарантия сохраняется при repeat, и для фиксированного количества, и для диапазона.

Работает это потому, что TDC сначала решает все длины (тоже точной квотой) и только потом наполняет получившиеся места. Раз длины известны заранее, известно и общее число мест — ничего не генерируется впустую и никакая квота не пропадает.

Проверено на 120 000 строк с repeat="1..4" и percent="40,30,20,10":

./run tags-big.tdc (count=120000, с подсчётом)
длины строк:   30000 x1    30000 x2    30000 x3    30000 x4     (ровно по четверти)
значения:     120000 news   90000 tech   60000 sport  30000 food   (ровно 40/30/20/10)

Всего 300 000 мест, и распределение сошлось точно. Запустите с --jobs 7 — те же числа, файл байт в байт тот же.

Значения внутри ячейки могут повторяться

[travel, travel, news] выше — это нормально, не ошибка. Каждое место заполняется независимо, поэтому одно значение может попасться дважды: два показания 40 или две одинаковые покупки в корзине — обычные данные. Когда нужно наоборот — скажите об этом явно: distinct="true".

Без повторов внутри ячейки: distinct

Иногда повтор не просто скучен, а прямо неверен. Самый наглядный случай — двойное имя: Jesus Jesus Gonzales — это не имя.

distinct="true" берёт значения строки без возврата, поэтому одно и то же значение не может попасть в ячейку дважды:

<gen name="First" type="template" value="person.male.firstName"
repeat="2" separator=" " distinct="true"/>
./run names.tdc (count=5)
William Robert Jones
Matthew Tyrone Smith
James Zachery Williams
Devin Jacob Brown
Thomas Preston Johnson

Те же пять тегов рядом — без атрибута и с ним:

<gen name="Tags" type="text" value="news,tech,sport,food,travel" repeat="1..3" separator=", "/>
<gen name="Unique" type="text" value="news,tech,sport,food,travel" repeat="1..3" separator=", " distinct="true"/>
./run tags.tdc (count=6)
tech   |   sport, tech, news
news, food, food   |   travel, tech
sport   |   news
news, travel   |   sport, travel
tech, travel   |   tech
sport, sport, tech   |   travel, news, food

Слева food, food и sport, sport; справа — ни разу.

Чем за это платим

distinct не бесплатен, и цену стоит понять заранее.

Без него колонка со списком значений раскладывается на весь прогон точной квотой — именно поэтому percent ложится ровно. Но строка, которой запрещено повторяться, не может прочитать заранее решённое место: ей приходится выбирать. Поэтому под distinct колонка тянет значения построчно, и её общая частотность становится приблизительной, а не точной.

Взвешенный пакет данных при этом работает и по-прежнему опирается на частые значения — распространённые имена остаются распространёнными. Просто они больше не ложатся ровным счётом на весь прогон.

Отсюда и запрет ставить percent и distinct на один генератор: percent обещает точность, которой колонка больше не даёт, и TDC отказывает паре (TDC291), а не выполняет молча одно и роняет другое. Если нужны доли по длинам списков — ставьте их на <mix> или <switch> снаружи, а repeat на генератор внутри.

Всё остальное про repeat не меняется: строки остаются независимыми, поэтому потоковая запись и --jobs продолжают работать.

Когда это невозможно

Из пяти значений не сделать шесть разных. TDC скажет об этом прямо, а не отдаст молча список покороче:

что написаночто произойдёт
distinct="true" без repeat=TDC290 — одно значение не может повториться
percent= и distinct= вместеTDC291 — см. выше
repeat="1..10" на списке из пятиTDC292, ещё до запуска — список виден в конфиге
то же, но из пакета или файлатот же отказ, но на запуске — пул известен только тогда

Последняя строка и объясняет разделение. Список value="a,b,c" можно пересчитать прямо из конфига, поэтому его ловит tdcv2 check; файл пакета или колонка CSV читаются во время генерации, поэтому отказ вынужден подождать. И в том, и в другом случае это отказ, а не короткая ячейка.

За отказом во время прогона стоит число: розыгрыш сдаётся после 64 попыток найти значение, которого в ячейке ещё нет, и так и говорит. Генератор, у которого значений больше шестидесяти четырёх, но перекос сильный — регулярка, почти всегда дающая одну и ту же строку, — может упереться в это, хотя на бумаге запас есть. check называет такой конфиг верным: достижим ли запас, из конфига не видно.

Где repeat работать не будет

Несколько генераторов привязывают каждое значение к номеру строки, и repeat к ним неприменим:

ГенераторПочему нельзя
increment, decrementзначение зависит от позиции строки
timeseries, patternто же — значение привязано к позиции

У них индекс элемента зависел бы от того, какой длины оказались все предыдущие строки — а это случайная величина. Тогда строку нельзя было бы посчитать без соседей, и сломались бы потоковый режим и --jobs. TDC откажет с понятной ошибкой (TDC204), а не сделает молча не то.

Что ещё TDC не пропустит

Что написалиЧто скажет
repeat="много", repeat="1.5"TDC195 — нужно целое число
repeat="-1", repeat="5..2"TDC195 — минимум не может быть отрицательным или больше максимума
repeat="1..65"TDC195 — верхняя граница 64
separator=";" без repeatTDC198 — склеивать нечего
repeat или separator на <mix>TDC196 — mix выбирает одну ветку, а не составляет список

Про последнее: <mix> выбирает из веток, поэтому «повторить его» не имеет определённого смысла. Если нужен список внутри ветки — ставьте repeat на <gen> внутри <case>.

На больших объёмах

repeat не мешает ни потоковому режиму, ни --jobs: строка по-прежнему считается независимо от соседей. Работают на это две разные вещи, и их легко перепутать:

  • Длину списка каждая строка берёт из квоты на весь прогон (см. выше). Строка узнаёт свою длину по собственному номеру — соседи не нужны.
  • Значения после этого тянутся на максимум (для repeat="1..5" — на пять), а лишние выбрасываются, поэтому позиция в потоке случайных чисел зависит только от номера строки и никогда — от того, какой длины вышли предыдущие.

Именно это позволяет посчитать строку, не считая предыдущие, и поэтому многопоточный вывод совпадает с однопоточным байт в байт.

Отсюда и ограничение в 64: большой repeat — это реально потраченная работа, даже если в строке в итоге окажется два элемента. См. Большие объёмы.

В Parquet — настоящий список

При выгрузке в Parquet колонка с repeat становится настоящим списком, а не склеенной строкой. Тип элемента выводится автоматически:

схема parquet
id       INT64       REQUIRED
items    LIST of BYTE_ARRAY (UTF8)
prices   LIST of INT64
city     BYTE_ARRAY  REQUIRED
строки parquet (как JSON)
{"id":1,"items":["хлеб","сыр"],           "prices":[262],         "city":"Москва"}
{"id":3,"items":["сыр","молоко"],         "prices":[469,241,188], "city":"Москва"}
{"id":6,"items":["молоко","хлеб","кофе"], "prices":[81,262],      "city":"Париж"}

Пустой список остаётся пустым списком — он не пропадает. А пропуск внутри списка даёт настоящий null на месте элемента: в тексте это неотличимо от пустой строки, но Parquet фиксирует это точно.

parquet — элемент null
{"t":[6,5,null]}

Тип можно задать и вручную: type="[]int64", type="[]decimal(18,2)". Запись type="[]int64|null" означает «список, элемент которого может быть null» — ровно то, что даёт поэлементный пропуск.

Смотрите также

  • text — списки через запятую и точные пропорции percent, которые repeat сохраняет.
  • Счётчикиincrement / decrement, привязанные к позиции генераторы, к которым repeat неприменим.
  • Маски и регистр — поэлементное форматирование, которое применяется к каждому значению в ячейке с repeat.
  • Большие объёмы — почему repeat остаётся безопасным для потока и идентичным при разном --jobs.