Saltar al contenido principal

Expresiones

El pequeño lenguaje con cuatro hogares, todos leídos de la misma manera:

DóndeQué responde
if=si un <gen>, una <line>, un <case> o un <data> participa en esta fila
filter=de qué miembros de un pool puede sortear esta fila
expr=el VALOR de una columna formula
un parámetro de distribuciónel valor de mean, sd, lambdaen esta fila

Los dos primeros consumen la respuesta como un sí/no y tiran el valor; los dos últimos lo conservan. Mismos operadores, mismas funciones, mismos nombres para las mismas columnas — que es lo que impide que una condición y una columna calculada acaben significando cosas distintas con las mismas palabras.

<sequence name="Zone">
<gen if="Country in [US, CA, MX]" type="text" value="NAFTA"/>
<gen if="Country in [FR, DE]" type="text" value="EU"/>
<gen type="text" value="ROW"/>
</sequence>
<sequence name="Handling">
<gen if="Weight > 20" type="text" value="freight"/>
<gen if="_count % 2 == 0" type="text" value="courier-even"/>
<gen type="text" value="parcel"/>
</sequence>
./run shipping.tdc
US NAFTA 2kg parcel
FR EU 14kg courier-even
CA NAFTA 7kg parcel
DE EU 30kg freight
MX NAFTA 5kg parcel
JP ROW 22kg freight

El último <gen> de cada secuencia no lleva if=, así que recoge todo lo que las condiciones de arriba dejaron pasar — la misma forma que un else.

Valores

Usted escribeQué significa
Countryel valor que esa secuencia produjo en esta fila
Person.Emailun campo de una secuencia compuesta
Gender.Male«¿Gender es Male ahora mismo?» — se lee como parent="Gender.Male"
Maleuna palabra desnuda: un nombre que no es secuencia es su propio texto
42, 1.5un número
'texto'una cadena entre comillas, cuando el texto lleva espacios o parece un nombre
_count, _lastun integrado — el número de fila, la marca de última fila

La palabra desnuda es lo que permite escribir Gender == Male sin comillas. También significa que una errata se compara consigo misma y no coincide con nada, en silencio — por eso un nombre desconocido a la derecha de un punto lanza TDC193 en vez de pasar.

Números enteros

Un double guarda todos los enteros hasta 2⁵³ y a partir de ahí empieza a saltárselos, así que una expresión construida solo sobre doubles responde así:

lo que un double dice de dos números distintos
9007199254740993 == 9007199254740992   true
9007199254740993 -  9007199254740992   0

Las dos cosas son falsas, y falsas en silencio — que para un generador de datos es la peor forma de estar equivocado: la ejecución termina y el archivo parece correcto. Por eso un operando que ES un número entero se lleva como tal:

un literal sin punto ni exponentesigue siendo entero
una columna cuyo valor se lee como dígitoscompara como entero contra otro entero
+ - * % sobre dos enterossiguen siendo enteros
abs round floor ceil trunc sobre un enterosiguen siendo enteros: redondear un entero es ese mismo entero, del tamaño que sea
min max sum mientras todos los argumentos sean enterossiguen siendo enteros
/siempre en coma flotante — la división no es cerrada sobre los enteros
lo que se pasa a sqrt, log, sinpasa a double: esas no tienen respuesta exacta que dar

El dominio son 64 bits con signo, el mismo que la capa compute. Un RESULTADO aritmético más allá es un rechazo, con las mismas palabras que usa compute:

tdcv2 ledger.tdc
tdcv2: integer overflow: 9223372036854775808 is outside the signed 64-bit range

El rechazo viene de la corrida, no de check: ambos operandos están dentro del dominio y solo la respuesta queda fuera, así que no hay nada que el validador pueda probar.

Un LITERAL más ancho que el dominio es otra cosa: es un double. 1 / 0 > 100000000000000000000 es la manera de escribir «mayor que cualquier entero que sostenemos», y funciona; el precio es que dos literales que redondean al mismo double se comparan iguales, así que 10000000000000000000 == 10000000000000000001 es verdadero. Si quería un identificador y no un número, escríbalo como texto.

Un borde que conviene saber: −2⁶³ se alcanza con aritmética pero no se escribe como literal.

Operadores

GrupoOperadores
comparación== != === !== < > <= >=
lógica&& || !
aritmética+ - * / %

