Несколько значений в ячейке (repeat)
Когда пригодится — когда поле не всегда содержит одно значение. В заказе один
товар или пять. У статьи один тег или четыре. Датчик за раз отдаёт три показания.
repeat заставляет один <gen> выдать в ячейку
несколько значений вместо одного — и количество может меняться от строки к
строке.
Раньше поле переменной длины приходилось изображать пятью отдельными последовательностями, половину строк оставляя пустыми. Теперь это один атрибут на генераторе.
Примеры вывода ниже — иллюстративные: конкретные значения при заданном seed
могут сдвинуться между версиями ядра, но структура, которую гарантирует
каждая форма (сколько значений попадёт в ячейку и чем они склеены), не меняется.
- 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>
заказ 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>
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>
статья 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=";"/>
[896;648;701;334] [] [765] [342;706] [447;991;569] [] [148] []
На 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>
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"/>
239.10, 568.84, 809.63 791.92, 1059.68, 1593.11 473.43, 785.34, 1006.51
Слева тот же генератор без accumulate=, справа — с ним. Последний элемент это итог, а
каждый перед ним — промежуточная сумма на этом шаге.
accumulate= | Каждый элемент становится |
|---|---|
sum | суммой всего до него включительно |
max | наибольшим из встреченных — «рекорд на этот момент» |
min | наименьшим из встреченных |
пик 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 стоит на генераторе, всё, что
генератор делает, происходит с каждым значением по отдельности, а не с ячейкой
целиком. Пропуски-пустышки, впрыск аномалий и
форматирование вывода работают элемент за элементом.
Практическая выгода видна на колонке-метке. Когда генератор помечает аномалии, его поле-флаг становится списком той же длины, что и показания — по одному флагу на показание, выровненные ровно:
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>
статья 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":
длины строк: 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"/>
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"/>
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=";" без repeat | TDC198 — склеивать нечего |
repeat или separator на <mix> | TDC196 — mix выбирает одну ветку, а не составляет список |
Про последнее: <mix> выбирает из веток, поэтому «повторить его» не имеет
определённого смысла. Если нужен список внутри ветки — ставьте repeat на
<gen> внутри <case>.
На больших объёмах
repeat не мешает ни потоковому режиму, ни --jobs: строка по-прежнему
считается независимо от соседей. Работают на это две разные вещи, и их легко
перепутать:
- Длину списка каждая строка берёт из квоты на весь прогон (см. выше). Строка узнаёт свою длину по собственному номеру — соседи не нужны.
- Значения после этого тянутся на максимум (для
repeat="1..5"— на пять), а лишние выбрасываются, поэтому позиция в потоке случайных чисел зависит только от номера строки и никогда — от того, какой длины вышли предыдущие.
Именно это позволяет посчитать строку, не считая предыдущие, и поэтому многопоточный вывод совпадает с однопоточным байт в байт.
Отсюда и ограничение в 64: большой repeat — это реально потраченная работа,
даже если в строке в итоге окажется два элемента. См.
Большие объёмы.
В Parquet — настоящий список
При выгрузке в Parquet колонка с repeat становится настоящим списком, а не
склеенной строкой. Тип элемента выводится автоматически:
id INT64 REQUIRED items LIST of BYTE_ARRAY (UTF8) prices LIST of INT64 city BYTE_ARRAY REQUIRED
{"id":1,"items":["хлеб","сыр"], "prices":[262], "city":"Москва"}
{"id":3,"items":["сыр","молоко"], "prices":[469,241,188], "city":"Москва"}
{"id":6,"items":["молоко","хлеб","кофе"], "prices":[81,262], "city":"Париж"}Пустой список остаётся пустым списком — он не пропадает. А пропуск внутри списка
даёт настоящий null на месте элемента: в тексте это неотличимо от пустой
строки, но Parquet фиксирует это точно.
{"t":[6,5,null]}Тип можно задать и вручную: type="[]int64", type="[]decimal(18,2)". Запись
type="[]int64|null" означает «список, элемент которого может быть null» —
ровно то, что даёт поэлементный пропуск.
Смотрите также
text— списки через запятую и точные пропорцииpercent, которыеrepeatсохраняет.- Счётчики —
increment/decrement, привязанные к позиции генераторы, к которымrepeatнеприменим. - Маски и регистр — поэлементное форматирование,
которое применяется к каждому значению в ячейке с
repeat. - Большие объёмы — почему
repeatостаётся безопасным для потока и идентичным при разном--jobs.