☰
数字转英文与中文表达:规则、坑点与工程化实现
2026/10/31 9:51:03 网站建设 项目流程

数字转英文和中文表达这件事,乍一看像是编程入门练习题,真动手写过一版能上生产的实现之后,你会发现坑比想象中多得多。我最早接触这个需求是在一个财务对账模块里,金额需要同时输出阿拉伯数字、英文大写金额和中文大写金额三份,当时觉得写个映射表递归一下不就完了,结果被"and"的位置、连字符的有无、零的处理、十位以上的进位规则轮番教育。后来陆续又在报表导出、发票打印、合同模板生成几个场景里反复重写这块逻辑,才慢慢摸清楚里面的门道。

这篇内容适合两类人看:一类是正在写数字转英文或中文表达、被各种边界情况卡住的朋友;另一类是手里有现成实现、但不确定它在极端输入下会不会出问题的开发者。我会把英文和中文两条线的规则拆开讲,重点放在那些文档里不写、只有实际跑过测试用例才会暴露的细节上,包括英式与美式表达的差异、中文"零"的省略逻辑、大数分组的对齐方式,以及金额场景下大写数字的特殊约定。所有代码示例都是可以直接拿去改改就用的思路,不是伪代码。

1. 先搞清楚英文数字表达的规则边界在哪

很多人写数字转英文,第一反应是建一个 0 到 19 的数组、一个十位数的数组,然后按百位分组递归。这个思路方向没错,但真正决定实现质量的不是递归本身,而是你对英文数字表达规则的边界认知有多清楚。规则没吃透,代码写得再优雅也会在特定输入上翻车。

1.1 连字符与空格:不是所有连接都长一个样

英文数字表达里,21 到 99 之间非整十的数,标准写法是带连字符的,比如 twenty-one、forty-five、ninety-nine。这个连字符是硬性要求,不是可选项。但到了百位以上,连接方式就变了:百位和后面的部分之间用空格加 and(英式)或纯空格(美式),比如 one hundred and twenty-one 或者 one hundred twenty-one。

这里第一个容易踩的坑是:连字符只出现在十位和个位之间,不会出现在百位和十位之间。有人图省事,把所有数字之间都用连字符连起来,结果输出 one-hundred-twenty-one,这在正式文档里是不规范的。正确的做法是在拼接十位和个位时插入连字符,其余层级用空格。

第二个坑是 and 的位置。英式英语里,百位和十位之间、千位和百位之间都可能出现 and,规则是"最后一个非零数字组之前加 and"。美式英语则倾向于省略 and,直接空格连接。这两种风格没有对错,但同一个系统里必须统一,不能一会儿有 and 一会儿没有。我在实际项目里的做法是抽一个配置项useAnd,默认走英式,需要美式时关掉,避免两套逻辑混在一起。

数值英式表达美式表达
121one hundred and twenty-oneone hundred twenty-one
1005one thousand and fiveone thousand five
1234one thousand two hundred and thirty-fourone thousand two hundred thirty-four

1.2 零的处理:什么时候说 zero,什么时候闭嘴

零在英文数字表达里是最容易出错的部分,因为它有三种完全不同的角色。第一种是数字本身就是 0,读作 zero。第二种是数字中间出现零,比如 105,读作 one hundred and five,中间的零不单独发音,但 and 要补上。第三种是整组为零,比如 1000,读作 one thousand,后面那组 000 直接跳过。

真正麻烦的是像 1000001 这种,读作 one million and one,中间那一大段零全部省略,只在最后一个非零组前加 and。而 1000100 读作 one million one hundred,注意这里百位组是 100,读作 one hundred,前面百万组和百位组之间没有 and,因为百位组本身以 hundred 结尾,and 的位置规则在这里要重新判断。

我的经验是把"组内表达"和"组间拼接"彻底分开。组内负责把 0 到 999 转成英文,组间负责处理 and 的插入和全零组的跳过。组内遇到 0 直接返回空字符串,组间判断当前组是否全零来决定要不要加 and。这样拆开之后,零的处理逻辑就收敛到两个地方,测试用例也好写。