+ suma siempre que ambos lados se lean como números, y toda columna se lee como texto, así que Price + Delivery sobre dos columnas decimales es aritmética y no una cadena unida. Solo concatena cuando un lado realmente no es un número: 'a' + 'b' da ab. Para unir valores a propósito, póngalos juntos donde se imprimen: ${{First}} ${{Last}}. | pertenencia | in | | elección | a ? b : c |

== pregunta si ambos lados son el mismo número; ===, si se imprimen con los mismos caracteres. Cada columna es texto, así que las dos preguntas son de verdad distintas — vea Comparación y verdad, que además fija qué cuenta como verdadero para un if="Flag" a secas.

% es euclidiano, y eso no es lo que hace su lenguaje

-3 % 2 es 1 aquí. JavaScript, Java, C# y Rust responden −1; Python responde 1.

Un DIVISOR negativo es donde Python también se separa: 7 % -3 es 1 aquí y −2 allí. El resultado nunca lleva signo — siempre está en 0 … |divisor| - 1.

La razón no es de gusto. La capa compute ya tenía <mod> y ya respondía 1, así que un % que tomara prestada la convención del anfitrión haría que un mismo motor diera dos respuestas distintas a la misma pregunta según a qué capa acudiera usted.

in toma una lista a su derecha y nada más — una lista en cualquier otro sitio lanza TDC259. La comparación de dentro es tan laxa como la de ==, así que una columna de texto contra una lista de palabras numéricas sigue coincidiendo.

<gen if="Country in [US, CA, MX]" .../> <!-- en vez de tres == unidos por || -->

Funciones

FunciónTomaDa
abs(x)1magnitud
ceil(x) floor(x)1arriba / abajo al entero
trunc(x)1hacia cero — trunc(-7.5) es −7, floor da −8
round(x)1el más cercano, la mitad alejándose del cero
min(…) max(…)1 o másel menor / el mayor
len(s)1cuántos caracteres
is_empty(s)1si el texto está vacío
starts_with(s, p)2prueba de prefijo
ends_with(s, p)2prueba de sufijo
contains(s, p)2prueba de subcadena
lower(s) upper(s)1mayúsculas y minúsculas
split(s, sep)2texto cortado en lista — ver Listas dentro de una fila
join(list, sep)2una lista de vuelta a texto
count(list)1cuántos elementos
at(list, i)2el elemento i, contando desde cero
sum(list)1el total — sigue entero mientras todos lo sean
mean(list) median(list)1media y valor central
stddev(list)1desviación típica poblacional, dividida por n
sqrt(x)1raíz cuadrada
pow(x, y)2x elevado a y
exp(x)1e elevado a x
log(x) log10(x)1logaritmo natural / decimal
sin(x) cos(x) tan(x)1funciones circulares, en radianes
asin(x) acos(x) atan(x)1sus inversas, en radianes
atan2(y, x)2el ángulo del punto (x, y), sobre (−π, π]
sinh(x) cosh(x) tanh(x)1funciones hiperbólicas
cbrt(x)1raíz cúbica — también de negativos, a diferencia de pow
expm1(x) log1p(x)1eˣ−1 y log(1+x), exactas cerca de cero
log2(x)1logaritmo binario — exacto en una potencia de dos
asinh(x) acosh(x) atanh(x)1funciones hiperbólicas inversas
hypot(x, y)2longitud del vector, sin desbordar por el camino
hash(n, salt)2valor repetible en [0, 1) a partir de dos números
noise(t, scale, salt)3deriva suave: un valor nuevo cada scale filas, suavizado
gauss(x, c, w)3campana centrada en c: exp(-((x - c) / w)²)
clamp(x, lo, hi)3x contenido en [lo, hi]; con lo > hi gana el techo
lerp(a, b, t)3fracción t del camino de a a b; exacto en ambos extremos, extrapola fuera
sign(x)1−1, 0 o 1
erf(x) erfc(x)1la función de error y su complemento
gamma(x) lgamma(x)1Γ(x), y log |Γ(x)| para cuando Γ desborda
beta(a, b)2Γ(a)Γ(b)/Γ(a+b)
digamma(x)1ψ(x), la derivada de log Γ
zeta(s)1la función zeta de Riemann, para s real
degrees(x) radians(x)1entre las dos formas de escribir un ángulo
prev(Column, initial)2esa columna una fila atrás — requiere mode="sequential", véase abajo

