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

Маски и регистр

Генератор выдаёт сырые значения — ровно такими, как они устроены. СНИЛС — это одиннадцать цифр подряд, дата — 2020-05-14, имя — как оно лежит в списке. Сам генератор про то, какие это данные, а не про то, как они выглядят: разделители, регистр и порядок слов — не его забота.

Вот что выдаёт обычный генератор text для пачки СНИЛСов (order="sequential" — чтобы значения ниже совпадали и повторялись по порядку):

<gen type="text" value="37898432363,88973572402,85213975315,26324315851" order="sequential"/>
./run demo.tdc
37898432363
88973572402
85213975315
26324315851

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

сырое значение → обработчик → готовый вид — например, 37898432363 → маска xxx-xxx-xxx xx378-984-323 63.

Есть два независимых обработчика:

  • маска — режет строку на куски и расставляет между ними разделители (x, w, *);
  • регистрupper, lower, capitalize, title.

Плюс операции slice, replace, trim, group, compact (ниже), экранирующие фильтры csv / sql и order="sequential" — чтобы брать данные по порядку, а не случайно.

примечание

Выводы в примерах иллюстративны — конкретные значения зависят от версии ядра. Важна форма каждого преобразования, а не сами цифры.

Одно настоящее значение, прогнанное через одну настоящую маску.
  • Aсгенерированное значение до маски
  • Bмаска: x это гнездо, всё остальное — литерал, который остаётся как есть
  • Cрезультат — линии показывают, какой символ занял какое гнездо

Три места, одно поведение

Одно и то же форматирование доступно тремя способами — результат одинаковый, выбирайте удобный:

СпособКак пишетсяКогда удобнее
Фильтр в интерполяции${{X | mask:…}}одно значение в конкретном месте текста
Атрибут на <gen><gen … mask="…" case="…"/>оформить весь генератор разом
Тег в <compute><mask pattern="…">…</mask>когда форматирование — звено вычисления

Большинство примеров ниже — на фильтрах; они живут в интерполяции. У каждого compute-тега тоже есть свой дом в Строках и форматировании.

Маска — разрезать и расставить

Проблема. Номер приходит слитной строкой (37898432363) — глазами не разобрать, где группы.

Инструмент. Маска идёт по шаблону слева направо. Каждый слот «съедает» кусок входа, всё остальное печатается как литерал:

СлотЧто берёт из входа
xодин символ
wодно слово (буквы до пробела) и проглатывает один пробел
*всё, что ещё не потреблено
x[0] w[-1]названная позиция — см. Перестановка
\экранирует следующий символ (\x → буквальный x)
прочеелитерал: дефис, точка, пробел, скобки — печатается как есть

Одни и те же цифры под двумя шаблонами:

<sequence name="Snils">
<gen type="text" value="37898432363,88973572402,85213975315,26324315851" order="sequential"/>
</sequence>
...
<data>${{Snils}} -> ${{Snils | mask:xxx-xxx-xxx xx}} | ${{Snils | mask:xxx.xxx.xxx.xx}}</data>
./run demo.tdc
37898432363  ->  378-984-323 63   |   378.984.323.63
88973572402  ->  889-735-724 02   |   889.735.724.02
85213975315  ->  852-139-753 15   |   852.139.753.15
26324315851  ->  263-243-158 51   |   263.243.158.51

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

Слот w — работа по словам

Проблема. Из «имя фамилия» нужно собрать свой порядок. Слот w берёт слово и проглатывает один пробел за ним:

<sequence name="Name"><gen type="text" value="ivan petrov,maria orlova,oleg volkov" order="sequential"/></sequence>
...
<data>${{Name}} -> ${{Name | mask:w:w}}</data>
./run demo.tdc
ivan petrov   ->  ivan:petrov
maria orlova  ->  maria:orlova
oleg volkov   ->  oleg:volkov

w забрал ivan, съел пробел, напечатал литерал :, второй w забрал petrov. Пробела перед : нет — его проглотил первый w. Подробности со всеми крайними случаями — на <mask>.

Перестановка — x[0], w[0] и диапазоны

Задача. Значение пришло целиком и не в том порядке. Имя приходит из пакета как ivan petrov, а выгрузке нужна сперва фамилия. Адрес — 12 Baker St, а страна, для которой вы генерируете, пишет номер дома последним. Части никогда не были вашими: строка пришла из колонки файла, из адреса пакета или из regex — значит просто сгенерировать две последовательности и напечатать их в другом порядке не выйдет.

Инструмент. Поставьте на слот индекс в скобках. Он называет позицию в оригинальном входе:

