金额转化中文算法

最近要项目中用到了把数字类型的金额(1029.89元)转换成中文书写的方式(一仟零贰拾玖点八九元),参考了一些其他人写的算法,总觉得有些不太完善或者不严谨,例如10100转换成“十万一千元”,还是“十万零一千元”。我看到的一些算法都是转换成了前者,甚至iOS开发中支持中文转换的Api也是转换成了前者,但当我请教公司的财务同学时,给出的答案应该是后者。
所以,把自己写的转换过程分享出来,可能写的不是特别漂亮,欢迎大家指导。

抽象

第一步:抽象

面向对象编程中最重要的思想就是抽象,抽象成代码表示。

10100 —> 十万零一千元

中文数学计数采用的方式是
1、数字+权位。如100中 1 佰,1为数字位,佰为权位。权位包括:个、拾、佰、仟。
2、4位为一节,每一节有节权位,如:万、亿、兆。

所以根据以上特性,将每一位10100中的每一位抽象成中文的(数字+权位)。节权位没有数字,只有权位。


 /**
     * 中文数学计数
     * 数字+权位
     * 100中 1 佰,1为数字位,佰为权位
     * 权位包括 个 拾 佰 仟
     * 4位为一节,每一节有节权位如 万 亿 兆
     */
    private class ChinaMath {
        private String num;
        private String weight;

        public ChinaMath(String num, String weight) {
            this.num = num;
            this.weight = weight;
        }

        public String getNum() {
            return num;
        }

        public String getWeight() {
            return weight;
        }

        /**
         * 是否是节权位
         *
         * @return 当num为空时,返回true,否则返回false
         */
        public boolean isSectionWeight() {
            return (num == null || "".equalsIgnoreCase(num.trim()));
        }

        @Override
        public String toString() {
            return "ChinaMath{" +
                    "num='" + num + '\'' +
                    ", weight='" + weight + '\'' +
                    '}';
        }
    }

转换

第二步:转换

先遍历整个金额,简单的转换为中文表示后的列表ChinaMath[]。
如:11001 —> 壹 万 壹仟 零佰 零拾 壹

String integerPart = "10001";
List<ChinaMath> intPart = new ArrayList<>();
        //从低位到高位遍历
        for (int i = len - 1; i >= 0; i--) {
            //数字的地位
            int lowerIndex = len - i - 1;
            //字符串的高位
            char lowerChar = integerPart.charAt(i);

            int remainderDivide4 = lowerIndex % 4;

            if (remainderDivide4 == 0) {
                intPart.add(0, new ChinaMath("", unit[lowerIndex / 4]));
            }
            intPart.add(0, new ChinaMath(transferSingleNum(Character.getNumericValue(lowerChar)), places[remainderDivide4]));
        }

过滤+转换

第三步:过滤+转换

这一步是最麻烦的,我们要以第二步的结果为基础,再根据中文的金额读写特点进行加工。
例如:

  1. 金额中间有多个0,只读一个零
  2. 金额末尾有多个0,都不读
  3. 10要读拾,不能读成壹拾
  4. 文章开头提到,节权位前的零不能省