Los operadores — comparaciones, conectivas lógicas y aritmética — son exactos: construidos con lo que IEEE-754 fija sin ambigüedad, así que las cinco implementaciones no pueden discrepar sobre ellos. Las FUNCIONES de la tabla de arriba son las que TDC calcula por sí mismo, y por eso cada una lleva una cota de precisión en vez de una promesa de exactitud.

Dos reglas que conviene saber antes de apoyarse en ellas

round manda la mitad alejándose del cero. round(0.5) es 1 y round(-0.5) es −1. JavaScript redondea la mitad hacia +∞, Python al par, Java hacia arriba: tres anfitriones, tres respuestas, ninguna simétrica. TDC declara la suya para que una columna de negativos se comporte como una de positivos.

len cuenta puntos de código. len("😀") es 1, no los 2 que daría UTF-16 — pero un emoji de familia hecho de varios puntos de código cuenta como varios. Los grupos de grafemas serían la respuesta humana y necesitan una tabla de segmentación Unicode que no toda implementación puede llevar, así que gana la unidad portable. len("10") es 2: una función de cadena lee su argumento como texto, nunca como número.

Azar repetible, y formas donde ponerlo

Cinco de las funciones de la tabla anterior existen para un solo trabajo: construir un valor que VARÍE como varían los datos reales, sin sortear nada. Cada una es un cálculo corriente sobre sus argumentos, así que los mismos argumentos dan siempre la misma respuesta, en las cinco implementaciones.

hash(n, salt) — un valor repetible en [0, 1) a partir de dos números

La que se busca cuando cada fila quiere su propio coeficiente:

<tdc><env count="5" seed="w">
<sequence name="N"><gen type="increment" value="1"/></sequence>
<sequence name="Amp"><gen type="formula" decimals="4"
expr="1.4 * (0.94 + 0.12 * hash(N, 12.9))"/></sequence>
</env><block><line><data>${{N}},${{Amp}}</data></line></block></tdc>
1,1.3862
2,1.4128
3,1.4559
4,1.4518
5,1.4734

salt es lo que diferencia dos columnas de una misma fila: hash(N, 1) y hash(N, 2) son secuencias sin relación, de modo que una fila puede llevar varios coeficientes independientes sin una segunda semilla.

Sustituye al truco de los shaders — sin(n * 12.9898) * 43758.5453, menos su parte entera — al que se recurre habitualmente. AQUÍ ese truco es seguro, porque TDC calcula su propio sin y las cinco implementaciones coinciden hasta el último bit. Lo que cuesta son dos llamadas trascendentes por fila, calculadas por software, y una línea que nadie puede leer. hash es aritmética entera y dice lo que es.

noise(t, scale, salt) — deriva suave

Un valor nuevo cada scale filas, suavizado entre ellos. Así es como se comporta de verdad una línea de base errante, y tres senos modulados no saben hacerlo: medido sobre 4.096 muestras, los senos ponen el 74,6 por ciento de su potencia en tres bandas de frecuencia donde esto pone el 51,5.

<sequence name="Drift"><gen type="formula" expr="0.3 * (noise(_count, 300, 7) - 0.5)"/></sequence>

Dos propiedades en las que se puede confiar, ambas exactas y no aproximadas:

  • En un punto de la retícula el valor ES hash ahí. noise(k * scale, scale, salt) es igual a hash(k, salt) — los mismos bits, no «con precisión de un ulp» — porque el suavizado interpola con a * (1 - u) + b * u. El borde de una celda es, por tanto, continuo.
  • El punto medio de una celda es la media simple de sus extremos.

Un scale de cero divide por cero y da NaN, la misma respuesta que da aquí sqrt(-1). En una columna eso no se imprime: se rechaza por su nombre, porque un archivo lleno de NaN sin haber avisado a nadie es peor que una ejecución que se detiene.

gauss(x, c, w) — una campana centrada en c

exp(-((x - c) / w)²): uno en el centro, decayendo a ambos lados, y w fija el ancho.