1.3 大数分组:从 thousand 到 decillion 的命名体系

英文大数按三位一组分组,每组后面跟一个量级词:thousand、million、billion、trillion、quadrillion、quintillion、sextillion、septillion、octillion、nonillion、decillion。日常业务里用到 trillion 已经算极端了,但既然要写通用函数,量级表最好一次列全,避免以后扩展时改结构。

分组的方向是从低位往高位切,每三位一组。比如 1234567890 切成 1 | 234 | 567 | 890,对应 billion、million、thousand、个位。这里有个细节:切完之后高位组可能不足三位,比如最前面的 1,这没关系,组内转换函数本来就支持 1 到 3 位。但如果你用字符串切片的方式分组,要注意补零对齐,否则 1000000 可能被切成 1 | 000 | 000 之外的形式,导致量级词错位。

我见过一种实现是把数字转成字符串后从右往左每三位插一个分隔符,再按分隔符切分。这种做法直观但容易在负数和小数上出问题,因为负号和小数点也会被当成字符处理。更稳的方式是用数学运算:不断对 1000 取模得到当前组,整除 1000 推进到下一组,同时维护一个量级索引。这样负号和小数点在进入分组逻辑之前就已经被剥离,不会污染分组过程。

2. 中文数字表达的坑集中在"零"和"单位"上

中文数字转换的难点和英文完全不在一个维度。英文的难点在连接词和量级词,中文的难点在零的省略规则和单位的选择。中文里"一百零五"和"一百五"是两个不同的数,前者是 105,后者在口语里常指 150,这种歧义在正式表达里必须消除。写转换函数时,规则要严格按书面语来,不能跟着口语习惯走。

2.1 零的省略:连续零只读一个,末尾零不读

中文数字的零规则可以总结成三条:数字中间的连续零只读一个"零";末尾的零不读;如果某一位是零但后面还有非零数字,这个零要读出来。举几个例子就清楚了:105 读"一百零五",1005 读"一千零五",1050 读"一千零五十",1500 读"一千五百",10005 读"一万零五"。

这里最容易被忽略的是"万"和"亿"这种大单位前后的零。比如 100000005 读"一亿零五",中间的七个零压缩成一个"零"。而 100050000 读"一亿零五万",注意"五万"前面有零,"万"后面没有零。这种跨单位的零处理,如果只按位扫描很容易漏掉,我的做法是先按四位分组(中文是四位一级:个、万、亿、万亿),组内处理零,组间再判断是否需要补零。

组间补零的判断条件是:当前组的值小于 1000 且不是最高组,同时前一组存在且不为零。这个条件听起来绕,但用几个用例验证一下就清楚了。比如 100050000 分成 1 | 0005 | 0000,最高组是 1(亿级),第二组是 5(万级),第三组是 0。第二组值 5 小于 1000,前一组 1 不为零,所以要补零,读作"一亿零五万"。第三组全零直接跳过。

2.2 单位层级:个十百千与万亿的嵌套关系

中文数字的单位是两级嵌套的:个、十、百、千是一级,万、亿、万亿是另一级。每一级内部按四位处理,级与级之间用万、亿连接。这个结构和英文的三位分组完全不同,不能套用同一套分组逻辑。

具体来说,一个数字先按四位切成若干组,从低到高依次对应"个级""万级""亿级""万亿级"。每组内部用"千百十"表达,组后面跟对应的级单位。比如 123456789 切成 1 | 2345 | 6789,读作"一亿二千三百四十五万六千七百八十九"。注意"二千三百四十五万"这里,"万"是加在整个组后面的,不是加在每一位后面。

单位表要覆盖到万亿级,再往上就是"兆"之类的更大单位,日常业务基本用不到。但如果你的场景涉及超大数,单位表要提前规划好,避免出现"一万亿亿"这种不规范的表达。我在实现里把单位表定义成['', '万', '亿', '万亿'],索引对应分组位置,超出范围就抛异常或者降级处理,不让它静默输出错误结果。

2.3 口语与书面语的取舍:一百五到底是 105 还是 150

