Saltar al contenido principal

Máscaras y mayúsculas

Un generador produce valores crudos — exactamente como se construyen. Un número de seguro social de EE. UU. son nueve dígitos seguidos, una fecha es 2020-05-14, un nombre es como esté puesto en la lista. Al generador le importa qué son los datos, no cómo se ven: los separadores, las mayúsculas y el orden de las palabras no son su trabajo.

Esto es lo que emite un generador text a secas para un lote de SSN (order="sequential" para que los valores de abajo se mantengan estables y se repitan en orden):

<gen type="text" value="378984323,889735724,852139753,263243158" order="sequential"/>
./run demo.tdc
378984323
889735724
852139753
263243158

Los valores son válidos, pero leerlos así es doloroso. El formato le da forma a un valor ya generado, camino a la salida. Siempre son tres partes:

valor crudo → procesador → apariencia final — por ejemplo 378984323 → máscara xxx-xx-xxxx378-98-4323.

Hay dos procesadores independientes:

  • mask — corta la cadena en piezas y coloca separadores entre ellas (x, w, *);
  • caseupper, lower, capitalize, title.

Además están las operaciones slice, replace, trim, group, compact (abajo), los filtros de escape csv / sql, y order="sequential" — para tomar los datos en orden en vez de al azar.

nota

Las salidas de ejemplo son ilustrativas — los valores exactos pueden variar entre versiones del core. Lo que importa es la forma de cada transformación.

Un valor real pasando por una máscara real.
  • Ael valor generado, antes de la máscara
  • Bla máscara: una x es una ranura, cualquier otra cosa es un literal que se conserva tal cual
  • Cel resultado — las curvas muestran qué carácter del origen llenó cada ranura

Tres rutas, un mismo comportamiento

El mismo formato está disponible de tres maneras — mismo resultado, elija la que le convenga:

RutaCómo se escribeMejor cuando
Un filtro en la interpolación${{X | mask:…}}un valor en un punto específico del texto
Un atributo en <gen><gen … mask="…" case="…"/>se formatea el generador entero
Una etiqueta en <compute><mask pattern="…">…</mask>el formato es un paso de un cálculo

La mayoría de los ejemplos de abajo usan la ruta del filtro, que vive en la interpolación. Cada etiqueta de compute tiene además su casa en Cadenas y formato.

Máscara — cortar y espaciar

Problema. Un número llega como una sola cadena pegada (378984323) — no se ve dónde están los grupos.

Herramienta. Una máscara recorre su patrón de izquierda a derecha. Cada ranura se come un pedazo de la entrada; todo lo demás se imprime como literal:

RanuraToma de la entrada
xun carácter
wuna palabra (letras hasta un espacio) y se traga un espacio
*todo lo que aún no se ha consumido
x[0] w[-1]una posición nombrada — ver Mover piezas
\escapa el siguiente carácter (\x → una x literal)
cualquier otra cosaun literal: guion, punto, espacio, paréntesis — se imprime tal cual

Los mismos dígitos bajo dos patrones:

<sequence name="Ssn">
<gen type="text" value="378984323,889735724,852139753,263243158" order="sequential"/>
</sequence>
...
<data>${{Ssn}} -> ${{Ssn | mask:xxx-xx-xxxx}} | ${{Ssn | mask:xxx.xx.xxxx}}</data>
./run demo.tdc
378984323  ->  378-98-4323   |   378.98.4323
889735724  ->  889-73-5724   |   889.73.5724
852139753  ->  852-13-9753   |   852.13.9753
263243158  ->  263-24-3158   |   263.24.3158

A la izquierda está el valor crudo; luego el mismo SSN bajo dos máscaras. Cambie los separadores del patrón y obtiene otra apariencia — con los mismos datos.

La ranura w — trabajar por palabras

Problema. A partir de first last hay que armar un acomodo propio. La ranura w toma una palabra y se traga un espacio después de ella:

