前端知库

怎样把数字转成中文读法?

把整数从右往左每 4 位分成一组,组内按千、百、十、个转换,组后加万、亿。难点在“零”:组内连续的 0 只写一个零,末尾的 0 不写;高位有内容而这一组不满千时,组前补一个零;整组是 0 时连同它的单位一起省掉。

参考答案

简答

比如把 12345 写成“一万二千三百四十五”,把 100010 写成“十万零一十”。

中文数字是四位一节的:个十百千为一组,组后面再加“万”“亿”。所以先把整数从右往左每 4 位切一组,每组单独按千百十个转换,再拼上组的单位。

真正容易错的是“零”。组内连续几个 0 只写一个零,组末尾的 0 不写;组与组之间,如果前面已经有内容、这一组又不满一千,就要在组前补一个零。整组都是 0 时,这一组连同它的“万”一起省掉,比如 100000005 是“一亿零五”。

还要注意“一十”。整个数以十几开头时口语省略“一”,15 读作“十五”;可“一十”出现在中间时不能省,100010 里就要写“一十”。面试时先和面试官确认支持的范围,比如是否要处理负数和小数。

先把整数分组

中文读数是四位一节,所以 123456789 切成 1、2345、6789 三组,读作“一亿 / 二千三百四十五万 / 六千七百八十九”。每一组内部的转换方法都一样,只是后面接的单位不同。

下面的 toChineseNumber 先分组,再逐组转换。看它怎样处理几个带零的数。

JavaScript
const DIGITS = '零一二三四五六七八九';
const UNITS = ['', '十', '百', '千'];
const GROUP_UNITS = ['', '万', '亿'];

// 把不超过 4 位的数字串转成中文,zero 记录“前面有 0 还没写出来”。
function convertGroup(group, omitOne) {
  let text = '';
  let zero = false;
  const digits = group.padStart(4, '0');
  for (let i = 0; i < 4; i += 1) {
    const d = Number(digits[i]);
    const unit = UNITS[3 - i];
    if (d === 0) {
      if (text) zero = true;
      continue;
    }
    if (zero) text += DIGITS[0];
    zero = false;
    // 只有整个数以“十几”开头时省略“一”。
    const skipOne = omitOne && d === 1 && unit === '十' && text === '';
    text += (skipOne ? '' : DIGITS[d]) + unit;
  }
  return text;
}

function toChineseNumber(n) {
  if (!Number.isSafeInteger(n) || n < 0) throw new RangeError('只处理非负安全整数');
  const intStr = String(n);
  if (n === 0) return DIGITS[0];
  const groups = [];
  for (let end = intStr.length; end > 0; end -= 4) {
    groups.unshift(intStr.slice(Math.max(0, end - 4), end));
  }
  if (groups.length > GROUP_UNITS.length) throw new RangeError('最多 12 位');

  let result = '';
  groups.forEach((group, i) => {
    const value = Number(group);
    if (value === 0) return;
    // 高位已经写过内容,这一组又不满千,中间缺了位,要补一个零。
    if (result && value < 1000) result += DIGITS[0];
    result += convertGroup(group, result === '');
    result += GROUP_UNITS[groups.length - 1 - i];
  });
  return result;
}

console.log(toChineseNumber(12345)); // 一万二千三百四十五
console.log(toChineseNumber(15)); // 十五
console.log(toChineseNumber(100010)); // 十万零一十
console.log(toChineseNumber(100000005)); // 一亿零五
console.log(toChineseNumber(30105000)); // 三千零一十万五千
console.log(toChineseNumber(0)); // 零

15 读作“十五”而不是“一十五”,因为整个数以十几开头时,口语习惯省略“一”;但 100010 里的“一十”在中间,就不能省。代码用 omitOne 区分这两种情况:只有在第一组、而且“十”前面还没写过任何内容时才省略。

零是怎么处理的

“零”的规则分两层,代码里也分两处处理。

第一层在组内,由 convertGroup 负责。逐位扫描时,遇到 0 不马上写,只把 zero 标记成 true;等后面遇到非 0 的数字,才先补一个“零”再写数字。这样连续几个 0 只会补一个零,组末尾的 0 后面没有非 0 数字,零自然就不会写出来。text 为空时遇到的 0 是前导零,也不标记。

第二层在组与组之间,由 toChineseNumber 负责。100010 切成 10 和 0010 两组,第二组单独转换得到“一十”,它在组内是前导零,不会补零。可是放在“十万”后面,中间缺了千位和百位,必须读出“零”。所以规则是:前面已经写过内容,而这一组的值小于 1000,就先补一个零。100000005 切成三组,中间那组 0000 整组是 0,直接跳过,连“万”都不写;最后一组 0005 小于 1000,补零,得到“一亿零五”。

30105000 切成 3010 和 5000。“万”这一组里的 0 在组内补成了“零”;最后一组 5000 不小于 1000,所以“万”和“五千”之间不补零。

为什么只处理安全整数

Number.isSafeInteger 保证传进来的数能被精确表示。超过 Number.MAX_SAFE_INTEGER 的数字转成字符串时,末尾几位可能已经不是原来的值,转换结果也就不可信。代码同时限制最多 12 位,因为分组单位只写到“亿”;需要更大的数时,要先确认“万亿”“兆”这类单位的写法再扩展。浮点数相关的边界见《浮点精度、安全整数和 BigInt 有哪些边界?》。

常见追问

负数和小数怎么处理?

负数先判断符号,用绝对值转换,再在前面加“负”字。小数把整数部分和小数部分拆开,整数部分照常转换,小数部分逐位读数字,中间加“点”,比如 3.05 读作“三点零五”。示例里遇到负数和小数直接抛错,避免悄悄输出错误的结果。

为什么不直接从高位往低位逐位转换?

逐位转换要自己判断每一位后面接“万”还是“亿”,还要跨组判断零,很容易漏情况。按四位分组以后,组内只有千百十个四种单位,组间只需要判断“这一组要不要补零、要不要省掉”,两层规则分开处理,代码更好写也更好测。