Teaching a French date parser to stop guessing day and month

这篇内容暂时只有 English 版本,以下是原文。

A support ticket from a user in Lyon came in as “export shows wrong dates.” The export was a CSV we generate from a Postgres view, consumed by a small internal tool. Version: Node 20.11, date-fns 3.3, data volume about 240k rows per month, one column event_date stored as date. The file looked fine in LibreOffice and wrong in our own tool: 03/04/2024 rendered as March 4 in one place and April 3 in another. Same file, two parsers, two answers.

The root cause was embarrassingly simple. We had two code paths. The CSV writer used format(d, 'dd/MM/yyyy') for the French locale. The reader used new Date(value), which on Node treats slash dates as US month-first. So the writer was right and the reader was silently lying. I replaced the reader with an explicit parse and made the format a single constant shared by both sides:

import { format, parse, isValid } from 'date-fns';

export const FR_DATE = 'dd/MM/yyyy';

export function writeFrDate(d) {
  return format(d, FR_DATE);
}

export function readFrDate(s) {
  const d = parse(s, FR_DATE, new Date());
  if (!isValid(d)) throw new Error(`bad French date: ${s}`);
  return d;
}

I also added a guard at the boundary: any cell matching /^\d{2}\/\d{2}\/\d{4}$/ goes through readFrDate, everything else goes to a strict ISO parser. No heuristic fallback. If a value does not parse, the import fails loudly with the row number instead of guessing.

The reason this matters beyond one file: new Date('03/04/2024') is not a locale-aware call. It is a V8 convenience that assumes month-first for slash-separated strings, and that assumption is baked into the runtime, not into your locale settings. Setting LANG=fr_FR.UTF-8 does not change it. I had assumed for months that it did, because the container had the locale installed and Intl output looked French. Intl and Date parsing are separate subsystems; installing a locale only affects the former.

Where this breaks: ISO strings with a time component still behave predictably, but 2024-04-03T00:00:00 without a zone offset is parsed as local time, and on a machine set to Europe/Paris that is fine until you compare it to a UTC-stored timestamp. I hit that in the same ticket: the CSV looked correct, but a downstream join on created_at was off by an hour for two days around the DST switch. Also, parse with a reference date of new Date() inherits the current time, so a date-only parse gives you today’s clock time; if you later compare with startOfDay you can get off-by-one at midnight. I now pass new Date(2000, 0, 1) as the reference and call startOfDay on the result.

The cost of the strict approach is that bad rows stop the import. We chose that over silent wrong dates, but it means the support team sees the failure before the user does. For a 240k-row file, the guard adds a few hundred milliseconds; not a real trade-off.

What I have not solved: we still have old exports with ambiguous dd/MM/yyyy strings sitting in S3, and there is no way to tell March 4 from April 3 in those files without a source-of-truth join. If you have dealt with a backlog of ambiguous French date exports, how did you decide which ones to re-derive and which to leave alone?

0
赞同度 100% · 0 条评论 · 4 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。