凡是碰过合同、发票、对账批处理、支付单据的后端,基本都写过(或抄过)一个“阿拉伯数字 → 中文大写金额”的函数。这活儿看着像字符串查表,真正落到财务规范里,挂掉的几乎都在零的合并、整字边界、圆与元混用这三条线上。
1. “零”不是有多少个 0 就写多少个
《会计基础工作规范》和央行票据填写规范里对“零”的约定是带语言感的,不是机械替换:
中间单个 0 → 写“零”:
1409.50→ 壹仟肆佰零玖元伍角连续多个 0 → 只写一个“零”:
6007.14→ 陆仟零柒元壹角肆分元位是 0、角位非 0,可写可不写“零”:
1680.32→ 壹仟陆佰捌拾元零叁角贰分 / 也可省“零”角位是 0、分位非 0 → 元后必须补“零”:
325.04→ 叁佰贰拾伍元零肆分
很多自写实现用正则零+ → 零收尾,结果把“零角零分”误并成“零分”,或者把“壹仟零拾元整”里的结构性零吞掉。正确做法是用按权位扫描 + 状态机:记录“上一位是否零、当前组是否全零、跨万/亿组时是否补零”,而不是先拼完再正则擦屁股。
2. “整”字只在到元或到角时出场
规则很硬:
到元为止(无角分或角分 00)→ 元后加“整”或“正”:
532.00→ 伍佰叁拾贰元整到角为止(分位 0)→ 角后可写可不写“整”:
100.50→ 壹佰元伍角(或壹佰元伍角整)有分 → 分后绝不写整:
6007.14→ 陆仟零柒元壹角肆分(没有“整”)
线上出过真实故障:某合同系统对0.00输出“零元”,财务拒收(规范上零元一般写“零元整”或结合上下文);又有系统对12.34尾缀硬加“整”,被审计打回。这处边界必须用“小数两位是否全 0”单独分支,不能跟整数拼接共用后缀。
3. “圆”和“元”都能用,但别在一个系统里跳着用
人民币法定单位是“元”,但票据规范明确:用“圆”也算受理(繁体“圓”也行),前提是整套单据统一。
新系统、对外 API、电子合同 → 默认用“元”
对接老银行直连、支票、承兑汇票 → 看对方模板,用“圆”就全程“圆”
最忌讳:同一条记录 JSON 里返“元”,PDF 模板里写“圆”,人工核对时以为两版不一致
顺带一条:大写前是否带“人民币”三字、小写前是否带“¥”,都属于单据格式层的事,不该在“数字转大写”纯函数里硬编码,应该由调用方(单据渲染层)决定前缀。
4. 为什么别用 float64 直接乘 100
19.99 * 100 = 1998.9999999999998这种事在金额转换里是事故级。正确路径:
入参用
string或decimal.Big,不用float64按小数点切整数段 / 角分段 / 分分段
整数段从右往左每 4 位一组(个十百千 / 万 / 亿),组间插“万”“亿”,组内按千百十处理
小数段单独走角、分映射,角分全 0 才走“整”分支
Go 里用strings.SplitN(input, ".", 2)+big.Int收整数,小数补零到两位再int取角分,比任何float64 * 100都稳。
5. 联调时怎么快速核对
自写函数写完,拿一组边界用例对照就够:
0 → 零元整 0.04 → 零元零肆分 1409.50 → 壹仟肆佰零玖元伍角 6007.14 → 陆仟零柒元壹角肆分 1680.32 → 壹仟陆佰捌拾元零叁角贰分(或省零) 107000.53 → 壹拾万零柒仟元零伍角叁分(或省零) 325.04 → 叁佰贰拾伍元零肆分 532.00 → 伍佰叁拾贰元整 1234567.89 → 壹佰贰拾叁万肆仟伍佰陆拾柒元捌角玖分我习惯把这些用例在纯前端页里再跑一遍对照——不传数据到服务端、切“元/圆”开关看渲染差异,用https://zz365.top/daxie这种本地页做回归目检就行(同站 Base64/JWT/Crontab 是一套“浏览器跑完即清”的本地工具箱思路,不是独立产品,财务联调时开一个标签页对照用)。注意它只帮你看“财务写法对不对”,生产代码仍必须以服务端string/decimal实现 + 单测为准,前端页不替代单测。
小结:数字转大写不是“查表题”,是财务语言规则题。零的合并、整字边界、圆元统一这三条线在 code review 里盯住,比换更炫的 UI 重要;自写实现用 string 入参 + 权位状态机,别碰 float64。