<sequence name="Name"><gen type="text" value="juan lópez,rosa pérez,luis gómez" order="sequential"/></sequence>
...
<data>${{Name}} -> ${{Name | mask:w:w}}</data>
./run demo.tdc
juan lópez  ->  juan:lópez
rosa pérez  ->  rosa:pérez
luis gómez  ->  luis:gómez

w agarró juan, se comió el espacio, imprimió el literal :, y la segunda w agarró lópez. No hay espacio antes del : — la primera w se lo tragó. El detalle completo de los casos límite vive en <mask>.

Mover piezas — x[0], w[0] y rangos

Problema. El valor llega entero y en el orden equivocado. Un nombre sale de un pack como juan lópez, y la exportación quiere primero el apellido. Una dirección es 12 Baker St, y el país para el que genera escribe el número al final. Las partes nunca fueron suyas para ordenarlas: la cadena salió de una columna de un archivo, de una dirección de pack o de un regex, así que no puede simplemente generar dos secuencias e imprimirlas al revés.

Herramienta. Ponga un índice entre corchetes en la ranura. Nombra una posición de la entrada original:

RanuraToma
x[7]el carácter en el índice 7 — el octavo, contando desde x[0]
x[5..7]los caracteres 5, 6 y 7 — ambos extremos incluidos
x[-1]el último carácter
w[1]la palabra en el índice 1 — la segunda
w[-1]la última palabra

Los índices empiezan en 0, como en el filtro slice. Los rangos se escriben con .., igual que en todo TDC (value="10..99", repeat="1..5"); un guion sería ambiguo junto a x[-1].

Eso es todo lo que se agrega. Dos ejemplos, y cada uno es un problema real:

<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

Ninguno depende de lo largas que sean las palabras — para eso se cuenta por palabras y no por caracteres. w[-1] es la última palabra tenga el nombre dos partes o cuatro, y w[1..-1] es "todo menos la primera".

Qué ocurre en realidad: el pool

Una máscara con índices corre en dos canales que no se interfieren.

ABCDE bajo la máscara x[4]-xxxx. La ranura con índice tira de un carácter hacia el frente; las ranuras sin índice toman después lo que queda, en su orden original.
  • Ael valor original, con cada posición numerada
  • Blo que produjo la máscara
  • la posición que nombró un índice, y dónde aterrizó
  • lo que tomaron las ranuras sin índice, en el orden en que estaban

El primer canal es qué se imprime: un índice lee esa posición del original, y nada lo cambia. El segundo es con qué se quedan las ranuras sin índice x / w / *, y es lo único que el consumo toca. Una posición que un índice tomó sale del pool:

La misma corrida vista como pool: antes de la toma con índice y después de ella.
  • Ael valor original, antes de la toma con índice
  • Bel pool del que tiran las ranuras sin índice — la posición que tomó un índice ya no está
  • la posición que nombró x[4]

Por eso conviene releer qué es *: significa todo lo que aún no se consumió, no "la cola de la cadena". Mueva dos dígitos al frente y * sigue imprimiendo los otros nueve:

<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

El mismo índice dos veces — una copia en vez de un movimiento

Nada impide que dos ranuras nombren una misma posición. Cuando lo hacen, esa parte se imprime dos veces — y sale solo un código de almacén cuya cabeza se repite al final:

AB1234 bajo x[0..1]-*-x[0..1]. De las mismas dos celdas salen dos flechas: la cabeza se imprime al frente y otra vez al final.
  • Ael código original, con cada posición numerada
  • Bel resultado — diez caracteres a partir de seis
  • los dos caracteres nombrados dos veces, impresos en ambos extremos
  • el resto, tomado por * en su orden original
<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

De ahí sale lo único que la notación no puede decir por sí sola: x[2] no dice si es un movimiento o una copia. Copia si alguna otra ranura recoge también esa posición, y se lee como movimiento si ninguna lo hace. Se descubre leyendo la máscara entera, no la ranura.

Un rango descendente corre hacia atrás

x[-1..0] es "del último carácter al primero" — una inversión a la que no le importa cuán largo sea el valor. AB1234 se vuelve 4321BA. Sirve para fabricar datos de prueba deliberadamente estropeados; fuera de eso, no es algo que convenga buscar.

