{"version":3,"file":"format.cjs","names":[],"sources":["../../src/utils/format.ts"],"sourcesContent":["import { fallbackText, isAbsent, type FormatFallbackOptions } from \"@/utils/absent\";\nimport { dateTimeFormat, numberFormat } from \"@/utils/intl-cache\";\n\n/**\n * Format a number as Brazilian Real currency.\n *\n * An absent or unrepresentable amount renders as `\"—\"` rather than as a number.\n * That branch is a data-correctness fix, not cosmetics: `Intl` coerces `null` to\n * zero, so an amount the backend left out used to reach the screen as\n * **`\"R$ 0,00\"`** — a plausible figure a reader has no way to question — and\n * `undefined` as `\"R$ NaN\"`.\n *\n * @example\n * ```typescript\n * formatCurrency(1234.56);                      // \"R$ 1.234,56\"\n * formatCurrency(null);                         // \"—\"\n * formatCurrency(null, { fallback: \"sem valor\" }); // \"sem valor\"\n * ```\n *\n * @param value - The amount in BRL, or `null`/`undefined` when there is none.\n * @param options - Text to render when there is nothing to format.\n * @returns A locale-formatted string, e.g. \"R$ 1.234,56\".\n */\nexport function formatCurrency(\n    value: number | null | undefined,\n    options?: FormatFallbackOptions,\n): string {\n    if (isAbsent(value) || !Number.isFinite(value)) return fallbackText(options);\n    return numberFormat(\"pt-BR\", { style: \"currency\", currency: \"BRL\" }).format(value);\n}\n\n/**\n * Format an ISO date or Date instance as `dd/MM/yyyy`.\n *\n * Absent input answers `\"—\"`. It used to **throw**: `new Date(null)` is epoch and\n * `null.getTime` is a `TypeError`, so a nullable column — which is every\n * `deleted_at`, `expires_at` and half the `updated_at` a FastAPI backend sends —\n * took the screen down instead of rendering a gap.\n *\n * A value that is present and unparseable still answers `\"\"`, deliberately: on a\n * screen that reads a log, \"nobody filled this in\" and \"what they filled in is\n * broken\" are different findings, and the second is the one worth chasing.\n *\n * @example\n * ```typescript\n * formatDate(\"2026-05-16T12:00:00Z\"); // \"16/05/2026\"\n * formatDate(null);                   // \"—\"\n * formatDate(\"banana\");               // \"\"\n * ```\n *\n * @param value - ISO string, Date, or `null`/`undefined`.\n * @param options - Text to render when the value is absent.\n * @returns Formatted date string, the fallback when absent, or empty string when\n *   the value is present and invalid.\n */\nexport function formatDate(\n    value: string | Date | null | undefined,\n    options?: FormatFallbackOptions,\n): string {\n    if (isAbsent(value)) return fallbackText(options);\n    const date = typeof value === \"string\" ? new Date(value) : value;\n    if (Number.isNaN(date.getTime())) return \"\";\n    return dateTimeFormat(\"pt-BR\").format(date);\n}\n\n/**\n * Format an ISO date or Date instance as `yyyy-MM-dd`, the value an\n * `<input type=\"date\">` accepts.\n *\n * Built from the **local** calendar parts rather than `toISOString().slice(0, 10)`,\n * which is the reflex and which is wrong: `toISOString` converts to UTC first, so\n * anything after 21:00 in UTC-3 reports the next day and the form opens on the\n * wrong date. `formatDate` cannot fill this role because a date input rejects\n * `dd/MM/yyyy` outright.\n *\n * A value that is already `yyyy-MM-dd` is returned untouched, and that shortcut\n * is load-bearing rather than an optimisation: `new Date(\"2026-05-16\")` is parsed\n * as **UTC** midnight, which in UTC-3 is the 15th at 21:00, so round-tripping the\n * exact value a backend sent would move it back a day.\n *\n * @example\n * <input type=\"date\" defaultValue={formatDateForInput(order.createdAt)} />\n *\n * Absent input answers `\"\"` here — **not** the em dash the reading formatters\n * use — because that is what an input reads as \"no value\"; an em dash would\n * arrive as content the user has to delete before typing a date.\n *\n * @param value - ISO string, Date, or `null`/`undefined`.\n * @param options - Text for an absent value; defaults to `\"\"`.\n * @returns The `yyyy-MM-dd` value, or an empty string when the input is invalid —\n *   which is what a date input reads as \"no value\", unlike `\"Invalid Date\"`.\n */\nexport function formatDateForInput(\n    value: string | Date | null | undefined,\n    options?: FormatFallbackOptions,\n): string {\n    if (isAbsent(value)) return fallbackText(options, \"\");\n    if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return value;\n    const date = typeof value === \"string\" ? new Date(value) : value;\n    if (Number.isNaN(date.getTime())) return \"\";\n    const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n    const day = `${date.getDate()}`.padStart(2, \"0\");\n    return `${date.getFullYear()}-${month}-${day}`;\n}\n\n/**\n * Format an ISO date or Date instance as `yyyy-MM-ddTHH:mm`, the value an\n * `<input type=\"datetime-local\">` accepts.\n *\n * The sibling of {@link formatDateForInput}, and it exists for the same reason:\n * `toISOString().slice(0, 16)` is the reflex and it is wrong. `toISOString`\n * converts to UTC first, so a 22:00 appointment in UTC-3 opens the form on the\n * next day at 01:00 — here the trap costs the hour as well as the date.\n *\n * A value already in `yyyy-MM-ddTHH:mm` is returned untouched. A value carrying\n * a zone (`...Z`, `...-03:00`) deliberately does **not** take that shortcut: it\n * is a different instant from the naive string that looks like it, so it is\n * converted to the local calendar parts the input has to show.\n *\n * Seconds are dropped. A `datetime-local` steps by the minute unless the app\n * sets `step`, so a `:ss` the field cannot represent would be silently discarded\n * on the first edit anyway — truncating here keeps the rendered value and the\n * submitted value the same.\n *\n * @example\n * <input\n *     type=\"datetime-local\"\n *     defaultValue={formatDateTimeForInput(appointment.startsAt)}\n * />\n *\n * @param value - ISO string or Date.\n * @returns The `yyyy-MM-ddTHH:mm` value, or an empty string when the input is\n *   invalid — which is what a datetime input reads as \"no value\", unlike\n *   `\"Invalid Date\"`.\n */\nexport function formatDateTimeForInput(\n    value: string | Date | null | undefined,\n    options?: FormatFallbackOptions,\n): string {\n    if (isAbsent(value)) return fallbackText(options, \"\");\n    if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$/.test(value)) return value;\n    const date = typeof value === \"string\" ? new Date(value) : value;\n    if (Number.isNaN(date.getTime())) return \"\";\n    const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n    const day = `${date.getDate()}`.padStart(2, \"0\");\n    const hours = `${date.getHours()}`.padStart(2, \"0\");\n    const minutes = `${date.getMinutes()}`.padStart(2, \"0\");\n    return `${date.getFullYear()}-${month}-${day}T${hours}:${minutes}`;\n}\n\n/**\n * Format an ISO date or Date instance as `dd/MM/yyyy HH:mm`.\n *\n * Same contract as {@link formatDate}: absent answers the fallback (`\"—\"` by\n * default) instead of throwing, and present-but-invalid answers `\"\"`.\n *\n * @param value - ISO string, Date, or `null`/`undefined`.\n * @param options - Text to render when the value is absent.\n * @returns Formatted datetime string, the fallback when absent, or empty string\n *   when the value is present and invalid.\n */\nexport function formatDateTime(\n    value: string | Date | null | undefined,\n    options?: FormatFallbackOptions,\n): string {\n    if (isAbsent(value)) return fallbackText(options);\n    const date = typeof value === \"string\" ? new Date(value) : value;\n    if (Number.isNaN(date.getTime())) return \"\";\n    return dateTimeFormat(\"pt-BR\", { dateStyle: \"short\", timeStyle: \"short\" }).format(date);\n}\n\nexport interface FormatPhoneOptions {\n    /**\n     * Treat the number as a mobile line: insert the mandatory `9` after the area\n     * code when it is missing, and group the subscriber part `5+4` from the\n     * first digit typed instead of waiting for the eleventh.\n     *\n     * Default `false`, which keeps the length-based behaviour: `4+4` up to ten\n     * digits, `5+4` at eleven.\n     */\n    mobile?: boolean;\n}\n\n/**\n * Apply the Brazilian phone mask `(XX) XXXXX-XXXX` or `(XX) XXXX-XXXX`.\n *\n * By default the grouping is decided by **length**, which is what a field\n * accepting both landlines and mobiles needs.\n *\n * `mobile: true` is for a field that only accepts mobile numbers, and it exists\n * because the default is wrong as an as-you-type mask there. Reading anything up\n * to ten digits as a landline puts the hyphen after the fourth subscriber digit,\n * so a half-typed mobile renders `(11) 9123-4`; it only becomes `(11) 91234-5`\n * once the eleventh digit lands. The separator visibly jumps backwards while the\n * user is still typing. With `mobile`, the same input reads `(11) 91234` and the\n * hyphen never moves. It also inserts the leading `9` every Brazilian mobile\n * carries, so a ten-digit number gets corrected rather than masked as a landline.\n *\n * @param value - Raw digits or partially masked string.\n * @param options - Masking options.\n * @returns Masked phone string.\n *\n * @example\n * formatPhone(\"1191234\");                      // \"(11) 9123-4\"\n * formatPhone(\"1191234\", { mobile: true });    // \"(11) 91234\"\n * formatPhone(\"1112345678\", { mobile: true }); // \"(11) 91234-5678\" — 9 inserted\n */\nexport function formatPhone(value: string, options: FormatPhoneOptions = {}): string {\n    const digits = value.replace(/\\D/g, \"\").slice(0, 11);\n\n    if (!options.mobile) {\n        if (digits.length <= 10) {\n            return digits.replace(/(\\d{2})(\\d)/, \"($1) $2\").replace(/(\\d{4})(\\d)/, \"$1-$2\");\n        }\n        return digits.replace(/(\\d{2})(\\d)/, \"($1) $2\").replace(/(\\d{5})(\\d)/, \"$1-$2\");\n    }\n\n    if (digits.length <= 2) return digits;\n\n    const area = digits.slice(0, 2);\n    let subscriber = digits.slice(2);\n    if (subscriber[0] !== \"9\") subscriber = `9${subscriber}`;\n    subscriber = subscriber.slice(0, 9);\n\n    const prefix = subscriber.slice(0, 5);\n    const suffix = subscriber.slice(5);\n    return suffix ? `(${area}) ${prefix}-${suffix}` : `(${area}) ${prefix}`;\n}\n\n/**\n * Apply the Brazilian CPF mask `XXX.XXX.XXX-XX`.\n *\n * @param value - Raw digits or partially masked string.\n * @returns Masked CPF string.\n */\nexport function formatCPF(value: string): string {\n    return value\n        .replace(/\\D/g, \"\")\n        .slice(0, 11)\n        .replace(/(\\d{3})(\\d)/, \"$1.$2\")\n        .replace(/(\\d{3})(\\d)/, \"$1.$2\")\n        .replace(/(\\d{3})(\\d{1,2})$/, \"$1-$2\");\n}\n\n/** How {@link formatPercent} renders a fraction. */\nexport interface FormatPercentOptions extends FormatFallbackOptions {\n    /**\n     * Decimal places to show, fixed (padded as well as truncated).\n     *\n     * Defaults to `1`, which is what the function always did. Two is the case\n     * that produced this option: a confidence of 98,74% and one of 98,7% are the\n     * same number on a summary card and two different readings on a detail\n     * screen, so the precision belongs to the call site.\n     */\n    decimals?: number;\n}\n\n/**\n * Format a fraction (0-1) as a percentage.\n *\n * A non-finite input renders as `\"—\"` rather than `\"NaN%\"` or `\"∞%\"`, which is\n * what `Intl.NumberFormat` produces and what a division by zero upstream puts on\n * the screen. It matches `formatDurationMs`, which already answers that way.\n *\n * @example\n * ```typescript\n * formatPercent(0.9874);                // \"98,7%\"\n * formatPercent(0.9874, { decimals: 2 }); // \"98,74%\"\n * formatPercent(0 / 0);                 // \"—\"\n * ```\n *\n * @param value - Fraction between 0 and 1.\n * @param options - Rendering options; `decimals` defaults to 1.\n * @returns Formatted percent string, e.g. \"12,5%\", or `\"—\"` for a non-finite input.\n */\nexport function formatPercent(\n    value: number | null | undefined,\n    options: FormatPercentOptions = {},\n): string {\n    if (isAbsent(value) || !Number.isFinite(value)) return fallbackText(options);\n    const decimals = options.decimals ?? 1;\n    return numberFormat(\"pt-BR\", {\n        style: \"percent\",\n        minimumFractionDigits: decimals,\n        maximumFractionDigits: decimals,\n    }).format(value);\n}\n"],"mappings":"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"}