СлотБерёт
x[7]символ с индексом 7 — восьмой, если считать от x[0]
x[5..7]символы 5, 6 и 7 — обе границы включены
x[-1]последний символ
w[1]слово с индексом 1 — второе
w[-1]последнее слово

Индексы начинаются с нуля, как у фильтра slice. Диапазон пишется через .. — так же, как везде в TDC (value="10..99", repeat="1..5"); дефис рядом с x[-1] был бы двусмысленным.

Это всё нововведение. Два примера, и каждый — настоящая задача:

<data>${{Name}} -> ${{Name | mask:w[-1], w[0]}}</data>
<data>${{Addr}} -> ${{Addr | mask:w[1..-1] w[0]}}</data>
./run demo.tdc
james miller   ->  miller, james
mary jones     ->  jones, mary
anna lee       ->  lee, anna

12 Baker St -> Baker St 12
7 Elm Road -> Elm Road 7
140 Oak Lane -> Oak Lane 140

Ни то, ни другое не зависит от длины слов — ради этого и считаем словами, а не символами. w[-1] — последнее слово, будь в имени две части или четыре, а w[1..-1] — «всё, кроме первого».

Что происходит на самом деле: пул

Маска с индексом работает в двух каналах, которые друг другу не мешают.

ABCDE под маской x[4]-xxxx. Индексированный слот вытягивает символ вперёд, а голые слоты дальше берут то, что осталось, в исходном порядке.
  • Aисходное значение, каждая позиция пронумерована
  • Bчто получилось на выходе
  • позиция, которую назвал индекс, и куда она попала
  • что взяли голые слоты — в том порядке, в каком стояли

Первый канал — что печатается: индекс читает эту позицию оригинала, и изменить это нельзя ничем. Второй — с чем остаются голые x / w / *, и только на него влияет потребление. Позиция, которую забрал индекс, из пула выбывает:

Тот же прогон со стороны пула: до индексированной выборки и после неё.
  • Aисходное значение, до индексированной выборки
  • Bпул, из которого тянут голые слоты, — позиции, забранной индексом, в нём больше нет
  • позиция, которую назвал x[4]

Поэтому стоит перечитать, что такое *: это всё непотреблённое, а не «хвост строки». Перенесите две цифры вперёд — и * всё равно напечатает остальные девять:

<data>${{Phone}} -> ${{Phone | mask:x[9]x[10] xxx-xxx-xxx}}</data>
./run demo.tdc
26324315851  ->  51 263-243-158
19875550142  ->  42 198-755-501
44207946001  ->  01 442-079-460

Один индекс дважды — копия вместо переноса

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

AB1234 под x[0..1]-*-x[0..1]. Из одних и тех же двух клеток выходит по две стрелки: голова печатается спереди и ещё раз в конце.
  • Aисходный код, каждая позиция пронумерована
  • Bрезультат — десять символов из шести
  • два символа, названные дважды: печатаются с обоих концов
  • остальное, взятое `*` в исходном порядке
<data>${{Sku}} -> ${{Sku | mask:x[0..1]-*-x[0..1]}}</data>
./run demo.tdc
AB1234  ->  AB-1234-AB
CD5678  ->  CD-5678-CD
EF9012  ->  EF-9012-EF

Отсюда единственное, чего сама запись сказать не может: x[2] не говорит, перенос это или копия. Копия — если ту же позицию подберёт ещё какой-то слот; читается как перенос — если не подберёт никто. Узнаётся из всей маски целиком, а не из слота.

Обратный диапазон идёт назад

x[-1..0] — это «от последнего символа к первому», разворот, которому всё равно, какой длины значение. AB1234 превращается в 4321BA. Пригодно, чтобы нарочно готовить покорёженные тестовые данные; для остального тянуться к нему незачем.

Три вещи, на которых спотыкаются

Скобка — индекс только сразу после x или w. Везде ещё это обычный литерал, поэтому mask="[тел.] xxx-xxx" не требует экранирования вовсе. А если литеральная скобка нужна прямо за слотом — экранируйте: mask="x[1]\[*\]" на ABC даёт B[AC].

Индекс за концом печатает пустоту и молчит. w[4] на значении из двух слов — пустая строка, ровно как x за концом короткого значения. Длина входа не известна до генерации строки, проверить заранее нечего, а останавливать прогон на миллион строк из-за одного короткого значения было бы хуже. Следите за пустыми ячейками, когда берёте фиксированный индекс на данных переменной формы: w[-1] обычно безопаснее говорит «последнее».