Tres cosas con las que va a tropezar

Un corchete es índice solo justo después de x o w. En cualquier otro lugar es un literal corriente, así que mask="[tel.] xxx-xxx" no necesita escape alguno. Si de verdad quiere un corchete literal justo después de una ranura, escápelo: mask="x[1]\[*\]" sobre ABC da B[AC].

Un índice más allá del final no imprime nada, y no avisa. w[4] sobre un valor de dos palabras es una cadena vacía, igual que una x pasada del final de un valor corto. El largo de la entrada no se conoce hasta generar la fila, así que no hay nada que comprobar de antemano — y detener una corrida de un millón de filas por un valor corto sería peor. Vigile las celdas vacías cuando use un índice fijo sobre datos de forma variable; w[-1] suele ser la manera más segura de decir "la última".

Un guion en un rango se rechaza, no se adivina. x[1-2] es el error de tipeo fácil, y si se tratara como texto literal produciría datos equivocados en silencio. En su lugar es TDC199, informado antes de generar una sola fila:

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

Case — upper / lower / capitalize / title

Problema. Los datos llegan con mayúsculas y minúsculas mezcladas (fuentes distintas, importaciones): iPhone CASE, maría LÓPEZ. Se necesita una sola forma consistente.

NombreQué hace
upperTODO EN MAYÚSCULAS
lowertodo en minúsculas
capitalizesolo la primera letra en mayúscula, el resto igual
titlela primera letra de cada palabra en mayúscula, el resto igual

La misma cadena a través de los cuatro:

<sequence name="W"><gen type="text" value="iPhone CASE,maría LÓPEZ,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
maría LÓPEZ   ->  upper=MARÍA LÓPEZ | lower=maría lópez | capitalize=María LÓPEZ | title=María LÓPEZ
ANNA von lee  ->  upper=ANNA VON LEE | lower=anna von lee | capitalize=ANNA von lee | title=ANNA Von Lee

capitalize toca únicamente el primerísimo carácter (iPhone CASE queda casi igual — su i inicial se vuelve I), mientras que title levanta la primera letra de cada palabra (vonVon, leeLee). upper/lower cambian todo.

Distinto case según una condición, sobre los mismos datos

Como el formato ocurre en la salida, se puede aplicar un case distinto por fila desde un solo generador — digamos los apellidos masculinos capitalizados y los femeninos en mayúsculas:

<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 garcía  ->  GARCÍA
M garcía  ->  García
F ruiz  ->  RUIZ
F moreno  ->  MORENO
M lópez  ->  López
M ruiz  ->  Ruiz

Un solo generador Word, pero su apariencia depende de Gender. Esto no se puede hacer dentro del generador mismo — el formato en la salida lo resuelve en una sola línea.

Como atributo de <gen> — formatear la columna entera

Problema. Uno no quiere envolver cada sustitución — quiere que la columna entera salga ya formateada.

Herramienta. Ponga mask="…" / case="…" directamente en el <gen>. A la izquierda, el mismo generador sin el atributo (crudo); a la derecha, el mismo con mask=:

<sequence name="Raw"><gen type="text" value="378984323,889735724,852139753,263243158" order="sequential"/></sequence>
<sequence name="Nice"><gen type="text" value="378984323,889735724,852139753,263243158" order="sequential" mask="xxx-xx-xxxx"/></sequence>
...
<data>${{Raw}} -> ${{Nice}}</data>
./run demo.tdc
378984323  ->  378-98-4323
889735724  ->  889-73-5724
852139753  ->  852-13-9753
263243158  ->  263-24-3158

Así es exactamente como se le da a un generador de plantilla su apariencia «bonita»:

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

Ambas rutas vienen del generador template. Si se definen los dos atributos, el orden es primero mask, luego case.

Cadenas de filtros — varias operaciones seguidas

Problema. Se necesita más de una transformación — por ejemplo, reacomodar con una máscara y subir el case.

Herramienta. Encadene filtros uno tras otro con |, de izquierda a derecha: crudo → mask → case:

