Подъязык вычислений <compute>
<compute> — такая же часть языка TDC, как <switch> или <mix>, только про
вычисления: он выводит значение из других значений. Он не выдумывает
данные случайно (этим занимается <gen>) — он
обрабатывает уже готовые значения. Пишется прямо в конфиге, внутри
<sequence>, — именно так строятся настоящие
контрольные суммы: контрольная цифра ИНН, номер карты по алгоритму Луна, IBAN по
mod-97.
<gen>порождает значение (случайное, из диапазона/списка/шаблона).<compute>выводит значение как чистую функцию от других значений.
<compute> читает другие последовательности через <field name="…"/> — ровно
те же имена, что и в ${{…}}.
У <compute> нет своей случайности — вообще никакой. Дайте ему те же входы, и он
вернёт тот же ответ, всегда. Последовательность, у которой единственный ребёнок —
<compute>, полностью игнорирует seed прогона: поменяйте сид, и колонка будет
байт в байт та же, потому что ничего в ней никогда не разыгрывалось.
Поэтому uniq="true" на такой последовательности запрещён
(TDC218). Обработчик не может обещать уникальность: у него
нет пула, из которого тянуть без возврата, нет своих колонок, чтобы их переставить, и
нет костей, чтобы перебросить при совпадении — f(x) это f(x). Повторяется ли
результат — свойство формулы, а не <compute>. Просите уникальность у тех
<gen>-последовательностей, которые он читает, либо оберните их в
<uniq>.
Все паки данных, собирающие идентификатор, устроены именно так: <gen>-соседи бросают
кости, а <compute> считает контрольную цифру. Из 188 встроенных паков, использующих
<compute>, ни один не обходится без <gen> рядом.
Поэтому <sequence> содержит либо одно, либо другое, но не оба сразу: <compute>
рядом с <gen> — это TDC219. Вынесите <compute> в свою
<sequence> и читайте разыгранную через <field name="…"/> — ровно так, как разложены
паки.
- Aсгенерированные цифры
- Bвыведенная контрольная цифра
- Cарифметика: каждая вторая цифра удвоена, всё сложено, и добавлена цифра, доводящая сумму до кратной десяти
Труба
Читайте <compute> как трубу из трёх частей.
- Вход.
<field name="First"/>втягивает колонку, которая уже есть. - Работа. Операции вкладываются друг в друга, считаются изнутри наружу.
- Выход.
<result>держит готовое значение, по одному на запись.
<result> обязателен, и он ровно один. Всё остальное в блоке — это либо привязка
<let>, либо дерево внутри этого <result>.
- Aколонки, которые <compute> читает через <field> — их разыграли в другом месте
- Bоперации, каждая кормит следующую; первой выполняется самая внутренняя
- Cготовое значение — то, что держит <result>
Вот вся форма в самом маленьком виде — логин из имени и фамилии:
<tdc>
<env count="3" seed="pipe" local="en">
<sequence name="First"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Last"><gen type="template" value="person.lastName"/></sequence>
<sequence name="Login">
<compute>
<result>
<lower>
<concat>
<slice from="0" to="1"><field name="First"/></slice>
<field name="Last"/>
</concat>
</lower>
</result>
</compute>
</sequence>
</env>
<block><line><data>${{First}} ${{Last}} → ${{Login}}</data></line></block>
</tdc>
James Williams → jwilliams Robert Johnson → rjohnson John Smith → jsmith
Три операции, считаются изнутри наружу: <slice> берёт первую букву, <concat>
приклеивает её к фамилии, <lower> переводит результат в нижний регистр. Ничего здесь
не разыгрывается — оба имени разыграли свои <gen>, а <compute> только переставил то,
что они выдали.
Слоты: дети с должностью
Большинство тегов принимают детей как обычные значения, по порядку. Некоторые вместо
этого принимают именованные роли — таких детей называют слотами. <at> достаёт
элемент из списка, и ему нужны две разные вещи — список и позиция, — поэтому у каждой
свой тег:
<tdc>
<env count="3" seed="slot" local="en">
<sequence name="Score"><gen type="number" value="0..2"/></sequence>
<sequence name="Grade">
<compute>
<result>
<at>
<in><list v="100,200,300"/></in>
<index><to_number><field name="Score"/></to_number></index>
</at>
</result>
</compute>
</sequence>
</env>
<block><line><data>${{Score}} → ${{Grade}}</data></line></block>
</tdc>
1 → 200 2 → 300 0 → 100
<in> и <index> — слоты тега <at>. Сами по себе они не значат ничего, больше нигде
не встречаются, и от перестановки местами ничего не меняется — смысл несёт имя, а не
позиция. Четырнадцать тегов на этих страницах — такие же слоты или переменные цикла, и
каждый вводится вместе с тем тегом, которому принадлежит.
Разбор примера — валидный ИНН
ИНН почти случаен: последняя цифра — контрольная, посчитанная из остальных.
Сгенерируем 9 случайных цифр, а <compute> пусть допишет ту единственную цифру,
которая делает номер валидным:
<tdc>
<env count="100" seed="demo">
<sequence name="Base">
<gen type="number" value="100000000..999999999"/> <!-- 9 случайных цифр -->
</sequence>
<sequence name="Inn">
<compute> <!-- полный ИНН -->
<let name="check">
<mod><mod>
<reduce>
<over><field name="Base"/></over>
<init><int v="0"/></init>
<do><add><acc/><multiply><current/>
<at><in><list v="2,4,10,3,5,9,4,6,8"/></in>
<index><current_index/></index></at>
</multiply></add></do>
</reduce>
<int v="11"/></mod><int v="10"/></mod>
</let>
<result><concat><field name="Base"/><var name="check"/></concat></result>
</compute>
</sequence>
</env>
<block><line><data>${{Inn}}</data></line></block>
</tdc>
5962157388 4783743000 6257705772 2280980875 6591231869
Примеры вывода на этой странице иллюстративные — точные значения зависят от сида и версии ядра, но каждая строка выше — настоящий валидный десятизначный ИНН: 9 случайных цифр плюс корректная контрольная.
Разберём по частям: <reduce> сворачивает 9 цифр в одну бегущую
сумму. Каждую цифру умножаем на весовой коэффициент, который берём по её позиции
из списка коэффициентов через <at> — номер позиции даёт
<current_index/>. Затем mod 11, а следом mod 10 превращают
сумму в итоговую контрольную цифру, и <concat> приклеивает её к
базе.
Три типа значений
Любое compute-выражение вычисляется в одно из трёх значений.
| Тип | Что это | Литерал |
|---|---|---|
int | целое число (64 бита) | <int v="10"/> |
str | строка | <str v="AB"/> |
list | список из int/str | <list v="2,4,10"/> |
Значение литерала пишется в атрибуте v — в TDC теги не могут содержать
текст между <тег> и </тег>. Дробных чисел и логических «истина/ложь» нет.
Знакомые имена, другое поведение
Несколько тегов носят имя из языков программирования и ведут себя иначе. Знакомое имя здесь хуже незнакомого: документацию про тег, который вы «и так знаете», никто не перечитывает. Каждая строка ниже измерена, а не выведена рассуждением.
| Вы пишете | Чего можно ждать | Что даёт TDC |
|---|---|---|
<divide> 7 на 2 | 3.5 | 3 — целочисленное деление, остаток отброшен |
<divide> 1 на 3 | 0.33 | 0 |
<mod> -7 на 3 | -1, как в C, Java и JavaScript | 2 — остаток никогда не отрицательный |
<list v="a,b"/> | список из двух строк | ошибку: "a" is not an integer |
<replace from="[ab]"/> | регулярное выражение | буквальное совпадение, поэтому не заменится ничего |
<replace from="a"/> на banana | первую a | bXnXnX — все вхождения |
<equals> для 5 и "5" | разные типы, значит не равны | равны |
<slice from="99"> на abc | ошибку | пустую строку, молча |
<pad width="2"> на 12345 | 12 | 12345 — width это минимум, а не обрезка |
<each> там, где ждут строку | строку | ошибку: cannot use a list where a string is expected |
Три из них заслуживают больше, чем строку таблицы.
<let> и <var> — не два вида переменной
Это объявление и чтение. <let name="x"> связывает имя; <var name="x"/>
читает его обратно. В большинстве языков программирования и let, и var объявляют переменную —
именно поэтому пара здесь сбивает с толку.
Отсюда три правила, и движок следит за всеми тремя:
| Правило | Что получите, если нарушить |
|---|---|
| Имя должно быть связано до того, как его прочитают | TDC182: <var name="x"> is not bound by an enclosing <let> |
| Имя связывается один раз и не перепривязывается | TDC185: <let name="x"> shadows an outer binding of the same name |
| Привязка видна только внутри своего слота | <let> внутри <do> снаружи не виден — снова TDC182 |
Проще говоря: <let> считает значение один раз и даёт ему имя, а <var> — это способ
попросить это значение снова, не пересчитывая всё заново. После того как имя дано,
значение уже не меняется — ради этого его и называют. Ровно так же считают на бумаге:
пусть s — это оценка числом, записали один раз и дальше пользуются до конца.
<divide> выбрасывает остаток
Эта ловушка стоит дороже всех, потому что ничего не ломается вслух. Процент, посчитанный
как <divide> части на целое, равен 0 в каждой записи, и колонка выглядит правдоподобно,
пока кто-нибудь не проверит. Умножайте сначала, делите в конце — или держите значение в
целых единицах: в копейках, а не в рублях.
Деление на ноль не пропускается молча, а отклоняется:
<divide>: the divisor (second child) must not be zero.
Значение из <field> — строка
<field name="Score"/> отдаёт строку, даже если в колонке цифры. Арифметике нужно число,
и на границе между ними стоит <to_number>. Забудете — ошибка назовёт тег, которому
достался не тот тип.
Обратный переход происходит сам: число, попавшее в <concat>, становится своими цифрами.
Семейства тегов
У каждого семейства своя страница с разобранными примерами; полный алфавитный каталог — в справочнике по функциям compute.
Литералы и ссылки
Значения, с которых вы стартуете, и имена, которые вы даёте промежуточным результатам.
| Тег | Что делает |
|---|---|
<int v="10"/> | целое число (v — десятичное, можно с ведущим -) |
<str v="AB"/> | строка |
<list v="2,4,10"/> | список целых через запятую |
<field name="X"/> | значение последовательности X в области видимости — как ${{X}} |
<var name="X"/> | значение, связанное объемлющим <let name="X"> |
<let name="X">… | назвать промежуточный результат для соседних тегов |
<current/> | текущий элемент (только внутри <do>) |
<current_index/> | номер текущего элемента, отсчёт с нуля |
<acc/> | накопитель (только внутри <reduce><do>) |
Арифметика
Целочисленная математика. <add> суммирует своих детей, <subtract> — это
первое минус сумма остальных, <multiply> — произведение, <divide> —
целочисленное деление, а <mod> — всегда неотрицательный остаток. Рядом с
ними — <to_number> (строка из цифр → целое) и <encode> (символ → число в
некоторой системе счисления).
Списки и перебор
<each> применяет <do> к каждому элементу списка, <reduce> сворачивает
список в одно значение через <acc>, <join> превращает список в строку, <at>
берёт элемент по номеру, а <length> измеряет длину строки или списка. Обёртки
<over> / <do> / <init> / <in> / <index> явно указывают, какой ребёнок
какую роль играет.
Строки и форматирование
<concat> склеивает части; <upper> / <lower> / <capitalize> / <title>
меняют регистр; а <mask>, <slice>, <replace>, <trim>, <group> и <pad>
преобразуют строку. То же форматирование доступно и через
атрибуты <gen> и фильтры ${{X|…}}.
Условия
<choose> берёт первый <when>, чей <test> истинен, иначе — <otherwise>
(он обязателен). Предикаты живут только внутри <test> и не возвращают значения:
<equals>, <greater_than>, <less_than> (сравнивают два целых) и <is_digit>
(символ 0–9).
<choose>
<when><test><greater_than><var name="d"/><int v="9"/></greater_than></test>
<then><subtract><var name="d"/><int v="9"/></subtract></then></when>
<otherwise><var name="d"/></otherwise>
</choose>
В пакетах данных: параметры и <valid>
Встроенные генераторы идентификаторов — это пакеты данных .tdc, где <compute> стоит
рядом с <gen>. Автору пакета помогают два механизма.
Параметры
Любой атрибут на вызывающем <gen type="template" …> (кроме служебных
type / value / local) переопределяет одноимённую локальную <sequence>
в пакете константой. Автор пакета объявляет параметр как последовательность со
значением по умолчанию, а вызывающий может его зафиксировать:
<gen type="template" value="russia.tax.inn_org" tax_office="7712"/>
Теперь каждый ИНН начинается с фиксированного кода налоговой (tax_office), а
контрольная цифра по-прежнему получается верной — <compute> пересчитывает её от
любой полученной базы.
7712702700 7712682927 7712220601
Когда применять: когда один пресет должен покрыть множество конкретных вариантов — один пакет ИНН, которому можно задать любой код налоговой, один пакет телефонов, нацеленный на конкретный код региона.
Отклонить и повторить — <valid>
Иногда вычисленная контрольная цифра непредставима в целевом формате или
недопустима для предметной области. Например, польский NIP не выдаётся, если
контрольная равна 10; такие строки приходится отбрасывать. Пакет добавляет один
предикат <valid>, и движок перегенерирует базу, пока она не пройдёт проверку
(с предохранителем, чтобы невозможное условие не зациклилось навечно):
<sequence name="check"><compute><result> …контрольная цифра… </result></compute></sequence>
<valid><less_than><to_number><field name="check"/></to_number><int v="10"/></less_than></valid>
Среди тысяч строк ни в одной контрольная не будет равна 10 — каждый выданный NIP состоит только из чистых цифр.
7027013633 6829260874 2206093132
Когда применять: когда корректно вычисленное значение всё равно может быть недопустимым для предметной области (невыданный диапазон, запрещённая контрольная цифра), и нужно, чтобы пакет гарантировал только хорошие строки.
Ограничения
- Никаких строк-выражений — каждая операция это отдельный тег.
- Только целые числа — переполнение 64 бита это ошибка, а не «тихое» переполнение; дробных чисел и логических значений нет.
- Только ограниченные циклы (
<each>,<reduce>по конечному входу) — язык всегда завершается. - Имена тегов пишутся через
_(какbefore_block):current_index,to_number,greater_than,less_than,is_digit. - Ошибки в дереве ловятся до запуска (коды
TDC180–TDC187): неизвестный тег, несвязанная<var>,<choose>без<otherwise>и т. п.
Смотрите также
- Справочник по функциям compute — полный каталог.
- Условия —
<choose>, предикаты и<valid>подробно. - Маски и регистр — то же форматирование через
фильтры и атрибуты
<gen>.