货币金额大写转换工具类
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 人民币金额大写转换工具.
* <p>
* 提供阿拉伯数字金额与中文大写金额之间的双向转换,采用万进位系统(每四位一级:万、亿、兆、京、垓)。
* 支持两种运行模式,默认使用标准模式:
* <ul>
* <li><b>标准模式</b>:对齐通用财务规范,金额范围 0.01(分) ~ 999999999999.99(千亿级),仅使用元、角、分、拾、佰、仟、万、亿标准单位</li>
* <li><b>拓展模式</b>:支持拓展大额单位(兆、京、垓),金额范围扩展至仟垓级(1E+24),用于超大额特殊场景</li>
* </ul>
*
* <p>大写书写规则遵循财务规范:
* <ul>
* <li>大写金额数字应用正楷或行书填写:壹、贰、叁、肆、伍、陆、柒、捌、玖、拾、佰、仟、万、亿、元、角、分、零、整(正)</li>
* <li>到"元"为止的,在"元"之后应写"整"(或"正")字;到"角"为止的,在"角"之后可以不写"整"字;有"分"的,"分"后面不写"整"字</li>
* <li>阿拉伯数字中间有"0"时,中文大写要写"零"字;连续有几个"0"时,可以只写一个"零"字</li>
* <li>阿拉伯金额数字角位是"0",而分位不是"0"时,中文大写金额"元"后面应写"零"字</li>
* <li>负数金额在大写金额前加"负"字</li>
* </ul>
*
* <pre>设计说明:
* 1. 本工具仅负责金额数字的合规转换,不包含业务前缀。
* - 由调用方自行拼接:"人民币" + toText(...)
* 2. 反向解析(toAmount)仅支持标准语序:"人民币负XXX",不支持"负人民币XXX"。
* 3. 核心工具类采用无状态纯函数设计,线程安全;上下文透传由上层业务框架自行封装。
* 4. 所有异常采用统一结构化格式:[错误码] 描述文本; 实体: '值1', '值2',便于上层解析与转译。
* </pre>
*/
public class ChineseCurrencyConverter {
// region ========== 内部状态类 ==========
/**
* 倒序扫描上下文状态.
* <p>统一维护正序视角下的下一个字符及其类型,消除散落的状态变量与魔数。
*/
private static class ScanState {
/**
* 正序视角下的下一个字符
*/
private char prevChar = Character.MIN_VALUE;
/**
* 正序视角下的下一个字符的数字类型
*/
private DigitType prevDigitType = DigitType.NONE;
/**
* 正序视角下的下一个字符的下标
*/
private int prevIndex = -1;
/**
* 当前字符处理完毕后,更新上下文状态.
*
* @param ch 当前字符
* @param isDigit 当前字符是否为数字
* @param index 当前字符下标
*/
void track(char ch, boolean isDigit, int index) {
this.prevIndex = index;
this.prevChar = ch;
this.prevDigitType = isDigit ? (
(ch == CHN_ZERO)
? DigitType.ZERO
: DigitType.NON_ZERO
) : DigitType.NONE;
}
boolean isIndexOne() {
return this.prevIndex == 1;
}
boolean isUnInit() {
return Character.MIN_VALUE == this.prevChar && -1 == this.prevIndex;
}
}
/**
* 分段校验状态(词法分析器段内状态).
* <p>用于反向解析过程中,记录当前分段的层级与所属分段单位,
* 保证同一段内拾/佰/仟单位严格递增,遇量级大单位自动重置分段。
*/
private static class SegmentState {
/**
* 分段内层级状态
*/
private int level;
/**
* 所属分段单位(元/万/亿/兆/京/垓),用于异常信息精准定位
*/
private char unit;
/**
* 构造分段状态
*
* @param unit 初始分段单位
*/
SegmentState(char unit) {
this.level = 0;
this.unit = unit;
}
/**
* 重置为新分段:层级归零,更新分段名称
*
* @param newUnit 新分段单位
*/
void reset(char newUnit) {
this.level = 0;
this.unit = newUnit;
}
}
/**
* 跨段连接零校验状态(词法分析器跨段语法状态).
* <p>仅量级单位(元/万/亿/兆/京/垓)更新分段号,系数单位(拾/佰/仟)继承当前分段。
* 分段号通过基准公式通用计算,无硬编码,新增量级单位自动适配。
*/
private static class CrossZeroState {
/**
* 上一个遇到的单位的层级
*/
private int prevUnitLevel;
/**
* 自上一个单位以来是否遇到了非零数字 (壹-玖)
*/
private boolean intervalHasNonZero;
/**
* 当前所属分段号:0=个级,1=万级,2=亿级,3=兆级...
*/
private int currentSegmentIndex;
CrossZeroState() {
this.prevUnitLevel = 0;
this.intervalHasNonZero = false;
this.currentSegmentIndex = 0;
}
/**
* 更新单位状态。
* 仅量级单位更新分段号,系数单位(拾/佰/仟)不修改分段,继承当前分段。
*
* @param level 当前单位层级
*/
void trackUnit(int level) {
this.prevUnitLevel = level;
this.intervalHasNonZero = false;
int yuanLevel = UNIT_LEVEL_MAP.get(UNIT_YUAN);
int wanLevel = UNIT_LEVEL_MAP.get(UNIT_WAN);
// 仅量级单位更新分段号
if (level == yuanLevel) {
this.currentSegmentIndex = 0;
} else if (level >= wanLevel) {
// 通用公式:万级为第1段,每升一级分段号+1
this.currentSegmentIndex = level - wanLevel + 1;
}
// 拾/佰/仟等系数单位:分段号保持不变,不做修改
}
/**
* 更新数字状态:标记当前区间是否出现非零数字
*
* @param ch 当前数字字符
*/
void trackDigit(char ch) {
this.intervalHasNonZero = (ch != CHN_ZERO);
}
}
// endregion
// region ========== 常量定义 ==========
/**
* 数字字符连用状态(用于倒序遍历时追踪前置字符类型).
*/
private enum DigitType {
/**
* 非数字字符(如单位、前缀等)
*/
NONE,
/**
* 零
*/
ZERO,
/**
* 非零数字(壹~玖)
*/
NON_ZERO
}
// region ---------- 错误码常量(全局统一) ----------
/**
* 参数类错误:空值、非法入参
*/
private static final String ERR_PARAM = "ERR_PARAM";
/**
* 格式类错误:非法字符、长度超限、结构无效
*/
private static final String ERR_FORMAT = "ERR_FORMAT";
/**
* 单位类错误:单位逆序、非法邻接、不支持的拓展单位
*/
private static final String ERR_UNIT = "ERR_UNIT";
/**
* 范围类错误:金额超出模式上限
*/
private static final String ERR_RANGE = "ERR_RANGE";
// endregion
// region ---------- 基础字符常量 ----------
// 中文大写数字(char)
private static final char CHN_ZERO = '零';
private static final char CHN_ONE = '壹';
private static final char CHN_TWO = '贰';
private static final char CHN_THREE = '叁';
private static final char CHN_FOUR = '肆';
private static final char CHN_FIVE = '伍';
private static final char CHN_SIX = '陆';
private static final char CHN_SEVEN = '柒';
private static final char CHN_EIGHT = '捌';
private static final char CHN_NINE = '玖';
// 中文大写数字(String形式,仅提取高频使用的零)
private static final String STR_ZERO = "零";
// 金额单位(char,从小到大)
private static final char UNIT_FEN = '分';
private static final char UNIT_JIAO = '角';
private static final char UNIT_YUAN = '元';
private static final char UNIT_SHI = '拾';
private static final char UNIT_BAI = '佰';
private static final char UNIT_QIAN = '仟';
private static final char UNIT_WAN = '万';
private static final char UNIT_YI = '亿';
private static final char UNIT_ZHAO = '兆';
private static final char UNIT_JING = '京';
private static final char UNIT_GAI = '垓';
// 业务特殊字符(char)
private static final char CHN_ZHENG = '整';
private static final char CHN_ZHENG_ALT = '正';
private static final char CHN_NEGATIVE = '负';
// 业务特殊字符(String形式)
private static final String STR_ZHENG = "整";
private static final String STR_ZHENG_ALT = "正";
private static final String STR_NEGATIVE = "负";
private static final String STR_RMB_PREFIX = "人民币";
private static final String STR_ZERO_YUAN = "零元";
private static final String STR_ZERO_YUAN_ZHENG = "零元整";
private static final String STR_ZERO_JIAO_FEN = "零角零分";
private static final String STR_ZERO_FEN = "零分";
// endregion
// region ---------- 正则常量 ----------
/**
* 匹配小数点".",用于 toText 方法中移除小数点符号。
*/
private static final Pattern RE_DOT = Pattern.compile(".", Pattern.LITERAL);
/**
* 匹配以"零元"开头且后接非零数字的情况,用于省略"零元"前缀。
* <p>生效条件:{@code stripLeadingZeroYuan = true}
* <p>例如:零元壹角 → 壹角, 零元零壹分 → 壹分 (省略"零元"前缀)
*/
private static final Pattern RE_LEADING_ZERO_YUAN = Pattern.compile("^零元(零)?(?=[壹贰叁肆伍陆柒捌玖])");
// 零值规范化处理组(执行顺序:先后缀替换 → 再空单位消除 → 最后连续零合并)
/**
* 匹配"零角零分",替换为"整"。
* <p>例如:壹佰元零角零分 → 壹佰元整
*/
private static final Pattern RE_ZERO_JIAO_FEN = Pattern.compile(STR_ZERO_JIAO_FEN, Pattern.LITERAL);
/**
* 匹配"零分",用于删除无意义的"零分"后缀。
* <p>例如:壹元零角零分 → 壹元零角整 (先经 RE_ZERO_JIAO_FEN 处理后再执行此规则)
*/
private static final Pattern RE_ZERO_FEN = Pattern.compile(STR_ZERO_FEN, Pattern.LITERAL);
/**
* 匹配"零"后接系数单位(仟佰拾角) ,替换为单个"零"。
* <p>例如:壹佰零仟元 → 壹佰零元 (消除无数字间隔的空单位)
*/
private static final Pattern RE_ZERO_UNIT = Pattern.compile("零[仟佰拾角]");
/**
* 匹配连续 2 个及以上"零",替换为单个"零"。
* <p>例如:壹仟零零零壹元整 → 壹仟零壹元整 (合并连续零)
*/
private static final Pattern RE_ZERO_CONTINUOUS = Pattern.compile("零{2,}");
// 大单位间隙零处理组(执行顺序:从低位到高位依次消除跨段冗余零)
/**
* 匹配全零段的冗余大单位(零后接万亿兆京垓,且前面非拾佰仟),替换为单个"零"。
* <p>例如:壹亿零万壹仟 → 壹亿零壹仟 (保留跨段连接零,删除冗余万单位)
*/
private static final Pattern RE_REDUNDANT_ZERO_LARGE_UNIT = Pattern.compile("(?<![仟佰拾])零([亿万兆京垓])");
// 单位间零省略处理组(最后执行,处理跨级与元位前零)
/**
* 匹配位于低位系数单位(仟佰拾)与高位量级单位(垓京兆亿万)之间的"零",用于删除。
* <p>例如:壹仟零万 → 壹仟万 (消除跨级进位间的冗余零)
*/
private static final Pattern RE_ZERO_BETWEEN_UNIT = Pattern.compile("(?<=[仟佰拾])零(?=[垓京兆亿万])");
/**
* 匹配位于单位/数字与"元"之间的"零",用于删除。
* <p>例如:壹佰零元 → 壹佰元 (消除元位前无意义的零)
*/
private static final Pattern RE_ZERO_BEFORE_YUAN = Pattern.compile("(?<=[垓京兆亿万仟佰拾壹贰叁肆伍陆柒捌玖])零(?=元)");
// endregion
// region ---------- 数值常量 ----------
// @formatter:off
/** 分位权值:0.01 */
private static final BigDecimal RATE_FEN = new BigDecimal("0.01");
/** 角位权值:0.1 */
private static final BigDecimal RATE_JIAO = new BigDecimal("0.1");
/** 拾位权值:10 */
private static final BigDecimal RATE_SHI = BigDecimal.TEN;
/** 佰位权值:100 */
private static final BigDecimal RATE_BAI = new BigDecimal("100");
/** 仟位权值:1000 */
private static final BigDecimal RATE_QIAN = new BigDecimal("1000");
/** 万位权值:10,000 */
private static final BigDecimal RATE_WAN = new BigDecimal("10000");
/** 亿位权值:100,000,000 */
private static final BigDecimal RATE_YI = new BigDecimal("100000000");
/** 兆位权值:1,000,000,000,000 */
private static final BigDecimal RATE_ZHAO = new BigDecimal("1000000000000");
/** 京位权值:10,000,000,000,000,000 */
private static final BigDecimal RATE_JING = new BigDecimal("10000000000000000");
/** 垓位权值:100,000,000,000,000,000,000 (1E+20) */
private static final BigDecimal RATE_GAI = new BigDecimal("1E+20");
/** 标准模式金额上限(不含),最大合法值 999999999999.99(千亿级,通用财务规范) */
private static final BigDecimal STANDARD_MAX_AMOUNT = new BigDecimal("1000000000000.00");
/** 拓展模式金额上限(不含),最大合法值至仟垓级(拓展单位扩展) */
private static final BigDecimal EXTENDED_MAX_AMOUNT = new BigDecimal("1E+24");
// @formatter:on
// endregion
// region ---------- 字符映射常量 ----------
/**
* 单位链模板(拓展模式完整),用于正向转换拼接,从高位到低位依次排列
*/
private static final String UNIT_CHAIN = "仟佰拾垓仟佰拾京仟佰拾兆仟佰拾亿仟佰拾万仟佰拾元角分";
/**
* 拓展模式单位链总长度
*/
private static final int UNIT_LENGTH = UNIT_CHAIN.length();
/**
* 标准模式单位链长度(亿级及以下,千亿封顶)
*/
private static final int STANDARD_UNIT_LENGTH = 14;
/**
* 中文大写数字字符数组,下标对应数值
*/
private static final char[] DIGITS = {
CHN_ZERO, CHN_ONE, CHN_TWO, CHN_THREE, CHN_FOUR,
CHN_FIVE, CHN_SIX, CHN_SEVEN, CHN_EIGHT, CHN_NINE
};
/**
* 合法单位字符集合(从小到大排列,用于索引映射)
*/
private static final String VALID_UNIT_CHARS = "分角元拾佰仟万亿兆京垓";
/**
* 中文数字字符 → 数值 的映射表
*/
private static final Map<Character, Integer> DIGIT_VALUE_MAP = new HashMap<>(16);
/**
* 单位字符 → 层级值 的映射表,层级越高单位越大
*/
private static final Map<Character, Integer> UNIT_LEVEL_MAP = new HashMap<>(16);
static {
// 初始化数字映射表
for (int i = 0; i < DIGITS.length; i++) {
DIGIT_VALUE_MAP.put(DIGITS[i], i);
}
// 初始化单位层级映射表(下标+1为层级值,从1开始)
for (int i = 0; i < VALID_UNIT_CHARS.length(); i++) {
UNIT_LEVEL_MAP.put(VALID_UNIT_CHARS.charAt(i), i + 1);
}
}
// endregion
// endregion
// region ========== 统一异常生成入口 ==========
/**
* 统一异常信息生成入口(强约束格式).
* <p>所有异常必须通过本方法抛出,保证全局格式一致,便于上层解析与转译。
* 格式规范:[错误码] 描述文本; 实体: '值1', '值2'
*
* @param code 错误码常量
* @param message 人类可读错误描述
* @param entities 关键实体值,自动按格式包裹
*/
private static void throwIllegal(CharSequence code, CharSequence message, CharSequence... entities) {
StringBuilder sb = new StringBuilder(64);
sb.append('[').append(code).append("] ").append(message);
if (entities != null && entities.length > 0) {
sb.append("; 实体: ");
for (int i = 0; i < entities.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append('\'').append(entities[i]).append('\'');
}
}
throw new IllegalArgumentException(sb.toString());
}
// endregion
// region ========== 基础辅助方法 ==========
private static final String concat(char... chars) {
return new String(chars);
}
/**
* Number 安全转换为 BigDecimal,分层处理兼顾精度与性能.
* <p>强约束:拦截 null、NaN、无穷等非法浮点金额.
* <ol>
* <li>BigDecimal/BigInteger:原生构造,无字符串开销</li>
* <li>基础整型+原子整型:longValue 快速构建,性能最优</li>
* <li>Float/Double:toString 构造规避二进制精度丢失,前置校验非法浮点</li>
* <li>其他 Number 实现:兜底 toString 兼容第三方扩展类</li>
* </ol>
*
* @param number 待转换数值
* @return 转换后的 BigDecimal 对象
*/
@SuppressWarnings("unused")
private static BigDecimal toBigDecimal(Number number) {
if (number == null) {
throwIllegal(ERR_PARAM, "金额不能为空");
}
// 1. 高精度原生数值
if (number instanceof BigDecimal) {
return (BigDecimal) number;
}
if (number instanceof BigInteger) {
return new BigDecimal((BigInteger) number);
}
// 2. 整型、原子计数器统一快速转换
if (number instanceof Integer || number instanceof Long
|| number instanceof Short || number instanceof Byte
|| number instanceof AtomicInteger || number instanceof AtomicLong) {
return BigDecimal.valueOf(number.longValue());
}
// 3. 浮点数:校验非法值 + 文本构造防失真
if (number instanceof Float || number instanceof Double) {
double val = number.doubleValue();
if (Double.isNaN(val) || Double.isInfinite(val)) {
throwIllegal(ERR_PARAM, "禁止传入NaN或无穷数值作为金额");
}
return new BigDecimal(number.toString());
}
// 4. 兜底兼容自定义Number实现
return new BigDecimal(number.toString());
}
/**
* 执行单次字面量精确替换(使用 {@link Pattern#LITERAL} 模式)。
*
* @param pattern 预编译的字面量正则模式
* @param src 源字符串
* @param replacement 替换内容
* @return 替换后的字符串
*/
private static String replaceLiteral(Pattern pattern, CharSequence src, String replacement) {
return pattern.matcher(src).replaceAll(Matcher.quoteReplacement(replacement));
}
/**
* 执行全局正则替换。
*
* @param pattern 预编译的正则模式
* @param src 源字符串
* @param replacement 替换内容
* @return 替换后的字符串
*/
private static String replaceRegex(Pattern pattern, CharSequence src, String replacement) {
return pattern.matcher(src).replaceAll(replacement);
}
/**
* 判断是否为拓展模式专属的拓展大额单位(兆、京、垓)。
* <p>标准模式下禁止使用此类单位,仅通用财务规范内的亿级及以下单位为合法。
*
* @param unit 待判断单位字符
* @return true=拓展大额单位,false=标准范围内单位
*/
private static boolean isExtendedUnit(char unit) {
return UNIT_LEVEL_MAP.get(unit) >= UNIT_LEVEL_MAP.get(UNIT_ZHAO);
}
// endregion
// region ========== 格式清洗方法 ==========
/**
* 中文大写金额格式标准化清洗。
* <p><b>执行顺序不可随意调整</b>,各阶段存在前后依赖关系,按以下顺序执行:
* <ol>
* <li>后缀标准化:先处理角分结尾,替换整字、删除冗余零分</li>
* <li>零值规范化:消除空单位零、合并连续零</li>
* <li>跨段零优化:从低到高消除大单位之间的间隙零</li>
* <li>元位收尾:消除元位前冗余零、可选剥离零元前缀</li>
* </ol>
*
* @param chinese 原始构造的中文大写金额字符串
* @param stripLeadingZeroYuan 是否剥离开头的"零元"。
* 仅当"零元"位于字符串起始位置且后接非零数字时生效。
* @return 清洗后的标准化中文大写金额字符串
*/
private static String cleansing(String chinese, boolean stripLeadingZeroYuan) {
// 快速路径: 绝大多数整数金额不含"零",无需清洗,直接返回
if (null == chinese || !chinese.contains(STR_ZERO)) {
return chinese;
}
// 阶段一:角分后缀标准化(必须最先执行,先替换整字)
chinese = replaceLiteral(RE_ZERO_JIAO_FEN, chinese, STR_ZHENG);
chinese = replaceLiteral(RE_ZERO_FEN, chinese, "");
// 阶段二:零值规范化(消除空单位 → 合并连续零)
chinese = replaceRegex(RE_ZERO_UNIT, chinese, STR_ZERO);
chinese = replaceRegex(RE_ZERO_CONTINUOUS, chinese, STR_ZERO);
// 阶段三:跨段间隙零优化(从低位到高位依次处理,顺序不可乱)
// 删除全零段冗余的大单位(如 亿零万 -> 亿零),保留跨段连接零
chinese = replaceRegex(RE_REDUNDANT_ZERO_LARGE_UNIT, chinese, STR_ZERO);
// 删除大单位可能引发新的连续零,需再次合并
chinese = replaceRegex(RE_ZERO_CONTINUOUS, chinese, STR_ZERO);
// 阶段四:元位与前缀收尾(最后执行)
chinese = replaceRegex(RE_ZERO_BETWEEN_UNIT, chinese, "");
chinese = replaceRegex(RE_ZERO_BEFORE_YUAN, chinese, "");
if (stripLeadingZeroYuan) {
chinese = replaceRegex(RE_LEADING_ZERO_YUAN, chinese, "");
}
return chinese;
}
// endregion
// region ========== 词法校验方法 ==========
/**
* 全局层级单位顺序校验(顶层语法规则)。
* <p>校验规则:全串范围内,带层级单位必须严格从高到低出现,禁止重复、禁止逆序。
*
* @param unit 当前遍历到的层级单位
* @param maxSeenUnit 已出现的最大层级单位
*/
private static char checkGlobalUnitOrder(char unit, char maxSeenUnit) {
if (UNIT_SHI == unit || UNIT_BAI == unit || UNIT_QIAN == unit) {
return maxSeenUnit;
}
if (maxSeenUnit != Character.MIN_VALUE) {
final int curtLv = UNIT_LEVEL_MAP.get(unit);
if (curtLv <= UNIT_LEVEL_MAP.get(maxSeenUnit)) {
throwIllegal(ERR_UNIT, "单位语序非法,禁止逆序",
String.valueOf(unit), String.valueOf(maxSeenUnit));
}
}
return unit;
}
/**
* 相邻单位对合法性校验(词法邻接规则)。
* <p>校验规则:仅允许「系数单位+量级单位」「系数单位+元」的合法相邻,其余单位裸连均为非法。
*
* @param curt 当前遍历单位(右侧低位)
* @param left 前序遍历单位(左侧高位)
*/
private static void checkAdjacentUnitPair(char curt, char left) {
if (DIGIT_VALUE_MAP.containsKey(left)) {
return;
}
// 元单位:仅禁止分/角前置
if (UNIT_YUAN == curt) {
if (UNIT_FEN == left || UNIT_JIAO == left) {
throwIllegal(ERR_UNIT, "元左侧禁止出现分/角单位",
String.valueOf(left), String.valueOf(curt));
}
return;
}
// 量级大单位(万及以上):仅允许拾/佰/仟前置
if (UNIT_LEVEL_MAP.get(curt) >= UNIT_LEVEL_MAP.get(UNIT_WAN)) {
if (UNIT_SHI != left && UNIT_BAI != left && UNIT_QIAN != left) {
throwIllegal(ERR_UNIT, "量级单位左侧仅允许拾/佰/仟前置",
String.valueOf(left), String.valueOf(curt));
}
return;
}
// 段内小单位直接相邻:全部非法
throwIllegal(ERR_UNIT, "段内单位禁止直接相邻",
String.valueOf(left), String.valueOf(curt));
}
/**
* 段内单位顺序校验(分段语法规则)。
* <p>校验规则:同一段内单位层级必须严格递增;遇量级大单位(万及以上)自动重置分段状态。
*
* @param unit 当前单位字符
* @param state 分段校验状态对象(会被修改)
*/
private static void checkSegmentUnitOrder(char unit, SegmentState state) {
final int unitLv = UNIT_LEVEL_MAP.get(unit);
// 量级大单位:重置分段,开启下一段
if (unitLv >= UNIT_LEVEL_MAP.get(UNIT_WAN)) {
state.reset(unit);
return;
}
// 段内小单位:严格递增,重复/倒序均非法
if (unitLv <= state.level) {
final char prevUnit = VALID_UNIT_CHARS.charAt(state.level - 1);
throwIllegal(ERR_UNIT, "段内单位语序非法",
String.valueOf(unit), String.valueOf(prevUnit));
}
state.level = unitLv;
}
/**
* 跨段连接零校验(跨段语法规则)。
* <p>校验规则:当单位发生非相邻数位跳跃时,若低位存在非零数字,则必须存在连接"零"字。
* 豁免场景:段内相邻单位、仟位直接跳转下一段量级单位(数位相邻)。
*
* @param unit 当前遍历单位字符
* @param state 跨段零校验状态对象(校验后自动更新)
*/
private static void checkCrossSegmentZero(char unit, CrossZeroState state) {
final int curtLv = UNIT_LEVEL_MAP.get(unit);
final int lastLv = state.prevUnitLevel;
if (lastLv > 0 && state.intervalHasNonZero) {
// 倒序降序(正序升序),且两个单位之间出现了非零数字
if (curtLv < lastLv) {
// 场景:拾/佰/仟 直接修饰 万/亿 等大单位,且中间有数字
// 只有拾位可以直接走向大单位(如 叁拾肆万),因为拾是最低系数,不存在更低位的断层
// 佰和仟走向大单位时,必须补零(如 壹佰零肆万),否则触发拦截
if (curtLv > UNIT_LEVEL_MAP.get(UNIT_SHI)) {
throwIllegal(ERR_FORMAT, "非相邻数位间缺少连接零", String.valueOf(unit));
}
}
// 倒序升序(正序降序),且两个单位之间出现了非零数字
else if (curtLv > lastLv) {
// 仅当之前遇到过单位,且期间出现过非零数字,且层级上升时才执行校验
boolean isExempt = isIntraSegmentAdjacent(curtLv, lastLv)
|| isQianToNextSegment(curtLv, lastLv, state.currentSegmentIndex);
if (!isExempt) {
throwIllegal(ERR_FORMAT, "非相邻数位间缺少连接零", String.valueOf(unit));
}
}
}
// 无论是否校验,都必须更新当前单位层级并重置非零数字标记
state.trackUnit(curtLv);
}
/**
* 判断是否为段内相邻单位(元→拾、拾→佰、佰→仟),数位相邻无需连接零
*/
private static boolean isIntraSegmentAdjacent(int curtLv, int lastLv) {
return curtLv - lastLv == 1 && lastLv < UNIT_LEVEL_MAP.get(UNIT_WAN);
}
/**
* 判断是否为仟位直接跳转下一段量级单位(数位相邻,无需连接零)
* 例如:千万→亿、千亿→兆,均为相邻数位
*/
private static boolean isQianToNextSegment(int curtLv, int lastLv, int currentSegment) {
int qianLevel = UNIT_LEVEL_MAP.get(UNIT_QIAN);
int wanBaseLevel = UNIT_LEVEL_MAP.get(UNIT_WAN);
// 修正公式:当前量级单位层级 = 万级基准层级 + 当前分段号
return lastLv == qianLevel && curtLv == wanBaseLevel + currentSegment;
}
/**
* 校验数字连用合法性,并返回当前数字状态.
* <p>校验规则:
* <ul>
* <li>禁止「零」与「零」直接相邻(如 零零)</li>
* <li>禁止「非零数字」与「非零数字」直接相邻(如 壹贰)</li>
* </ul>
*
* @param ch 当前字符
* @param prev 倒序遍历的前一个字符状态(即正序的后一个字符)
* @return 当前字符的数字状态,供下一轮校验使用
*/
private static void checkDigitAdjacency(char ch, ScanState prev) {
if (prev.isUnInit()) {
return;
}
if (ch != CHN_ZERO) {
if (prev.prevDigitType == DigitType.NON_ZERO
|| prev.prevDigitType == DigitType.ZERO) {
throwIllegal(ERR_UNIT, "非零数字缺少单位修饰",
String.valueOf(ch), String.valueOf(prev.prevChar));
}
return;
}
if (prev.prevDigitType == DigitType.ZERO) {
throwIllegal(ERR_FORMAT, "禁止连续多零结构",
concat(ch, prev.prevChar));
}
}
/**
* 校验数字连用合法性,并返回当前数字状态.
* <p>校验规则:
* <ul>
* <li>禁止「零」与「单位」直接相邻.(如 零万)</li>
* </ul>
*
* @param ch 当前字符
* @param prev 倒序遍历的前一个字符状态(即正序的后一个字符)
* @return 当前字符的数字状态,供下一轮校验使用
*/
private static void checkZeroFollowedByUnit(char ch, ScanState prev) {
if (prev.isUnInit()) {
return;
}
if (ch == CHN_ZERO && prev.prevDigitType == DigitType.NONE) {
if (prev.isIndexOne()) {
// 允许开头的零元
if (prev.prevChar == UNIT_YUAN) {
return;
}
throwIllegal(ERR_FORMAT, "零字开头仅允许零元",
concat(ch, prev.prevChar));
}
throwIllegal(ERR_UNIT, "禁止零后直接接单位",
String.valueOf(ch), String.valueOf(prev.prevChar));
}
}
// endregion
// region ========== 预处理方法 ==========
/**
* 中文大写金额预处理:清洗前缀后缀、执行基础合法性校验。
* <p>校验顺序:空值校验 → 原始长度粗筛 → 前缀剥离 → 后缀剥离 → 核心串非空校验 → 单独负号校验 → 核心长度精校
*
* @param raw 原始输入字符串
* @param extendedMode 是否开启拓展模式,用于长度阈值控制
* @return 剥离人民币前缀、整/正后缀后的核心金额串(保留负号)
*/
private static String preprocess(String raw, boolean extendedMode) {
String input = null;
// 1. 空值与空白校验
if (raw == null || (input = raw.trim()).isEmpty()) {
throwIllegal(ERR_PARAM, "金额字符串不能为空");
}
// 2. 原始输入长度粗筛(前置拦截超长输入,避免后续字符串操作)
// 纯金额最大长度 = 单位数 * 2(每个单位对应一个数字)
int maxPureLength = extendedMode ? UNIT_LENGTH * 2 : STANDARD_UNIT_LENGTH * 2;
// 原始输入最大合法长度 = 人民币前缀(3) + 负号(1) + 纯金额 + 整/正后缀(1)
int maxRawLength = maxPureLength + 3 + 1 + 1;
if (input.length() > maxRawLength) {
String mode = extendedMode ? "拓展模式" : "标准模式";
throwIllegal(ERR_FORMAT, mode + "下输入长度超过最大允许值",
String.valueOf(maxRawLength));
}
// 3. 剥离"人民币"前缀
if (input.startsWith(STR_RMB_PREFIX)) {
input = input.substring(3);
}
// 4. 金额结尾必须为分/角/元整
int len = input.length();
if (len > 0) {
final char lastCh = input.charAt(len - 1);
if (lastCh != UNIT_JIAO && lastCh != UNIT_FEN && lastCh != CHN_ZHENG && lastCh != CHN_ZHENG_ALT) {
throwIllegal(ERR_FORMAT, "金额结尾必须为分/角/整/正", String.valueOf(lastCh));
} else if (lastCh == CHN_ZHENG || lastCh == CHN_ZHENG_ALT) {
if (len > 1) {
final char theSecondToLastCh = input.charAt(len - 2);
if (theSecondToLastCh != UNIT_YUAN && theSecondToLastCh != UNIT_JIAO) {
throwIllegal(ERR_FORMAT, "金额结尾必须为分/角/整/正",
concat(theSecondToLastCh, lastCh));
}
} else {
throwIllegal(ERR_FORMAT, "金额格式非法,缺少有效主体", String.valueOf(lastCh));
}
}
}
// 5. 剥离"整/正"后缀
if (input.endsWith(STR_ZHENG) || input.endsWith(STR_ZHENG_ALT)) {
input = input.substring(0, input.length() - 1);
}
// 6. 核心串非空校验:剥离前后缀后必须保留有效金额内容
if (input.isEmpty()) {
throwIllegal(ERR_FORMAT, "未包含有效金额内容");
}
// 7. 禁止单独负号(无实际金额数字)
if (input.length() == 1 && input.charAt(0) == CHN_NEGATIVE) {
throwIllegal(ERR_FORMAT, "负号后缺少有效金额内容");
}
// 8. 核心串长度精确校验(允许包含1位负号)
int maxCoreLength = maxPureLength + 1;
if (input.length() > maxCoreLength) {
String mode = extendedMode ? "拓展模式" : "标准模式";
throwIllegal(ERR_FORMAT, mode + "下核心金额长度超过最大允许值",
String.valueOf(maxPureLength));
}
return input;
}
// endregion
// region ========== 正向转换:数字转中文大写 ==========
// region ----- 正则构造版 -----
/**
* 金额转中文大写(默认配置:省略零元前缀 + 标准模式)。
* <p>等价于 {@code toTextWithRegex(amount, true, false)},适用于绝大多数常规财务场景。
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
@Deprecated
public static String toTextWithRegex(BigDecimal amount) {
return toTextWithRegex(amount, true, false);
}
/**
* 金额转中文大写(可配置零元前缀 + 标准模式)。
* <p>等价于 {@code toTextWithRegex(amount, stripLeadingZeroYuan, false)}
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @param stripLeadingZeroYuan 是否省略零元前缀。
* {@code true}: 金额小于1元时省略(如 0.11 → 壹角壹分);
* {@code false}: 保留完整结构(如 0.11 → 零元壹角壹分)。
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
@Deprecated
public static String toTextWithRegex(BigDecimal amount, boolean stripLeadingZeroYuan) {
return toTextWithRegex(amount, stripLeadingZeroYuan, false);
}
/**
* 金额转中文大写(全参数版本)。
* <p>万进位系统(垓→京→兆→亿→万→元→角→分),支持负数与大数转换。
* 内部通过 {@link StringBuilder#insert(int, char)} 构造原始串,
* 再经 {@link #cleansing(String, boolean)} 标准化。
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @param stripLeadingZeroYuan 是否省略零元前缀
* @param extendedMode 是否开启拓展模式:
* {@code false}=标准模式(千亿级,通用财务规范);
* {@code true}=拓展模式(仟垓级,拓展大额单位)。
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
@Deprecated
public static String toTextWithRegex(BigDecimal amount, boolean stripLeadingZeroYuan, boolean extendedMode) {
if (amount == null) {
throwIllegal(ERR_PARAM, "金额不能为空");
}
// 零元特殊处理(含负零)
if (BigDecimal.ZERO.compareTo(amount) == 0) {
return STR_ZERO_YUAN_ZHENG;
}
// 符号单独处理,绝对值参与转换
boolean negative = amount.signum() < 0;
BigDecimal absAmount = amount.abs().setScale(2, RoundingMode.HALF_UP);
// 按模式执行范围校验
BigDecimal maxAmount = extendedMode ? EXTENDED_MAX_AMOUNT : STANDARD_MAX_AMOUNT;
if (absAmount.compareTo(maxAmount) >= 0) {
String mode = extendedMode ? "拓展模式" : "标准模式";
throwIllegal(ERR_RANGE, mode + "金额超出上限",
maxAmount.stripTrailingZeros().toPlainString());
}
// 转纯数字字符串(去掉小数点)
final String numStr = replaceLiteral(RE_DOT, absAmount.toPlainString(), "");
int numLen = numStr.length();
if (numLen > UNIT_LENGTH) {
throwIllegal(ERR_RANGE, "金额超出最大支持范围");
}
// 数字+单位拼接(插入法,逻辑稳定准确)
StringBuilder buff = new StringBuilder(UNIT_CHAIN);
for (int i = numLen - 1; i >= 0; i--) {
int digit = numStr.charAt(i) - '0';
buff.insert(UNIT_LENGTH - numLen + i, DIGITS[digit]);
}
String chinese = buff.substring(UNIT_LENGTH - numLen, UNIT_LENGTH + numLen);
chinese = cleansing(chinese, stripLeadingZeroYuan);
// 负数拼接标准前缀
return negative ? STR_NEGATIVE + chinese : chinese;
}
// endregion
// region ----- 状态机构造版 -----
/**
* 金额转中文大写(默认配置:缺省零元前缀 + 标准模式 + 缺省万位零)。
* <p>等价于 {@code toText(amount, true, false, true)},适用于绝大多数常规财务场景。
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
public static String toText(BigDecimal amount) {
return toText(amount, true, false, true);
}
/**
* 金额转中文大写(可配置零元前缀 + 标准模式 + 缺省万位零)。
* <p>等价于 {@code toText(amount, stripLeadingZeroYuan, false, false)}
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @param stripLeadingZeroYuan 是否省略零元前缀。
* {@code true}: 金额小于1元时省略(如 0.11 → 壹角壹分);
* {@code false}: 保留完整结构(如 0.11 → 零元壹角壹分)。
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
public static String toText(BigDecimal amount, boolean stripLeadingZeroYuan) {
return toText(amount, stripLeadingZeroYuan, false, true);
}
/**
* 金额转中文大写(可配置零元前缀 + 可配置运行模式 + 缺省万位零)。
* <p>等价于 {@code toText(amount, stripLeadingZeroYuan, extendedMode, false)}
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @param stripLeadingZeroYuan 是否省略零元前缀。
* {@code true}: 金额小于1元时省略(如 0.11 → 壹角壹分);
* {@code false}: 保留完整结构(如 0.11 → 零元壹角壹分)。
* @param extendedMode 是否开启拓展模式:
* {@code false}=标准模式(千亿级,通用财务规范);
* {@code true}=拓展模式(仟垓级,拓展大额单位)。
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
public static String toText(BigDecimal amount, boolean stripLeadingZeroYuan, boolean extendedMode) {
return toText(amount, stripLeadingZeroYuan, extendedMode, true);
}
/**
* 金额转中文大写(全参数版本)。
* <p>万进位系统(垓→京→兆→亿→万→元→角→分),支持负数与大数转换。
* 内部采用单次遍历状态机直接拼接最终的标准化字符串,性能优于正则清洗法。
*
* @param amount 金额(非空,自动四舍五入保留2位小数)
* @param stripLeadingZeroYuan 是否省略零元前缀
* {@code true}: 金额小于1元时省略(如 0.11 → 壹角壹分);
* {@code false}: 保留完整结构(如 0.11 → 零元壹角壹分)。
* @param extendedMode 是否开启拓展模式:
* {@code false}=标准模式(千亿级,通用财务规范);
* {@code true}=拓展模式(仟垓级,拓展大额单位)。
* @param wanZeroTolerant 是否万位零宽容:
* {@code false}=严格(如 101000.00 -> 壹拾万零壹仟元整);
* {@code true}=宽容(缺省万位零,如 101000.00 -> 壹拾万壹仟元整)。
* @return 符合财务规范的大写金额(不含"人民币"前缀,由调用方按需拼接)
*/
public static String toText(BigDecimal amount, boolean stripLeadingZeroYuan,
boolean extendedMode, boolean wanZeroTolerant) {
if (amount == null) {
throwIllegal(ERR_PARAM, "金额不能为空");
}
// 零元特殊处理(含负零)
if (BigDecimal.ZERO.compareTo(amount) == 0) {
return STR_ZERO_YUAN_ZHENG;
}
boolean negative = amount.signum() < 0;
BigDecimal absAmount = amount.abs().setScale(2, RoundingMode.HALF_UP);
// 按模式执行范围校验
BigDecimal maxAmount = extendedMode ? EXTENDED_MAX_AMOUNT : STANDARD_MAX_AMOUNT;
if (absAmount.compareTo(maxAmount) >= 0) {
String mode = extendedMode ? "拓展模式" : "标准模式";
throwIllegal(ERR_RANGE, mode + "金额超出上限", maxAmount.stripTrailingZeros().toPlainString());
}
// 转纯数字字符串(去掉小数点)
final String numStr = absAmount.toPlainString().replace(".", "");
int numLen = numStr.length();
if (numLen > UNIT_LENGTH) {
throwIllegal(ERR_RANGE, "金额超出最大支持范围");
}
StringBuilder sb = new StringBuilder(numLen * 2);
if (negative) {
sb.append(CHN_NEGATIVE);
}
// 整数部分长度
int intLen = numLen - 2;
// 整数部分按4位分组处理,避免切片,直接操作字符数组
int padLen = (4 - intLen % 4) % 4;
char[] intChars = new char[intLen + padLen];
for (int i = 0; i < padLen; i++) {
intChars[i] = '0';
}
numStr.getChars(0, intLen, intChars, padLen);
int segCount = intChars.length / 4;
// 段单位:' '为个段,其后依次为万、亿、兆、京、垓
char[] segUnits = {' ', '万', '亿', '兆', '京', '垓'};
// 段内单位:0是个位,1是拾位...
char[] posUnits = {'仟', '佰', '拾', ' '};
// 标记是否遇到了需要补“零”的0
boolean zeroPending = false;
// 标记整数部分是否有非零值
boolean intHasValue = false;
// 标记前一段是否在"个位"有值
boolean prevSegEndedOnUnit = false;
// 1. 遍历处理整数部分
for (int s = 0; s < segCount; s++) {
boolean segHasValue = false;
for (int p = 0; p < 4; p++) {
int digit = intChars[s * 4 + p] - '0';
if (digit != 0) {
segHasValue = true;
intHasValue = true;
// 判断是否需要补零
if (zeroPending) {
// 如果是新段的仟位有值,且跨段了,属于相邻数位,无需补零
if (p == 0 && prevSegEndedOnUnit) {
// 相邻段无需不补零
} else {
sb.append(CHN_ZERO);
}
zeroPending = false;
}
sb.append(DIGITS[digit]);
// 不是个位,追加段内单位
if (p < 3) {
sb.append(posUnits[p]);
}
} else {
// 只要之前出现过非零数字(无论是否跨段),遇到0就标记需要补零
if (intHasValue) {
zeroPending = true;
}
}
}
// 段处理完毕,如果该段有值,且不是个段(s < segCount - 1),则输出段单位
if (segHasValue && s < segCount - 1) {
sb.append(segUnits[segCount - 1 - s]);
}
// 判断是否为万段
boolean isWanSegment = (segCount - 1 - s) == 1;
// 记录本段是否在"个位"有值 (用于下一段的相邻判定)
prevSegEndedOnUnit = segHasValue && (
intChars[s * 4 + 3] != '0' || (
// 设计说明:遵循社会默认习惯,缺省万位零
// 示例:101000.00 → "壹拾万壹仟元整"(而非"壹拾万零壹仟元整")
// 如需严格表述,可在令 wanZeroTolerant 为 false
isWanSegment && wanZeroTolerant
)
);
}
// 2. 处理小数部分
int jiao = numStr.charAt(intLen) - '0';
int fen = numStr.charAt(intLen + 1) - '0';
if (intHasValue) {
sb.append(UNIT_YUAN);
if (jiao == 0 && fen == 0) {
sb.append(STR_ZHENG);
} else if (jiao == 0) {
// 只有分 (如 X.0Y) 只要元有值,角为0,分有值,必定无条件补零(如 1.01 -> 壹元零壹分)
sb.append(CHN_ZERO);
sb.append(DIGITS[fen]).append(UNIT_FEN);
} else if (fen == 0) {
// 只有角 (如 X.Y0)
if (zeroPending) {
// 设计说明:遵循社会默认习惯,元位和角位之间不强制补零
// 示例:1000.50 → "壹仟元伍角"(而非"壹仟元零伍角")
// 如需严格表述,可在此强制补零
// sb.append(CHN_ZERO);
}
sb.append(DIGITS[jiao]).append(UNIT_JIAO);
} else {
// 有角有分
sb.append(DIGITS[jiao]).append(UNIT_JIAO).append(DIGITS[fen]).append(UNIT_FEN);
}
} else {
if (jiao == 0 && fen == 0) {
// 整数部分为0 (即 0.xx)
sb.append(STR_ZERO_YUAN_ZHENG);
} else if (jiao == 0) {
// 0.0X
if (!stripLeadingZeroYuan) {
// 零元零X分
sb.append(STR_ZERO_YUAN).append(CHN_ZERO);
}
// X分
sb.append(DIGITS[fen]).append(UNIT_FEN);
} else if (fen == 0) {
// 0.X0
if (!stripLeadingZeroYuan) {
sb.append(STR_ZERO_YUAN);
}
// X角
sb.append(DIGITS[jiao]).append(UNIT_JIAO);
} else {
// 0.XY
if (!stripLeadingZeroYuan) {
sb.append(STR_ZERO_YUAN);
}
// X角X分
sb.append(DIGITS[jiao]).append(UNIT_JIAO).append(DIGITS[fen]).append(UNIT_FEN);
}
}
return sb.toString();
}
// endregion
// endregion
// region ========== 反向转换:中文大写转数字 ==========
/**
* 中文大写金额转数字(标准模式)。
* <p><b>仅接纳标准化的大写金额格式</b>,非标格式直接抛出异常,不做静默兼容。
* 仅支持标准语序: "人民币负XXX",不支持"负人民币XXX"等错误语序。
*
* @param chinese 中文大写金额(支持"人民币负"前缀、"整/正"后缀,方法会自动去除)
* @return 对应数值(BigDecimal 类型,四舍五入保留 2 位小数)
*/
public static BigDecimal toAmount(String chinese) {
return toAmount(chinese, false);
}
/**
* 中文大写金额转数字(全参数版本)。
* <p><b>仅接纳标准化的大写金额格式</b>,非标格式直接抛出异常,不做静默兼容。
* 仅支持标准语序: "人民币负XXX",不支持"负人民币XXX"等错误语序。
*
* @param chinese 中文大写金额(支持"人民币负"前缀、"整/正"后缀,方法会自动去除)
* @param extendedMode 是否开启拓展模式:
* {@code false}=标准模式(禁止兆/京/垓拓展单位);
* {@code true}=拓展模式(支持拓展大额单位)。
* @return 对应数值(BigDecimal 类型,四舍五入保留 2 位小数)
*/
public static BigDecimal toAmount(String chinese, boolean extendedMode) {
// 1. 预处理:清洗前缀后缀、基础校验
String coreInput = preprocess(chinese, extendedMode);
// 2. 语义属性识别:负号
boolean negative = coreInput.charAt(0) == CHN_NEGATIVE;
if (negative) {
coreInput = coreInput.substring(1);
}
final int coreLen = coreInput.length();
// 3. 初始化词法状态与计算变量
char maxSeenUnit = Character.MIN_VALUE;
SegmentState segmentState = new SegmentState(UNIT_YUAN);
CrossZeroState crossZeroState = new CrossZeroState();
ScanState scanState = new ScanState();
boolean fen = false, jiao = false, shi = false, bai = false, qian = false;
boolean wan = false, yi = false, zhao = false, jing = false, gai = false;
BigDecimal result = BigDecimal.ZERO;
// 4. 逐字符词法扫描 + 语义计算(倒序遍历:从低位到高位)
for (int i = coreLen - 1; i >= 0; i--) {
final char ch = coreInput.charAt(i);
final boolean isDigit = DIGIT_VALUE_MAP.containsKey(ch);
final boolean isUnit = !isDigit && UNIT_LEVEL_MAP.containsKey(ch);
// @formatter:off
// 4.1 单位字符集中校验(模式准入 → 相邻配对 → 段内层级 → 连接零 → 全局层级)
if (isUnit) {
// 4.1.1 标准模式准入校验:禁止拓展大额单位(快速失败)
if (!extendedMode && isExtendedUnit(ch)) {
throwIllegal(ERR_UNIT, "标准模式不支持拓展大额单位", String.valueOf(ch));
}
// 4.1.2 相邻单位对校验
if (i > 0) { checkAdjacentUnitPair(ch, coreInput.charAt(i - 1)); }
// 4.1.3 段内单位层级校验
checkSegmentUnitOrder(ch, segmentState);
// 4.1.4 跨段连接零校验
checkCrossSegmentZero(ch, crossZeroState);
// 4.1.5 全局顺序校验
maxSeenUnit = checkGlobalUnitOrder(ch, maxSeenUnit);
// 4.1.6 单位标记
switch (ch) {
case UNIT_FEN : fen = true; break;
case UNIT_JIAO : jiao = true; break;
case UNIT_YUAN : break;
case UNIT_SHI : shi = true; break;
case UNIT_BAI : bai = true; break;
case UNIT_QIAN : qian = true; break;
case UNIT_WAN : wan = true; break;
case UNIT_YI : yi = true; break;
case UNIT_ZHAO : zhao = true; break;
case UNIT_JING : jing = true; break;
case UNIT_GAI : gai = true; break;
}
}
else if (isDigit) {
// 4.2.1 数字连用合法性校验(统一拦截连续零、连续非零数字)
checkDigitAdjacency(ch, scanState);
// 4.2.2 零后接单位的合法性校验
checkZeroFollowedByUnit(ch, scanState);
// 4.2.3 维护连接零词法状态
crossZeroState.trackDigit(ch);
// 4.2.4 语义计算:数值加权累加
int digit = DIGIT_VALUE_MAP.getOrDefault(ch, 0);
if (digit > 0) {
BigDecimal current = BigDecimal.valueOf(digit);
// 乘以小单位
if (fen) { current = current.multiply(RATE_FEN); }
else if (jiao) { current = current.multiply(RATE_JIAO); }
else if (shi) { current = current.multiply(RATE_SHI); }
else if (bai) { current = current.multiply(RATE_BAI); }
else if (qian) { current = current.multiply(RATE_QIAN); }
// 乘以大进位单位
if (gai) { current = current.multiply(RATE_GAI); }
else if (jing) { current = current.multiply(RATE_JING); }
else if (zhao) { current = current.multiply(RATE_ZHAO); }
else if (yi) { current = current.multiply(RATE_YI); }
else if (wan) { current = current.multiply(RATE_WAN); }
result = result.add(current);
}
// 复位小单位标记
fen = jiao = shi = bai = qian = false;
}
else {
// 4.3 字符合法性兜底校验
String desc = i == 0 ? "首位必须为数字字符" : "包含非法字符";
throwIllegal(ERR_FORMAT, desc, String.valueOf(ch));
}
// @formatter:on
// 4.4 记录当前字符、类型和下标
scanState.track(ch, isDigit, i);
}
// 5. 结果处理:范围校验、符号、精度
BigDecimal maxAmount = extendedMode ? EXTENDED_MAX_AMOUNT : STANDARD_MAX_AMOUNT;
if (result.compareTo(maxAmount) >= 0) {
String mode = extendedMode ? "拓展模式" : "标准模式";
throwIllegal(ERR_RANGE, mode + "解析金额超出上限",
maxAmount.stripTrailingZeros().toPlainString());
}
if (negative) {
result = result.negate();
}
return result.setScale(2, RoundingMode.HALF_UP);
}
// endregion
/**
* 内置测试入口
*/
public static void main(String[] args) {
System.out.println("----- ----- ----- ----- ----- ----- ----- ----- -----");
System.out.println("===== ===== ===== 正向转换测试 ===== ===== ===== =====");
System.out.println("大额测试: " + toText(new BigDecimal("100000001000.10")));
System.out.println("负额测试: " + toText(new BigDecimal("-98765432.10")));
System.out.println("大额测试: " + toText(new BigDecimal("101001011011.01")));
System.out.println("零元测试: " + toText(new BigDecimal("0.00")));
System.out.println("整元测试: " + toText(new BigDecimal("100")));
System.out.println("无分测试: " + toText(new BigDecimal("123.50")));
System.out.println("单分测试: " + toText(new BigDecimal("0.05")));
System.out.println("无角测试: " + toText(new BigDecimal("1.05")));
System.out.println("多零测试: " + toText(new BigDecimal("100000001.00")));
System.out.println("拓展模式-兆级测试: " + toText(new BigDecimal("1000000000000.00"), true, true));
System.out.println("===== ===== ===== 反向转回测试 ===== ===== ===== =====");
System.out.printf("负额转回: %,.2f%n", toAmount("人民币负玖仟捌佰柒拾陆万伍仟肆佰叁拾贰元壹角"));
System.out.printf("大额转回: %,.2f%n", toAmount("人民币壹仟零壹拾亿零壹佰零壹万壹仟零壹拾壹元零壹分"));
System.out.printf("零元转回: %,.2f%n", toAmount("人民币零元整"));
System.out.printf("整元转回: %,.2f%n", toAmount("人民币壹佰元整"));
System.out.printf("无分转回: %,.2f%n", toAmount("人民币壹佰贰拾叁元伍角"));
System.out.printf("单分转回: %,.2f%n", toAmount("人民币伍分"));
System.out.printf("无角转回: %,.2f%n", toAmount("人民币壹元零伍分"));
System.out.printf("多零转回: %,.2f%n", toAmount("人民币壹亿零壹元整"));
System.out.printf("拓展模式-兆级转回: %,.2f%n", toAmount("人民币壹兆元整", true));
System.out.println("----- ----- ----- ----- ----- ----- ----- ----- -----");
}
}
增强包装器
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 人民币金额大写工具增强包装器.
* <p>
* 采用 Wrapper Pattern 组合增强 {@link ChineseCurrencyConverter},不侵入原工具类代码,
* 所有兼容、校验、异常优化逻辑均在本层实现,严格遵循开闭原则。
*
* <p>核心特性:
* <ol>
* <li>输入兼容:自动兼容口语化单位、异体字、小写数字、空白等非标输入</li>
* <li>闭环校验:反向解析后自动正向回比对齐,兜底语义一致性</li>
* <li>异常模板化:所有错误话术集中映射表管理,表驱动分发,无 switch 分支</li>
* </ol>
*/
public final class ChineseCurrencyEnhancer {
private ChineseCurrencyEnhancer() {
throw new UnsupportedOperationException("工具类禁止实例化");
}
// region ========== 配置映射表(新增/修改规则仅需调整本区) ==========
/**
* 单字符替换映射表(性能最优).
* 顺序:异体字 → 小写数字 → 小写单位
* 格式:{原字符, 目标字符}
*/
private static final char[][] CHAR_REPLACE_MAPPING = {
// 异体字
{'圆', '元'},
// 中文小写数字
{'〇', '零'},
{'一', '壹'},
{'二', '贰'},
{'三', '叁'},
{'四', '肆'},
{'五', '伍'},
{'六', '陆'},
{'七', '柒'},
{'八', '捌'},
{'九', '玖'},
// 中文小写单位
{'十', '拾'},
{'百', '佰'},
{'千', '仟'}
};
/**
* 正则替换规则表(按优先级从高到低排列,长匹配优先).
* 格式:{正则表达式, 替换内容}
* 类加载时自动预编译,运行期无编译开销
*/
private static final String[][] REGEX_RULES_DEF = {
// 补全开头拾位缺省的「壹」:拾万元 → 壹拾万元
{"^拾(?=[万亿兆京兆垓元角分])", "壹拾"},
// 俗字「两」替换:仅数字位替换,避免误伤普通文本
{"两(?=[拾佰仟万亿元角分])", "贰"},
// 复合单位归一:万万亿 → 京(长匹配优先)
{"(?<=[壹贰叁肆伍陆柒捌玖拾佰仟])万万亿", "京"},
// 复合单位归一:万亿 → 兆
{"(?<=[壹贰叁肆伍陆柒捌玖拾佰仟])万亿", "兆"}
};
/**
* 异常单位反向映射:内部标准单位 → 用户侧口语化单位.
* 格式:{内部表述, 用户侧表述}
*/
private static final String[][] EXCEPTION_UNIT_MAPPING = {
{"兆", "万亿"},
{"京", "万万亿"},
{"垓", "万京"}
};
// region ----- 与原工具类对齐的错误码常量 (不可乱动) -----
private static final String ERR_PARAM = "ERR_PARAM";
private static final String ERR_FORMAT = "ERR_FORMAT";
private static final String ERR_UNIT = "ERR_UNIT";
private static final String ERR_RANGE = "ERR_RANGE";
// endregion
/**
* 异常话术模板映射表(表驱动核心).
* <p>Key:错误码;Value:按实体数量索引的话术模板数组
* <p>数组下标 = 实体数量,占位符 {0} {1} 对应实体顺序。
* <p>新增错误码仅需在此追加配置,无需修改核心转译逻辑。
*/
private static final Map<String, String[]> EXCEPTION_TEMPLATE_MAP;
static {
Map<String, String[]> templateMap = new HashMap<>();
// 参数类错误
templateMap.put(ERR_PARAM, new String[]{
"金额不能为空"
});
// 格式类错误:0个实体 / 1个实体 / 2个及以上实体
templateMap.put(ERR_FORMAT, new String[]{
"输入格式无效,请检查金额内容",
"输入包含非法字符「{0}」",
"输入格式不符合规范"
});
// 单位类错误:0个实体 / 1个实体 / 2个实体
templateMap.put(ERR_UNIT, new String[]{
"单位格式不符合规范",
"标准模式不支持大额单位「{0}」",
"单位顺序错误:「{0}」不能出现在「{1}」前面"
});
// 范围类错误:0个实体 / 1个实体
templateMap.put(ERR_RANGE, new String[]{
"金额超出支持范围",
"金额超出支持范围,最大支持 {0}"
});
EXCEPTION_TEMPLATE_MAP = Collections.unmodifiableMap(templateMap);
}
// endregion
// region ========== 常量与预编译对象 ==========
/**
* 预编译后的正则替换规则列表(静态初始化一次,终身复用)
*/
private static final List<Map.Entry<Pattern, String>> REGEX_REPLACE_RULES;
static {
List<Map.Entry<Pattern, String>> ruleList =
new ArrayList<Map.Entry<Pattern, String>>(REGEX_RULES_DEF.length);
for (String[] rule : REGEX_RULES_DEF) {
ruleList.add(new AbstractMap.SimpleEntry<Pattern, String>(
Pattern.compile(rule[0]), rule[1]));
}
REGEX_REPLACE_RULES = Collections.unmodifiableList(ruleList);
}
/**
* 解析结构化异常的正则:提取错误码、描述、实体列表
*/
private static final Pattern EXCEPTION_STRUCT_PATTERN =
Pattern.compile("^\\[(\\w+)] (.+?)(?:; 实体: (.+))?$");
/**
* 匹配所有空白字符(含全角空格、制表符、换行)
*/
private static final Pattern RE_ALL_WHITESPACE = Pattern.compile("\\s+");
/**
* 标准化比对:匹配「人民币」前缀
*/
private static final Pattern RE_CMP_PREFIX = Pattern.compile("^人民币");
/**
* 标准化比对:匹配「整/正」后缀
*/
private static final Pattern RE_CMP_SUFFIX = Pattern.compile("[整正]$");
// endregion
// region ========== 正向转换:直接透传原工具类 ==========
/**
* 金额转中文大写(默认配置:缺省零元前缀 + 标准模式 + 缺省万位零).
*
* @param amount 金额
* @return 标准中文大写金额
*/
public static String toText(BigDecimal amount) {
return ChineseCurrencyConverter.toText(amount);
}
/**
* 金额转中文大写(可配置零元前缀 + 标准模式 + 缺省万位零).
*
* @param amount 金额
* @param stripLeadingZeroYuan 是否省略零元前缀
* @return 标准中文大写金额
*/
public static String toText(BigDecimal amount, boolean stripLeadingZeroYuan) {
return ChineseCurrencyConverter.toText(amount, stripLeadingZeroYuan);
}
/**
* 金额转中文大写(可配置零元前缀 + 可配置运行模式 + 缺省万位零).
*
* @param amount 金额
* @param stripLeadingZeroYuan 是否省略零元前缀
* @param extendedMode 是否开启拓展模式
* @return 标准中文大写金额
*/
public static String toText(BigDecimal amount, boolean stripLeadingZeroYuan, boolean extendedMode) {
return ChineseCurrencyConverter.toText(amount, stripLeadingZeroYuan, extendedMode);
}
/**
* 金额转中文大写(全参数版本).
*
* @param amount 金额
* @param stripLeadingZeroYuan 是否省略零元前缀
* @param extendedMode 是否开启拓展模式
* @param wanZeroTolerant 是否万位零宽容
* @return 标准中文大写金额
*/
public static String toText(BigDecimal amount, boolean stripLeadingZeroYuan, boolean extendedMode, boolean wanZeroTolerant) {
return ChineseCurrencyConverter.toText(amount, stripLeadingZeroYuan, extendedMode);
}
// endregion
// region ========== 反向转换:增强版 ==========
/**
* 中文大写转数字(默认配置).
*
* @param chinese 中文大写金额
* @return 对应数值
* @throws IllegalArgumentException 格式非法时抛出
*/
public static BigDecimal toAmount(String chinese) {
return toAmount(chinese, false, false);
}
/**
* 中文大写转数字(可配置拓展模式).
*
* @param chinese 中文大写金额
* @param extendedMode 是否开启拓展模式
* @return 对应数值
* @throws IllegalArgumentException 格式非法时抛出
*/
public static BigDecimal toAmount(String chinese, boolean extendedMode) {
return toAmount(chinese, extendedMode, false);
}
/**
* 中文大写转数字(全参数增强版).
*
* @param chinese 中文大写金额(支持异体字、小写数字、口语化单位)
* @param extendedMode 是否开启拓展模式
* @param roundTripVerify 是否启用闭环回比校验
* @return 对应数值
* @throws IllegalArgumentException 格式非法、校验不通过时抛出
*/
public static BigDecimal toAmount(String chinese, boolean extendedMode, boolean roundTripVerify) {
String normalized = preprocessCompatible(chinese);
BigDecimal result;
try {
result = ChineseCurrencyConverter.toAmount(normalized, extendedMode);
} catch (IllegalArgumentException e) {
// 结构化解析 + 模板化转译,原始异常保留为根因
String friendlyMsg = translateException(e.getMessage());
throw new IllegalArgumentException(friendlyMsg, e);
}
if (roundTripVerify) {
verifyRoundTrip(normalized, result, extendedMode);
}
return result;
}
/**
* 校验中文大写金额是否合法(默认标准模式).
*
* @param chinese 中文大写金额
* @return true=合法,false=非法
*/
public static boolean isValid(String chinese) {
return isValid(chinese, false);
}
/**
* 校验指定模式下中文大写金额是否合法.
*
* @param chinese 中文大写金额
* @param extendedMode 是否开启拓展模式
* @return true=合法,false=非法
*/
public static boolean isValid(String chinese, boolean extendedMode) {
try {
toAmount(chinese, extendedMode, true);
return true;
} catch (Exception e) {
return false;
}
}
// endregion
// region ========== 内部核心方法 ==========
/**
* 非标输入标准化预处理,分层执行:
* 去空白 → 单字符替换 → 正则规则替换(按优先级)
*
* @param raw 原始输入
* @return 标准化后的标准格式输入
*/
private static String preprocessCompatible(String raw) {
if (raw == null) {
return null;
}
String result = raw;
// 1. 清除所有空白字符
result = RE_ALL_WHITESPACE.matcher(result).replaceAll("");
// 2. 单字符批量替换(性能最优)
for (char[] mapping : CHAR_REPLACE_MAPPING) {
result = result.replace(mapping[0], mapping[1]);
}
// 3. 正则规则按优先级依次替换
for (Map.Entry<Pattern, String> rule : REGEX_REPLACE_RULES) {
result = rule.getKey().matcher(result).replaceAll(rule.getValue());
}
return result;
}
/**
* 异常信息结构化解析 + 模板化转译(表驱动,无 switch 分支).
* 固定流程:解析结构 → 实体映射 → 匹配模板 → 填充占位符 → 返回
*
* @param originalMsg 原工具类结构化异常信息
* @return 友好化后的业务话术
*/
private static String translateException(String originalMsg) {
if (originalMsg == null || originalMsg.isEmpty()) {
return originalMsg;
}
Matcher matcher = EXCEPTION_STRUCT_PATTERN.matcher(originalMsg);
if (!matcher.find()) {
// 非结构化异常,兜底直接返回
return originalMsg;
}
String errorCode = matcher.group(1);
String entityStr = matcher.group(3);
String[] entities = parseEntities(entityStr);
// 1. 单位类异常:实体做口语化反向映射
if (ERR_UNIT.equals(errorCode)) {
for (int i = 0; i < entities.length; i++) {
for (String[] mapping : EXCEPTION_UNIT_MAPPING) {
entities[i] = entities[i].replace(mapping[0], mapping[1]);
}
}
}
// 2. 匹配模板:按实体数量选择对应话术模板
String[] templates = EXCEPTION_TEMPLATE_MAP.get(errorCode);
if (templates == null || templates.length == 0) {
return originalMsg;
}
int entityIndex = Math.min(entities.length, templates.length - 1);
String template = templates[entityIndex];
// 3. 填充占位符,返回最终话术
return fillTemplate(template, entities);
}
/**
* 解析实体列表字符串:'a', 'b' → [a, b]
*
* @param entityStr 原始实体串
* @return 实体数组
*/
private static String[] parseEntities(String entityStr) {
if (entityStr == null || entityStr.isEmpty()) {
return new String[0];
}
String[] parts = entityStr.split(", ");
for (int i = 0; i < parts.length; i++) {
parts[i] = parts[i].replace("'", "").trim();
}
return parts;
}
/**
* 轻量占位符填充:{0} {1} 依次替换为对应参数.
* 比 java.text.MessageFormat 更轻量,无特殊字符转义与本地化问题,
* 适配纯中文固定模板场景。
*
* @param template 模板字符串
* @param args 占位符参数
* @return 填充后的字符串
*/
private static String fillTemplate(String template, String[] args) {
if (args == null || args.length == 0) {
return template;
}
String result = template;
for (int i = 0; i < args.length; i++) {
result = result.replace("{" + i + "}", args[i]);
}
return result;
}
/**
* 闭环回比校验:解析结果正向转回后做语义比对.
*
* @param originalInput 标准化后的原始输入
* @param parsedValue 解析得到的数值
* @param extendedMode 模式开关
* @throws IllegalArgumentException 比对不一致时抛出
*/
private static void verifyRoundTrip(String originalInput, BigDecimal parsedValue, boolean extendedMode) {
String roundTrip = ChineseCurrencyConverter.toTextWithRegex(parsedValue, false, extendedMode);
String src = normalizeForCompare(originalInput);
String target = normalizeForCompare(roundTrip);
if (!src.equals(target)) {
throw new IllegalArgumentException("金额语义校验不通过,解析结果与原输入不一致");
}
}
/**
* 标准化为纯语义比对串:去除前缀、去除整后缀.
* 消除「人民币前缀」「整/正后缀」等合法格式差异。
*
* @param text 待标准化字符串
* @return 纯语义比对串
*/
private static String normalizeForCompare(String text) {
String result = text;
result = RE_CMP_PREFIX.matcher(result).replaceAll("");
result = RE_CMP_SUFFIX.matcher(result).replaceAll("");
return result;
}
// endregion
// region ========== 测试入口 ==========
public static void main(String[] args) {
System.out.println("===== 兼容能力测试 =====");
System.out.println("小写数字: " + toAmount("一千二百三十元五角"));
System.out.println("两+小写: " + toAmount("两千一百元整"));
System.out.println("异体字圆: " + toAmount("壹佰圆整"));
System.out.println("开头十: " + toAmount("十万元整"));
System.out.println("万亿口语: " + toAmount("两万亿元整", true));
System.out.println("带空白: " + toAmount(" 壹佰 贰拾 元 整 "));
System.out.println("\n===== 异常友好化测试 =====");
try {
toAmount("壹万亿元整");
} catch (Exception e) {
System.out.println("万亿输入异常: " + e.getMessage());
System.out.println("原始栈保留: " + (e.getCause() != null));
}
try {
toAmount("壹佰x元整");
} catch (Exception e) {
System.out.println("非法字符异常: " + e.getMessage());
}
try {
toAmount("壹万佰元整");
} catch (Exception e) {
System.out.println("单位乱序异常: " + e.getMessage());
}
try {
toAmount(null);
} catch (Exception e) {
System.out.println("空值异常: " + e.getMessage());
}
System.out.println("\n===== 合法性校验 =====");
System.out.println("合法小写: " + isValid("三千元整"));
System.out.println("非法乱序: " + isValid("壹仟万佰元整"));
}
// endregion
}