<tdc><env count="7" seed="w">
<sequence name="X"><gen type="increment" value="1"/></sequence>
<sequence name="G"><gen type="formula" decimals="4" expr="gauss(X, 4, 1.5)"/></sequence>
</env><block><line><data>${{X}},${{G}}</data></line></block></tdc>
1,0.0183
2,0.1690
3,0.6412
4,1.0000
5,0.6412
6,0.1690
7,0.0183

No es una densidad de probabilidad: no está escalada para integrar uno. Es la FORMA, para multiplicar otra cosa por ella.

clamp(x, lo, hi) — mantener un valor dentro de un rango

La barrera de todo lo que se acumula. Si lo es mayor que hi, gana el techo: clamp(x, 9, 2) es 2 para cualquier x. El orden importa y se declara en vez de dejarlo a la comparación que cada implementación haga primero: la referencia y sus cuatro puertos discreparon una vez sobre esto, y lo atrapó una prueba compartida.

lerp(a, b, t) — la fracción t del camino de a a b

Exacta en ambos extremos: lerp(10, 20, 0) es exactamente 10 y lerp(10, 20, 1) es exactamente 20. Por eso existe en vez de escribirse a mano: la forma natural a + (b - a) * t falla b en t = 1 en el 41 por ciento de 200.000 pares aleatorios, porque la resta pierde bits bajos que la suma no puede devolver. Aquí se calcula a * (1 - t) + b * t, que no puede perderlos.

Fuera de [0, 1] extrapola en lugar de rechazar — lerp(10, 20, 2) es 30 — porque un valor que se pasó de rango suele ser la aritmética de quien llama, no un error, y clamp está justo al lado para cuando sí lo es.

Una columna que lee su propio pasado

Todas las funciones anteriores responden desde UNA fila. prev(Columna, inicial) es la excepción: lee Columna de la fila anterior y, en la primera, entrega inicial. Es lo que necesita un paseo aleatorio — cada fila es la anterior más un paso — y lo que ninguna expresión podía decir antes.

<env count="3600" seed="p001" mode="sequential">
<sequence name="RR">
<gen type="formula" decimals="3"
expr="clamp(prev(RR, 700) + (hash(_count, 3) - 0.5) * 180, 350, 1400)"/>
</sequence>
</env>

prev toma el NOMBRE de la columna, sin evaluar. Escribir RR en cualquier otro lugar de esa expresión daría el valor de ESTA fila, que es justamente lo que prev mira más atrás.

Requiere mode="sequential" en <env>, y lo dice si se olvida. El modo es una promesa: la fila N se calcula después de la fila N−1. Sin él, el motor puede resolver cualquier fila sin tocar la anterior — así transmite una ejecución mayor que la memoria — y prev() estaría leyendo una fila que todavía no existe.

mode="sequential" y order="sequential" no son lo mismo

order="sequential" en un <gen> recorre los valores de ese generador en el orden escrito, en vez de sortearlos. Trata de los valores de UNA columna.

mode="sequential" en <env> trata de la EJECUCIÓN entera: las filas se calculan una tras otra. Las palabras coinciden; los sujetos no.

Lo que cuesta el modo, y lo que rechaza:

  • La ejecución se mantiene en memoria (motor 1) y las optimizaciones por columna quedan apagadas. Es el precio honesto de una columna que lee su propio pasado, y solo lo paga quien lo pidió.
  • mode="sequential" junto con engine="2" o engine="3" se rechaza, nombrando ambos atributos. Esos motores resuelven cualquier fila sin la anterior — ese es su diseño — así que no se pueden cumplir las dos cosas.
  • prev() dentro de un if= también se rechaza. Un if= es una elección por fila, y el motor que la responde puede tomar las filas en cualquier orden.

Todo lo demás sigue funcionando al lado: percent=, uniq y distinct no se ven afectados, porque las columnas se registran en el orden de declaración y ese es el mismo orden del que depende prev().

Listas dentro de una fila

Una secuencia con repeat= pone varios valores en un mismo campo, unidos por su separator=. Una expresión ve el texto unido, porque eso es lo que el campo contiene: split es el puente que convierte una lista en una lista.

<sequence name="Prices">
<gen type="number" value="10..200" repeat="3" separator=","/>
</sequence>
<sequence name="Basket">
<gen if="sum(split(Prices, ',')) > 300" type="text" value="large"/>
<gen type="text" value="ordinary"/>
</sequence>