Дефис в диапазоне отвергается, а не угадывается. x[1-2] — лёгкая опечатка, и если бы её сочли обычным текстом, она тихо дала бы неверные данные. Вместо этого — TDC199, до того как сгенерирована хоть одна строка:

./run demo.tdc
error[TDC199]: mask: invalid index "[1-2]" after "x" — use x[0], x[0..4] or x[-1]

Регистр — upper / lower / capitalize / title

Проблема. Данные приходят в «пёстром» регистре (разные источники, импорт): iPhone CASE, mary SUE. Нужно привести к единому виду.

ИмяЧто делает
upperвсё в ВЕРХНИЙ регистр
lowerвсё в нижний регистр
capitalizeтолько первая буква заглавная, остальное как есть
titleпервая буква каждого слова заглавная, остальное как есть

Одна и та же строка через все четыре:

<sequence name="W"><gen type="text" value="iPhone CASE,mary SUE,ANNA von lee" order="sequential"/></sequence>
...
<data>${{W}} -> upper=${{W | upper}} | lower=${{W | lower}} | capitalize=${{W | capitalize}} | title=${{W | title}}</data>
./run demo.tdc
iPhone CASE   ->  upper=IPHONE CASE | lower=iphone case | capitalize=IPhone CASE | title=IPhone CASE
mary SUE      ->  upper=MARY SUE | lower=mary sue | capitalize=Mary SUE | title=Mary SUE
ANNA von lee  ->  upper=ANNA VON LEE | lower=anna von lee | capitalize=ANNA von lee | title=ANNA Von Lee

capitalize трогает только самый первый символ (iPhone CASE остаётся почти как есть — первая i становится I), а title поднимает первую букву каждого слова (vonVon, leeLee). upper/lower меняют всё.

Разный регистр по условию, на одних данных

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

<line if="Gender == M"><data>${{Gender}} ${{Word}} -> ${{Word | capitalize}}</data></line>
<line if="Gender == F"><data>${{Gender}} ${{Word}} -> ${{Word | upper}}</data></line>
./run demo.tdc
F шевченко   ->  ШЕВЧЕНКО
M шевченко   ->  Шевченко
F черных     ->  ЧЕРНЫХ
F коваленко  ->  КОВАЛЕНКО
M коваленко  ->  Коваленко
M черных     ->  Черных

Генератор Word один, но его вид зависит от Gender. Внутри самого генератора так не сделать — форматирование на выходе делает это одной строкой.

Как атрибут <gen> — оформить весь столбец

Проблема. Не хочется оборачивать каждую подстановку — нужно, чтобы весь столбец выходил уже оформленным.

Инструмент. Поставьте mask="…" / case="…" прямо на <gen>. Слева — тот же генератор без атрибута (сырьё), справа — он же с mask=:

<sequence name="Raw"><gen type="text" value="37898432363,88973572402,85213975315,26324315851" order="sequential"/></sequence>
<sequence name="Nice"><gen type="text" value="37898432363,88973572402,85213975315,26324315851" order="sequential" mask="xxx-xxx-xxx xx"/></sequence>
...
<data>${{Raw}} -> ${{Nice}}</data>
./run demo.tdc
37898432363  ->  378-984-323 63
88973572402  ->  889-735-724 02
85213975315  ->  852-139-753 15
26324315851  ->  263-243-158 51

Именно так готовому генератору задаётся «красивый» вид:

<gen type="template" value="russia.docs.snils" mask="xxx-xxx-xxx xx"/>
<gen type="template" value="common.payment.card.pan" mask="xxxx xxxx xxxx xxxx"/>

Оба пути — из генератора template. Если заданы оба атрибута, порядок такой: сначала маска, потом регистр.

Цепочки фильтров — несколько операций подряд

Проблема. Нужно не одно преобразование, а несколько — например, собрать по маске и поднять регистр.

Инструмент. Через «трубу» | фильтры цепляются друг за другом, слева направо: сырьё → маска → регистр:

<sequence name="Name"><gen type="text" value="ivan petrov,maria orlova,oleg volkov" order="sequential"/></sequence>
...
<data>${{Name}} -> ${{Name | mask:w:w}} -> ${{Name | mask:w:w | upper}}</data>
./run demo.tdc
ivan petrov   ->  ivan:petrov   ->  IVAN:PETROV
maria orlova  ->  maria:orlova  ->  MARIA:ORLOVA
oleg volkov   ->  oleg:volkov   ->  OLEG:VOLKOV

Средний столбец — после маски, правый — после маски и upper. Каждый фильтр получает результат предыдущего.

примечание