<sequence name="Name"><gen type="text" value="juan lópez,rosa pérez,luis gómez" order="sequential"/></sequence>
...
<data>${{Name}} -> ${{Name | mask:w:w}} -> ${{Name | mask:w:w | upper}}</data>
./run demo.tdc
juan lópez  ->  juan:lópez  ->  JUAN:LÓPEZ
rosa pérez  ->  rosa:pérez  ->  ROSA:PÉREZ
luis gómez  ->  luis:gómez  ->  LUIS:GÓMEZ

La columna del medio es después de la máscara; la de la derecha es después de la máscara y de upper. Cada filtro recibe el resultado anterior.

nota

El argumento de una máscara se lee hasta el siguiente | o hasta el }} de cierre, así que los espacios y los dos puntos viven felices dentro de él (mask:w:w, mask:xxx-xx-xxxx).

Más filtros: slice, replace, trim, group

Las mismas tres rutas (filtro / atributo de <gen> / etiqueta de <compute>). Cada uno de abajo sigue el camino crudo → herramienta → resultado, con una variación.

slice — cortar una parte por índice

Problema. De la fecha 2020-05-14 solo se necesita el año, o solo el mes.

<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 son los caracteres 0–3 (el año), slice:5,7 son 5–6 (el mes), y slice:5 sin segundo número significa «del 5 hasta el final». Son índices de caracteres, empezando en cero. Vea <slice>.

replace — reemplazar todas las apariciones

Problema. La fecha trae guiones, pero se necesita otro separador.

<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

El formato es replace:from,to — se reemplazan todas las apariciones. Hay tres cosas que no hace, y cada una falla en silencio en vez de avisar:

  • from se busca literalmente, nunca como expresión regular. replace:[abc],Z busca los cinco caracteres [abc] y, al no encontrarlos, no cambia nada.
  • from no puede contener una coma. La primera coma lo termina, así que todo lo que sigue pertenece a to: replace:-,+,x reemplaza cada - por +,x.
  • Un from vacío no hace nada. replace:,+ devuelve el valor intacto.

Donde algo de esto importe, use la etiqueta <replace> dentro de <compute>: toma from= y to= como atributos separados, así que una coma es solo un carácter.

trim — quitar los espacios de los extremos

Problema. Los datos que vienen de un archivo o de un CSV a veces cargan espacios sueltos en los bordes. Aquí los espacios se agregan a propósito (como si vinieran de una fuente), y los corchetes del texto solo sirven para hacerlos visibles:

<sequence name="City"><gen type="text" value="Toluca,Mérida,Colima" order="sequential"/></sequence>
<!-- pega espacios extra en los bordes, imitando datos "sucios" -->
<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
[  Toluca   ]  ->  [Toluca]
[  Mérida   ]  ->  [Mérida]
[  Colima   ]  ->  [Colima]

Solo los extremos — los espacios internos se dejan en paz. Vea <trim>.

group — agrupar dígitos desde la derecha

Problema. Un número largo es ilegible: 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

La agrupación corre desde la derecha, así que el grupo corto termina a la izquierda (1 234 567). group:3 da miles (el separador por omisión es un espacio), group:3,- define un separador propio, y group:4 se lee como bloques de tarjeta. Un 42 corto es más chico que un grupo, así que vuelve sin cambios. Vea <group>.

Resumen

OperaciónFiltroEtiqueta de <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 o compact:16
csvcsv— (sin etiqueta)
sqlsql— (sin etiqueta)

Orden — order="sequential"

Problema. Por omisión, text y file eligen valores al azar. A veces los datos tienen un orden pensado (una lista en un archivo, una serie especial) y hay que conservarlo.

Herramienta. order="sequential": la fila i toma el i-ésimo valor en orden, dando la vuelta. A la izquierda está el generador común (aleatorio); a la derecha, la misma lista Ene,Feb,Mar en orden:

