最近要项目中用到了把数字类型的金额(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]));
}
过滤+转换
第三步:过滤+转换
这一步是最麻烦的,我们要以第二步的结果为基础,再根据中文的金额读写特点进行加工。
例如:
- 金额中间有多个0,只读一个零
- 金额末尾有多个0,都不读
- 10要读拾,不能读成壹拾
- 文章开头提到,节权位前的零不能省
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];
}
}