modulo 08 / lezione 43

Debugging di dati, backend e integrazioni

Timezone, encoding e locale

Riconoscere bug che dipendono da fuso orario, salvataggio in UTC vs locale, encoding misto, formattazione numerica e date.

55 min Debugging

In parole semplici

Le date e i testi sembrano semplici. Sono la sorgente di bug più sottili e ricorrenti del software. Tre categorie:

  1. Timezone: la stessa data appare a giorni diversi a utenti diversi.
  2. Encoding: stringhe con accenti diventano spazzatura.
  3. Locale: numeri formattati con virgola/punto, ordinamenti differenti, separatori inattesi.

Parole nuove

  • UTC (Coordinated Universal Time): fuso “zero” di riferimento.
  • timezone: fuso orario (Europe/Rome, America/New_York).
  • ISO 8601: formato standard per date (2025-05-16T14:30:00Z).
  • UTF-8: encoding standard, supporta tutti i caratteri Unicode.
  • locale: combinazione lingua + paese (it-IT, en-US).

Cosa impari

  • Salvare e mostrare date senza confusione di fuso.
  • Diagnosticare e correggere encoding misti.
  • Formattare numeri secondo locale.

Timezone: il problema universale

Tre fonti di confusione:

  1. Il server è in un fuso, il client in un altro.
  2. Il database salva senza fuso (TIMESTAMP vs TIMESTAMPTZ in Postgres).
  3. Le librerie convertono in modo diverso.

Regola d’oro

Salva sempre in UTC. Mostra sempre in locale.

In codice:

// salvataggio
const now = new Date();  // momento attuale, internamente UTC
const isoString = now.toISOString();  // "2025-05-16T14:30:00.000Z"
db.salva({ creato_il: isoString });

// lettura
const dateUTC = new Date(record.creato_il);  // legge UTC
const formattata = dateUTC.toLocaleString("it-IT", { timeZone: "Europe/Rome" });
// "16/05/2025, 16:30:00"

Mai salvare "15/05/2025 14:30" senza fuso. È ambiguo: di chi?

Bug tipico: il capodanno

2024-12-31T23:00Z salvato senza fuso. Mostrato a utente in Europe/Rome (UTC+1): 2025-01-01 00:00. La query “ordini del 2024” lo esclude. L’utente non capisce.

Fix: usa sempre Intl.DateTimeFormat con timeZone esplicito.

Date e database

Postgres:

  • TIMESTAMP (senza fuso): “muto”, interpretato come locale del DB. Sconsigliato.
  • TIMESTAMPTZ: salva sempre internamente UTC, converte in locale all’output.

MySQL:

  • DATETIME vs TIMESTAMP: differenza sottile. TIMESTAMP ha range limitato e conversione UTC.

Regola: usa il tipo “con fuso” sempre che possibile.

Encoding: tre regole

  1. UTF-8 ovunque: file, database, header HTTP. Tag HTML:
<meta charset="utf-8">

Header HTTP:

Content-Type: text/html; charset=utf-8

DB Postgres: CREATE DATABASE x ENCODING 'UTF8'.

  1. Mai mescolare encoding: una stringa Latin-1 letta come UTF-8 produce caff�. Convertire una volta sola, al confine.

  2. BOM (Byte Order Mark): alcuni editor aggiungono  all’inizio dei file. CSV con BOM aperti in tool diversi mostrano un carattere strano in alto a sinistra. Verifica con un hex viewer.

Diagnosticare encoding misto

Sintomi:

  • “Caffè” diventa “Caff” + caratteri strani.
  • Emoji non rese.
  • Caratteri sostituiti con ? o � (replacement character).

Diagnosi:

file -i nome-file.csv
# nome-file.csv: text/plain; charset=iso-8859-1

Conversione:

iconv -f ISO-8859-1 -t UTF-8 nome-file.csv > nome-file-utf8.csv

In JS (browser/Node):

const decoded = new TextDecoder("iso-8859-1").decode(byteArray);

Locale: numeri e date

1.000,50 in italiano = 1,000.50 in inglese. Confondere è bug.

const n = 1234.56;
n.toLocaleString("it-IT");  // "1.234,56"
n.toLocaleString("en-US");  // "1,234.56"
n.toLocaleString("de-DE");  // "1.234,56"

Mai usare parseFloat("1.234,56"): ritorna 1.234. Per parsing user input localizzato:

function parseLocaleNumber(s, locale = "it-IT") {
  const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
  const decimal = parts.find(p => p.type === "decimal").value;
  const group = parts.find(p => p.type === "group").value;
  return parseFloat(
    s.replaceAll(group, "").replace(decimal, ".")
  );
}

Date e locale

new Date().toLocaleDateString("it-IT");  // "16/05/2025"
new Date().toLocaleDateString("en-US");  // "5/16/2025"
new Date().toLocaleDateString("ja-JP");  // "2025/5/16"

Per formati specifici:

new Intl.DateTimeFormat("it-IT", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
}).format(new Date());
// "venerdì 16 maggio 2025"

Ordinamento

["mela", "Banana", "ciliegia"].sort() ordina per codepoint Unicode: ["Banana", "ciliegia", "mela"] (maiuscole prima).

Per locale-aware:

const collator = new Intl.Collator("it-IT", { sensitivity: "base" });
["mela", "Banana", "ciliegia"].sort(collator.compare);
// ["Banana", "ciliegia", "mela"]  con tolleranza

Esercizio guidato

  1. Crea un piccolo programma che salva un evento a “31 dicembre 2024, 23:00” come stringa locale.
  2. Mostra l’evento a un utente in Europe/Rome, uno in America/New_York, uno in Asia/Tokyo. Cosa vedono?
  3. Cambia salvataggio a UTC ISO 8601. Ripeti. Coerenza?
  4. Esporta un CSV con accenti in UTF-8 vs Latin-1. Apri in Excel e in un text editor. Differenze?

Errori frequenti del principiante

  • Salvare date senza fuso. “Bug del fuso” tornano sempre.
  • new Date("15/05/2025"): parsing dipendente dalla locale del runtime.
  • Mescolare UTF-8 e Latin-1 senza saperlo.
  • parseFloat su numero localizzato: rovesciato.