Saltar al contenido principal

Varios valores en una celda (repeat)

Conviene usarlo cuando un campo no siempre tiene un solo valor. Un pedido lleva un artículo o cinco. Un artículo carga una sola etiqueta o cuatro. Un sensor reporta tres lecturas a la vez. repeat hace que un mismo <gen> emita varios valores dentro de una celda en lugar de uno — y la cantidad puede variar de fila en fila.

Antes de repeat, la única manera de fingir un campo de longitud variable eran cinco secuencias separadas con la mitad de las filas en blanco. Ahora es un atributo en el generador.

Las salidas de ejemplo de abajo son ilustrativas — los valores exactos que produce un seed dado pueden cambiar entre versiones del core, pero la estructura que garantiza cada forma (cuántos valores caen en una celda, y cómo se unen) no cambia.

Las primeras ocho filas de una corrida de 2000 filas, y la longitud de cada lista en ella.
  • Auna celda por fila, con entre uno y cuatro valores
  • Bcuántas filas salieron con cada longitud — los conteos son una cuota exacta, no una aproximación

De un vistazo

AtributoQué significa
repeat="3"exactamente tres valores
repeat="1..5"entre uno y cinco — cada longitud recibe una parte exacta de las filas
repeat="0..3"se permite el cero — la celda puede salir vacía
separator=" "con qué unirlos; por omisión es una coma

El límite superior es 64. separator sin repeat es un error — no hay nada que unir.

Un rango es una cuota, no un volado

repeat="1..4" no tira un dado por fila. TDC le da a cada longitud una parte exacta de la corrida, igual que percent: 200 filas salen 50 / 50 / 50 / 50 y 201 filas salen 51 / 50 / 50 / 50. Eso es lo que mantiene exactas también las proporciones de los valores (vea «Listas de palabras y proporciones exactas» más abajo).

El precio es el mismo que paga todo diseño que abarca la corrida entera: cambie count y las longitudes se reparten de nuevo, así que una corrida corta no es el comienzo de una larga. Vea Determinismo y proporciones.

repeat="1..5" — una cantidad que varía por fila

Este es el caso de todos los días. Una línea de pedido debería llevar la cantidad de artículos que el pedido haya tenido. Aquí cada canasta saca de uno a cuatro SKU, unidos por espacios:

<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>order ${{Id}}: ${{Items}}</data></line>
</block>
./run basket.tdc (count=8)
order 1: SKU-5365 SKU-2241 SKU-0758 SKU-9382
order 2: SKU-2033 SKU-3412 SKU-3799
order 3: SKU-3278
order 4: SKU-3984 SKU-4578
order 5: SKU-5351 SKU-5903
order 6: SKU-3412
order 7: SKU-0258 SKU-3326 SKU-1157
order 8: SKU-3205 SKU-4821 SKU-3618 SKU-2450

El tamaño de la canasta se mueve entre uno y cuatro artículos — como en los pedidos reales. Por qué: cualquier campo cuya cardinalidad en el mundo real varíe (artículos de un pedido, etiquetas, números telefónicos, direcciones) se traduce directo a un rango min..max.

repeat="3" — una cantidad fija de valores

Dé un solo entero y cada celda llevará exactamente esa cantidad de valores. Útil para tokens de forma fija — un código de tres grupos, una terna (x, y, z), un conjunto de tres lecturas:

<sequence name="Code">
<gen type="regex" value="[A-Z]{2}" repeat="3" separator="-"/>
</sequence>
./run code.tdc (count=6)
QR-LM-ZP
BX-TT-KD
WM-AE-RH
NP-CC-JU
FL-GO-VS
DK-HY-QN

Por qué: cuando la forma es fija y solo varían los valores, repeat="3" es más claro que tres campos separados pegados en el bloque de salida.

repeat="0..3" — se permite el cero

Un rango que empieza en 0 deja que una celda salga vacía. Eso es justo lo correcto para una lista opcional — algunos artículos no tienen ninguna etiqueta:

<sequence name="Tags">
<gen type="text" value="news,tech,sport,food,travel" repeat="0..2" separator=", "/>
</sequence>
./run tags.tdc (count=6)
article 1: tech, news
article 2:
article 3: sport
article 4:
article 5: travel, food
article 6: news

Los artículos 2 y 4 sacaron longitud cero, así que su celda queda en blanco — no es un valor especial, solo una lista vacía. Por qué: «a veces presente, a veces ausente» es una forma real de los datos, y 0..n la modela sin una segunda secuencia.

separator= — cómo se unen los valores

Sin separator, los valores se unen con una coma. Póngale cualquier cadena para que coincida con el formato que está construyendo — un espacio, una barra vertical, " | ", "; ", lo que necesite la columna.