这个问题在实际项目里必须明确表态。书面语规范里,"一百五"不是标准表达,标准写法是"一百零五"(105)或"一百五十"(150)。转换函数的输出应该严格遵循书面语,所有零都按规则补全,所有单位都写完整。这样输出的结果不会有歧义,适合合同、发票、报表这类正式场景。

如果产品需求里明确要口语化表达,那要单独做一套规则,并且要在文档里写清楚"一百五"代表 150 而不是 105。我的建议是转换函数只做书面语,口语化需求在上层做二次处理,不要把两套规则揉进一个函数里,否则测试用例会爆炸,维护成本也会失控。

3. 金额场景下的大写转换是另一套规则

普通数字转中文和金额大写转中文,规则差异比很多人想象的大。金额大写有一套独立的用字规范:壹贰叁肆伍陆柒捌玖拾佰仟万亿,零写作"零",角分用"角""分",整金额加"整"字。这套规则不是随便定的,是为了防篡改,所以每个字的写法都有讲究。

3.1 大写数字用字表与常见错用

大写数字的标准用字是:零、壹、贰、叁、肆、伍、陆、柒、捌、玖、拾、佰、仟、万、亿。注意"贰"不能写成"貳"或"弍","叁"不能写成"參","佰"不能写成"百"。这些细节在打印发票时如果写错,是要返工的。

我见过最常见的错用是把"元"写成"圆"。规范用法里,金额单位是"元","圆"是异体字,正式票据上应该用"元"。另一个常见错误是"整"和"正"混用,规范写法是"整",比如"壹佰元整"。还有角分位的处理:如果金额有角无分,写作"壹佰元伍角整";如果有分,写作"壹佰元伍角陆分",末尾不加"整"。

错误写法正确写法说明
壹佰圆整壹佰元整单位用"元"不用"圆"
壹佰元正壹佰元整结尾用"整"不用"正"
壹佰元伍角壹佰元伍角整有角无分要加"整"
零元整零元整零金额也要完整表达

3.2 角分位的进位与"整"字的添加条件

金额转换里,角和分是两位小数,处理时要先把金额乘以 100 转成整数分,再拆成元、角、分三段。这样做的原因是浮点数运算会有精度问题,0.1 + 0.2 不等于 0.3 这种经典问题在金额场景里是致命的。用整数分做中间表示,可以彻底避开浮点误差。

"整"字的添加条件是:角为零且分为零。也就是说,金额是整数元时加"整",有角或分时不加。但有个例外:如果角为零但分不为零,比如 100.05,读作"壹佰元零伍分",这里"零"要补上,表示角位是空的。这个零的位置很容易漏,测试用例里一定要覆盖。

3.3 负数金额与零金额的表达约定

负数金额在大写表达里通常写成"负"字开头,比如 -100 写作"负壹佰元整"。但有些业务场景不允许负数金额出现,这时候转换函数应该抛异常而不是静默输出,让调用方明确处理。零金额写作"零元整",不能省略成空字符串,因为票据上留空会被认为是不规范。

我在实现里对输入做了三层校验:类型必须是数字或数字字符串,范围在业务允许的区间内,小数位不超过两位。超出范围的输入直接抛错,不做静默截断。这个策略在财务系统里很重要,静默截断会导致金额对不上,排查起来非常痛苦。

4. 把两条线写成可测试的代码结构

规则讲清楚了,落到代码上要考虑的是可测试性和可扩展性。我的做法是把英文转换和中文转换拆成两个独立模块,各自暴露一个主函数,内部再拆成"分组""组内转换""组间拼接"三层。这样每一层都可以单独写单元测试,出问题时能快速定位是哪一层的问题。

4.1 英文转换的分层实现思路

英文转换的主函数接收一个整数,返回字符串。内部先处理负号和零,再按三位分组,然后对每组调用组内转换,最后按量级词拼接。组内转换函数只处理 0 到 999,逻辑是:百位非零则输出"X hundred",十位和个位合并处理,十位为 1 时走 teens 表,否则十位词加连字符加个位词。

