1. 需求近期项目中遇到了一个需求,需要将英文字符串转换为阿拉伯数字。在stackoverflow上和csdn上都找了份代码,不过试验之后发现效果不太理想,所以打算自己写个工具类。参考这两份代码,自己也收获了一点灵感,然后就完成了这份这个工具类,且很好地满足了我的需求。
2. 代码实现
package com.frank.test.arithmetic;
import org.apache.commons.lang3.StringUtils;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
//import java.util.Objects;
/**
* 将英文单词解析成数字。
* 核心思想是:将字符串按量级拆分,拆分得到的量级前面的数字值不能超过1000
*
* @author Frank
* @date 2016-12-26
*/
public class EnglishWordsToNumber {
/*
* 小数点
*/
private static final String POINT = "point";
/*
* 数量级--百。用得比较多,所以单拿出来
*/
private static final String HUNDRED_MANITUDE = "hundred";
/*
* 数量级
*/
private static final String[] MAGNITUDES = {"billion", "million", "thousand", "hundred"};
/*
* 个位数
*/
private static final String[] DIGITS = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};
/*
* 十位数
*/
private static final String[] TENS = {"twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"};
/*
* 只能单独标识的数字[10, 19]
*/
private static final String[] TEENS = {"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"};
private static final Map map = new HashMap<>();
/*
* 十个阿拉伯数字
*/
private static final Map arabicNumerals = new HashMap<>();
static {
map.put("zero", 0);
// 添加1~9
for (int i = 0; i < DIGITS.length; i++) {
map.put(DIGITS[i], i + 1);
}
// 添加10~19
for (int i = 0; i < TEENS.length; i++) {
map.put(TEENS[i], i + 10);
}
// 添加TENS
for (int i = 0; i < TENS.length; i++) {
map.put(TENS[i], (i + 2) * 10);
}
for (int i = 0; i < TENS.length; i++) {
for (int j = 0; j < DIGITS.length; j++) {
map.put(TENS[i] + " " + DIGITS[j], (i + 2) * 10 + (j + 1));
map.put(TENS[i] + "-" + DIGITS[j], (i + 2) * 10 + (j + 1));
}
}
// "hundred", "thousand", "million", "billion"
map.put("hundred", 100);
map.put("thousand", 1000);
map.put("million", 1000000);
map.put("billion", 1000000000);
arabicNumerals.put("zero", "0");
for (int i = 0; i < DIGITS.length; i++) {
arabicNumerals.put(DIGITS[i], String.valueOf(i + 1));
}
}
/**
* 将英文单词解析为一个数字
*
* @param input
* @return 如果无法进行合理化解析,就返回原值;否则返回解析后的值
*/
public static String parse(String input) {
if (StringUtils.isBlank(input)) {
return input;
}
// 拆分为整数部分和小数部分
String[] parts = input.toLowerCase().replaceAll(" and ", " ").trim().split(POINT);
// 如果整数部分只有zero
if (parts[0].trim().equals("zero")) {
// 判断小数部分的值
if (parts.length == 1) {// 没有小数部分
return "0";
} else if (parts.length == 2) {
String decimal = parseAfterPoint(parts[1]);
return decimal == null ? input : "0." + decimal;
} else {// point出现了超过一次
return input;
}
}
String decimal = null;
if (parts.length > 2) {// point出现了超过一次
return input;
}
// 小数部分
if (parts.length == 2) {
decimal = parseAfterPoint(parts[1]);
if (decimal == null) {// 如果有小数部分,但是对小数部分解析后返回null,说明小数部分解析失败
return input;
}
}
// 整数部分
String part0 = parts[0].trim();
BigInteger bi = BigInteger.ZERO;
StringBuilder result = new StringBuilder();
for (int i = 0; i < MAGNITUDES.length && part0.length() > 0; i++) {
Map ret = parseEveryMagnitude(part0, MAGNITUDES[i]);
if (ret == null) {
return input;
}
bi = bi.add((BigInteger) ret.get("number"));
part0 = ret.get("part0").toString();
}
// 加上整数部分
result.append(bi.toString());
// 加上小数部分
if (decimal != null) {
result.append('.').append(decimal);
}
return result.toString();
}
/**
* 解析小数部分
*
* @param str
* @return 返回null表示小数部分解析失败,返回非null值表示解析成功
*/
private static String parseAfterPoint(String str) {
if (StringUtils.isBlank(str)) {
return "0";
}
StringBuilder builder = new StringBuilder();
String[] arr = StringUtils.split(str);
for (String s : arr) {
String num = arabicNumerals.get(s);
if (num == null) {// 也就是说,小数点后面只能出现0~9的英文单词
return null;
}
builder.append(num);
}
return builder.toString();
}
/**
* 解析得到每个数量级前面的数字,然后和数量级相乘,得到该数量级的值
*
* @param part0
* @param magnitude 数量级字符串
* @return 如果解析不成功,返回null,否则表示解析成功
*/
private static Map parseEveryMagnitude(String part0, String magnitude) {
Map ret = new HashMap<>(2);
if (magnitude.equals(HUNDRED_MANITUDE)) {// 到了hundred这个数量级
int num = parseHundred(part0);
if (num == -1) {
return null;
}
ret.put("part0", "");
ret.put("number", BigInteger.valueOf(num));
return ret;
}
String[] arr = part0.split(magnitude);
if (arr.length > 2) {// 字符串中包含多个数量级字符串
return null;
}
if (arr.length == 1) {// arr的长度是1,有两种情况:不包含数量级字符串,或者比如one billion
if (part0.contains(magnitude)) {
int num = parseHundred(arr[0].trim());
if (num == -1) {
return null;
}
int magnitudeNum = map.get(magnitude).intValue();
ret.put("part0", "");
ret.put("number", BigInteger.valueOf(num).multiply(BigInteger.valueOf(magnitudeNum)));
return ret;
} else {// 不包含数量级字符串,就将part0原样返回
ret.put("part0", part0);
ret.put("number", BigInteger.ZERO);
return ret;
}
}
if (arr.length == 2) {// 字符串中包含数量级字符串,那么只解析数量级字符串前面的数字内容
int num = parseHundred(arr[0].trim());
if (num == -1) {
return null;
}
int magnitudeNum = map.get(magnitude).intValue();
ret.put("part0", arr[1].trim());
ret.put("number", BigInteger.valueOf(num).multiply(BigInteger.valueOf(magnitudeNum)));
return ret;
}
return null;
}
/**
* 解析每个数量级前面的数字及hundred数量级的数字。根据英语数字表示,该部分的值的范围是[1, 999]。
*
* @param hundred
* @return 如果解析失败,返回-1,否则返回解析后的值
*/
private static int parseHundred(String hundred) {
String[] arr = hundred.split(HUNDRED_MANITUDE);
if (arr.length > 2) {
return -1;
}
if (arr.length == 1) {// arr长度为1,有如下两种情况:one 或者 one hundred
Integer num = map.get(arr[0].trim());
if (hundred.contains(HUNDRED_MANITUDE)) {// one hundred
return (num != null && num.intValue() > 0 && num.intValue() < 10) ? num.intValue() * map.get(HUNDRED_MANITUDE) : -1;
} else {// one
return (num != null && num.intValue() > 0 && num.intValue() < 100) ? num.intValue() : -1;
}
}
if (arr.length == 2) {// 包含hundred,那么hundred前面的值的范围就是[1, 9],hundred后面的值的范围就是[1, 99]
Integer beforeHundred = map.get(arr[0].trim());
Integer afterHundred = map.get(arr[1].trim());
return (beforeHundred != null && beforeHundred.intValue() > 0 && beforeHundred.intValue() < 10 &&
afterHundred != null && afterHundred.intValue() > 0 && afterHundred.intValue() < 100)
? beforeHundred.intValue() * map.get(HUNDRED_MANITUDE).intValue() + afterHundred.intValue()
: -1;
}
return -1;
}
// public static void main(String[] args) {
// String[] words = {"one", "two", "five", "eleven", "twenty five", "fifty", "one hundred and one",
// "one hundred and fifteen", "one hundred and ninety nine", "one hundred ten",
// "one thousand one",
// "one thousand and one", "one thousand and eleven", "one thousand and ninety nine",
// "one thousand ninety nine", "one thousand one hundred and eleven",
// "one thousand nine hundred and ninety nine", "ten thousand and one",
// "ten thousand and twenty three", "ten thousand two hundred and twenty three",
// "twelve thousand two hundred and twenty three", "one hundred thousand and one",
// "two hundred two million fifty three thousand", "zero"};
// int[] expectedValue = {1, 2, 5, 11, 25, 50, 101,
// 115, 199, 110,
// 1001,
// 1001, 1011, 1099,
// 1099, 1111,
// 1999, 10001,
// 10023, 10223,
// 12223, 100001,
// 202053000, 0};
// for (int i = 0; i < words.length; i++) {
// String word = words[i];
// String num = null;
// try {
// num = parse(word);
// } catch (Exception e) {
// e.printStackTrace();
// }
// boolean result = Objects.equals(String.valueOf(expectedValue[i]), num);
// if (result) {
// System.out.println(word + "--->" + num + ", expected value is " + expectedValue[i] + ", result is " + result);
// } else {
// System.err.println(word + "--->" + num + ", expected value is " + expectedValue[i] + ", result is " + result);
// }
// }
// }
}
该代码将字符串 " and " 全替换成空字符了,所以对于给定英文字符串,无论有多少个 " and ",都可以进行解析,这从语义上来说不是太正确。
目前的代码只支持解析到billion,如果想要更大的数量级,那么需要做两步:
第一,将更大的数量级单词添加到MAGNITUDES数组的开头,各个数量级按照数量级从大到小排列;
第二,最好是修改静态属性 map 的 Value 的泛型为 BigInteger。