min y max leen una lista igual de bien que argumentos sueltos: funcionan tanto max(split(Prices, ',')) como max(1, 9, 4). Un separador vacío corta en caracteres sueltos, la misma unidad que cuenta len, así que count(split(s, '')) y len(s) nunca discrepan.

at cuenta desde cero y rechaza un índice que no lo es

at(list, 0) es el primer elemento. Pasado el final el resultado es texto vacío, y a propósito: repeat="1..4" hace filas de longitudes distintas, y preguntar por el tercer elemento de una fila de dos es una pregunta real con una respuesta vacía. Para preguntar antes está count(list).

Todo lo demás se rechaza en vez de responder con esa misma cadena vacía: un índice negativo, uno fraccionario, uno que no es un número, y un sujeto que nunca se cortó. Ese último es el error que todo el mundo comete primero —

<gen if="at(Prices, 1) > 100" /> <!-- rechazado: TDC260 -->
<gen if="at(split(Prices, ','), 1) > 100" /> <!-- lo que se quería decir -->

Prices es el texto unido, así que la primera línea pedía el segundo elemento de una lista de uno y dejaba la columna en blanco mientras la ejecución informaba de éxito. Los errores escritos tal cual los detecta tdcv2 check antes de que exista una fila; un índice que se calcula sobre la marcha — at(list, _count - 1) — se comprueba al construir la fila.

Por qué TDC calcula sus propias funciones trascendentes

IEEE-754 fija para + - * / y sqrt exactamente una respuesta legal, así que sobre ellas todos los lenguajes coinciden. No dice nada sobre sin, cos, exp, log ni pow — cada libm elige su propio algoritmo — y la diferencia es medible, no teórica:

tan(1)
Node3ff8eb245cbee3a6
Python3ff8eb245cbee3a5

De setenta y siete valores muestreados, dieciséis discrepan en al menos dos de las cinco implementaciones. En un timeseries eso no se ve: cada número se redondea a una cadena decimal antes de convertirse en salida, y el último bit muere de camino. Una comparación no tiene paso de redondeo, así que ese bit se vuelve otra fila y otro archivo — en una herramienta cuya promesa entera es que cinco implementaciones producen los mismos bytes.

Por eso TDC las calcula por sí mismo, igual que ya calcula sus propios números aleatorios en vez de confiar en los de cada lenguaje. Cada una cae dentro de 4 ulp del valor verdadero — el mismo vecindario que ocupa una libm — y, mucho más importante, sobre el mismo double en las cinco. Coincidir con una libm concreta no es el objetivo ni podría serlo: las libms no coinciden entre sí.

Ese 4 se comprueba, no se afirma, sobre mallas que llegan a los extremos del rango de cada función. Los extremos son lo que importa: una serie truncada dos términos antes de tiempo es invisible en mitad de un intervalo y se desvía trece ulp en el borde — que es exactamente el fallo tras el cual se escribió la comprobación.

pow es la única con una cota más ancha, por una razón que conviene conocer:

exponentecómo se calculadesviación
entero, o un medioelevación al cuadrado, sqrt para el mediocrece con el exponente: ~4 ulp en 3, ~22 en 20
cualquier otroexp(y · log x)crece con |y · log x|: ~2 ulp en 1, ~457 en 400

Ambas son amplificación, no un defecto: elevar al cuadrado duplica el error recibido, y exp convierte un error absoluto en su argumento en uno relativo en su respuesta. Doce cifras significativas sobreviven en los dos casos.

<sequence name="Month"><gen type="increment" value="1"/></sequence>
<sequence name="Load">
<gen if="cos(Month / 2) > 0.5" type="text" value="peak"/>
<gen if="cos(Month / 2) < -0.5" type="text" value="trough"/>
<gen type="text" value="normal"/>
</sequence>
<sequence name="Tier">
<gen if="pow(2, Month) > 100" type="text" value="large"/>
<gen type="text" value="small"/>
</sequence>
tdcv2 seasonal.tdc
1 peak small
2 peak small
3 normal small
4 normal small
5 trough small
6 trough small
7 trough large
8 trough large

Ese archivo se pasó por las cinco implementaciones y todas produjeron esos mismos bytes.

De calcularlas en vez de tomarlas prestadas se siguen dos cosas. pow con un exponente entero pasa por elevación al cuadrado, así que pow(10, 3) es exactamente 1000 y no 999.9999999999998 — una configuración que compare contra un número redondo lo habría notado. Y las funciones circulares toman radianes, sin variante en grados: un convenio, dicho una sola vez.

