src/lib/money.ts
Rama main · réplica de lectura del repositorio original.
/**
* An offer amount, in whole units — the one reading of it in the app.
*
* No imports, so the offer card can run the same parse in the browser that the
* data layer runs on the server. The card used to have its own, which turned
* "€215.000" into 215 and "$" into 0 after this one had been taught to refuse
* both: two parsers, and the screen where people compare offers had the wrong
* one.
*
* Assistants send "215k", "$215,000" and 215000 for the same figure, and a
* person typing into the form sends "". All three of the first shapes mean the
* same thing and are accepted; "" and null mean "not sent" and come back
* undefined; anything else that is not a number is refused rather than stored
* as zero, because zero on a comparison table reads as "they offered nothing".
*/
/** Postgres INTEGER, and the reason amounts are whole units rather than cents. */
export const MAX_AMOUNT = 2_147_483_647;
export function parseAmount(value: unknown, field: string): number | undefined {
if (value === undefined || value === null || value === "") return undefined;
let n: number;
if (typeof value === "number") n = value;
else if (typeof value === "string") {
const raw = value.trim().toLowerCase();
// A string with no digit in it is not a figure. Without this, " ", "$" and
// "k" all reach Number("") and land as a real zero.
if (!/\d/.test(raw)) throw new Error(`${field} is not a number: "${value}"`);
// "215.000" is two hundred and fifteen thousand in half of Europe and two
// hundred and fifteen everywhere else, and guessing wrong is a 1000× error
// sitting next to correctly-read columns. Refused rather than guessed.
if (/\d\.\d{3}(\D|$)/.test(raw)) {
throw new Error(
`${field} is ambiguous: "${value}" could be thousands or a decimal. Send it as digits, like 215000.`,
);
}
const clean = raw.replace(/[$£€,\s]/g, "");
const k = clean.endsWith("k");
const parsed = Number(k ? clean.slice(0, -1) : clean);
if (!Number.isFinite(parsed)) throw new Error(`${field} is not a number: "${value}"`);
n = k ? parsed * 1000 : parsed;
} else throw new Error(`${field} is not a number`);
if (!Number.isFinite(n) || n < 0) throw new Error(`${field} must be zero or more`);
if (n > MAX_AMOUNT) throw new Error(`${field} is larger than this column can hold`);
return Math.round(n);
}