Codice fiscale in JavaScript
Aggiornato il 30 luglio 2026 · 8 minuti di lettura
Un modulo completo e senza dipendenze: normalizza l’input, ne verifica la forma, scioglie l’omocodia, calcola il carattere di controllo e ricava data, sesso e comune di nascita. I sei blocchi qui sotto, incollati nell’ordine in cui compaiono, formano un unico file funzionante che si chiude con le proprie verifiche.
È la trasposizione diretta del modulo che alimenta il decoder in home page. Prima della pubblicazione è stato eseguito e confrontato con l’originale su oltre centosettantamila casi generati automaticamente, senza una sola divergenza.
Le tabelle e la forma
Il carattere di controllo assegna a ogni carattere due valori diversi a seconda che si trovi in posizione dispari o pari. È l’unico punto in cui conviene copiare e non riscrivere: sono settantadue voci e un solo numero sbagliato produce un errore che si manifesta su un codice su ventisei.
/* Codice fiscale: validazione e calcolo, senza dipendenze.
Riferimento: DM 23 dicembre 1976, Ministero delle Finanze. */
/* Valore di ogni carattere nelle posizioni DISPARI (1a, 3a, 5a, ... da 1). */
const DISPARI = {
'0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21,
A: 1, B: 0, C: 5, D: 7, E: 9, F: 13, G: 15, H: 17, I: 19, J: 21, K: 2, L: 4, M: 18,
N: 20, O: 11, P: 3, Q: 6, R: 8, S: 12, T: 14, U: 16, V: 10, W: 22, X: 25, Y: 24, Z: 23,
};
/* Valore di ogni carattere nelle posizioni PARI (2a, 4a, 6a, ...). */
const PARI = {
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9, K: 10, L: 11, M: 12,
N: 13, O: 14, P: 15, Q: 16, R: 17, S: 18, T: 19, U: 20, V: 21, W: 22, X: 23, Y: 24, Z: 25,
};
const RESTO = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
/* Lettera del mese: A = gennaio ... T = dicembre. */
const MESI = { A: 1, B: 2, C: 3, D: 4, E: 5, H: 6, L: 7, M: 8, P: 9, R: 10, S: 11, T: 12 };
const LETTERE_MESE = 'ABCDEHLMPRST';
/* Omocodia: le cifre sostituite da lettere e le sette posizioni numeriche
(indici 0-based) che possono esserne interessate. */
const OMOCODIA = { L: '0', M: '1', N: '2', P: '3', Q: '4', R: '5', S: '6', T: '7', U: '8', V: '9' };
const POSIZIONI_NUMERICHE = [6, 7, 9, 10, 12, 13, 14];
/* Forma ammessa: nelle posizioni numeriche stanno anche le dieci lettere omocodiche. */
const FORMA =
/^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$/;
const VOCALI = new Set(['A', 'E', 'I', 'O', 'U']);Nella regex, le posizioni numeriche accettano anche L M N P Q R S T U V: sono le lettere che l’omocodia mette al posto delle cifre quando due persone genererebbero lo stesso codice. Chi la scrive con [0-9] rifiuta codici realmente attribuiti.
Il carattere di controllo
/** Sedicesimo carattere, calcolato sui primi quindici. */
function checkChar(primi15) {
if (primi15.length < 15) throw new Error('Servono almeno 15 caratteri.');
let somma = 0;
for (let i = 0; i < 15; i++) {
/* i è 0-based: l'indice 0 è la PRIMA posizione, quindi dispari. */
const valore = i % 2 === 0 ? DISPARI[primi15[i]] : PARI[primi15[i]];
if (valore === undefined) throw new Error('Carattere non ammesso: ' + primi15[i]);
somma += valore;
}
return RESTO[somma % 26];
}Normalizzazione e omocodia
Due normalizzazioni diverse per due scopi diversi. normalizza ripulisce un codice fiscale digitato, che spesso arriva con spazi o trattini; soloLettere prepara un cognome o un nome, scomponendo gli accenti in forma NFD per poi eliminare i segni diacritici: così Nicolò diventa NICOLO e l’apostrofo di D’Angelo sparisce.
/** Maiuscolo, via tutto ciò che non è lettera o cifra. */
function normalizza(input) {
return String(input == null ? '' : input).toUpperCase().replace(/[^A-Z0-9]/g, '');
}
/** "D'Angelo" diventa "DANGELO", "Nicolò" con accento diventa "NICOLO". */
function soloLettere(input) {
return String(input == null ? '' : input)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toUpperCase()
.replace(/[^A-Z]/g, '');
}
/** Riporta le lettere omocodiche alle cifre originarie. */
function sciogliOmocodia(cf) {
const c = cf.split('');
let livello = 0;
for (const p of POSIZIONI_NUMERICHE) {
const cifra = OMOCODIA[c[p]];
if (cifra !== undefined) { c[p] = cifra; livello++; }
}
return { base: c.join(''), livello };
}sciogliOmocodia restituisce anche il livello, cioè quante cifre sono state sostituite. Serve a due cose: interpretare correttamente anno, giorno e codice catastale, e poterlo dire all’utente invece di mostrargli un errore.
Calcolo del codice
La regola del nome è quella che sfugge più spesso: con quattro o più consonanti si prendono la prima, la terza e la quarta, saltando la seconda. È il motivo per cui GIOVANNI dà GNN e non GVN. Per il cognome, invece, si prendono sempre le prime tre consonanti.
/** Cognome: consonanti in ordine, poi vocali, prime tre, riempite con X. */
function encodeSurname(cognome) {
const s = soloLettere(cognome);
const cons = [...s].filter(ch => !VOCALI.has(ch));
const voc = [...s].filter(ch => VOCALI.has(ch));
return (cons.join('') + voc.join('') + 'XXX').slice(0, 3);
}
/** Nome: con quattro o più consonanti si prendono la 1a, la 3a e la 4a. */
function encodeName(nome) {
const s = soloLettere(nome);
const cons = [...s].filter(ch => !VOCALI.has(ch));
if (cons.length >= 4) return cons[0] + cons[2] + cons[3];
const voc = [...s].filter(ch => VOCALI.has(ch));
return (cons.join('') + voc.join('') + 'XXX').slice(0, 3);
}
/** Costruisce il codice completo. Data in formato AAAA-MM-GG. */
function buildCF({ cognome, nome, dataNascita, sesso, codiceCatastale }) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(dataNascita || ''));
if (!m) throw new Error('Data di nascita attesa nel formato AAAA-MM-GG.');
const anno = Number(m[1]), mese = Number(m[2]), giorno = Number(m[3]);
const d = new Date(Date.UTC(anno, mese - 1, giorno));
if (d.getUTCFullYear() !== anno || d.getUTCMonth() !== mese - 1 || d.getUTCDate() !== giorno) {
throw new Error('La data di nascita non esiste.');
}
const sx = String(sesso || '').toUpperCase();
if (sx !== 'M' && sx !== 'F') throw new Error('Sesso: attesi "M" o "F".');
const belfiore = String(codiceCatastale || '').toUpperCase().trim();
if (!/^[A-Z]\d{3}$/.test(belfiore)) throw new Error('Codice catastale non valido.');
if (!soloLettere(cognome)) throw new Error('Cognome mancante.');
if (!soloLettere(nome)) throw new Error('Nome mancante.');
const primi15 =
encodeSurname(cognome) +
encodeName(nome) +
String(anno).slice(2) +
LETTERE_MESE[mese - 1] +
String(sx === 'F' ? giorno + 40 : giorno).padStart(2, '0') +
belfiore;
return primi15 + checkChar(primi15);
}Decodifica
La funzione non lancia eccezioni sui codici malformati: raccoglie gli errori in un array e restituisce comunque tutto ciò che è riuscita a leggere. Per un campo di un modulo è il comportamento giusto, perché consente di dire che cosa non torna invece di limitarsi a un “non valido”.
/* Due cifre non bastano a esprimere il secolo. Si parte dall'anno corrente e,
se la data così ottenuta è nel futuro, si torna indietro di cento anni: un
codice fiscale non può codificare una nascita non ancora avvenuta. */
function risolviAnno(aa, mese, giorno, adesso) {
const correnteAA = adesso.getFullYear() % 100;
let anno = (aa > correnteAA ? 1900 : 2000) + aa;
if (Date.UTC(anno, mese - 1, giorno) > adesso.getTime()) anno -= 100;
return anno;
}
/** Decodifica un codice fiscale e ne verifica il carattere di controllo. */
function decodeCF(input, adesso = new Date()) {
const cf = normalizza(input);
const esito = {
cf,
valido: false,
errori: [],
codiceCognome: null,
codiceNome: null,
dataNascita: null,
sesso: null,
codiceCatastale: null,
omocodia: 0,
controllo: { trovato: null, atteso: null },
};
if (cf.length !== 16) {
esito.errori.push('Lunghezza errata: ' + cf.length + ' caratteri invece di 16.');
return esito;
}
if (!FORMA.test(cf)) {
esito.errori.push('Forma non valida: 6 lettere, 2 cifre, 1 lettera, 2 cifre, 1 lettera, 3 cifre, 1 lettera.');
return esito;
}
const atteso = checkChar(cf.slice(0, 15));
esito.controllo = { trovato: cf[15], atteso };
if (cf[15] !== atteso) {
esito.errori.push('Carattere di controllo errato: trovato ' + cf[15] + ', atteso ' + atteso + '.');
}
const { base, livello } = sciogliOmocodia(cf);
esito.omocodia = livello;
esito.codiceCognome = cf.slice(0, 3);
esito.codiceNome = cf.slice(3, 6);
const aa = Number(base.slice(6, 8));
const mese = MESI[base[8]];
let giorno = Number(base.slice(9, 11));
if (mese === undefined) {
esito.errori.push('Lettera del mese non valida: ' + base[8] + '.');
} else {
esito.sesso = giorno > 40 ? 'F' : 'M';
if (giorno > 40) giorno -= 40;
if (giorno < 1 || giorno > 31) {
esito.errori.push('Giorno di nascita non valido: ' + giorno + '.');
} else {
const anno = risolviAnno(aa, mese, giorno, adesso);
const d = new Date(Date.UTC(anno, mese - 1, giorno));
if (d.getUTCMonth() !== mese - 1 || d.getUTCDate() !== giorno) {
esito.errori.push('Data di nascita inesistente.');
} else {
esito.dataNascita = d.toISOString().slice(0, 10);
}
}
}
esito.codiceCatastale = base.slice(11, 15);
esito.valido = esito.errori.length === 0;
return esito;
}Ecco che cosa restituisce:
decodeCF('MLLSNT82P65Z404U');
// {
// cf: 'MLLSNT82P65Z404U',
// valido: true,
// errori: [],
// codiceCognome: 'MLL',
// codiceNome: 'SNT',
// dataNascita: '1982-09-25',
// sesso: 'F',
// codiceCatastale: 'Z404',
// omocodia: 0,
// controllo: { trovato: 'U', atteso: 'U' }
// }
decodeCF('RSSMRA85M01H501Z').errori;
// [ 'Carattere di controllo errato: trovato Z, atteso Q.' ]
buildCF({
cognome: 'Rossi', nome: 'Mario',
dataNascita: '1985-08-01', sesso: 'M', codiceCatastale: 'H501',
});
// 'RSSMRA85M01H501Q'Il campo codiceCatastale va poi risolto in un nome di comune. I dati pronti all’uso sono in /comuni-lookup.json, un oggetto di 10.533 voci che copre i 7.896 comuni attivi, i 197 Stati esteri (codici Z) e 2.440 comuni soppressi. Il valore è Nome|SIGLA|slug per una voce attiva e Nome|SIGLA||TIPO|anno|SUCCESSORI per un comune soppresso: conta i campi per distinguerli, perché lo slug dei soppressi è vuoto e non va usato per costruire un URL.
I test
Coprono i tre vettori pubblicati, i casi noti dell’encoder, il round trip completo, l’omocodia di secondo livello, un carattere di controllo errato e il pivot del secolo. Se un blocco è stato copiato male, qui salta fuori.
/* ------------------------------------------------------------------ *
* Verifica. Esegui il file con Node oppure incollalo nella console del
* browser: stampa solo le righe fallite, poi il totale.
* ------------------------------------------------------------------ */
let falliti = 0;
function verifica(descrizione, ottenuto, atteso) {
if (JSON.stringify(ottenuto) !== JSON.stringify(atteso)) {
falliti++;
console.error('FALLITO: ' + descrizione
+ ' -> ' + JSON.stringify(ottenuto) + ' invece di ' + JSON.stringify(atteso));
}
}
/* La data è fissata: la lettura dell'anno dipende dal secolo corrente. */
const ADESSO = new Date('2026-07-30T12:00:00Z');
/* I tre vettori pubblicati. */
verifica('checkChar MRTMTT25D09F205', checkChar('MRTMTT25D09F205'), 'Z');
verifica('checkChar FOXDRA26C24H872', checkChar('FOXDRA26C24H872'), 'Y');
verifica('checkChar MLLSNT82P65Z404', checkChar('MLLSNT82P65Z404'), 'U');
for (const cf of ['MRTMTT25D09F205Z', 'FOXDRA26C24H872Y', 'MLLSNT82P65Z404U']) {
verifica('valido ' + cf, decodeCF(cf, ADESSO).valido, true);
}
/* Decodifica completa. */
const donna = decodeCF('MLLSNT82P65Z404U', ADESSO);
verifica('data', donna.dataNascita, '1982-09-25');
verifica('sesso', donna.sesso, 'F');
verifica('comune', donna.codiceCatastale, 'Z404');
/* Codifica di cognome e nome. */
verifica('ROSSI', encodeSurname('ROSSI'), 'RSS');
verifica('RUSSO', encodeSurname('RUSSO'), 'RSS');
verifica('ROSSO', encodeSurname('ROSSO'), 'RSS');
verifica('RE', encodeSurname('RE'), 'REX');
verifica('D-ANGELO', encodeSurname("D'ANGELO"), 'DNG');
verifica('Nicolò accentato', encodeSurname('Nicolò'), 'NCL');
verifica('MARIO', encodeName('MARIO'), 'MRA');
verifica('MARIA', encodeName('MARIA'), 'MRA');
verifica('MAURO', encodeName('MAURO'), 'MRA');
verifica('GIOVANNI', encodeName('GIOVANNI'), 'GNN');
/* Calcolo diretto: si torna esattamente ai vettori. */
verifica('build MRTMTT', buildCF({
cognome: 'Moretti', nome: 'Matteo', dataNascita: '1925-04-09', sesso: 'M', codiceCatastale: 'F205',
}), 'MRTMTT25D09F205Z');
verifica('build MLLSNT', buildCF({
cognome: 'Molli', nome: 'Santa', dataNascita: '1982-09-25', sesso: 'F', codiceCatastale: 'Z404',
}), 'MLLSNT82P65Z404U');
verifica('build RSSMRA', buildCF({
cognome: 'Rossi', nome: 'Mario', dataNascita: '1985-08-01', sesso: 'M', codiceCatastale: 'H501',
}), 'RSSMRA85M01H501Q');
/* Omocodia: due cifre sostituite, carattere di controllo ricalcolato. */
const omo = decodeCF('RSSMRA85M01H5LMT', ADESSO);
verifica('omocodico valido', omo.valido, true);
verifica('omocodico livello', omo.omocodia, 2);
verifica('omocodico comune', omo.codiceCatastale, 'H501');
verifica('omocodico data', omo.dataNascita, '1985-08-01');
/* Carattere di controllo sbagliato: la forma è giusta, il codice no. */
const errato = decodeCF('RSSMRA85M01H501Z', ADESSO);
verifica('checksum errato', errato.valido, false);
verifica('atteso Q', errato.controllo.atteso, 'Q');
verifica('la forma da sola non basta', FORMA.test('RSSMRA85M01H501Z'), true);
/* Nessuna data nel futuro. */
verifica('pivot del secolo',
decodeCF('RSSMRA26T71H501' + checkChar('RSSMRA26T71H501'), ADESSO).dataNascita,
'1926-12-31');
if (falliti > 0) throw new Error(falliti + ' verifiche fallite.');
console.log('Tutte le verifiche superate.');Tre note prima di metterlo in produzione
Un input più lungo di sedici caratteri viene rifiutato
È l’unica differenza voluta rispetto al modulo del sito, che invece tronca a sedici perché serve un campo che si aggiorna mentre si digita. In una funzione di validazione troncare è pericoloso: farebbe passare un codice valido seguito da un carattere di troppo.
L’anno è ambiguo, e la scelta è documentata
Con due cifre, 25 può essere 1925 o 2025. La funzione sceglie il secolo più recente compatibile con una data già passata. Se il tuo dominio ha un vincolo migliore (una maggiore età, per esempio), sostituisci risolviAnno: è isolata apposta.
Non validare il codice catastale contro l’elenco corrente
I comuni soppressi restano nei codici fiscali già attribuiti. Usa la tabella dei comuni per arricchire il risultato, mai per rifiutarlo: il codice non trovato va mostrato così com’è, non segnalato come errore.
Domande frequenti
Lo stesso algoritmo in Python e in Excel. Fonte: DM 23 dicembre 1976 (Ministero delle Finanze). Fonti e metodo