<sequence name="Rand"><gen type="text" value="Ene,Feb,Mar"/></sequence>
<sequence name="Seq"><gen type="text" value="Ene,Feb,Mar" order="sequential"/></sequence>
...
<data>random=${{Rand}} sequential=${{Seq}}</data>
./run demo.tdc
random=Feb   sequential=Ene
random=Feb   sequential=Feb
random=Mar   sequential=Mar
random=Ene   sequential=Ene
random=Ene   sequential=Feb
random=Feb   sequential=Mar
random=Mar   sequential=Ene

La columna de la derecha corre estrictamente Ene, Feb, Mar, Ene, Feb, Mar, Ene… — vuelta tras vuelta.

  • order="random" — el valor por omisión.
  • order="sequential" — estrictamente en orden, dando la vuelta.
  • cycle="false" — falla con un error claro cuando los datos se acaban, en vez de dar la vuelta.
  • Funciona igual con archivos: <gen type="file" src="@data/cities.txt" order="sequential"/> emite las líneas del archivo estrictamente en su orden.

compact — un número largo, escrito corto

Convierte un entero a base 36 (dígitos + letras minúsculas). Útil donde un número es una cola única que además una persona tiene que leer:

<data>${{F|lower}}.${{L|lower}}.${{Id|compact}}@example.com</data>
./run demo.tdc
carlos.rivera.1@example.com          <- primera fila
ana.molina.lfls@example.com          <- la millonésima
pablo.serrano.x2qxvk@example.com     <- la dos mil millonésima
NúmeroDecimalcompact
1 000 0007 dígitoslfls — 4 caracteres
2 000 000 00010 dígitosx2qxvk — 6 caracteres
1 000 000 000 00013 dígitoscre66i9s — 8 caracteres

Seis caracteres cubren 2170 millones de filas; siete cubren 78 mil millones. El mapeo es uno a uno, así que números distintos siempre dan cadenas distintas — la unicidad por la que agregó el número se conserva por completo.

nota

Solo minúsculas, a propósito. La base 62 (con mayúsculas) sería aún más corta, pero muchos sistemas pasan el correo a minúsculas — entonces aB y Ab colapsarían en una sola dirección y los duplicados volverían en silencio.

Defina la base con compact:16 (hexadecimal). Un valor que no sea entero se deja intacto.

Escape para un formato: csv y sql

<data> construye texto y no sabe nada del archivo que se está escribiendo, así que un valor con una coma parte en silencio una fila de CSV, y un apóstrofo rompe el SQL. Esto no es teoría: un solo nombre de producto como Juego de cuchillos, 3 pzas puede convertir miles de filas en registros con un campo de más — la categoría se desliza al precio, el precio a la cantidad, y no se levanta ni un solo error. Dos filtros cierran ambos casos.

csv — un campo según el RFC 4180

<data>${{Id}},${{Name | csv}},${{Category}}</data>
./run demo.tdc
7,"Juego de cuchillos, 3 pzas",Cocina
2,"Café ""Arábica"" 250 g",Alimentación

Las comillas se agregan siempre, no «cuando hacen falta» — una regla sin excepciones le gana a una adivinanza que tarde o temprano se topa con una coma o un salto de línea, y cualquier lector de CSV acepta las comillas de más. Vea también Formatos de salida → CSV.

Lo que el filtro deliberadamente no hace: los valores que empiezan con =, +, - o @ se convierten en fórmulas vivas al abrir el archivo en una hoja de cálculo. Los datos generados conservan sus bytes tal cual — si el archivo va a Excel y eso importa, agrégueles un prefijo usted mismo con el filtro replace.

sql — el cuerpo de un literal de cadena

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

El filtro duplica el apóstrofo y devuelve solo el contenido, sin comillas externas — esas las escribe usted, así que la forma de la consulta sigue siendo visible en la configuración. Para JSON no hay un filtro aparte: escape la comilla con una barra invertida usando el mismo filtro replace.

Este es el entrecomillado del SQL estándar (PostgreSQL, SQLite, Oracle, ANSI). MySQL en su modo por defecto también trata \ como carácter de escape — habilite allí NO_BACKSLASH_ESCAPES, o duplique antes las barras invertidas con replace.

Vea también