Jour 56 Day 56 · jeudi 29 octobre 2026 Thursday 29 October 2026 CS Intermédiaire
Les nids à bugs : UTF-8, dates & regex Bug magnets: UTF-8, dates & regex
Trois sujets banals, une part démesurée des bugs de prod : encodage, fuseaux horaires et expressions régulières. Les maîtriser prouve en entretien qu'on a déjà maintenu du vrai code. Three mundane topics, an outsized share of production bugs: encoding, timezones and regular expressions. Mastering them proves in an interview that you've maintained real code.
L’essentiel
Trois domaines concentrent une part démesurée des bugs de production : les chaînes de caractères (encodage), les dates (fuseaux horaires) et les expressions régulières. Aucun n’est difficile en théorie ; tous punissent les hypothèses implicites — « un caractère = un octet », « minuit c’est minuit », « ma regex marche sur mes exemples ». Les recruteurs aiment ces sujets parce qu’ils distinguent l’étudiant qui a écrit du code de celui qui l’a débuggé.
| Symptôme | Cause | Fix |
|---|---|---|
été affiché au lieu de été | octets UTF-8 décodés en Latin-1 (mojibake) | UTF-8 déclaré partout : fichiers, HTTP, DB |
'é'.length === 2 | unités UTF-16 ≠ graphèmes ; forme NFD | normaliser, segmenter par graphème |
"café" !== "café" | NFC vs NFD : é précomposé vs e + accent | .normalize('NFC') avant comparaison |
| Rendez-vous décalé d’une heure | stockage en heure locale + DST | stocker UTC + ISO 8601, convertir à l’affichage |
| Anniversaire décalé d’un jour | date-seule stockée comme minuit local | type DATE sans heure, jamais un timestamp |
| API gelée sur une entrée précise | catastrophic backtracking (ReDoS) | regex sans quantificateurs imbriqués, timeout |
Comment ça marche
Encodage. ASCII code 128 caractères sur 7 bits — l’anglais, point. Unicode attribue un numéro (code point) à plus de 150 000 caractères : é = U+00E9, € = U+20AC. UTF-8 encode chaque code point sur 1 à 4 octets, à longueur variable, en restant compatible ASCII :
Code point → octets UTF-8
U+0041 'A' → 01000001 (1 octet)
U+00E9 'é' → 110_00011 10_101001 (2 octets)
U+20AC '€' → 1110_0010 10_000010
10_101100 (3 octets)
U+1F44D '👍' → 11110_000 10_011111 … (4 octets)
Le préfixe du 1er octet code la longueur ;
chaque octet de continuation commence par 10.
Un « caractère » à l’écran (graphème) peut occuper plusieurs code points : é existe précomposé (U+00E9, forme NFC) ou décomposé en e + accent combinant (U+0065 U+0301, forme NFD). Visuellement identiques, binairement différents — d’où la normalisation avant toute comparaison ou recherche. Le mojibake (é) apparaît quand des octets UTF-8 sont relus avec le mauvais charset.
'é'.length // 1 (forme NFC : U+00E9)
'é'.normalize('NFD').length // 2 : e + accent combinant
'👍'.length // 2 : paire de substitution —
// JS compte en unités UTF-16
[...'👍'].length // 1 : itération par code point
'👨👩👧'.length // 8 ! 3 emojis + 2 ZWJ invisibles
// Comparaison correcte de chaînes accentuées :
'café'.normalize('NFC') === 'café'.normalize('NFC')
// → true (sans normalize : false)
// Compter ce que voit l'utilisateur (graphèmes) :
[...new Intl.Segmenter().segment('👨👩👧')].length // 1
Dates. La règle d’or : stocker en UTC, au format ISO 8601 (2026-10-29T14:30:00Z), et convertir dans le fuseau de l’utilisateur uniquement à l’affichage. Un fuseau n’est pas un offset fixe : il change avec le DST (une heure qui n’existe pas au printemps, une heure qui existe deux fois à l’automne) et avec les décisions politiques — la base tz est mise à jour plusieurs fois par an. Le bug du « minuit local » : stocker une date-seule (anniversaire, deadline) comme timestamp à minuit local, puis l’afficher dans un autre fuseau → la date recule d’un jour.
Date en JavaScript cumule les pièges : mois indexés à zéro (new Date(2026, 9, 29) = 29 octobre), parsing incohérent, objets mutables. En 2026, on utilise l’API Temporal (en cours de déploiement dans les moteurs JS) ou une bibliothèque comme date-fns ou Luxon.
// Deux pièges en deux lignes :
new Date('2026-10-29') // minuit UTC
new Date('2026-10-29T00:00') // minuit LOCAL
// À Paris (UTC+1), une heure d'écart entre les deux —
// comparer naïvement ces dates décale des deadlines.
Regex. Les briques utiles : classes ([a-z], \d, \w), quantificateurs (*, +, ?, {n,m}), ancres (^, $, \b), groupes capturants (…), nommés (?<nom>…), non capturants (?:…). Le piège central : les quantificateurs sont gloutons (greedy) par défaut — ils avalent le maximum puis reculent (backtracking).
'<b>gras</b> et <i>ital</i>'.match(/<.+>/)[0]
// → '<b>gras</b> et <i>ital</i>' glouton : tout !
'<b>gras</b>'.match(/<.+?>/)[0] // '<b>' lazy : minimum
'<b>gras</b>'.match(/<[^>]+>/)[0] // '<b>' classe négée :
// rapide, sans backtracking
Concepts clés à maîtriser
- Octet ≠ code point ≠ graphème : trois niveaux distincts.
length,substring,reversetravaillent souvent au mauvais niveau et coupent un emoji en deux (�). - Normalisation aux frontières : normaliser en NFC à l’entrée du système (formulaires, imports, noms de fichiers — macOS produit du NFD), comparer et indexer sur la forme normalisée.
- UTC + ISO 8601, avec une exception : un événement futur lié à un lieu (« réunion à 9 h à Paris en 2027 ») se stocke en heure locale + identifiant IANA (
Europe/Paris), car les règles de fuseau peuvent changer d’ici là. Un instant passé ou absolu (log, paiement) se stocke en UTC. - Types date-seule : un anniversaire n’a pas d’heure ni de fuseau.
DATEen SQL,Temporal.PlainDateen JS — jamais un timestamp à minuit. - Greedy vs lazy vs classe négée :
.+?répond au symptôme,[^>]+exprime l’intention et supprime le backtracking. - Quand ne pas utiliser une regex : dès que le format est imbriqué ou récursif — HTML en tête. Une regex ne compte pas les niveaux d’imbrication (langage régulier vs langage algébrique) : utiliser un vrai parseur (DOMParser, BeautifulSoup). Pour les emails : validation minimale + email de confirmation, pas une regex de 400 caractères.
- Tester ses regex : sur regex101 avec des cas limites (chaîne vide, accents, entrées hostiles), puis des tests unitaires qui documentent les cas couverts.
⚠️ ReDoS — une regex avec quantificateurs imbriqués comme
(a+)+$explose en backtracking exponentiel sur une entrée hostile ("aaaaaaaaaaaaaaaaaaaaab"suffit). Une seule requête peut geler un thread : Stack Overflow (2016) et Cloudflare (2019) sont tombés à cause d’une regex. Parades : pas de quantificateurs imbriqués ni d’alternatives qui se recouvrent, timeout sur l’exécution, ou moteur linéaire garanti (RE2, crate regex de Rust).
💡 UTC partout, conversion à l’affichage — le backend, la DB et les logs ne connaissent qu’UTC ; le fuseau de l’utilisateur n’intervient qu’à la toute dernière couche (le rendu). Un seul point de conversion = une seule classe de bugs possible, au lieu d’une par couche.
En entretien
« Pourquoi 'é'.length peut-il valoir 2 en JavaScript ? » — Deux raisons possibles. Si la chaîne est en NFD, é est composé de deux code points (e + accent combinant). Et length compte des unités UTF-16, pas des graphèmes : '👍'.length === 2 (paire de substitution). Réponse complète : normaliser en NFC, et segmenter par graphème (Intl.Segmenter) quand on veut compter ce que voit l’utilisateur.
« Comment stockes-tu les dates dans une application internationale ? » — UTC + ISO 8601 en base, conversion dans le fuseau de l’utilisateur à l’affichage. Nuance qui marque des points : un événement futur localisé se stocke en heure locale + identifiant IANA, parce que les règles de DST peuvent changer entre le stockage et l’événement.
« Greedy vs lazy ? » — Un quantificateur glouton (.+) avale le maximum puis rétrocède jusqu’à ce que le reste du motif matche ; lazy (.+?) prend le minimum puis étend. Sur <b>x</b>, <.+> capture toute la chaîne, <.+?> capture <b>. La meilleure réponse propose la classe négée <[^>]+> : même résultat, sans backtracking.
« Pourquoi ne pas parser du HTML avec une regex ? » — Le HTML est un langage imbriqué : une regex ne peut pas compter les niveaux d’ouverture/fermeture (c’est la limite des langages réguliers). Ça marche sur trois exemples puis casse sur les attributs, les commentaires, l’imbrication. Un parseur existe déjà dans chaque écosystème : DOMParser, BeautifulSoup, lxml.
« Qu’est-ce que le DST change pour un développeur ? » — Deux fois par an, l’heure locale saute : une heure inexistante au printemps, une heure ambiguë à l’automne. Conséquences : « ajouter 24 h » ≠ « demain même heure », les crons entre 2 h et 3 h sautent ou doublent, les durées calculées en heure locale se trompent d’une heure. D’où le calcul en UTC et les bibliothèques tz.
Pièges & idées reçues
- « UTF-8 = 1 caractère par octet » — seulement pour l’ASCII.
éen prend 2,€3, les emojis 4. Tronquer une chaîne à N octets peut couper un caractère au milieu et produire�. substring/slicecassent les graphèmes — tronquer « pour l’aperçu » à 20 caractères peut couper un emoji ou un accent. Segmenter par graphème avant de tronquer.\dn’est pas[0-9]partout — en Python,\dmatche les chiffres Unicode ('٣'compris) ;re.ASCIIou[0-9]si on veut des chiffres arabes occidentaux.- Additionner des offsets à la main (
heure + 2pour Paris) — l’offset dépend de la date à cause du DST. Toujours passer par la base tz via une bibliothèque. - « Ma regex email est correcte » — la grammaire RFC 5322 est monstrueuse et une regex « parfaite » refuse des adresses valides. Vérifier
qqch@qqch.qqch, puis envoyer un email de confirmation : c’est le seul test fiable. - Comparer des dates avec
==— en JS, deuxDateidentiques sont deux objets différents :d1 == d2estfalse. ComparergetTime(), ou utiliser une bibliothèque.
Pour aller plus loin
- The Absolute Minimum Every Software Developer Must Know About Unicode — le classique de Joel Spolsky
- UTC is enough for everyone, right? — Zach Holman, drôle et complet sur les fuseaux
- Falsehoods programmers believe about time — la liste des hypothèses fausses
- Post-mortem Cloudflare 2019 — une regex qui fait tomber un CDN mondial
- regex101 pour tester, documentation Temporal pour les dates JS modernes
The essentials
Three areas concentrate an outsized share of production bugs: strings (encoding), dates (timezones) and regular expressions. None is hard in theory; all of them punish implicit assumptions — “one character = one byte”, “midnight is midnight”, “my regex works on my examples”. Interviewers like these topics because they separate the student who wrote code from the one who debugged it.
| Symptom | Cause | Fix |
|---|---|---|
été displayed instead of été | UTF-8 bytes decoded as Latin-1 (mojibake) | declare UTF-8 everywhere: files, HTTP, DB |
'é'.length === 2 | UTF-16 units ≠ graphemes; NFD form | normalize, segment by grapheme |
"café" !== "café" | NFC vs NFD: precomposed é vs e + accent | .normalize('NFC') before comparing |
| Meeting shifted by one hour | stored in local time + DST | store UTC + ISO 8601, convert on display |
| Birthday shifted by one day | date-only stored as local midnight | DATE type without time, never a timestamp |
| API frozen on one specific input | catastrophic backtracking (ReDoS) | regex without nested quantifiers, timeout |
How it works
Encoding. ASCII encodes 128 characters in 7 bits — English, full stop. Unicode assigns a number (code point) to more than 150,000 characters: é = U+00E9, € = U+20AC. UTF-8 encodes each code point on 1 to 4 bytes, variable length, while staying ASCII-compatible:
Code point → UTF-8 bytes
U+0041 'A' → 01000001 (1 byte)
U+00E9 'é' → 110_00011 10_101001 (2 bytes)
U+20AC '€' → 1110_0010 10_000010
10_101100 (3 bytes)
U+1F44D '👍' → 11110_000 10_011111 … (4 bytes)
The first byte's prefix encodes the length;
every continuation byte starts with 10.
One on-screen “character” (grapheme) can span several code points: é exists precomposed (U+00E9, NFC form) or decomposed into e + combining accent (U+0065 U+0301, NFD form). Visually identical, binary different — hence normalization before any comparison or search. Mojibake (é) appears when UTF-8 bytes are re-read with the wrong charset.
'é'.length // 1 (NFC form: U+00E9)
'é'.normalize('NFD').length // 2: e + combining accent
'👍'.length // 2: surrogate pair —
// JS counts UTF-16 units
[...'👍'].length // 1: iteration by code point
'👨👩👧'.length // 8! 3 emojis + 2 invisible ZWJs
// Correct comparison of accented strings:
'café'.normalize('NFC') === 'café'.normalize('NFC')
// → true (without normalize: false)
// Count what the user sees (graphemes):
[...new Intl.Segmenter().segment('👨👩👧')].length // 1
Dates. The golden rule: store in UTC, in ISO 8601 format (2026-10-29T14:30:00Z), and convert to the user’s timezone only at display time. A timezone is not a fixed offset: it changes with DST (an hour that doesn’t exist in spring, an hour that exists twice in autumn) and with political decisions — the tz database is updated several times a year. The “local midnight” bug: store a date-only value (birthday, deadline) as a timestamp at local midnight, then display it in another timezone → the date moves back a day.
JavaScript’s Date stacks up the traps: zero-indexed months (new Date(2026, 9, 29) = October 29), inconsistent parsing, mutable objects. In 2026, use the Temporal API (rolling out in JS engines) or a library like date-fns or Luxon.
// Two traps in two lines:
new Date('2026-10-29') // midnight UTC
new Date('2026-10-29T00:00') // midnight LOCAL
// In Paris (UTC+1), one hour apart — comparing these
// naively shifts deadlines.
Regex. The useful building blocks: classes ([a-z], \d, \w), quantifiers (*, +, ?, {n,m}), anchors (^, $, \b), capturing groups (…), named (?<name>…), non-capturing (?:…). The central trap: quantifiers are greedy by default — they swallow the maximum then step back (backtracking).
'<b>bold</b> and <i>ital</i>'.match(/<.+>/)[0]
// → '<b>bold</b> and <i>ital</i>' greedy: everything!
'<b>bold</b>'.match(/<.+?>/)[0] // '<b>' lazy: minimum
'<b>bold</b>'.match(/<[^>]+>/)[0] // '<b>' negated class:
// fast, no backtracking
Key concepts to master
- Byte ≠ code point ≠ grapheme: three distinct levels.
length,substring,reverseoften work at the wrong level and cut an emoji in half (�). - Normalize at the boundaries: normalize to NFC at system entry points (forms, imports, filenames — macOS produces NFD), compare and index on the normalized form.
- UTC + ISO 8601, with one exception: a future event tied to a place (“meeting at 9am in Paris in 2027”) is stored as local time + IANA identifier (
Europe/Paris), because timezone rules may change before then. A past or absolute instant (log, payment) is stored in UTC. - Date-only types: a birthday has no time and no timezone.
DATEin SQL,Temporal.PlainDatein JS — never a timestamp at midnight. - Greedy vs lazy vs negated class:
.+?treats the symptom,[^>]+states the intent and removes the backtracking. - When not to use a regex: as soon as the format is nested or recursive — HTML first. A regex cannot count nesting levels (regular language vs context-free language): use a real parser (DOMParser, BeautifulSoup). For emails: minimal validation + a confirmation email, not a 400-character regex.
- Test your regexes: on regex101 with edge cases (empty string, accents, hostile inputs), then unit tests documenting the covered cases.
⚠️ ReDoS — a regex with nested quantifiers like
(a+)+$explodes into exponential backtracking on hostile input ("aaaaaaaaaaaaaaaaaaaaab"is enough). A single request can freeze a thread: Stack Overflow (2016) and Cloudflare (2019) went down because of one regex. Countermeasures: no nested quantifiers or overlapping alternations, a timeout on execution, or a guaranteed-linear engine (RE2, Rust’s regex crate).
💡 UTC everywhere, convert on display — the backend, the DB and the logs know only UTC; the user’s timezone appears only at the very last layer (rendering). One conversion point = one possible class of bugs, instead of one per layer.
In an interview
“Why can 'é'.length be 2 in JavaScript?” — Two possible reasons. If the string is in NFD, é is two code points (e + combining accent). And length counts UTF-16 units, not graphemes: '👍'.length === 2 (surrogate pair). Complete answer: normalize to NFC, and segment by grapheme (Intl.Segmenter) when you want to count what the user sees.
“How do you store dates in an international application?” — UTC + ISO 8601 in the database, conversion to the user’s timezone at display time. The nuance that scores points: a future localized event is stored as local time + IANA identifier, because DST rules may change between storage and the event.
“Greedy vs lazy?” — A greedy quantifier (.+) swallows the maximum then backs off until the rest of the pattern matches; lazy (.+?) takes the minimum then extends. On <b>x</b>, <.+> captures the whole string, <.+?> captures <b>. The best answer offers the negated class <[^>]+>: same result, no backtracking.
“Why not parse HTML with a regex?” — HTML is a nested language: a regex cannot count opening/closing levels (that’s the limit of regular languages). It works on three examples then breaks on attributes, comments, nesting. A parser already exists in every ecosystem: DOMParser, BeautifulSoup, lxml.
“What does DST change for a developer?” — Twice a year, local time jumps: a nonexistent hour in spring, an ambiguous hour in autumn. Consequences: “add 24h” ≠ “tomorrow same time”, cron jobs between 2am and 3am skip or run twice, durations computed in local time are off by an hour. Hence computing in UTC and using tz libraries.
Pitfalls & misconceptions
- “UTF-8 = 1 character per byte” — only for ASCII.
étakes 2 bytes,€3, emojis 4. Truncating a string at N bytes can cut a character in half and produce�. substring/slicebreak graphemes — truncating “for the preview” at 20 characters can cut an emoji or an accent. Segment by grapheme before truncating.\dis not[0-9]everywhere — in Python,\dmatches Unicode digits (including'٣'); usere.ASCIIor[0-9]if you want Western Arabic digits.- Adding offsets by hand (
hour + 2for Paris) — the offset depends on the date because of DST. Always go through the tz database via a library. - “My email regex is correct” — the RFC 5322 grammar is monstrous and a “perfect” regex rejects valid addresses. Check
something@something.something, then send a confirmation email: the only reliable test. - Comparing dates with
==— in JS, two identicalDates are two different objects:d1 == d2isfalse. ComparegetTime(), or use a library.
Going further
- The Absolute Minimum Every Software Developer Must Know About Unicode — Joel Spolsky’s classic
- UTC is enough for everyone, right? — Zach Holman, funny and thorough on timezones
- Falsehoods programmers believe about time — the list of wrong assumptions
- Cloudflare 2019 post-mortem — one regex taking down a global CDN
- regex101 for testing, Temporal documentation for modern JS dates