<sequence name="Letters">
<gen type="text" value="a,b,c,d,e" repeat="3"/> <!-- coma por omisión -->
</sequence>
<sequence name="Piped">
<gen type="text" value="a,b,c,d,e" repeat="3" separator=" | "/>
</sequence>
./run sep.tdc (count=3) — por omisión y con barra vertical
d,a,c        d | a | c
b,e,b        b | e | b
a,c,e        a | c | e

Por qué: la celda unida es apenas texto, así que el separador es la manera de hacerla encajar en una columna de CSV, en una lista en línea estilo JSON o en una línea legible para una persona.

accumulate= — un total acumulado a lo largo de la lista

Muchas veces los valores de una celda son pasos de una misma cosa, no tres sorteos independientes: las líneas de un ticket, los tramos de un viaje, los minutos de una sesión. accumulate= reemplaza la lista por su total acumulado.

<gen type="number" value="150..900" decimals="2" repeat="3" separator=", " accumulate="sum"/>
./run cart.tdc
792.47, 325.07, 563.18   →   459.93, 1277.62, 1909.65
814.04, 304.59, 456.76   →   495.46, 984.22, 1420.44
471.89, 479.67, 795.74   →   718.26, 1309.23, 1948.80

A la izquierda, el mismo generador sin accumulate=; a la derecha, con él. El último elemento es el total, y cada uno antes de él es el subtotal en ese paso.

accumulate=Cada elemento pasa a ser
sumla suma de todo lo anterior, incluido él
maxel mayor visto hasta ahí — un récord parcial
minel menor visto hasta ahí
./run peaks.tdc
pico 22,22,22,83,83
pico 11,48,54,54,54
pico 57,60,62,62,93

La aritmética es exacta, también con centavos. La suma se hace sobre enteros escalados por la fracción más ancha de la lista, nunca en punto flotante — por eso 19.99 + 0.01 da 20.00 y no 20.000000000000004, y da el mismo 20.00 en las cinco implementaciones.

min y max devuelven un elemento que ya existe, así que conservan su escritura. Un valor sorteado como 007 sigue siendo 007, no se convierte en 7.

Un elemento vacío se saltea. missing= deja celdas en blanco, y una vacía deja el acumulador quieto en vez de contar como cero: «ese día no se tomó la lectura» no debería poner el contador en cero.

accumulate= necesita una lista, o sea repeat= (TDC237), y la operación tiene que ser una de esas tres (TDC238).

¿Y si hace falta por columna?

Acá la acumulación ocurre dentro de un registro. Para un total que se arrastra de fila en fila —el saldo de una cuenta, un contador que solo sube— la construcción es otra: <gen type="running">.

Los valores faltantes, las anomalías y el formato actúan por elemento

Esto es lo único que hay que recordar: como repeat vive en el generador, todo lo que ese generador hace le pasa a cada valor por separado, no a la celda como un todo. Los huecos de datos faltantes, la inyección de anomalías y el formato de salida operan todos elemento por elemento.

La ventaja práctica se ve con una columna de banderas. Cuando un generador marca anomalías, su campo de bandera se vuelve una lista de la misma longitud que las lecturas — una bandera por lectura, alineadas exactamente:

./run sensors.tdc (lecturas -> banderas de anomalía)
5,500,100     ->   false,true,true
800,200,1     ->   true,true,false
6,8,7         ->   false,false,false
100,300,900   ->   true,true,true

La bandera queda precisamente donde está el valor atípico. Se puede probar un detector no al nivel grueso de «algo en este lote andaba mal», sino exactamente — qué lectura se rompió.

Si en cambio lo que quiere es «el registro entero está corrupto», ese es otro trabajo para otra herramienta: una rama de <mix> con un flag a nivel de registro, no repeat.

Listas de palabras y proporciones exactas

La lista más común es un conjunto de etiquetas, y repeat funciona directo sobre el generador text:

<sequence name="Tags">
<gen type="text" value="news,tech,sport,food,travel" repeat="1..3" separator=", "/>
</sequence>
./run tags.tdc (count=6)
article 1: [food]
article 2: [tech, news]
article 3: [news, tech, travel]
article 4: [sport, tech]
article 5: [travel, travel, news]
article 6: [sport]

Aquí viene la parte sutil. text acomoda sus valores en proporciones exactas mediante una máscara percent, no aproximadas — y esa garantía sobrevive a repeat, tanto con una cantidad fija como con un rango.

Funciona porque TDC decide primero todas las longitudes (también por una cuota exacta) y solo después llena las ranuras resultantes. Como las longitudes se conocen de antemano, también se conoce la cantidad total de ranuras — no se genera nada en vano y no se pierde ninguna cuota.

