Vai al contenuto
codicefiscaleinverso

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 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

Approfondisci

Avvertenza

Questo strumento decodifica la struttura formale di un codice fiscale e ne verifica il carattere di controllo. Non conferma che il codice sia stato effettivamente attribuito a una persona: quella verifica è possibile solo attraverso il servizio ufficiale dell’Agenzia delle Entrate. Fonti e metodo