List<String> result = new ArrayList<>();
int chinaMatchLen = intPart.size();
        for (int i = 0; i < chinaMatchLen; i++) {
            ChinaMath chinaMath = intPart.get(i);
            String num = chinaMath.getNum();
            String weight = chinaMath.getWeight();
            if (isNotNull(num)) {
                //如果数字是0,则判断下一个数字或者是否是0 或者是否是节权位
                //如果不是,则现在要添加一个0,否则不添加
                if (num.equalsIgnoreCase(CHINEASE_DIGIT[0])) {
                    String nextNumOrSectionWeight = intPart.get(i + 1).getNum();
                    if (!CHINEASE_DIGIT[0].equalsIgnoreCase(nextNumOrSectionWeight)) {
                        integerList.add(CHINEASE_DIGIT[0]);
                    }
                } else {
                    //添加数字和权位
                    if (num.equalsIgnoreCase(CHINEASE_DIGIT[1]) && weight.equalsIgnoreCase(places[1]) && i == 0) {
                        //如果十位是1,则读成拾,而不是壹拾
                        if (isNotNull(weight)) {
                            integerList.add(weight);
                        }
                    } else {
                        integerList.add(num);
                        if (isNotNull(weight)) {
                            integerList.add(weight);
                        }
                    }
                }
            }

            //节权位并且不为空,因为个位也是节权位,但没有实际值
            boolean isSectionWight = chinaMath.isSectionWeight();
            if (isSectionWight) {
                String lastNum = intPart.get(i - 1).getNum();
                //如果是正常的节权位
                if (isNotNull(chinaMath.getWeight())) {
                    String nextNum = intPart.get(i + 1).getNum();

                    if (lastNum.equalsIgnoreCase(CHINEASE_DIGIT[0])) {
                        if (!nextNum.equalsIgnoreCase(CHINEASE_DIGIT[0])) {
                            //前一位是0,后一位不是0,则交换节权位和前一位的位置,例0万 -> 万0
                            integerList.set(integerList.size() - 1, chinaMath.getWeight());
                            integerList.add(CHINEASE_DIGIT[0]);
                        } else {
                            //前一位是0,后一位是0,需要把前一位的0去掉
                            integerList.remove(integerList.size() - 1);
                            integerList.add(chinaMath.getWeight());
                        }
                    } else {
                        //前一位不是0, 直接添加节权位
                        integerList.add(chinaMath.getWeight());
                    }
                } else {
                    //最后一个节权位,如果上一位是0,并且不仅有一个0,移除个位多余的0
                    if (lastNum.equalsIgnoreCase(CHINEASE_DIGIT[0]) && integerList.size() > 1) {
                        integerList.remove(integerList.size() - 1);
                    }
                }
            }
        }
        result.addAll(0, integerList);

        result.add(YUAN);

测试


0.12 -> [zero, point, one, two, yuan]
0.02 -> [zero, point, zero, two, yuan]
1005.20 -> [one, thousand, zero, five, point, two, yuan]
1234.30 -> [one, thousand, two, hundred, three, ten, four, point, three, yuan]
120 3023.02 -> [one, hundred, two, ten, wan, zero, three, thousand, zero, two, ten, three, point, zero, two, yuan]
1000 0003.02 -> [one, thousand, wan, zero, three, point, zero, two, yuan]
10.13 -> [ten, point, one, three, yuan]
1000 0703.02 -> [one, thousand, wan, zero, seven, hundred, zero, three, point, zero, two, yuan]
0010.13 -> [ten, point, one, three, yuan]
110.13 -> [one, hundred, one, ten, point, one, three, yuan]
12100010.13 -> [one, thousand, two, hundred, one, ten, wan, zero, one, ten, point, one, three, yuan]

从测试结果可以看出,所有特殊情况的金额都转换正确了。初次之外,在转换之前做了一些严格的参数判断和简单的格式化操作,保证输入的合法性。

附完整源码:


/**
 * 数字转换成大写汉字
 * <p>
 * 最大支持到千万
 * <p>
 * 1234 5678 .90
 * <p>
 * <p>
 * Created by joye on 2017/6/28.
 */

public class DigitTransfer2Chinese {

    /**
     * 小数点
     */
    protected final String DECIMAL_POINT = ".";