Аргумент маски читается до следующей | или до закрывающих }}, поэтому пробелы и двоеточия спокойно живут внутри него (mask:w:w, mask:xxx-xxx-xxx xx).

Ещё фильтры: slice, replace, trim, group

Те же три способа (фильтр / атрибут <gen> / тег в <compute>). Ниже — по формуле «сырьё → инструмент → результат», каждая с вариацией.

slice — вырезать часть по индексам

Проблема. Из даты 2020-05-14 нужен только год или только месяц.

<sequence name="D"><gen type="text" value="2020-05-14,2022-11-03,2021-07-19" order="sequential"/></sequence>
...
<data>${{D}} -> year=${{D | slice:0,4}} | month=${{D | slice:5,7}} | tail=${{D | slice:5}}</data>
./run demo.tdc
2020-05-14  ->  year=2020 | month=05 | tail=05-14
2022-11-03  ->  year=2022 | month=11 | tail=11-03
2021-07-19  ->  year=2021 | month=07 | tail=07-19

slice:0,4 — символы 0–3 (год), slice:5,7 — 5–6 (месяц), а slice:5 без второго числа — «от 5-го до конца». Индексы по символам, с нуля. См. <slice>.

replace — заменить все вхождения

Проблема. Дата с дефисами, а нужен другой разделитель.

<data>${{D}} -> slash=${{D | replace:-,/}} | dot=${{D | replace:-,.}}</data>
./run demo.tdc
2020-05-14  ->  slash=2020/05/14 | dot=2020.05.14
2022-11-03  ->  slash=2022/11/03 | dot=2022.11.03
2021-07-19  ->  slash=2021/07/19 | dot=2021.07.19

Формат — replace:from,to; заменяются все вхождения. Три вещи, которых фильтр не делает, и каждая ломается тихо, а не громко:

  • from ищется буквально, никогда как регулярное выражение. replace:[abc],Z ищет пять символов [abc], не находит их и ничего не меняет.
  • В from нельзя поставить запятую. Первая же запятая заканчивает его, поэтому всё, что идёт дальше, относится к to: replace:-,+,x заменяет каждый - на +,x.
  • Пустой from не делает ничего. replace:,+ возвращает значение без изменений.

Там, где это важно, берите вместо фильтра тег <replace> внутри <compute>: он принимает from= и to= отдельными атрибутами, поэтому запятая для него — обычный символ.

trim — убрать крайние пробелы

Проблема. В данных из файла или CSV бывают лишние пробелы по краям. Здесь пробелы добавлены нарочно (как будто пришли из источника), а скобки в тексте — чтобы их было видно:

<sequence name="City"><gen type="text" value="Moscow,Samara,Rostov" order="sequential"/></sequence>
<!-- приклеим лишние пробелы по краям, имитируя «грязные» данные -->
<sequence name="Padded">
<compute><result><concat><str v=" "/><field name="City"/><str v=" "/></concat></result></compute>
</sequence>
...
<data>[${{Padded}}] -> [${{Padded | trim}}]</data>
./run demo.tdc
[  Moscow   ]  ->  [Moscow]
[  Samara   ]  ->  [Samara]
[  Rostov   ]  ->  [Rostov]

Только по краям — внутренние пробелы trim не трогает. См. <trim>.

group — сгруппировать цифры справа

Проблема. Длинное число нечитаемо: 1234567.

<sequence name="N"><gen type="text" value="1234567,89150000,42" order="sequential"/></sequence>
...
<data>${{N}} -> group:3=${{N | group:3}} | group:3,-=${{N | group:3,-}} | group:4=${{N | group:4}}</data>
./run demo.tdc
1234567   ->  group:3=1 234 567 | group:3,-=1-234-567 | group:4=123 4567
89150000  ->  group:3=89 150 000 | group:3,-=89-150-000 | group:4=8915 0000
42        ->  group:3=42 | group:3,-=42 | group:4=42

Группировка идёт справа, поэтому неполная группа оказывается слева (1 234 567). group:3 — разряды тысяч (разделитель по умолчанию — пробел), group:3,- — свой разделитель, group:4 читается как блоки карты. Короткое 42 меньше одной группы — вернулось как есть. См. <group>.

Сводка

ОперацияФильтрТег <compute>
sliceslice:from[,to]<slice from="0" to="4">
replacereplace:from,to<replace from="-" to="/">
trimtrim<trim>
groupgroup:size[,sep]<group size="3" sep=" ">
compactcompact или compact:16
csvcsv— (нет тега)
sqlsql— (нет тега)

Порядок — order="sequential"

Проблема. По умолчанию text и file берут значения случайно. Иногда у данных есть задуманный порядок (список из файла, спец-серия) — и его нужно сохранить.

