Маски и регистр
Генератор выдаёт сырые значения — ровно такими, как они устроены. СНИЛС — это
одиннадцать цифр подряд, дата — 2020-05-14, имя — как оно лежит в списке. Сам
генератор про то, какие это данные, а не про то, как они выглядят:
разделители, регистр и порядок слов — не его забота.
Вот что выдаёт обычный генератор text для пачки
СНИЛСов (order="sequential" — чтобы значения ниже совпадали и повторялись по
порядку):
<gen type="text" value="37898432363,88973572402,85213975315,26324315851" order="sequential"/>
37898432363 88973572402 85213975315 26324315851
Значения валидные, но читать их так тяжело. Форматирование придаёт вид уже готовому значению на выходе. Всегда три части:
сырое значение → обработчик → готовый вид — например, 37898432363 → маска
xxx-xxx-xxx xx → 378-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>
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>
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>
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] —
«всё, кроме первого».
Что происходит на самом деле: пул
Маска с индексом работает в двух каналах, которые друг другу не мешают.
- Aисходное значение, каждая позиция пронумерована
- Bчто получилось на выходе
- позиция, которую назвал индекс, и куда она попала
- что взяли голые слоты — в том порядке, в каком стояли
Первый канал — что печатается: индекс читает эту позицию оригинала, и изменить это
нельзя ничем. Второй — с чем остаются голые x / w / *, и только на него
влияет потребление. Позиция, которую забрал индекс, из пула выбывает:
- Aисходное значение, до индексированной выборки
- Bпул, из которого тянут голые слоты, — позиции, забранной индексом, в нём больше нет
- позиция, которую назвал x[4]
Поэтому стоит перечитать, что такое *: это всё непотреблённое, а не «хвост
строки». Перенесите две цифры вперёд — и * всё равно напечатает остальные девять:
<data>${{Phone}} -> ${{Phone | mask:x[9]x[10] xxx-xxx-xxx}}</data>
26324315851 -> 51 263-243-158 19875550142 -> 42 198-755-501 44207946001 -> 01 442-079-460
Один индекс дважды — копия вместо переноса
Ничто не мешает двум слотам назвать одну позицию. Когда так происходит, эта часть печатается дважды — и складской код, у которого голова повторена в хвосте, получается сам собой:
- Aисходный код, каждая позиция пронумерована
- Bрезультат — десять символов из шести
- два символа, названные дважды: печатаются с обоих концов
- остальное, взятое `*` в исходном порядке
<data>${{Sku}} -> ${{Sku | mask:x[0..1]-*-x[0..1]}}</data>
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, до того как сгенерирована хоть одна строка:
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>
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 поднимает первую букву
каждого слова (von → Von, lee → Lee). upper/lower меняют всё.
Разный регистр по условию, на одних данных
Раз форматирование происходит на выходе, из одного генератора можно на каждую строку применить свой регистр — скажем, у мужчин фамилия с заглавной, у женщин капсом. Фамилии здесь нейтральные (несклоняемые), поэтому одна и та же годится любому полу — меняется только оформление:
<line if="Gender == M"><data>${{Gender}} ${{Word}} -> ${{Word | capitalize}}</data></line>
<line if="Gender == F"><data>${{Gender}} ${{Word}} -> ${{Word | upper}}</data></line>
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>
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>
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>
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>
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>
[ 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>
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> |
|---|---|---|
slice | slice:from[,to] | <slice from="0" to="4"> |
replace | replace:from,to | <replace from="-" to="/"> |
trim | trim | <trim> |
group | group:size[,sep] | <group size="3" sep=" "> |
compact | compact или compact:16 | — |
csv | csv | — (нет тега) |
sql | sql | — (нет тега) |
Порядок — 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>
случайно=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>
petr.ivanov.1@example.com <- первая запись maria.orlova.lfls@example.com <- миллионная dmitriy.sokolov.x2qxvk@example.com <- двухмиллиардная
| запись | десятичное | compact |
|---|---|---|
| 1 000 000 | 7 цифр | lfls — 4 знака |
| 2 000 000 000 | 10 цифр | x2qxvk — 6 знаков |
| 1 000 000 000 000 | 13 цифр | 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>
7,"Набор ножей, 3 шт",Посуда 2,"Кофе ""Арабика"" 250 г",Бакалея
Кавычки ставятся всегда, а не «когда нужно»: правило без исключений надёжнее догадки, которая рано или поздно встретит запятую или перенос строки, а лишние кавычки понимает любой читатель CSV. См. также Форматы вывода → CSV.
Чего фильтр сознательно не делает: значения, начинающиеся с =, +, - или @,
при открытии файла в электронной таблице превращаются в живые формулы. Сгенерированные
данные остаются байт в байт такими, какими сгенерированы, — если файл пойдёт в Excel и
это важно, добавьте таким значениям префикс сами фильтром replace.
sql — тело строкового литерала
<data>INSERT INTO t VALUES ('${{Last | sql}}');</data>
INSERT INTO t VALUES ('O''Brien');Фильтр удваивает апостроф и отдаёт только содержимое, без внешних кавычек — их
вы пишете сами, и форма запроса остаётся видна прямо в конфиге. Для
JSON отдельного фильтра нет: там кавычка
экранируется обратным слэшем тем же фильтром replace.
Это экранирование стандартного SQL (PostgreSQL, SQLite, Oracle, ANSI). MySQL в
режиме по умолчанию считает \ символом экранирования — либо включите там
NO_BACKSLASH_ESCAPES, либо сначала удвойте обратные слэши фильтром replace.
См. также
- Строки и форматирование — те же операции как compute-теги.
- Вывод и форматирование — где живут интерполяция и фильтры.
- Форматы вывода — CSV, JSON и SQL от начала до конца.