ONES = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'] TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'] SCALES = ['', 'thousand', 'million', 'billion', 'trillion'] def chunk_to_words(n): if n == 0: return '' parts = [] if n >= 100: parts.append(ONES[n // 100] + ' hundred') n %= 100 if n >= 20: word = TENS[n // 10] if n % 10: word += '-' + ONES[n % 10] parts.append(word) elif n > 0: parts.append(ONES[n]) return ' '.join(parts)

这个组内函数的关键点是:百位处理完之后把 n 对 100 取模,剩下的部分走十位和个位逻辑。十位大于等于 20 时拼连字符,小于 20 时直接查 ONES 表。这样 0 到 999 的所有情况都覆盖到了,而且没有分支遗漏。

组间拼接的逻辑是:从低位往高位遍历分组,每组转成英文后加上对应的量级词,跳过全零组。and 的插入位置是最后一个非零组之前,如果启用了英式模式。这里要注意,and 只加一次,不是每组都加。

4.2 中文转换的递归与迭代选择

中文转换我倾向于用迭代而不是递归,原因是中文的分组是固定的四位一级,层级有限,迭代写起来更直观,也不会有递归深度的问题。主函数先按四位分组,然后从高到低遍历,每组内部调用组内转换,组间根据前一组是否为零决定要不要补"零"。

组内转换处理 0 到 9999,逻辑是:千位非零输出"X千",百位非零输出"X百",十位非零输出"X十",个位非零输出"X"。中间遇到零要判断后面是否还有非零位,有则补一个"零",没有则跳过。这个"后面是否还有非零位"的判断,可以用一个标志位在遍历过程中维护,不需要每次都往后扫描。

DIGITS = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'] UNITS = ['', '十', '百', '千'] BIG_UNITS = ['', '万', '亿', '万亿'] def chunk_to_chinese(n): if n == 0: return '' result = [] zero_flag = False for i in range(3, -1, -1): digit = (n // (10 ** i)) % 10 if digit == 0: if result and i > 0: zero_flag = True else: if zero_flag: result.append('零') zero_flag = False result.append(DIGITS[digit] + UNITS[i]) return ''.join(result)

这段代码里 zero_flag 的作用是标记"前面出现过零且后面还有非零位",只有满足这个条件才补"零"。注意i > 0这个判断,它保证个位是零时不补零,因为末尾零不读。这个细节如果漏掉,1050 会被读成"一千零五十零",明显错误。

4.3 测试用例该怎么设计才不漏

数字转换的测试用例设计有个技巧:不要只测正常数,要按"规则维度"来设计。我通常分五组用例:基础数(0 到 20)、整十整百整千、含零数、跨单位数、边界数(最大值、负数、小数)。每组里挑几个有代表性的,覆盖所有分支。

用例类型英文示例中文示例验证点
基础数0, 7, 13, 19零, 七, 十三, 十九个位数和 teens 表
整十整百20, 100, 1000二十, 一百, 一千单位词拼接
含零数105, 1005, 1050一百零五, 一千零五, 一千零五十零的省略规则
跨单位1000001, 1000100一百万零一, 一百万零一百组间补零和 and
边界-5, 0.5, 999999999负五, 零点五, 九亿...负号、小数、大数

跑完这五组用例,基本能覆盖 90% 以上的分支。剩下的 10% 是组合情况,比如负数带小数、超大数带零,这些可以在集成测试里补。我的习惯是每修一个 bug 就往用例表里加一条,时间长了这张表就是最好的回归测试集。

5. 实际项目里踩过的几个真实坑

规则和代码都讲完了,最后分享几个我在实际项目里踩过的坑,这些是文档里不会写、只有真正上线跑过才会遇到的。

5.1 浮点数精度导致的金额错位

最早做金额转换时,我直接用浮点数做运算,结果 0.29 转成大写变成了"贰角玖分"没问题,但 1.15 转出来是"壹元壹角伍分",看起来对,可 2.675 这种三位小数在乘以 100 之后变成了 267.49999,取整后变成 267,少了一分。这个问题在测试环境没暴露,因为测试用的都是整数金额,上线后遇到带小数的订单才炸出来。

修复方案是:输入进来先转成字符串,用字符串解析出整数部分和小数部分,小数部分补齐到两位,再拼成整数分。整个过程不碰浮点运算。这个改动看起来笨,但彻底解决了精度问题,而且字符串解析的性能在金额场景里完全够用。

5.2 英文 and 的位置在跨组时判断错误

英式表达里 and 的位置规则是"最后一个非零组之前",但我在实现时错误地理解成"每个非零组之前都加 and",结果 1234 输出成了 one thousand and two hundred and thirty-four,多了一个 and。正确的输出应该是 one thousand two hundred and thirty-four,and 只在百位组和十位组之间出现一次。

这个 bug 的根因是我把"组间 and"和"组内 and"混在一起处理了。修复方式是把 and 的插入逻辑全部收拢到组间拼接层,组内转换函数不负责 and,只负责 hundred 和连字符。这样职责清晰,and 只会在正确的位置出现一次。

5.3 中文"万"级为零时的补零遗漏

中文转换里有个隐蔽的坑:当万级为零但亿级和个级都非零时,比如 100000001,读作"一亿零一",中间的万级全零要压缩成一个"零"。我最初的实现是按组遍历,万级全零直接跳过,结果输出成了"一亿一",少了一个零,意思完全变了。

修复的关键是在组间拼接时维护一个"是否需要补零"的标志。具体规则是:如果当前组全零,标记需要补零;如果当前组非零且标志为真,先补"零"再输出当前组。这个标志在遇到非零组后重置。用这个逻辑跑 100000001,亿级输出"一亿",万级全零置标志,个级非零补"零"再输出"一",得到"一亿零一",正确。

5.4 大数超出量级表范围时的静默失败

英文量级表我最初只写到 trillion,结果有个测试用例传了 10 的 15 次方,超出了 trillion 的范围,代码没有做边界检查,直接输出了空字符串或者错位的量级词。这种静默失败最危险,因为调用方拿到结果不会报错,错误数据就流到下游了。

修复方案是在分组之前先判断数字的位数,超出量级表覆盖范围就抛异常,异常信息里带上输入值和最大支持值。这样调用方能明确知道是输入超限,而不是拿到一个看起来正常但实际错误的结果。中文侧同理,单位表写到万亿,超出就抛错。

6. 几个可以直接抄的工程化建议

最后给几条工程化层面的建议,都是我在多个项目里验证过有效的做法。

第一,把转换函数做成纯函数,不依赖任何外部状态,输入输出都是基本类型。这样单元测试好写,也方便在不同模块间复用。如果确实需要配置(比如英式美式切换),通过参数传入,不要用全局变量。

第二,英文和中文的转换逻辑不要试图抽象成一个通用框架。我试过,失败了。两种语言的规则差异太大,强行抽象出来的框架比直接写两套代码还难维护。正确的做法是各自独立实现,共享的只有输入校验和异常定义。

第三,金额大写转换要单独做一个模块,不要和普通数字转换混在一起。金额场景有额外的校验(小数位、范围、负数策略),混在一起会让普通转换函数变得臃肿,也会让金额逻辑的测试变得复杂。

第四,测试用例用表驱动的方式组织,每条用例包含输入、期望的英文输出、期望的中文输出、期望的大写输出。这样加用例只需要加一行数据,不用改测试代码。我现在的项目里这张表已经有两百多条,每次改转换逻辑跑一遍,心里就有底了。

第五,如果转换结果要用于展示,考虑加一个缓存层。数字转换是纯计算,同样的输入结果永远一样,缓存命中率会很高。特别是报表场景,同一批金额可能被反复转换,缓存能省不少 CPU。缓存 key 用"语言+模式+输入值"拼接,避免不同模式的结果串味。

这套东西写下来,核心其实就一句话:数字转文字看着简单,但每种语言都有一套自己的表达规范,规范里的每个细节都是坑。把规则拆清楚、把分层做干净、把测试用例按规则维度设计好,剩下的就是体力活了。我在最近一个项目里用这套结构重写了转换模块,代码量比之前少了三分之一,bug 率也降下来了,维护起来轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询