    /**
     * 中文大写数字
     */
    public static final String[] CHINEASE_DIGIT = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};


    public static final String[] unit = {"", "wan"};

    public static final String[] places = {"", "ten", "hundred", "thousand"};

    public static final String DIAN = "point";

    public static final String YUAN = "yuan";

    /**
     * 支持转义的小数最大精确度
     */
    private final int MAX_DECIMAL_PRECISION = 2;

    /**
     * 支持转义的最大位数
     */
    private final int MAX_DIGIT_BITS = 8;

    /**
     * 将数字转换为中文大写文字
     *
     * @param digit 小数
     * @return 中文大写
     * @throws IllegalArgumentException 参数异常
     */
    public List<String> transfer(float digit) throws IllegalArgumentException {
        if (digit <= 0) {
            throw new IllegalArgumentException("the param must be greater than zero, but the digit is " + digit);
        }
        return transfer(String.valueOf(digit));
    }

    /**
     * 将字符串类型数字转换为中文大写文字
     *
     * @param digit 字符串类型的数字
     * @return 中文大写
     * @throws IllegalArgumentException 参数异常
     */
    public List<String> transfer(String digit) throws IllegalArgumentException {
        //判断是否为空
        if (digit == null || digit.length() == 0) {
            throw new IllegalArgumentException("param must not be empty, but the digit is " + digit);
        }
        //去除空格
        digit = removeBlank(digit);

        //判断是否包含非法字符(小数点除外)
        if (!isAllNum(digit)) {
            throw new IllegalArgumentException("param must all be number, but the digit is " + digit);
        }
        //判断是否超出最大位数
        if (isOverMaxBits(digit)) {
            throw new IllegalArgumentException("param's max bits is " + MAX_DIGIT_BITS + ", but the digit is " + digit);
        }
        //判断是否超出小数精确度
        if (isOverMaxDecimalPrecision(digit)) {
            throw new IllegalArgumentException("param's max decimal precision is " + MAX_DECIMAL_PRECISION + ", but the digit is " + digit);
        }
        //判断是否以小数点结尾
        if (isDecimalPointEnding(digit)) {
            throw new IllegalArgumentException("param can not end with . , but the digit is " + digit);
        }

        digit = removeInvalidZero(digit);
        return transferInternal(digit);
    }

    private String reverse(String source) {
        int len = source.length();
        if (len == 0 || len == 1) {
            return source;
        }
        char[] chars = source.toCharArray();
        int replaceTime = len / 2;
        for (int i = 0; i < replaceTime; i++) {
            char temp = chars[i];
            chars[i] = chars[len - 1 - i];
            chars[len - 1 - i] = temp;
        }
        return String.valueOf(chars);
    }

    private String removeBlank(String digit) {
        return digit.replace(" ", "");
    }

    //是否以小数点结尾
    private boolean isDecimalPointEnding(String digit) {
        return digit.endsWith(DECIMAL_POINT);
    }

    //是否超出最大小数精确度
    private boolean isOverMaxDecimalPrecision(String digit) {
        if (!digit.contains(DECIMAL_POINT)) {
            return false;
        }
        String decimalPartWithPoint = digit.substring(digit.indexOf(DECIMAL_POINT), digit.length());
        return decimalPartWithPoint.length() > MAX_DECIMAL_PRECISION + 1;
    }

    //是否超出最大位数
    private boolean isOverMaxBits(String digit) {
        String integerPart = digit;
        if (digit.contains(DECIMAL_POINT)) {
            integerPart = digit.substring(0, digit.indexOf(DECIMAL_POINT));
        }
        return integerPart.length() > MAX_DIGIT_BITS;
    }

    //是否全是数字 小数点除外
    private boolean isAllNum(String digit) {
        int len = digit.length();
        for (int i = 0; i < len; i++) {
            char temp = digit.charAt(i);
            if (!String.valueOf(temp).equals(DECIMAL_POINT) && (temp < '0' || temp > '9')) {
                return false;
            }
        }
        return true;
    }

    //去除无效的0
    private String removeInvalidZero(String origin) {
        //去除末尾的0
        if (origin.contains(DECIMAL_POINT)) {
            if (origin.endsWith(".00") || origin.endsWith(".0")) {
                origin = origin.substring(0, origin.indexOf(DECIMAL_POINT));
            }
            while (origin.endsWith("0")) {
                origin = origin.substring(0, origin.length() - 1);
            }
        }

        while ("0".equalsIgnoreCase(String.valueOf(origin.charAt(0))) && (origin.length() >= 2 && !DECIMAL_POINT.equalsIgnoreCase(String.valueOf(origin.charAt(1))))) {
            origin = origin.substring(1, origin.length());
        }

        return origin;
    }

    private List<String> transferInternal(String digit) {
        List<String> result = new ArrayList<>();

        String integerPart = digit;
        if (digit.contains(DECIMAL_POINT)) {
            result = transferDecimal(digit.substring(digit.indexOf(DECIMAL_POINT), digit.length()));
            integerPart = digit.substring(0, digit.indexOf(DECIMAL_POINT));
        }

        int len = integerPart.length();
        List<String> integerList = new ArrayList<>();
        List<ChinaMath> intPart = new ArrayList<>();
        //从低位到高位遍历
        for (int i = len - 1; i >= 0; i--) {
            //数字的地位
            int lowerIndex = len - i - 1;
            //字符串的高位
            char lowerChar = integerPart.charAt(i);

            int remainderDivide4 = lowerIndex % 4;

            if (remainderDivide4 == 0) {
                intPart.add(0, new ChinaMath("", unit[lowerIndex / 4]));
            }
            intPart.add(0, new ChinaMath(transferSingleNum(Character.getNumericValue(lowerChar)), places[remainderDivide4]));
        }

        int chinaMatchLen = intPart.size();
        for (int i = 0; i < chinaMatchLen; i++) {
            ChinaMath chinaMath = intPart.get(i);
            String num = chinaMath.getNum();
            String weight = chinaMath.getWeight();
            if (isNotNull(num)) {
                //如果数字是0,则判断下一个数字或者是否是0 或者是否是节权位
                //如果不是,则现在要添加一个0,否则不添加
                if (num.equalsIgnoreCase(CHINEASE_DIGIT[0])) {
                    String nextNumOrSectionWeight = intPart.get(i + 1).getNum();
                    if (!CHINEASE_DIGIT[0].equalsIgnoreCase(nextNumOrSectionWeight)) {
                        integerList.add(CHINEASE_DIGIT[0]);
                    }
                } else {
                    //添加数字和权位
                    if (num.equalsIgnoreCase(CHINEASE_DIGIT[1]) && weight.equalsIgnoreCase(places[1]) && i == 0) {
                        //如果十位是1,则读成拾,而不是壹拾
                        if (isNotNull(weight)) {
                            integerList.add(weight);
                        }
                    } else {
                        integerList.add(num);
                        if (isNotNull(weight)) {
                            integerList.add(weight);
                        }
                    }
                }
            }

            //节权位并且不为空,因为个位也是节权位,但没有实际值
            boolean isSectionWight = chinaMath.isSectionWeight();
            if (isSectionWight) {
                String lastNum = intPart.get(i - 1).getNum();
                //如果是正常的节权位
                if (isNotNull(chinaMath.getWeight())) {
                    String nextNum = intPart.get(i + 1).getNum();

                    if (lastNum.equalsIgnoreCase(CHINEASE_DIGIT[0])) {
                        if (!nextNum.equalsIgnoreCase(CHINEASE_DIGIT[0])) {
                            //前一位是0,后一位不是0,则交换节权位和前一位的位置,例0万 -> 万0
                            integerList.set(integerList.size() - 1, chinaMath.getWeight());
                            integerList.add(CHINEASE_DIGIT[0]);
                        } else {
                            //前一位是0,后一位是0,需要把前一位的0去掉
                            integerList.remove(integerList.size() - 1);
                            integerList.add(chinaMath.getWeight());
                        }
                    } else {
                        //前一位不是0, 直接添加节权位
                        integerList.add(chinaMath.getWeight());
                    }
                } else {
                    //最后一个节权位,如果上一位是0,并且不仅有一个0,移除个位多余的0
                    if (lastNum.equalsIgnoreCase(CHINEASE_DIGIT[0]) && integerList.size() > 1) {
                        integerList.remove(integerList.size() - 1);
                    }
                }
            }
        }
        result.addAll(0, integerList);

        result.add(YUAN);

        return result;
    }

    /**
     * 中文数学计数
     * 数字+权位
     * 100中 1 佰,1位数字位,佰为权位
     * 权位包括 个 拾 佰 仟
     * 4位为一节,每一节有节权位如 万 亿 兆
     */
    private class ChinaMath {
        private String num;
        private String weight;

        public ChinaMath(String num, String weight) {
            this.num = num;
            this.weight = weight;
        }

        public String getNum() {
            return num;
        }

        public String getWeight() {
            return weight;
        }

        /**
         * 是否是节权位
         *
         * @return 当num为空时,返回true,否则返回false
         */
        public boolean isSectionWeight() {
            return (num == null || "".equalsIgnoreCase(num.trim()));
        }

        @Override
        public String toString() {
            return "ChinaMath{" +
                    "num='" + num + '\'' +
                    ", weight='" + weight + '\'' +
                    '}';
        }
    }

    private boolean isNotNull(String string) {
        return string != null && !"".equalsIgnoreCase(string);
    }

    /**
     * 转换小数
     *
     * @param decimalWithPoint 小数部分(带小数点)
     * @return 小数部分的转换结果
     */
    private List<String> transferDecimal(String decimalWithPoint) {
        List<String> result = new ArrayList<>(3);
        if (decimalWithPoint.startsWith(DECIMAL_POINT)) {
            result.add(DIAN);
        }
        String decimalWithoutPoint = decimalWithPoint.replace(DECIMAL_POINT, "");
        int len = decimalWithoutPoint.length();
        for (int i = 0; i < len; i++) {
            int num = Character.getNumericValue(decimalWithoutPoint.charAt(i));
            result.add(transferSingleNum(num));
        }
        return result;
    }

    //转换单个数字
    private String transferSingleNum(int num) {
        return CHINEASE_DIGIT[num];
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值