{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chinese-number",
  "title": "中文数字",
  "description": "人民币大写（按财务规范，万亿级、零的省略、分以下四舍五入）与整数中文读法的纯函数工具，无依赖，服务端可用。",
  "files": [
    {
      "path": "src/registry/lib/chinese-number.ts",
      "content": "/**\n * 中文数字工具：人民币大写（财务规范）与整数中文读法。\n * 纯函数、无依赖，服务端和客户端都能用。\n *\n * 输入统一按「十进制字符串」处理，不经过浮点运算：数字先用 `String(value)` 取最短表示再解析，\n * 所以 `0.1 + 0.2`（`0.30000000000000004`）按分四舍五入后是 `叁角`，不会出现误差。\n * 非法输入（NaN、Infinity、无法解析的字符串、超出范围）一律返回空串而不抛错，\n * 方便在输入框里边敲边显示，不必到处包 try/catch。\n */\n\n/** 整数部分最多 16 位（万亿以上到「九千九百九十九万亿」），再大的数没有通行读法。 */\nconst MAX_INTEGER_DIGITS = 16\n\nconst UPPER_DIGITS = \"零壹贰叁肆伍陆柒捌玖\"\nconst LOWER_DIGITS = \"零一二三四五六七八九\"\nconst UPPER_SMALL = [\"\", \"拾\", \"佰\", \"仟\"]\nconst LOWER_SMALL = [\"\", \"十\", \"百\", \"千\"]\n// 每四位一节：个、万、亿、万亿\nconst SECTION_UNITS = [\"\", \"万\", \"亿\", \"万亿\"]\n\ntype ParsedDecimal = {\n  negative: boolean\n  /** 整数部分，已去掉前导零；零写成 `\"0\"` */\n  int: string\n  /** 小数部分，可能为空串 */\n  frac: string\n}\n\n/** 把 `1.5e-7`、`1e21` 这类科学计数法展开成普通小数字符串。 */\nfunction expandExponent(text: string) {\n  const match = /^([+-]?)(\\d+)(?:\\.(\\d*))?e([+-]?\\d+)$/i.exec(text)\n  if (!match) return text\n  const [, sign = \"\", intPart = \"\", fracPart = \"\", exponent] = match\n  const shift = Number(exponent)\n  const digits = intPart + fracPart\n  const point = intPart.length + shift\n  if (point <= 0) return `${sign}0.${\"0\".repeat(-point)}${digits}`\n  if (point >= digits.length)\n    return `${sign}${digits}${\"0\".repeat(point - digits.length)}`\n  return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`\n}\n\nfunction parseDecimal(value: number | string): ParsedDecimal | null {\n  let text: string\n  if (typeof value === \"number\") {\n    // 超过安全整数的 number 本身就已经丢了精度，不给出一个看似精确的结果\n    if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER)\n      return null\n    text = expandExponent(String(value))\n  } else if (typeof value === \"string\") {\n    text = expandExponent(value.trim().replace(/,/g, \"\"))\n  } else {\n    return null\n  }\n  const match = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(text)\n  if (!match) return null\n  const [, sign = \"\", intPart = \"\", frac = \"\"] = match\n  if (!intPart && !frac) return null\n  const int = intPart.replace(/^0+(?=\\d)/, \"\") || \"0\"\n  if (int.length > MAX_INTEGER_DIGITS) return null\n  return { negative: sign === \"-\", int, frac }\n}\n\n/**\n * 把整数字符串读成「万」「亿」分节的中文。\n * 零的规则：节内连续的零读一个「零」，末尾的零不读；整节为零则省略该节的单位，\n * 但后面还有非零数字时补一个「零」（`1000001` → `一百万零一`）。\n */\nfunction readInteger(int: string, digits: string, small: string[]) {\n  let out = \"\"\n  let pendingZero = false\n  let sectionHasValue = false\n  for (let i = int.length - 1; i >= 0; i--) {\n    const digit = Number(int[int.length - 1 - i])\n    if (digit === 0) {\n      pendingZero = true\n    } else {\n      if (pendingZero && out) out += digits.charAt(0)\n      pendingZero = false\n      sectionHasValue = true\n      out += digits.charAt(digit) + small[i % 4]\n    }\n    if (i % 4 === 0) {\n      if (sectionHasValue) out += SECTION_UNITS[i / 4]\n      sectionHasValue = false\n    }\n  }\n  return out\n}\n\n/** 十进制数字串加一：`\"199\"` → `\"200\"`，`\"99\"` → `\"100\"`。 */\nfunction incrementDigits(digits: string) {\n  const chars = digits.split(\"\")\n  let i = chars.length - 1\n  while (i >= 0 && chars[i] === \"9\") chars[i--] = \"0\"\n  if (i < 0) chars.unshift(\"1\")\n  else chars[i] = String(Number(chars[i]) + 1)\n  return chars.join(\"\")\n}\n\ntype RmbUppercaseOptions = {\n  /**\n   * 分以下的小数怎么处理：`round` 四舍五入到分（0.005 → 一分），`truncate` 直接舍去。\n   * @default \"round\"\n   */\n  rounding?: \"round\" | \"truncate\"\n}\n\n/**\n * 人民币大写，按财务规范书写：\n * - 数字用「壹贰叁肆伍陆柒捌玖」，单位用「拾佰仟万亿」，**拾位保留「壹」**（`100000` → `壹拾万元整`），防止被改写；\n * - 到元为止在「元」后加「整」，有角无分时「角」后不加（`0.5` → `伍角`），有分时「分」后不加；\n * - 元位有数、角位为零而分位有数时补「零」（`10.05` → `壹拾元零伍分`），整数部分为零时不写「零元」（`0.05` → `伍分`）；\n * - 负数前加「负」；结果为零（包括 `-0.001` 舍入后）写 `零元整`。\n *\n * 支持到整数部分 16 位（九千九百九十九万亿）。传 number 时超过安全整数范围，传字符串可避开这个限制。\n * NaN、非法字符串、超出范围一律返回空串。\n *\n * @example\n * toRmbUppercase(1234.56) // \"壹仟贰佰叁拾肆元伍角陆分\"\n * toRmbUppercase(\"1000.00\") // \"壹仟元整\"\n */\nfunction toRmbUppercase(\n  value: number | string,\n  options: RmbUppercaseOptions = {},\n): string {\n  const { rounding = \"round\" } = options\n  const parsed = parseDecimal(value)\n  if (!parsed) return \"\"\n  // 全部换成「分」的整数串再算，避免任何小数运算（也不依赖 BigInt）\n  let cents = (parsed.int + parsed.frac.slice(0, 2).padEnd(2, \"0\")).replace(\n    /^0+(?=\\d)/,\n    \"\",\n  )\n  if (rounding === \"round\" && Number(parsed.frac[2] ?? 0) >= 5)\n    cents = incrementDigits(cents)\n  if (cents.length > MAX_INTEGER_DIGITS + 2) return \"\"\n\n  const yuan = cents.length > 2 ? cents.slice(0, -2) : \"0\"\n  const jiao = cents.length > 1 ? Number(cents[cents.length - 2]) : 0\n  const fen = Number(cents[cents.length - 1])\n\n  if (/^0+$/.test(cents)) return \"零元整\"\n  let out = parsed.negative ? \"负\" : \"\"\n  if (yuan !== \"0\") out += `${readInteger(yuan, UPPER_DIGITS, UPPER_SMALL)}元`\n  if (jiao === 0 && fen === 0) return `${out}整`\n  if (jiao > 0) out += `${UPPER_DIGITS[jiao]}角`\n  else if (yuan !== \"0\") out += \"零\"\n  if (fen > 0) out += `${UPPER_DIGITS[fen]}分`\n  return out\n}\n\ntype ChineseNumeralsOptions = {\n  /**\n   * 用大写数字（壹贰叁…拾佰仟）。财务写法不省略「壹拾」，也不写口语的「十」开头。\n   * @default false\n   */\n  financial?: boolean\n}\n\n/**\n * 数字的中文读法：`123` → `一百二十三`，`10010` → `一万零一十`。\n *\n * - 整个数以 10–19 开头时读「十」而不读「一十」（`10` → `十`，`110000` → `十一万`）；\n *   数中间的「一十」保留（`10010` → `一万零一十`，`110` → `一百一十`）。`financial: true` 时一律保留「壹拾」。\n * - 二一律写「二」，不写「两」（`2000` → `二千`）。\n * - 小数点后逐位读，`3.14` → `三点一四`；负数前加「负」。\n *\n * 支持到整数部分 16 位；NaN、非法字符串、超出范围返回空串。\n */\nfunction toChineseNumerals(\n  value: number | string,\n  options: ChineseNumeralsOptions = {},\n): string {\n  const { financial = false } = options\n  const parsed = parseDecimal(value)\n  if (!parsed) return \"\"\n  const digits = financial ? UPPER_DIGITS : LOWER_DIGITS\n  const small = financial ? UPPER_SMALL : LOWER_SMALL\n\n  let out =\n    parsed.int === \"0\"\n      ? digits.charAt(0)\n      : readInteger(parsed.int, digits, small)\n  if (!financial && out.startsWith(`${digits.charAt(1)}${small[1]}`))\n    out = out.slice(1)\n  if (parsed.frac)\n    out += `点${[...parsed.frac].map((digit) => digits.charAt(Number(digit))).join(\"\")}`\n  // 全零（含 -0）不带符号\n  const isZero = /^0*$/.test(parsed.int + parsed.frac)\n  return parsed.negative && !isZero ? `负${out}` : out\n}\n\nexport { toChineseNumerals, toRmbUppercase }\nexport type { ChineseNumeralsOptions, RmbUppercaseOptions }\n",
      "type": "registry:ui",
      "target": "lib/chinese-number.ts"
    }
  ],
  "type": "registry:lib"
}