Инструмент. order="sequential": строка i берёт i-е значение по порядку, зациклено. Слева — обычный (случайный) генератор, справа — тот же список Jan,Feb,Mar по порядку:

<sequence name="Rand"><gen type="text" value="Jan,Feb,Mar"/></sequence>
<sequence name="Seq"><gen type="text" value="Jan,Feb,Mar" order="sequential"/></sequence>
...
<data>случайно=${{Rand}} по порядку=${{Seq}}</data>
./run demo.tdc
случайно=Feb   по порядку=Jan
случайно=Feb   по порядку=Feb
случайно=Mar   по порядку=Mar
случайно=Jan   по порядку=Jan
случайно=Jan   по порядку=Feb
случайно=Feb   по порядку=Mar
случайно=Mar   по порядку=Jan

Правый столбец идёт строго Jan, Feb, Mar, Jan, Feb, Mar, Jan… — по кругу.

  • order="random" — по умолчанию.
  • order="sequential" — строго по порядку, зациклено.
  • cycle="false" — вместо цикла падает с понятной ошибкой, когда данные кончились.
  • Так же работает для файлов: <gen type="file" src="@data/villages.txt" order="sequential"/> выдаёт строки файла строго в их порядке.

compact — длинное число короткой записью

Превращает целое число в запись по основанию 36 (цифры и строчные латинские буквы). Удобно там, где число служит уникальным хвостом, а читать его всё равно должен человек:

<data>${{F|lower}}.${{L|lower}}.${{Id|compact}}@example.com</data>
./run demo.tdc
petr.ivanov.1@example.com           <- первая запись
maria.orlova.lfls@example.com       <- миллионная
dmitriy.sokolov.x2qxvk@example.com  <- двухмиллиардная
записьдесятичноеcompact
1 000 0007 цифрlfls — 4 знака
2 000 000 00010 цифрx2qxvk — 6 знаков
1 000 000 000 00013 цифрcre66i9s — 8 знаков

Шести знаков хватает на 2,17 миллиарда строк, семи — на 78 миллиардов. Преобразование взаимно однозначное, поэтому разные числа всегда дают разные записи — уникальность, ради которой номер и приписывали, сохраняется полностью.

примечание

Только строчные буквы, и это осознанно. Основание 62 (с заглавными) дало бы ещё короче, но многие системы приводят почту к нижнему регистру — тогда aB и Ab схлопнулись бы в один адрес, и дубликаты вернулись бы незаметно.

Основание задаётся: compact:16 даст шестнадцатеричную запись. Значение, которое не является целым числом, фильтр не трогает.

Экранирование под формат: csv и sql

<data> собирает текст и ничего не знает про файл, который вы пишете, поэтому значение с запятой молча разрывает строку CSV, а апостроф ломает SQL. Это не теория: одно название вроде Набор ножей, 3 шт способно превратить тысячи строк в записи с лишним полем — категория уезжает в цену, цена в количество, и ни одной ошибки при этом. Два фильтра закрывают оба случая.

csv — поле по стандарту RFC 4180

<data>${{Id}},${{Name | csv}},${{Category}}</data>
./run demo.tdc
7,"Набор ножей, 3 шт",Посуда
2,"Кофе ""Арабика"" 250 г",Бакалея

Кавычки ставятся всегда, а не «когда нужно»: правило без исключений надёжнее догадки, которая рано или поздно встретит запятую или перенос строки, а лишние кавычки понимает любой читатель CSV. См. также Форматы вывода → CSV.

Чего фильтр сознательно не делает: значения, начинающиеся с =, +, - или @, при открытии файла в электронной таблице превращаются в живые формулы. Сгенерированные данные остаются байт в байт такими, какими сгенерированы, — если файл пойдёт в Excel и это важно, добавьте таким значениям префикс сами фильтром replace.

sql — тело строкового литерала

<data>INSERT INTO t VALUES ('${{Last | sql}}');</data>
./run demo.tdc
INSERT INTO t VALUES ('O''Brien');

Фильтр удваивает апостроф и отдаёт только содержимое, без внешних кавычек — их вы пишете сами, и форма запроса остаётся видна прямо в конфиге. Для JSON отдельного фильтра нет: там кавычка экранируется обратным слэшем тем же фильтром replace.

Это экранирование стандартного SQL (PostgreSQL, SQLite, Oracle, ANSI). MySQL в режиме по умолчанию считает \ символом экранирования — либо включите там NO_BACKSLASH_ESCAPES, либо сначала удвойте обратные слэши фильтром replace.

См. также