初步想法

下面是一些将阿拉伯数字转为中文的例子:

  • 0 = 零
  • 10 = 十
  • 1000 = 一千
  • 9527 = 九千五百二十七
  • 10051 = 一万零五十一
  • 111111111 = 一亿一千一百一十一万一千一百一十一

一个最直观的想法就是按照从最低位到最高位的顺序,依次将数字映射为相应的中文字符并加上单位,比如:

  • 0 = 零
  • 10 = 一十零
  • 1000 = 一千零百零十零
  • 10051 = 一万零千零百五十一
  • 110010 = 一十一万零千零百一十零
  • 210010 = 二十一万零千零百一十零

但是这样的转换方法对于“零”的处理并不理想,我们中文的习惯一般是将连续出现的多个“零”合为一个,并去除末尾的“零”。因此我们需要对“零”进行单独处理。

此外,对于数字 110010 最高位的 11 而言,我们一般是读成“十一万”而不是“一十一万”,所以还应对位于开头的 10 ~ 19 之间的数字进行处理。

观察上面的例子也很容易注意到,按照这种朴素的处理方法,添加“万”、“亿”等特殊数位的时机也是一个棘手的问题。

通过上述分析可以看到,阿拉伯数字转中文并没有一个统一的规则,且因为中文的阅读习惯比较特殊,还应进行许多特殊处理。

算法设计与实现

以数字 123456789 为例,我们可以将数字从右到左按照每 4 位为一段进行单独处理,之后对每段处理后的结果进行拼接:

  1. 将 123456789 分为 1、2345、6789 三段,每段单独处理;
  2. 6789 = 六 九,2345 = 二 五,1 = 一;
  3. 对结果进行拼接,注意拼接的时候带上每段相应的数位,比如“万”和“亿”,拼接后为:一 亿 二千三百四十五 六千七百八十九。

按照这样的规则处理就可以解决“万”、“亿”等特殊单位的添加问题。

下面我们探讨对“零”的处理。根据上面的分析,对于“零”的处理大致有如下几种规则:

  1. 零不能加数位,比如“零百”、“零千”;

  2. 在每段处理结果中间部分出现的连续的零要去重;

  3. 每段末尾的“零”也要去除。

以 10000010 为例

  • 10000010 = 一千百一十
  • 使用第一个规则:一千零零零零零一十
  • 使用第二个规则:一千一十
  • 使用第三个规则:一千 万 零一十

这样就完成了对“零”的处理。

最后还需要对 110010 例子中提到的情况进行处理。处理的方法非常简单,检查结果是否以“一十”开头即可,若是这种情况则除去开头的“一”,比如“一十一”转为“十一”,“一十八”转为“十八”。

根据上述算法设计,编写相应的转换算法及其测试:

public class NumberToChinese {

    public static void main(String[] args) {
        test();
    }

    public static void test() {
        int[] numbers = new int[]{0, 1, 10, 18, 114, 343, 10000, 15000, 201058, 10018, 11018, 1100118, 1902020117};
        for (int number : numbers) {
            System.out.println(numberToChinese(number));
        }
    }

    public static String numberToChinese(int number) {
        String[] UNITS = {null, "十", "百", "千"};
        String[] TOKENS = {null, "万", "亿"};
        String[] NUMBERS = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"};

        String str = String.valueOf(number);
        String res = "";
        // 倒着将数字按照每 4 个为一段切片处理
        for (int right = str.length(), count = 0; right >= 0; right -= 4, count++) {
            int left = Math.max(0, right - 4);
            String temp = "";
            for (int j = right - 1, k = 0; j >= left; j--, k++) {
                int index = Integer.parseInt(String.valueOf(str.charAt(j)));
                // 连续的零去重
                if (index == 0 && temp.length() >= 1 && String.valueOf(temp.charAt(0)).equals("零")) {
                    continue;
                }
                if (UNITS[k] != null && index != 0) {
                    temp = UNITS[k] + temp;
                }
                temp = NUMBERS[index] + temp;
            }
            // 去除每段结果末尾的零
            if (temp.length() > 0 && String.valueOf(temp.charAt(temp.length() - 1)).equals("零")) {
                temp = temp.substring(0, temp.length() - 1);
            }
            if (TOKENS[count] != null && temp.length() > 0) {
                temp = temp + TOKENS[count];
            }
            res = temp + res;
        }
        // 处理结果开头的“一十”
        if (res.startsWith("一十")) {
            res = res.substring(1);
        }
        // 特例,如果输入的是 0,上述步骤得到的结果会为空字符串
        if (res.length() == 0) {
            res = "零";
        }
        return res;
    }
}

