Del VBA a una sola fórmula: calcular el RFC mexicano con LET, MAP y expresiones regulares
Hector llega al grupo con un problema que no se ve todos los días: tiene resuelto el cálculo del RFC mexicano, pero lo tiene en VBA, y quiere pasarlo a fórmulas de 365. Su módulo trae las reglas del SAT enteras: preposiciones que hay que ignorar, los nombres JOSE y MARIA que no cuentan, los dígrafos CH y LL, la homoclave y el dígito verificador. Y también la lista de palabras inconvenientes, porque el algoritmo oficial contempla que las cuatro letras iniciales puedan formar una palabrota y hay que sustituir la última por una X.
El RFC de una persona física son cuatro bloques: cuatro letras sacadas de los apellidos y el nombre, seis dígitos de la fecha de nacimiento, dos caracteres de homoclave y un dígito verificador. Cada bloque tiene sus propias reglas, y de ahí salieron tres formas muy distintas de resolverlo.
La versión matricial de John
John se pone a trabajar dentro del propio libro de Hector, poniendo su fórmula justo al lado de la llamada a la macro para ir comparando resultados. Va puliéndola en cuatro entregas a lo largo de la noche, y su enfoque resuelve la tabla entera de golpe, devolviendo un rango desbordado con todos los RFC:
``
=LET(
data; EXCLUIR(FILTRAR(A.:.D; INDICE(A.:.D;;1) & INDICE(A.:.D;;3) > ""); 1);
texto; REDUCE(MAYUSC(ESPACIOS(TOMAR(data;;3))); {1;2;3;4;5};
LAMBDA(a;v; SUSTITUIR(a; EXTRAE("ÁÉÍÓÚ";v;1); EXTRAE("AEIOU";v;1))));
limpio; ESPACIOS(REGEXREEMPLAZAR(" " & texto; " (DEL?|LAS?|LOS|Y|MA?C|V[AO]N)(?= |$)";));
pater; INDICE(limpio;;1);
mater; INDICE(limpio;;2);
nombs; INDICE(limpio;;3);
nombr; REGEXREEMPLAZAR(nombs; "^(JOSE|MARIA) ";);
raiz; SUSTITUIR(
SI((pater = "") + (mater = "");
IZQUIERDA(pater & mater; 2) & IZQUIERDA(nombr; 2);
SI(LARGO(pater) < 3;
IZQUIERDA(pater) & IZQUIERDA(mater) & IZQUIERDA(nombr; 2);
IZQUIERDA(pater) & SI.ERROR(REGEXEXTRACCION(EXTRAE(pater;2;99); "[AEIOU]"); "X") & IZQUIERDA(mater) & IZQUIERDA(nombr)));
"Ñ"; "X");
raizx; SI(REGEXPRUEBA(raiz; "^(BUE[IY]|CK|FETO|GUEY|...)$");
IZQUIERDA(raiz; 3) & "X"; raiz);
alfa; "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
homo; MAP(ESPACIOS(INDICE(texto;;1) & " " & INDICE(texto;;2) & " " & INDICE(texto;;3));
LAMBDA(nombre;
LET(
chars; EXTRAE(nombre; SECUENCIA(LARGO(nombre)); 1);
cods; CODIGO(chars);
vals; SI(chars = " ";; SI(chars = "&"; 10; SI(chars = "Ñ"; 40; cods - 54 + (cods > 73) + 2 (cods > 82))));
cad; 0 & UNIRCADENAS(;; TEXTO(vals; "00"));
pos; SECUENCIA(LARGO(cad) - 1);
resto; RESIDUO(SUMA(EXTRAE(cad; pos; 2) EXTRAE(cad; pos + 1; 1)); 1000);
EXTRAE(alfa; 1 + ENTERO(resto / 34); 1) & EXTRAE(alfa; 1 + RESIDUO(resto; 34); 1))));
fec; INDICE(data;;4);
clave; raizx & DERECHA(AÑO(fec); 2) & TEXTO(MES(fec); "00") & TEXTO(DIA(fec); "00") & homo;
codrfc; CODIGO(EXTRAE(clave; SECUENCIA(;12); 1));
digito; RESIDUO(-BYROW(SI(codrfc < 58; codrfc - 48; codrfc - 55 + (codrfc > 78)) (14 - SECUENCIA(;12)); SUMA); 11);
SI.ERROR(clave & SI(digito = 10; "A"; digito); "")
)
`
Hay varias cosas que merece la pena mirar despacio.
La primera, A.:.D. Es el operador de rango recortado, una incorporación reciente: se lee como "de la columna A a la D, pero sin el vacío de arriba y de abajo". Evita tener que acotar a mano hasta qué fila llegan los datos.
La segunda, cómo genera los dos caracteres de la homoclave. Cada letra del nombre completo se convierte a un código de dos cifras, se concatenan todos en una cadena larga precedida de un cero, y luego se recorren pares solapados: EXTRAE(cad; pos; 2) toma dos dígitos y EXTRAE(cad; pos + 1; 1) toma el siguiente, se multiplican y se suman todos. De los últimos tres dígitos de esa suma salen el cociente y el resto de dividir entre 34, que son los índices dentro del alfabeto de 34 caracteres.
Y la tercera, el dígito verificador: SECUENCIA(;12) genera las doce posiciones, 14 - SECUENCIA(;12) da los pesos descendentes, y BYROW(...; SUMA) suma cada fila por separado para que la fórmula siga funcionando sobre la tabla entera y no colapse en un único número.
Fíjate también en que la fecha la construye con AÑO, MES y DIA en vez de con un código de formato. Eso no es casualidad, y en un momento se entiende por qué.
La versión por filas de Raúl Ramos
De madrugada aparece Raúl con una propuesta construida por su cuenta, que apunta a un escenario distinto: en vez de desbordar toda la tabla desde una celda, que cada fila devuelva su propio RFC, para poder usarla dentro de una tabla de Excel como una columna calculada más. Él mismo lo resume al entregarla: "no resulta una matriz, pero la trabajé pensando en que puede ser una fórmula que se use en un formato tabla".
Por dentro apenas se parece a la de John. Donde aquella tira de expresiones regulares para todo, esta reparte el trabajo entre DIVIDIRTEXTO, CONTARA, ELEGIRFILAS y HALLAR. Lo único que comparten las dos es el recurso para quitar los acentos: un REDUCE que recorre las cinco vocales sustituyéndolas una a una.
`
=+LET(
_limpiar; LAMBDA(txt; REDUCE(MAYUSC(txt); SECUENCIA(5);
LAMBDA(acum;i; SUSTITUIR(acum; EXTRAE("ÁÉÍÓÚ";i;1); EXTRAE("AEIOU";i;1)))));
nombre; A4; pater; B4; mater; C4; nac; D4;
_vocals; {"A";"E";"I";"O";"U";"É"};
Ap; SI(ESBLANCO(pater);"";MAYUSC(ELEGIRFILAS(DIVIDIRTEXTO(pater;;" ");CONTARA(DIVIDIRTEXTO(pater;;" ")))));
Am; SI(ESBLANCO(mater);"";MAYUSC(ELEGIRFILAS(DIVIDIRTEXTO(mater;;" ");CONTARA(DIVIDIRTEXTO(mater;;" ")))));
_Na; CONTARA(pater;mater)=1;
_ape; UNIRCADENAS(" ";VERDADERO;Ap;Am);
_apem; UNIRCADENAS(" ";VERDADERO;Am;Ap);
_noms; DIVIDIRTEXTO(nombre;;" ");
_filtron; MAYUSC(UNIRCADENAS(" ";VERDADERO;FILTRAR(_noms;BYROW(--(_noms={"jose"\"maria"\"josé"\"maría"});SUMA)=0;_noms)));
_1; IZQUIERDA(_ape;1);
V; EXTRAE(Ap;MIN(SI.ERROR(HALLAR(_vocals;Ap;2);100));1);
_2; SI(_Na;EXTRAE(_ape;2;1);SI(LARGO(Ap)>2;CAMBIAR(V;"";"X";V);IZQUIERDA(Am;1)));
_3; SI(O(_Na;LARGO(Ap)<=2);MAYUSC(IZQUIERDA(_filtron;1));IZQUIERDA(_apem;1));
_4; SI(O(_Na;LARGO(Ap)<=2);EXTRAE(_filtron;2;1);EXTRAE(_filtron;1;1));
_A1; UNIRCADENAS("";1;_1;_2;_3;_4;TEXTO(nac;"aammdd"));
...
fin; _A1&_HC&_DV;
fin)
`
El detalle bonito está en cómo descarta JOSE y MARIA: --(_noms={"jose"\"maria"\"josé"\"maría"}) compara la columna de nombres contra los cuatro literales puestos en horizontal, lo que genera una matriz de coincidencias; el doble menos convierte los VERDADERO/FALSO en unos y ceros, BYROW(...;SUMA) cuenta cuántas hubo en cada fila, y FILTRAR se queda con las que sumaron cero. Cuatro comparaciones sin anidar un solo SI.
También resuelve la vocal interna del primer apellido buscando las cinco vocales de golpe con HALLAR a partir de la posición 2, quedándose con la primera que aparezca (MIN) y usando 100 como valor de relleno para las que no están.
La versión modular de Hector: una LAMBDA por regla
Y en paralelo, el propio Hector hace algo distinto a los dos: en vez de buscar una fórmula que lo resuelva todo, parte el algoritmo en piezas con nombre, una por cada regla del SAT, y las guarda en el administrador de nombres:
`
RFC_NORMALIZA quita acentos y pasa a mayúsculas
RFC_QUITA_GENERALES elimina DE, DEL, LA, LOS, Y, MC, MAC, VON, VAN
RFC_QUITA_NOMBRE descarta JOSE, MARIA, J y MA cuando hay más nombres
RFC_DIGRAFO trata CH y LL como una sola letra
RFC_VOCAL_INTERNA primera vocal del apellido a partir de la segunda letra
RFC_INICIALES monta las cuatro letras y aplica la lista de prohibidas
RFC_CODIGO convierte un carácter en su valor de dos dígitos
RFC_HOMOCLAVE los dos caracteres de la homoclave
RFC_DV el dígito verificador
fRFC orquesta todo lo anterior
`
Y la fórmula que usa el usuario final se queda en esto:
`
=fRFC(nombre; paterno; materno; fecha)
`
Lo interesante viene después. Con las tres soluciones sobre la mesa, Hector reescribe sus piezas incorporando las técnicas de John, y el resultado se ve mejor comparando el antes y el después de cada una:
| Pieza | Antes | Después |
|---|---|---|
| RFC_NORMALIZA | cinco SUSTITUIR encadenados | un REDUCE sobre SECUENCIA(5) |
| RFC_QUITA_GENERALES | once SUSTITUIR anidados | un REGEXREEMPLAZAR |
| RFC_QUITA_NOMBRE | cuatro SUSTITUIR más un SI | un REGEXREEMPLAZAR |
| RFC_VOCAL_INTERNA | cinco HALLAR, un MIN y un relleno de 999 | REGEXEXTRACCION buscando [AEIOU] |
| RFC_INICIALES | lista de prohibidas recorrida con HALLAR | REGEXPRUEBA con el patrón agrupado |
| RFC_DV | 11-RESIDUO(...) más un CAMBIAR para los casos 10 y 11 | RESIDUO(-suma; 11) |
Cada pieza se queda en dos o tres líneas, pero sigue habiendo una pieza por regla, así que se puede auditar la lista de preposiciones sin leer la homoclave. Y Hector deja escrito el criterio con el que decidió qué compactar y qué no:
> "No compactar por compactar. Si una expresión más corta se vuelve más difícil de auditar, prefiero claridad."
Es la respuesta menos vistosa de las tres y probablemente la más útil el día que haya que tocar el algoritmo.
La homoclave de Nacho, desmontada
Mientras tanto, Nacho ataca solo la parte que nadie quería tocar, la homoclave, y la publica primero como un monstruo de una sola línea que él mismo bautiza como "menudo sudoku". Pero lo interesante es el fichero que manda después, donde está el mismo cálculo desglosado paso a paso, que es como de verdad se entiende:
`
C27: =AJUSTARFILAS(ENCOL(A2:F18);2)
F27: =ENCOL(REGEXEXTRACCION(C22;".";1))
G27: =BUSCARX(F27#;TOMAR(C27#;;1);TOMAR(C27#;;-1))
H27: ="0"&CONCAT(G27#) → 0251113182601311291415251123
H29: =SECUENCIA(LARGO(H27)-1)
I29: =EXTRAE(H27;H29#;2) → pares consecutivos: 02, 25, 51, 11...
J29: =ENTERO(IZQUIERDA(TEXTO(SUMA(SCAN(1;I29#;LAMBDA(X;Y;XY)));"0");3)/34)
J30: =RESIDUO(IZQUIERDA(TEXTO(SUMA(SCAN(1;I29#;LAMBDA(X;Y;XY)));"0");3);34)
J32: =EXTRAE(L30;J29;1) → V
J33: =EXTRAE(L30;J30;1) → P
`
La tabla de equivalencias letra-código venía repartida en varias columnas emparejadas, y en vez de reordenarla a mano la remodela con AJUSTARFILAS(ENCOL(A2:F18);2): ENCOL lo aplana todo en una columna y AJUSTARFILAS lo vuelve a montar en dos, listas para que TOMAR saque los vectores de BUSCARX.
El detalle que hacía fallar todo (y que no daba error)
Con las fórmulas ya funcionando, a Hector le seguían saliendo RFC que no cuadraban: "¿por qué le doy enter en la fórmula y me lo cambió?". Nadie veía dónde estaba el fallo, hasta que Leo señaló una sola línea:
`
clave; raizx & TEXTO(ELEGIRCOLS(data;4); "yymmdd") & homo;
`
El problema es que los códigos de formato de TEXTO están traducidos al idioma de Excel. "yymmdd" es la forma inglesa; en español hay que escribir "aammdd" (y en alemán sería tt en lugar de dd). Y lo peligroso es que no da error de fórmula: devuelve un texto, solo que mal. Hector hizo el cambio y respondió "excelente ya quedó"; después lo repitió sobre el libro de Raúl y los seis casos de prueba pasaron a coincidir con el RFC esperado.
John recogió el aviso y en su siguiente entrega cambió el criterio: "por el tema de la función TEXTO, la he cambiado para que sea compatible en cualquier idioma de Excel". Por eso su versión final construye la fecha con AÑO, MES y DIA, sin depender de ningún literal traducido.
El fleco de la Ñ: la prueba que lo ha decantado
Al probar apellidos con Ñ las soluciones dejaban de coincidir entre sí, porque no había acuerdo sobre qué valor numérico le corresponde a esa letra al calcular la homoclave: unas versiones usaban 10 y otras 40. Con JESUS NIETO CASTAÑEDA salían homoclaves distintas según cuál se aplicase, y John aportó documentación oficial que apuntaba a 40.
Lo que ha zanjado la discusión no ha sido la documentación, sino una prueba capaz de distinguir entre las dos hipótesis. Hector metió los dos resultados en un sistema real del SAT que rechaza los RFC inválidos: aceptó el que sale con 10 y no el que sale con 40. Un documento puede estar desactualizado o describir otra variante del algoritmo, pero un validador que dice que no, no admite interpretación.
Así que la Ñ vale 10, y John lo incorporó a su versión posterior. Es también el valor que lleva la función publicada en el add-in.
Un matiz de honestidad, porque el hilo sigue vivo: nadie llegó a escribir la conclusión con todas las letras, se deduce de la prueba y del código que vino después, y Hector ha avisado de otro caso que se le mueve al actualizar. Si trabajas con apellidos que llevan Ñ, el resultado con 10 es el que hoy pasa la validación real, pero contrástalo igualmente.
Ampliación: John lo rehace entero
Un día después, John volvió sobre el problema y reescribió su fórmula de arriba abajo, con un objetivo declarado: que diera exactamente igual que el VBA de Hector. No es un retoque de la anterior, es otra fórmula, y trae tres técnicas que no estaban en la primera.
Los dígrafos CH y LL, resueltos con \K. En una expresión regular, \K descarta todo lo que se ha casado hasta ese punto, de forma que la sustitución solo afecta a lo que viene después. Eso permite borrar la H de CH y la segunda L de LL conservando la primera letra, sin anidar condicionales ni usar grupos de captura:
`
=REGEXREEMPLAZAR(texto; "^C\KH|^L\KL"; )
`
CHAVEZ se convierte en CAVEZ y LLAMAS en LAMAS. Hector, que fue quien lo probó, lo resumió así: "la expresión \K permite conservar lo anterior y sustituir solamente: H de CH, segunda L de LL".
Los saltos de la tabla de códigos, con huecos artificiales. La versión anterior convertía cada carácter en su valor haciendo aritmética sobre CODIGO y luego corrigiendo los saltos con sumas condicionales. La nueva se ahorra todo eso metiendo caracteres de relleno dentro de la propia cadena de búsqueda, de modo que la posición ya es el valor:
`
=HALLAR(caracter; "0123456789&ABCDEFGHI.JKLMNOPQR..STUVWXYZ")-1
`
Los puntos son huecos que reproducen los saltos de la tabla del SAT. Ni un solo SI.
La fecha, sin código de formato. Es la consecuencia directa del fallo del "yymmdd": en vez de pedirle a TEXTO un formato de fecha, que está traducido, se construyen los seis dígitos con aritmética y un formato numérico puro, que no se traduce.
`
=DERECHA(AÑO(fecha);2) & TEXTO(100MES(fecha)+DIA(fecha);"0000")
`
La fórmula completa, con las tres técnicas integradas y la Ñ ya valiendo 10:
`
=LET(
Dat; A.:.D;
DatV; EXCLUIR(FILTRAR(Dat; (INDICE(Dat;;4)>0)(INDICE(Dat;;1)&INDICE(Dat;;2)>"")(INDICE(Dat;;3)>"")); 1);
Pat; INDICE(DatV;;1); Mat; INDICE(DatV;;2); Nom; INDICE(DatV;;3); Fec; INDICE(DatV;;4);
LAcen; LAMBDA(t; REDUCE(MAYUSC(ESPACIOS(t)); {1;2;3;4;5;6};
LAMBDA(a;v; SUSTITUIR(a; EXTRAE("ÁÉÍÓÚÜ";v;1); EXTRAE("AEIOUU";v;1)))));
LPart; LAMBDA(t; LET(x; ESPACIOS(REGEXREEMPLAZAR(" "&t; " (DEL?|LAS?|LOS|Y|MA?C|V[AO]N)(?= |$)";));
SI(x=""; t; x)));
Lchll; LAMBDA(t; REGEXREEMPLAZAR(LPart(t); "^C\KH|^L\KL";));
PatC; LAcen(Pat); MatC; LAcen(Mat); NomC; LAcen(Nom);
PatL; Lchll(PatC); MatL; Lchll(MatC);
NomL; REGEXREEMPLAZAR(LPart(NomC); "^(JOSE|MARIA) (?=.)";);
Base; SI((PatL="")+(MatL="");
IZQUIERDA(PatL&MatL;2)&IZQUIERDA(NomL;2);
IZQUIERDA(PatL)&SI(LARGO(PatL)<3;
IZQUIERDA(MatL)&IZQUIERDA(NomL;2);
SI.ND(REGEXEXTRACCION(EXTRAE(PatL;2;99);"[AEIOU]");"X")&IZQUIERDA(MatL)&IZQUIERDA(NomL)));
RaizX; SUSTITUIR(Base;"Ñ";"X");
Filt; SI(REGEXPRUEBA(RaizX; "^(BUE[IY]|CK|FETO|GUEY|...)$");
IZQUIERDA(RaizX;3)&"X"; RaizX);
RfcF; Filt & DERECHA(AÑO(Fec);2) & TEXTO(100MES(Fec)+DIA(Fec);"0000");
NHom; ESPACIOS(PatC&" "&MatC&" "&NomC);
MAP(NHom; RfcF; LAMBDA(nh;rf;
LET(
StrH; 0 & CONCAT(TEXTO(SI.ERROR(HALLAR(
SUSTITUIR(SUSTITUIR(EXTRAE(nh;SECUENCIA(LARGO(nh));1);" ";0);"Ñ";"&");
"0123456789&ABCDEFGHI.JKLMNOPQR..STUVWXYZ")-1;);"00"));
IdxH; SECUENCIA(LARGO(StrH)-1);
ModH; RESIDUO(SUMA(EXTRAE(StrH;IdxH;2)EXTRAE(StrH;1+IdxH;1));1000);
Alfa; "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
RfcH; rf & EXTRAE(Alfa;1+COCIENTE(ModH;34);1) & EXTRAE(Alfa;1+RESIDUO(ModH;34);1);
ValD; SI.ERROR(HALLAR(EXTRAE(RfcH;SECUENCIA(12);1);"0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ")-1;);
DigD; RESIDUO(-SUMA(ValD(14-SECUENCIA(12)));11);
RfcH & SI(DigD=10;"A";DigD)
)))
)
`
Fíjate en un detalle de método que se repite en las tres técnicas: la Ñ y el espacio dejan de ser casos especiales. En vez de tratarlos con condicionales, se sustituyen por caracteres que ya viven en la cadena de búsqueda (& y 0), y a partir de ahí son un carácter más. La reacción del grupo fue unánime: "John, me ha estallado la cabeza, qué genialidad", "es toda una artesanía esa fórmula".
La excepción que faltaba: la diéresis
Con el caso ya publicado, Hector pasó un último libro con una excepción que ninguna de las versiones anteriores contemplaba, y que se le había escapado a todo el mundo menos a una persona.
El aviso lo había dejado Raúl el 21 de agosto, al explicar cómo construía su vector del abecedario. Después de detallar la mecánica de la serie de valores, remata con una advertencia sobre su propia fórmula:
> "OJO: no metí la ü en el abecedario, y si sale esa letra será un valor 00, como un espacio."
Y ahí está el problema. La tabla del SAT que convierte cada carácter en un número de dos dígitos trata la Ü igual que la Ñ: ambas valen 10. Si tu fórmula no la conoce, no falla ni avisa, simplemente la toma por un carácter desconocido y le asigna 00, con lo que la homoclave sale mal. Y apellidos con diéresis como ARGÜELLES o GÜEMES no son ninguna rareza en México.
La versión final de Hector lo recoge en la pieza que hace esa conversión, y de paso añade otro carácter que tampoco estaba contemplado: el guion de los apellidos compuestos, que se trata como un espacio.
`
RFC_CODIGO = LAMBDA(caracter;
LET(
c; MAYUSC(caracter);
SI(O(c=" "; c="-"); "00";
SI(O(c="Ñ"; c="Ü"); "10";
SI(Y(c>="A"; c<="I"); TEXTO(CODIGO(c)-54; "00");
SI(Y(c>="J"; c<="R"); TEXTO(CODIGO(c)-53; "00");
SI(Y(c>="S"; c<="Z"); TEXTO(CODIGO(c)-51; "00");
SI(Y(c>="0"; c<="9"); TEXTO(VALOR(c); "00");
"00")))))))
)
`
Fíjate en que los tres tramos de letras (A-I, J-R, S-Z) llevan tres correcciones distintas sobre el código del carácter: 54, 53 y 51. Son exactamente los mismos saltos que John resolvía metiendo puntos de relleno dentro de su cadena de búsqueda. Dos maneras de decir lo mismo, y las dos correctas.
Un matiz honesto para quien vaya a usar esto: el libro final valida once casos contra la macro de VBA y coinciden todos, incluidos el de la Ñ y otro con acento en el nombre, pero ninguno de esos once lleva diéresis. O sea, la excepción está implementada según la tabla oficial, no comprobada contra un caso real. Si te toca un ARGÜELLES, contrástalo antes de darlo por bueno, igual que con la Ñ.
Y de aquí, a la biblioteca del add-in
En mitad del hilo, Hector soltó una idea que ha acabado cumpliéndose: "casi que inclúyela en el addin"*. Pues eso es exactamente lo que hemos hecho. La versión por filas está ya disponible en la biblioteca de funciones del add-in de Influexcel, parametrizada y lista para usar:
`
=RFCMEXICO(nombre; paterno; materno; nacimiento)
`
Dentro de la propia fórmula queda escrito quién participó en ella, así que el crédito viaja con la función a cualquier libro donde se copie.
Y la biblioteca es un sitio vivo, no un archivo: en cuanto apareció la excepción de la diéresis, la función se actualizó a la versión 1.2 para recogerla. La que estaba publicada heredaba el vector de letras sin la Ü`, es decir, exactamente el punto que su autor había avisado que no cubría.
Un caso que empieza siendo una traducción de VBA a fórmulas y acaba enseñando varias cosas a la vez: que una fórmula que desborda toda la tabla y otra que devuelve un valor por fila no compiten, sirven para escenarios distintos; que partir un cálculo en piezas con nombre y compactarlas después no son estrategias opuestas sino consecutivas; que una fórmula puede estar perfecta y aun así dar el resultado equivocado por un literal de tres letras; y que cuando dos explicaciones compiten, lo que decide no es quién trae mejor documentación, sino quién monta la prueba que las distingue.
El fichero adjunto incluye el planteamiento original con la macro, las tres soluciones, el desglose de la homoclave y los libros de pruebas con todo integrado.
Traducir una macro de VBA a fórmulas de Excel 365 no suele ser un ejercicio mecánico. Cuando la macro implementa un algoritmo oficial con decenas de reglas y excepciones, la traducción obliga a replantear el problema entero: lo que en VBA es un bucle con condicionales, en fórmula tiene que ser una operación sobre matrices. Este caso de la comunidad recorre ese camino con el cálculo del RFC mexicano, y acaba con tres soluciones que no compiten entre sí.
El problema
Hector tenía resuelto en VBA el cálculo del RFC de personas físicas, con todas las reglas del SAT: preposiciones y partículas que hay que ignorar en los apellidos, los nombres de pila JOSE y MARIA que no cuentan cuando hay más nombres, los dígrafos CH y LL que valen como una sola letra, la lista de palabras inconvenientes cuya última letra se sustituye por una X, la homoclave y el dígito verificador. Su pregunta al grupo fue directa: cómo pasar todo eso a fórmulas de 365.
El RFC de una persona física tiene cuatro bloques encadenados. Cuatro letras derivadas de los apellidos y el nombre, seis dígitos con la fecha de nacimiento, dos caracteres de homoclave y un dígito verificador final. Cada bloque tiene reglas propias, y los casos límite (una sola persona con un apellido, apellidos de menos de tres letras, caracteres como la eñe o el ampersand) son justo donde se rompen las soluciones ingenuas.
Tres formas de resolverlo
La versión matricial de John procesa la tabla entera desde una sola celda y devuelve un rango desbordado con todos los RFC. La fue puliendo en cuatro entregas a lo largo de una noche, trabajando dentro del propio libro de quien planteó el problema y colocando su fórmula al lado de la llamada a la macro para poder comparar resultados. Se apoya en LET para nombrar cada etapa del cálculo, en REDUCE para quitar los acentos recorriendo las cinco vocales, y en la familia de funciones de expresiones regulares para eliminar las partículas de los apellidos y detectar las palabras inconvenientes. La homoclave la resuelve dentro de un MAP, convirtiendo cada carácter del nombre completo a un código de dos cifras, concatenándolos en una cadena y recorriendo después pares solapados de dígitos que se multiplican y se suman. El dígito verificador usa SECUENCIA para generar las doce posiciones y sus pesos, y BYROW para que la suma se haga fila a fila en lugar de colapsar en un único número.
Un detalle que merece atención es el rango recortado, escrito como A.:.D. Es una incorporación reciente de Excel y significa "de la columna A a la D, descartando los vacíos de arriba y de abajo". Ahorra tener que acotar a mano hasta dónde llegan los datos y hace que la fórmula siga funcionando cuando se añaden filas.
La versión por filas de Raúl Ramos llega al mismo destino por otro camino, y apunta a un escenario distinto: que cada fila devuelva su propio RFC, para poder usar la fórmula como una columna calculada dentro de una tabla de Excel. Por dentro apenas se parece a la anterior. Donde aquella se apoya en expresiones regulares para casi todo, esta reparte el trabajo entre funciones de división de texto, conteo y búsqueda; lo único que ambas comparten es el recurso para quitar los acentos. El truco más elegante está en cómo descarta los nombres de pila: compara la lista de nombres contra los cuatro literales de una sola vez, convierte el resultado lógico en unos y ceros con un doble menos, suma por filas con BYROW y se queda con las que sumaron cero mediante FILTRAR. Cuatro comparaciones resueltas sin anidar un solo condicional.
La versión modular de Hector es la tercera vía, y la que menos se parece a lo que suele entenderse por "resolverlo con una fórmula". En lugar de buscar una expresión única, parte el algoritmo en nueve funciones LAMBDA con nombre, guardadas en el administrador de nombres: una normaliza acentos, otra elimina las partículas, otra descarta los nombres de pila, otra trata los dígrafos, otra localiza la vocal interna, otra monta las cuatro letras iniciales, otra convierte un carácter en su valor numérico, y las dos últimas calculan la homoclave y el dígito verificador. Una función orquestadora las encadena, y lo que escribe el usuario final es simplemente el nombre de esa función con cuatro argumentos.
La parte instructiva llega después. Con las tres soluciones sobre la mesa, Hector reescribe sus piezas incorporando las técnicas de John: los cinco SUSTITUIR encadenados que quitaban acentos pasan a ser un REDUCE; los once SUSTITUIR anidados que eliminaban las preposiciones se convierten en una sola expresión regular; la búsqueda de la vocal interna con cinco funciones de búsqueda y un valor de relleno se reduce a una extracción por patrón; y el cálculo del dígito verificador se simplifica aprovechando que el resto de un número negativo devuelve directamente el valor buscado. Cada pieza se queda en dos o tres líneas, pero sigue habiendo una pieza por regla, así que se puede revisar la lista de preposiciones sin tener que leer la homoclave.
El criterio con el que decidió qué compactar y qué no lo dejó escrito él mismo: no compactar por compactar, porque si una expresión más corta se vuelve más difícil de auditar, es preferible la claridad.
El fallo que no daba error
Con las fórmulas ya escritas, los resultados seguían sin cuadrar. La causa la identificó Leo y estaba en un único literal: los códigos de formato de la función TEXTO están traducidos al idioma de Excel. La forma inglesa para año, mes y día no funciona en la versión española, donde el año se escribe con la inicial de "año". Lo relevante es que no se produce ningún error de fórmula: Excel devuelve un texto perfectamente válido, solo que equivocado. Es el tipo de fallo que sobrevive a cualquier revisión visual del código.
El autor de la fórmula recogió el aviso y en su siguiente entrega cambió el criterio para que fuese compatible con cualquier idioma: la versión final construye la fecha con AÑO, MES y DIA en lugar de con un código de formato, de modo que ya no depende de ningún literal traducido.
Los dos flecos de los caracteres especiales
El hilo dejó abierta una cuestión que después se cerró, y destapó una segunda que nadie había visto.
La primera era el valor numérico de la eñe al calcular la homoclave: unas versiones usaban diez y otras cuarenta, con documentación oficial apoyando la segunda. Lo que zanjó la discusión no fue la documentación, sino una prueba capaz de distinguir entre las dos hipótesis: se metieron ambos resultados en un sistema real que rechaza los identificadores inválidos, y solo pasó el que usa diez. Un documento puede estar desactualizado o describir otra variante del algoritmo; un validador que dice que no, no admite interpretación.
La segunda apareció al revisar el vector de letras que usan varias de las soluciones. La tabla oficial trata la u con diéresis igual que la eñe, y también le asigna diez, pero ninguna de las fórmulas la contemplaba. El detalle es que ese olvido no produce ningún error visible: la letra desconocida simplemente no se encuentra en el vector de búsqueda, se le asigna el valor de relleno y la homoclave sale mal en silencio. Con apellidos como Argüelles o Güemes, que no son raros, el resultado es un identificador incorrecto de aspecto perfectamente normal.
La versión final del libro lo resuelve tratando la diéresis como la eñe y añadiendo además el guion de los apellidos compuestos, que se comporta como un espacio. La lección general vale para cualquier tabla de equivalencias construida a mano: lo peligroso no son los caracteres que incluyes, son los que faltan, porque una búsqueda que no encuentra nada devuelve un valor por defecto en lugar de avisar.
Funciones clave
LET: nombra cada etapa intermedia y hace legible una fórmula que de otro modo sería impracticable.LAMBDA: permite guardar una regla completa con nombre y reutilizarla desde otras fórmulas.MAP,REDUCEySCAN: aplican o acumulan un cálculo a lo largo de una matriz.BYROW: fuerza a que una agregación se haga fila a fila en lugar de sobre toda la matriz.SECUENCIA: genera posiciones y pesos sin escribirlos a mano.ENCOL,AJUSTARFILASyTOMAR: remodelan un bloque de datos en la forma que necesitaBUSCARX.TEXTO: útil y traicionera a partes iguales, por sus códigos de formato dependientes del idioma.
Conclusión
El caso deja tres lecturas. Una fórmula que desborda toda la tabla y otra que devuelve un valor por fila no son rivales: resuelven escenarios distintos y conviene saber cuál toca. Descomponer un cálculo en piezas con nombre y compactar esas piezas después no son estrategias opuestas, sino consecutivas. Y una fórmula puede ser impecable en su lógica y aun así producir el resultado equivocado por un literal de tres letras que cambia con el idioma de la aplicación.
Este tipo de hilos, en los que alguien plantea un problema real y varios miembros lo atacan desde ángulos distintos hasta cerrarlo, es lo que hace que la comunidad de Influexcel funcione.
Más casos con estas funciones
Más contenido de Excel en InflueXcel
- Descomposición de Cholesky con matrices dinámicas: de VBA a LAMBDA+REDUCE CasoJuan Pablo lanza un reto al grupo: tiene una descomposición de Cholesky resuelta con VBA y quiere saber si se puede hacer con matrices dinám
- Repetir valores N veces: tres enfoques con REDUCE, SCAN y ARCHIVOMAKEARRAY CasoJoan Recasens plantea un problema clásico: tiene una lista de categorías (A, B, C) con el número de repeticiones de cada una (4, 2, 3) y qui
- Buscar prefijos de longitud variable en otra columna: BYROW, MAP, REGEX y COINCIDIRX CasoInteresante problema planteado por un miembro: tiene una columna A con ~1.200 referencias de longitud variable y una columna C con ~276 text
- Error #CALC con MAP y AGRUPARPOR: solución con REDUCE+APILARV CasoNuevo reto de Excel resuelto por la comunidad: un usuario necesita aplicar AGRUPARPOR de forma iterativa sobre un rango de códigos de cuenta
- Generar la serie de Fibonacci con REDUCE, APILARV y LAMBDA CasoInteresante ejercicio compartido en la comunidad: generar los primeros N números de la serie de Fibonacci usando exclusivamente fórmulas de
- Filtro multicriteria dinámico con LET, FILTRAR y LAMBDA CasoHector comparte con la comunidad una fórmula avanzada para filtrar una tabla de productos/servicios por múltiples criterios opcionales (clav
- Filtrar una tabla por una lista de valores: una LAMBDA propia y su inversa TutorialTe pasan una lista de 30 números de albarán y hay que sacar esas filas de una tabla de miles. Con el autofiltro es marcar casillas una a una
- Funciones personalizadas en Excel: LET, LAMBDA y recursividad TutorialCómo pasar de una fórmula escrita a mano a una función propia que puedes llamar por su nombre en cualquier libro. Los tres vídeos de esta pá
- Convertir fechas de texto a fecha real con LAMBDA y REGEX CasoUn miembro de la comunidad está aprendiendo LAMBDA y comparte su primera función personalizada: extraer una fecha real a partir de un texto
- Unpivot de asientos contables: generar contrapartidas cuando las columnas no están en el orden esperado CasoEsta semana surgió una duda muy práctica en la comunidad, ligada al caso que Nacho resolvió unas semanas atrás sobre desglose de asientos co