El par que existe porque la resta pierde cosas

expm1 y log1p no son atajos para exp(x) - 1 ni log(1 + x). Son esas expresiones calculadas de modo que la respuesta sobreviva:

cerca de cero, las definiciones no devuelven nada
expm1(1e-20)   1e-20        exp(1e-20) - 1     0
log1p(1e-20)   1e-20        log(1 + 1e-20)     0

La segunda columna no es un error de redondeo: es la respuesta entera, desaparecida. 1 + 1e-20 ES 1 como double, así que el logaritmo nunca llega a ver el argumento; y exp(1e-20) es 1,0000…, de modo que la resta cancela todas las cifras que importaban. asinh y atanh están construidas sobre log1p por la misma razón.

hypot evita el problema simétrico en el otro extremo: sqrt(x² + y²) desborda a infinito para x = 10²⁰⁰, aunque la respuesta es perfectamente representable. Y log2 separa el exponente antes de tomar ningún logaritmo, así que log2(8) es 3 y no 2,9999999999999996.

Donde la cota deja de medirse en ulp

Estas cuatro llevan una cota que no se resume en «dentro de 4 ulp», y decirlo es parte de la referencia, no una nota al pie:

FunciónLo que se garantiza
erfdentro de 4 ulp
erfcdentro de 8 ulp — pasa por e^(−x²), y esa exponencial arrastra el redondeo del cuadrado
gammaexacta en los enteros hasta 23, dentro de 7 ulp en los 171 que caben en un double; doce cifras significativas en el resto
lgammadentro de 32 ulp lejos de sus ceros; en x = 1 y x = 2 la cota con sentido es absoluta, bajo 10⁻¹³

lgamma es el caso interesante. Vale cero en 1 y en 2, y ningún método que sume términos de tamaño 1 puede ser relativamente exacto sobre que se cancelen a nada — allí la afirmación tiene que ser absoluta, y lo es. Ambos ceros salen exactamente cero.

gamma fuera de los enteros termina en una exponencial, así que su desvío crece con log Γ(x): la misma amplificación que tiene pow. Por eso un entero toma el camino del factorial.

Lo que falta a propósito

La matemática que un generador de datos no tiene por qué cargar. besselj, bessely, airy, elliptic_k, elliptic_e y polygamma se rechazan por nombre:

tdcv2 check seasonal.tdc
error[TDC257]: besselj() is not available yet in an if expression

Fíjese en lo que NO dice: «¿quiso decir beta?». La distancia de edición habría ofrecido justo eso, y las dos nombran funciones completamente distintas. Un nombre de esta lista recibe la razón, no una conjetura.

Cada una de ellas es un proyecto y no una función, y ninguna ha pertenecido nunca de verdad a un predicado de fila. Se quedan en la lista para que quien alcance una reciba una respuesta y no «función desconocida».

Bucles y recursión. El motor se elige a partir de la configuración antes de generar una fila, preflight() estima la memoria antes de la corrida, y --jobs reparte las filas entre workers. Los tres necesitan saber el trabajo por fila sin hacerlo. Un bucle rompe los tres, y aquello para lo que se busca un bucle — «¿esta fila es par?» — es %.

Operadores de bits. _count & 1 es _count % 2 escrito para una máquina. Se analizan, para que el mensaje pueda nombrarlos, y luego se rechazan.

Cuando una expresión no alcanza

Una secuencia <compute> tiene división entera, restos, cirugía de cadenas, codificaciones y dígitos de control. Produce un valor como cualquier otra secuencia, y if= compara ese valor:

<sequence name="Checksum">
<compute><result><mod><to_number><field name="Account"/></to_number><int v="97"/></mod></result></compute>
</sequence>
<sequence name="Flag">
<gen if="Checksum == 0" type="text" value="divisible"/>
<gen type="text" value="."/>
</sequence>

La configuración tiene forma de XML, pero no es XML

TDC no expande entidades, así que &lt; son cuatro caracteres literales y no <. Escriba el carácter directamente:

<gen if="Weight > 20" .../> <!-- sí -->
<gen if="Weight &gt; 20" .../> <!-- no: TDC103, la entidad son cuatro caracteres -->