测试输出结果如下:

十八
一百一十四
三百四十三
一万
一万五千
二十万一千零五十八
一万零一十八
一万一千零一十八
一百一十万零一百一十八
十九亿零二百零二万零一百一十七

进阶

我们目前所实现的转换算法仅适用于整数,对于浮点数而言,整数部分按照我们实现的转换算法进行转换即可,小数部分则只需将数字映射为对应的中文字符,不需要做任何特殊处理。

算法实现如下:

public static String numberToChinese(BigDecimal number) {
    String[] NUMBERS = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"};
    String[] str = number.toPlainString().split("\\.");
    String res = "";
    String cur = str[1];
    for (int i = 0; i < str[1].length(); i++) {
        int index = Integer.parseInt(String.valueOf(str[1].charAt(i)));
        res += NUMBERS[index];
    }
    // 整数部分处理结果 + 点 + 小数部分处理结果
    return numberToChinese(Integer.parseInt(str[0])) + "点" + res;
}

这里有几点需要注意:

  1. 利用 split() 拆分整数和小数部分时,小数点 . 要进行转义处理;
  2. 数位较多(较长)的 double 转为字符串后会以科学计数法的形式表示,比如:123456789.123456789 转为字符串后就变成了 1.2345678912345679E8。如果想要得到非科学计数法表示的浮点字符,需要用到 BigDecimal 类提供的 toPlainString() 方法;
  3. 浮点数的精度存在损失,比如 123456789.123456789101112 在拆分后,小数部分并不一定会是 123456789101112。想要实现完美的转换就需要将小数部分和整数部分拆开,分别以整数形式存储,使用的时候再进行单独处理。

将浮点数的小数部分和整数部分拆分存储,不仅可以避免精度损失问题,还可以扩大数字的表示范围。

整数部分和小数部分单独存储后的转换算法如下:

public static String numberToChinese(int integer, int decimal) {
    String[] NUMBERS = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"};
    String res = "";
    String cur = String.valueOf(decimal);
    for (int i = 0; i < cur.length(); i++) {
    int index = Integer.parseInt(String.valueOf(cur.charAt(i)));
    res += NUMBERS[index];
    }
    // 整数部分处理结果 + 点 + 小数部分处理结果
    return numberToChinese(integer) + "点" + res;
}

这里又出现了一个问题,int 存储的范围是有限的,既然已经将整数部分和小数部分单独存储了,不妨再进一步,直接以字符串的形式存储数字,进一步扩大数字的表示范围:

public static String numberToChinese(String integer, String decimal) {
    String[] NUMBERS = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"};
    String res = "";
    for (int i = 0; i < decimal.length(); i++) {
    int index = Integer.parseInt(String.valueOf(decimal.charAt(i)));
    res += NUMBERS[index];
    }
    // 整数部分处理结果 + 点 + 小数部分处理结果
    return numberToChinese(integer) + "点" + res;
}

对于负数情况,添加相应的判断逻辑进行处理即可,这里不再赘述。此外,如果想要转换成零、壹、贰、叁、肆、伍、陆、柒、捌、玖、拾、佰、仟、万、亿的形式,替换代码中对应的中文字符即可,元、角、分等计数单位同理。