Comprobado sobre 120 000 filas con repeat="1..4" y percent="40,30,20,10":

./run tags-big.tdc (count=120000, con conteo)
row lengths:   30000 x1    30000 x2    30000 x3    30000 x4     (exactamente un cuarto cada una)
values:       120000 news   90000 tech   60000 sport  30000 food   (exactamente 40/30/20/10)

300 000 ranuras en total, y la distribución cae exacta. Ejecútelo con --jobs 7 y obtiene los mismos números — el archivo es idéntico byte por byte.

Los valores dentro de una celda pueden repetirse

El [travel, travel, news] de arriba está bien, no es un error. Cada ranura se llena de forma independiente, así que el mismo valor puede caer dos veces — dos lecturas de 40 o dos veces el mismo artículo en un carrito son datos normales. No hay nada integrado del tipo «hazlos distintos» para repeat; si necesita eso, es otra restricción distinta.

Dónde repeat no funciona

Unos cuantos generadores atan cada valor al número de fila, y repeat no se les puede aplicar:

GeneradorPor qué no
increment, decrementel valor depende de la posición de la fila
timeseries, patternlo mismo — el valor está atado a la posición

Para estos, el índice de un elemento dependería de qué tan largas resultaron todas las filas anteriores — una cantidad aleatoria. Eso haría imposible calcular una fila sin sus vecinas, rompiendo el streaming y --jobs. TDC se niega con un error claro (TDC204) en vez de hacer lo incorrecto en silencio.

Qué más no acepta TDC

Lo que escribióLo que dice
repeat="many", repeat="1.5"TDC195 — se requiere un entero
repeat="-1", repeat="5..2"TDC195 — el mínimo no puede ser negativo ni mayor que el máximo
repeat="1..65"TDC195 — el límite superior es 64
separator=";" sin repeatTDC198 — no hay nada que unir
repeat o separator en un <mix>TDC196 — un mix elige una rama, no construye una lista

Sobre esa última: un <mix> elige entre ramas, así que «repítelo» no tiene un significado bien definido. Si quiere una lista dentro de una rama, ponga repeat en el <gen> que está dentro del <case>.

A gran escala

repeat no le estorba ni al streaming ni a --jobs: una fila se sigue calculando de forma independiente de sus vecinas. Dos cosas distintas lo hacen posible, y es fácil confundirlas:

  • La longitud de la lista de cada fila viene de la cuota sobre la corrida entera (arriba). Una fila puede deducirla de su propio número — no necesita a sus vecinas.
  • Los valores se sacan luego para el máximo (para repeat="1..5", cinco) y los sobrantes se descartan, de modo que la posición en el flujo aleatorio depende solo del número de fila y nunca de qué longitud tuvieron las filas anteriores.

Eso es lo que permite calcular una fila sin calcular las anteriores, y es la razón por la que la salida multihilo coincide byte por byte con la de un solo hilo.

De ahí sale también el tope de 64: un repeat grande es trabajo real gastado, aunque la fila termine con solo dos elementos. Vea Salidas grandes.

En Parquet, una lista de verdad

Cuando se exporta a Parquet, una columna con repeat se vuelve una lista real, no una cadena unida. El tipo del elemento se infiere automáticamente:

esquema parquet
id       INT64       REQUIRED
items    LIST of BYTE_ARRAY (UTF8)
prices   LIST of INT64
city     BYTE_ARRAY  REQUIRED
filas parquet (como JSON)
{"id":1,"items":["bread","cheese"],        "prices":[262],         "city":"Boston"}
{"id":3,"items":["cheese","milk"],         "prices":[469,241,188], "city":"Boston"}
{"id":6,"items":["milk","bread","coffee"], "prices":[81,262],      "city":"Denver"}

Una lista vacía sigue siendo una lista vacía — no desaparece. Y un valor faltante dentro de una lista produce un null real en el lugar del elemento: en texto eso es indistinguible de una cadena vacía, pero Parquet lo registra con exactitud.

parquet — un elemento null
{"t":[6,5,null]}

También se puede definir el tipo a mano: type="[]int64", type="[]decimal(18,2)". Escribir type="[]int64|null" significa «una lista cuyo elemento puede ser null» — precisamente lo que produce un valor faltante por elemento.

Vea también

  • text — listas separadas por comas y las proporciones exactas de percent, que repeat conserva.
  • Contadoresincrement / decrement, los generadores atados a la posición a los que repeat no se les puede aplicar.
  • Máscaras y mayúsculas — el formato por elemento que se aplica a cada valor de una celda con repeat.
  • Salidas grandes — por qué repeat sigue siendo seguro para streaming e idéntico entre distintos --jobs.