import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 人民币金额大写转换工具.
* <p>提供阿拉伯数字金额与中文大写金额之间的双向转换,采用「两级单调 + 分层校验」架构.
*
* <h3>支持两种运行模式,默认使用标准模式:</h3>
* <ul>
* <li><b>标准模式 (默认)</b>:对齐通用财务规范,金额范围 0.01(分) ~ 999999999999.99(千亿级), 仅使用元、角、分、拾、佰、仟、万、亿标准单位</li>
* <li><b>拓展模式</b>:支持拓展大额单位(兆、京、垓), 金额范围扩展至仟垓级(1E+24), 用于超大额特殊场景</li>
* </ul>
* <h3>支持两种分段体系,共享同一套万进制内核:</h3>
* <ul>
* <li><b>4位分段 (默认)</b>:万进位,标准段单位:元/万/亿,拓展段单位:兆/京/垓</li>
* <li><b>8位分段</b>:亿进位(段内复用万进制双子段结构),标准段单位:元/亿,拓展段单位:京</li>
* </ul>
* <h3>核心校验规则:</h3>
* <ol>
* <li><b>全局级单调性</b>:大段单位正序严格递减,不可重复、不可逆序</li>
* <li><b>段内级单调性</b>:双子段结构,子段分隔符「万」唯一,子段内仟/佰/拾严格递减</li>
* <li><b>边界零规则</b>:非相邻数位必须补连接零;万位为0且千位非0时,可通过配置宽容省略</li>
* <li><b>邻接规则</b>:非零数字必须带单位修饰,禁止连续零,零后禁止直接接单位(零元除外)</li>
* </ol>
* <pre>设计说明:
* 1. 本工具仅负责金额数字的合规转换,不包含业务前缀。
* - 由调用方自行拼接:"人民币" + toText(...)
* 2. 反向解析(toAmount)仅支持标准语序:"人民币负XXX",不支持"负人民币XXX"。
* 3. 核心工具类采用无状态纯函数设计,线程安全;上下文透传由上层业务框架自行封装。
* 4. 所有异常采用统一结构化格式:[错误码] 描述文本; 实体: '值1', '值2',便于上层解析与转译。
* </pre>
* @since JDK 1.8
*/
public class ChineseCurrencyConverterV2 {
// region ========== 配置类:不可变参数封装 ==========
/**
* 转换配置类
* <p>使用不可变对象封装所有开关参数,通过预设实例 + with 派生方法构建,
* 避免多参数重载的维护成本,所有配置项在正向生成、反向解析、校验逻辑中严格一致生效。
*
* <h3>配置项说明</h3>
* <ul>
* <li>{@code stripLeadingZeroYuan}:纯小数场景是否省略「零元」前缀,仅正向生成生效</li>
* <li>{@code extendedMode}:是否启用兆/京/垓拓展大额单位,正反双向同步生效</li>
* <li>{@code wanZeroTolerant}:是否启用万位零宽容,正反双向同步生效;
* 开启后万位为0且千位非0时,可省略连接零,严格对齐《支付结算办法》可选规则</li>
* <li>{@code eightSegmentMode}:是否启用8位分段(亿进位)体系,正反双向同步生效</li>
* </ul>
*/
public static final class Config {
/**
* 默认配置:省略零元前缀、标准模式、万位零宽容、4位分段
*/
public static final Config DEFAULT = new Config(true, false, true, false);
/**
* 拓展模式配置:支持兆/京/垓大额单位
*/
public static final Config EXTENDED = DEFAULT.withExtendedMode(true);
/**
* 严格模式配置:关闭万位零宽容,仅接受标准带零写法,适用于银行票据等强合规场景
*/
public static final Config STRICT = DEFAULT.withWanZeroTolerant(false);
/**
* 8位分段配置:亿进位体系
*/
public static final Config EIGHT_SEG = DEFAULT.withEightSegmentMode(true);
private final boolean stripLeadingZeroYuan;
private final boolean extendedMode;
private final boolean wanZeroTolerant;
private final boolean eightSegmentMode;
/**
* 私有构造,禁止外部直接实例化
* @param stripLeadingZeroYuan 纯小数场景是否省略「零元」前缀
* @param extendedMode 是否启用拓展大额单位
* @param wanZeroTolerant 是否启用万位零宽容模式
* @param eightSegmentMode 是否启用8位分段模式
*/
private Config(boolean stripLeadingZeroYuan, boolean extendedMode,
boolean wanZeroTolerant, boolean eightSegmentMode) {
this.stripLeadingZeroYuan = stripLeadingZeroYuan;
this.extendedMode = extendedMode;
this.wanZeroTolerant = wanZeroTolerant;
this.eightSegmentMode = eightSegmentMode;
}
/**
* @return 纯小数场景是否省略「零元」前缀
*/
public boolean isStripLeadingZeroYuan() { return stripLeadingZeroYuan; }
/**
* @return 是否启用拓展大额单位模式
*/
public boolean isExtendedMode() { return extendedMode; }
/**
* @return 是否启用万位零宽容模式
*/
public boolean isWanZeroTolerant() { return wanZeroTolerant; }
/**
* @return 是否启用8位分段模式
*/
public boolean isEightSegmentMode() { return eightSegmentMode; }
/**
* 派生新配置:设置是否省略零元前缀
* @param value 是否省略
* @return 新的配置实例
*/
public Config withStripLeadingZeroYuan(boolean value) {
return new Config(value, extendedMode, wanZeroTolerant, eightSegmentMode);
}
/**
* 派生新配置:设置是否启用拓展模式
* @param value 是否启用
* @return 新的配置实例
*/
public Config withExtendedMode(boolean value) {
return new Config(stripLeadingZeroYuan, value, wanZeroTolerant, eightSegmentMode);
}
/**
* 派生新配置:设置是否启用万位零宽容
* @param value 是否启用
* @return 新的配置实例
*/
public Config withWanZeroTolerant(boolean value) {
return new Config(stripLeadingZeroYuan, extendedMode, value, eightSegmentMode);
}
/**
* 派生新配置:设置是否启用8位分段
* @param value 是否启用
* @return 新的配置实例
*/
public Config withEightSegmentMode(boolean value) {
return new Config(stripLeadingZeroYuan, extendedMode, wanZeroTolerant, value);
}
/**
* 内部派生:大段单位的最低层级阈值
* @return 判定为大段单位的最低层级
*/
int getLargeSegmentThreshold() {
return eightSegmentMode
? UNIT_LEVEL_MAP.get(UNIT_YI)
: UNIT_LEVEL_MAP.get(UNIT_WAN);
}
}
// endregion
// region ========== 内部状态机:分层校验上下文 ==========
/** 数字类型枚举,用于邻接状态追踪 */
private enum DigitType { NONE, ZERO, NON_ZERO }
/**
* 字符邻接状态
* <p>追踪上一字符的类型与位置,支撑数字连用、零后接单位等邻接校验
*/
private static class CharAdjacencyState {
char prevChar = Character.MIN_VALUE;
DigitType prevDigitType = DigitType.NONE;
int prevIndex = -1;
/** 重置为初始状态 */
void reset() {
prevChar = Character.MIN_VALUE;
prevDigitType = DigitType.NONE;
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;
}
/** @return 上一字符是否处于第1位 */
boolean isIndexOne() { return prevIndex == 1; }
/** @return 状态是否未初始化 */
boolean isUninit() { return prevChar == Character.MIN_VALUE && prevIndex == -1; }
}
/**
* 段内排序状态机
* <p>原生支持双子段结构,保障段内单位严格单调:
* 倒序视角下子段内单位层级严格递增,子段分隔符「万」仅允许出现一次
*/
private static class SegmentOrderState {
int currentLevel;
int segmentIndex;
final int subSegThreshold;
/**
* 构造分段排序状态机
* @param subSegThreshold 子段分隔单位的层级(万的层级)
*/
SegmentOrderState(int subSegThreshold) {
this.subSegThreshold = subSegThreshold;
reset();
}
/** 重置为初始状态,复用于下一个大段 */
void reset() {
this.currentLevel = 0;
this.segmentIndex = 0;
}
/**
* 接收单位并校验单调性,非法则直接抛出异常
* @param unitLevel 当前单位层级
* @param segmentUnit 所属大段单位,用于异常上下文
* @param position 当前字符位置,用于异常快照
*/
void accept(int unitLevel, char segmentUnit, int position) {
// 子段分隔符仅允许切换一次
if (isSubSegmentSeparator(unitLevel)) {
if (segmentIndex != 0) {
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(VALID_UNIT_CHARS.charAt(subSegThreshold - 1)));
fail(ERR_UNIT, "段内单位重复,子段分隔符仅允许出现一次:{unit}", position, entities);
}
segmentIndex++;
currentLevel = 0;
return;
}
// 段内禁止出现拾以下的低位单位
if (unitLevel < UNIT_LEVEL_MAP.get(UNIT_SHI)) {
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(VALID_UNIT_CHARS.charAt(unitLevel - 1)));
fail(ERR_UNIT, "段内禁止出现低位单位:{unit}", position, entities);
}
// 子段内严格递增校验(对应正序严格递减)
if (unitLevel <= currentLevel) {
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(VALID_UNIT_CHARS.charAt(unitLevel - 1)));
fail(ERR_UNIT, "段内单位语序非法(逆序或重复):{unit}", position, entities);
}
currentLevel = unitLevel;
}
/**
* 判断是否为子段分隔单位
* @param unitLevel 单位层级
* @return 是否为分隔符
*/
boolean isSubSegmentSeparator(int unitLevel) {
return unitLevel == subSegThreshold;
}
}
/**
* 段边界零校验状态
* <p>基于单位层级差 + 间隔非零标记,判断是否缺少连接零
*/
private static class SegmentBoundaryState {
int prevUnitLevel;
boolean intervalHasNonZero;
int currentSegmentIndex;
final int baseLevel;
/**
* 构造边界零校验状态
* @param baseLevel 大段基准层级
*/
SegmentBoundaryState(int baseLevel) {
this.baseLevel = baseLevel;
reset();
}
/** 重置为初始状态 */
void reset() {
prevUnitLevel = 0;
intervalHasNonZero = false;
currentSegmentIndex = 0;
}
/**
* 遇到单位时更新状态
* @param level 当前单位层级
*/
void trackUnit(int level) {
this.prevUnitLevel = level;
this.intervalHasNonZero = false;
int yuanLevel = UNIT_LEVEL_MAP.get(UNIT_YUAN);
if (level == yuanLevel) {
this.currentSegmentIndex = 0;
} else if (level >= baseLevel) {
this.currentSegmentIndex = level - baseLevel + 1;
}
}
/**
* 遇到数字时更新间隔标记
* @param ch 当前数字字符
*/
void trackDigit(char ch) {
this.intervalHasNonZero = (ch != CHN_ZERO);
}
}
/**
* 单段解析内核
* <p>与外层大段调度完全解耦,仅负责单段内的语法校验与数值累加,可复用原子组件
*/
private static class SegmentParser {
private final CharAdjacencyState chAdjState = new CharAdjacencyState();
private final SegmentOrderState orderState;
private final SegmentBoundaryState boundState;
private final boolean tolerant;
private boolean shi, bai, qian, wan;
private long segmentValue;
private char segmentChar;
private int maxPower;
private int currentIndex;
/**
* 构造单段解析器
* @param subSegBase 子段基准层级
* @param tolerant 是否启用万位零宽容
*/
SegmentParser(int subSegBase, boolean tolerant) {
this.orderState = new SegmentOrderState(subSegBase);
this.boundState = new SegmentBoundaryState(subSegBase);
this.tolerant = tolerant;
}
/**
* 重置解析器状态,复用于下一个大段
* @param segChar 当前大段单位字符,用于异常上下文
*/
void reset(char segChar) {
chAdjState.reset();
orderState.reset();
boundState.reset();
shi = bai = qian = wan = false;
segmentValue = 0;
segmentChar = segChar;
maxPower = 0;
currentIndex = -1;
}
/**
* 处理单个字符(倒序输入)
* @param ch 当前字符
* @param leftChar 正序左侧字符,用于邻接校验
* @param index 当前字符索引
*/
void processChar(char ch, char leftChar, int index) {
this.currentIndex = index;
boolean isDigit = DIGIT_VALUE_MAP.containsKey(ch);
boolean isUnit = !isDigit && UNIT_LEVEL_MAP.containsKey(ch);
// @formatter:off
if (isUnit) {
if (!chAdjState.isUninit()) {
checkAdjacentUnitPair(ch, leftChar, index);
}
orderState.accept(UNIT_LEVEL_MAP.get(ch), segmentChar, index);
checkBoundaryZero(ch, boundState, tolerant, index);
boundState.trackUnit(UNIT_LEVEL_MAP.get(ch));
switch (ch) {
case UNIT_SHI : shi = true; break;
case UNIT_BAI : bai = true; break;
case UNIT_QIAN : qian = true; break;
case UNIT_WAN : wan = true; break;
default: break;
}
} else if (isDigit) {
checkDigitAdjacency(ch, chAdjState, index);
checkZeroFollowByUnit(ch, chAdjState, index);
boundState.trackDigit(ch);
int digit = DIGIT_VALUE_MAP.get(ch);
if (digit > 0) {
long val = digit;
int power = 0;
if (shi) { val *= 10; power += 1; }
if (bai) { val *= 100; power += 2; }
if (qian) { val *= 1000; power += 3; }
if (wan) { val *= 10000; power += 4; }
segmentValue += val;
if (power > maxPower) maxPower = power;
}
shi = bai = qian = false;
} else {
fail(ERR_FORMAT, "包含非法字符:{char}", "char", String.valueOf(ch));
}
// @formatter:on
chAdjState.track(ch, isDigit, index);
}
/** @return 当前段的数值 */
long getValue() { return segmentValue; }
/** @return 当前段是否包含非零数字 */
boolean hasNonZero() { return segmentValue > 0; }
/** @return 当前段最高有效位的相对幂次(相对于段基准) */
int getMaxPower() { return hasNonZero() ? maxPower : -1; }
}
/**
* 预处理结果封装
*/
private static class PreprocessResult {
/** 整数部分字符串(已移除元单位) */
final String intPart;
/** 小数部分数值 */
final BigDecimal decimalValue;
/** 是否为负数 */
final boolean negative;
/**
* @param intPart 整数部分字符串
* @param decimalValue 小数部分数值
* @param negative 是否为负数
*/
PreprocessResult(String intPart, BigDecimal decimalValue, boolean negative) {
this.intPart = intPart;
this.decimalValue = decimalValue;
this.negative = negative;
}
}
// endregion
// region ========== 常量定义 ==========
// 错误码分类:严格对应异常类型
// @formatter:off
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";
// @formatter:on
// 数字字符:0~9 对应中文大写
// @formatter:off
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 = '玖';
// @formatter:on
// 单位字符:按层级从低到高排列,层级 = 索引 + 1
// @formatter:off
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 = '垓';
// @formatter:on
// 特殊字符
// @formatter:off
private static final char CHN_ZHENG = '整';
private static final char CHN_ZHENG_ALT = '正';
private static final char CHN_NEGATIVE = '负';
private static final String STR_ZHENG = "整";
private static final String STR_RMB_PREFIX = "人民币";
private static final String STR_ZERO_YUAN_ZHENG = "零元整";
// @formatter:off
// 权值常量:与单位一一对应
// @formatter:off
private static final BigDecimal RATE_FEN = new BigDecimal("0.01");
private static final BigDecimal RATE_JIAO = new BigDecimal("0.1");
private static final BigDecimal RATE_SHI = BigDecimal.TEN;
private static final BigDecimal RATE_BAI = new BigDecimal("100");
private static final BigDecimal RATE_QIAN = new BigDecimal("1000");
private static final BigDecimal RATE_WAN = new BigDecimal("10000");
private static final BigDecimal RATE_YI = new BigDecimal("100000000");
private static final BigDecimal RATE_ZHAO = new BigDecimal("1000000000000");
private static final BigDecimal RATE_JING = new BigDecimal("10000000000000000");
private static final BigDecimal RATE_GAI = new BigDecimal("1E+20");
// @formatter:off
// 范围常量
private static final BigDecimal STANDARD_MAX = new BigDecimal("1000000000000.00");
private static final BigDecimal EXTENDED_MAX = new BigDecimal("1E+24");
// 映射表:运行时快速查询
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<Character, Integer>(16);
private static final Map<Character, Integer> UNIT_LEVEL_MAP = new HashMap<Character, Integer>(16);
static {
for (int i = 0; i < DIGITS.length; i++) {
DIGIT_VALUE_MAP.put(DIGITS[i], i);
}
for (int i = 0; i < VALID_UNIT_CHARS.length(); i++) {
UNIT_LEVEL_MAP.put(VALID_UNIT_CHARS.charAt(i), i + 1);
}
}
// endregion
// region ========== 结构化异常 + 极简fail工具 ==========
/**
* 金额解析异常
* <p>继承 {@link IllegalArgumentException},完全兼容原有捕获逻辑;
* 携带结构化上下文字段,便于包装器生成友好提示与问题定位。
*/
public static class AmountParseException extends IllegalArgumentException {
private final String errorCode;
private final int position;
private final Map<String, String> entities;
/**
* 构造结构化解析异常
* @param message 人类可读异常消息
* @param errorCode 错误码
* @param position 出错字符正序位置,从0开始;无位置信息为-1
* @param entities 命名实体集合
*/
AmountParseException(String message, String errorCode, int position, Map<String, String> entities) {
super(message);
this.errorCode = errorCode;
this.position = position;
this.entities = entities != null
? Collections.unmodifiableMap(entities)
: Collections.emptyMap();
}
/**
* @return 错误码
*/
public String getErrorCode() { return errorCode; }
/**
* @return 出错字符正序位置,从0开始;无位置信息返回-1
*/
public int getPosition() { return position; }
/**
* 根据键获取实体值
* @param key 实体键名
* @return 实体值,不存在则返回空字符串
*/
public String getEntity(String key) { return entities.getOrDefault(key, ""); }
/**
* @return 全部实体的不可变Map
*/
public Map<String, String> getEntities() { return entities; }
}
/**
* 抛出无实体无位置的异常
* @param code 错误码
* @param msg 错误描述
*/
private static void fail(String code, String msg) {
throw new AmountParseException(msg, code, -1, null);
}
/**
* 抛出单命名实体、无位置的异常
* @param code 错误码
* @param msg 错误描述(含命名占位符)
* @param key 实体键名
* @param value 实体值
*/
private static void fail(String code, String msg, String key, String value) {
Map<String, String> entities = new HashMap<>(2);
entities.put(key, value);
String message = msg.replace("{" + key + "}", value);
throw new AmountParseException(message, code, -1, entities);
}
/**
* 抛出多命名实体、带位置的异常
* @param code 错误码
* @param msg 错误描述(含命名占位符)
* @param position 出错字符位置
* @param entities 命名实体集合
*/
private static void fail(String code, String msg, int position, Map<String, String> entities) {
StringBuilder sb = new StringBuilder(msg);
for (Map.Entry<String, String> entry : entities.entrySet()) {
String placeholder = "{" + entry.getKey() + "}";
int idx = sb.indexOf(placeholder);
if (idx >= 0) {
sb.replace(idx, idx + placeholder.length(), entry.getValue());
}
}
if (position >= 0) {
sb.append("(位置:第").append(position + 1).append("个字符)");
}
throw new AmountParseException(sb.toString(), code, position, entities);
}
// endregion
// region ========== 语法校验核心方法 ==========
/**
* 校验单位邻接合法性
* <p>规则:单位左侧必须是数字,或允许的前置单位
* @param cur 当前单位
* @param left 正序左侧字符
* @param position 当前字符位置
*/
private static void checkAdjacentUnitPair(char cur, char left, int position) {
if (DIGIT_VALUE_MAP.containsKey(left)) { return; }
if (cur == UNIT_YUAN) {
if (left == UNIT_FEN || left == UNIT_JIAO) {
Map<String, String> entities = new HashMap<>(4);
entities.put("left", String.valueOf(left));
entities.put("curr", String.valueOf(cur));
fail(ERR_UNIT, "元左侧禁止出现分/角单位:{left}", position, entities);
}
return;
}
if (UNIT_LEVEL_MAP.get(cur) >= UNIT_LEVEL_MAP.get(UNIT_WAN)) {
if (left != UNIT_SHI && left != UNIT_BAI && left != UNIT_QIAN) {
Map<String, String> entities = new HashMap<>(4);
entities.put("left", String.valueOf(left));
entities.put("curr", String.valueOf(cur));
fail(ERR_UNIT, "量级单位左侧仅允许拾/佰/仟前置:{left}", position, entities);
}
return;
}
Map<String, String> entities = new HashMap<>(4);
entities.put("left", String.valueOf(left));
entities.put("curr", String.valueOf(cur));
fail(ERR_UNIT, "段内单位禁止直接相邻:{left}、{curr}", position, entities);
}
/**
* 校验段边界连接零合法性
* @param unit 当前单位
* @param state 边界零状态
* @param tolerant 是否启用万位零宽容
* @param position 当前字符位置
*/
private static void checkBoundaryZero(char unit, SegmentBoundaryState state, boolean tolerant, int position) {
int curLevel = UNIT_LEVEL_MAP.get(unit);
int lastLevel = state.prevUnitLevel;
if (lastLevel <= 0 || !state.intervalHasNonZero) { return; }
if (curLevel < lastLevel) {
if (curLevel > UNIT_LEVEL_MAP.get(UNIT_SHI)) {
// 万位零宽容:严格对齐规范,仅万位为0且千位非0时豁免
boolean wanTolerant = tolerant
&& curLevel == UNIT_LEVEL_MAP.get(UNIT_WAN)
&& lastLevel == UNIT_LEVEL_MAP.get(UNIT_QIAN);
if (!wanTolerant) {
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(unit));
fail(ERR_FORMAT, "非相邻数位间缺少连接零:{unit}", position, entities);
}
}
} else if (curLevel > lastLevel) {
boolean adjacent = (curLevel - lastLevel == 1 && lastLevel < UNIT_LEVEL_MAP.get(UNIT_WAN))
|| (lastLevel == UNIT_LEVEL_MAP.get(UNIT_QIAN)
&& curLevel == state.baseLevel + state.currentSegmentIndex);
if (!adjacent) {
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(unit));
fail(ERR_FORMAT, "非相邻数位间缺少连接零:{unit}", position, entities);
}
}
}
/**
* 校验数字邻接规则
* <p>规则:非零数字必须带单位修饰;禁止连续出现多个零字
* @param ch 当前数字
* @param state 邻接状态
* @param position 当前字符位置
*/
private static void checkDigitAdjacency(char ch, CharAdjacencyState state, int position) {
if (state.isUninit()) { return; }
if (ch != CHN_ZERO) {
if (state.prevDigitType == DigitType.NON_ZERO || state.prevDigitType == DigitType.ZERO) {
Map<String, String> entities = new HashMap<>(4);
entities.put("curr", String.valueOf(ch));
entities.put("prev", String.valueOf(state.prevChar));
fail(ERR_UNIT, "非零数字缺少单位修饰:{curr}", position, entities);
}
return;
}
if (state.prevDigitType == DigitType.ZERO) {
Map<String, String> entities = new HashMap<>(2);
entities.put("chars", String.valueOf(ch) + state.prevChar);
fail(ERR_FORMAT, "禁止连续多零结构:{chars}", position, entities);
}
}
/**
* 校验零后接单位规则
* <p>规则:零不能直接跟单位,「零元」除外
* @param ch 当前字符
* @param state 邻接状态
* @param position 当前字符位置
*/
private static void checkZeroFollowByUnit(char ch, CharAdjacencyState state, int position) {
if (state.isUninit()) { return; }
if (ch == CHN_ZERO && state.prevDigitType == DigitType.NONE) {
if (state.isIndexOne()) {
if (state.prevChar == UNIT_YUAN) return;
Map<String, String> entities = new HashMap<>(2);
entities.put("chars", String.valueOf(ch) + state.prevChar);
fail(ERR_FORMAT, "零字开头仅允许零元:{chars}", position, entities);
}
Map<String, String> entities = new HashMap<>(4);
entities.put("zero", String.valueOf(ch));
entities.put("unit", String.valueOf(state.prevChar));
fail(ERR_UNIT, "禁止零后直接接单位:{zero}、{unit}", position, entities);
}
}
// endregion
// region ========== 通用工具方法 ==========
/**
* 判断是否为拓展单位(兆及以上)
* @param unit 单位字符
* @return 是否为拓展单位
*/
private static boolean isExtendedUnit(char unit) {
return UNIT_LEVEL_MAP.get(unit) >= UNIT_LEVEL_MAP.get(UNIT_ZHAO);
}
/**
* 判断是否为大段单位(达到分段阈值)
* @param unit 单位字符
* @param threshold 分段阈值
* @return 是否为大段单位
*/
private static boolean isLargeSegmentUnit(char unit, int threshold) {
return UNIT_LEVEL_MAP.getOrDefault(unit, Integer.MIN_VALUE) >= threshold;
}
/**
* 获取大段单位对应的权值
* @param unit 单位字符
* @return 权值
*/
private static BigDecimal getLargeSegmentRate(char unit) {
// @formatter:off
switch (unit) {
case UNIT_YUAN: return BigDecimal.ONE;
case UNIT_WAN: return RATE_WAN;
case UNIT_YI: return RATE_YI;
case UNIT_ZHAO: return RATE_ZHAO;
case UNIT_JING: return RATE_JING;
case UNIT_GAI: return RATE_GAI;
default: return BigDecimal.ONE;
}
// @formatter:on
}
/**
* 获取大段单位的基准幂次(以元为0位)
* @param unit 单位字符
* @return 10的幂次
*/
private static int getBasePower(char unit) {
// @formatter:off
switch (unit) {
case UNIT_YUAN: return 0;
case UNIT_WAN: return 4;
case UNIT_YI: return 8;
case UNIT_ZHAO: return 12;
case UNIT_JING: return 16;
case UNIT_GAI: return 20;
default: return 0;
}
// @formatter:on
}
/**
* 计算高位段最低非零数位的全局幂次
* <p>依据:大写金额段内末尾零省略规则,大段单位左侧紧邻字符直接对应最低有效位
* @param segUnit 大段单位字符
* @param index 大段单位的倒序索引
* @param text 整数部分文本
* @return 最低非零位的全局幂次
*/
private static int calcHighSegMinNonZeroPower(char segUnit, int index, String text) {
int basePower = getBasePower(segUnit);
if (index <= 0) {
return basePower;
}
char leftChar = text.charAt(index - 1);
if (DIGIT_VALUE_MAP.containsKey(leftChar)) {
return basePower;
}
// @formatter:off
switch (leftChar) {
case UNIT_SHI: return basePower + 1;
case UNIT_BAI: return basePower + 2;
case UNIT_QIAN: return basePower + 3;
default: return basePower;
}
// @formatter:on
}
// endregion
// region ========== 预处理逻辑 ==========
/**
* 输入预处理:清洗字符串、提取角分、基础格式校验
* <p>执行顺序:零值判定 → 长度粗筛 → 去前缀 → 负号识别 → 结尾校验 → 去整字 → 提取角分 → 移除元单位
* @param raw 原始输入
* @param config 转换配置
* @return 预处理结果
*/
private static PreprocessResult preprocess(String raw, Config config) {
String input = null;
if (raw == null || (input = raw.trim()).isEmpty()) {
fail(ERR_PARAM, "金额字符串不能为空");
}
// 合法零值快速通道:仅零元整为财务标准写法
if (STR_ZERO_YUAN_ZHENG.equals(input) || "零元正".equals(input)) {
return new PreprocessResult("", BigDecimal.ZERO, false);
}
// 冗余零值拦截:零元零角零分属于不规范写法
if ("零元零角零分".equals(input)) {
fail(ERR_FORMAT, "零金额标准写法为「零元整」,禁止零角零分冗余表述");
}
// 长度粗筛
int maxPureLen = config.isExtendedMode()
? (config.isEightSegmentMode() ? 38 : 32) * 2
: (config.isEightSegmentMode() ? 22 : 14) * 2;
if (input.length() > maxPureLen + 5) {
String mode = config.isExtendedMode() ? "拓展模式" : "标准模式";
fail(ERR_FORMAT, mode + "下输入长度超过最大允许值");
}
// 移除人民币前缀
if (input.startsWith(STR_RMB_PREFIX)) {
input = input.substring(3);
}
// 识别负号
boolean negative = false;
if (!input.isEmpty() && input.charAt(0) == CHN_NEGATIVE) {
negative = true;
input = input.substring(1);
}
if (input.isEmpty()) {
fail(ERR_FORMAT, "未包含有效金额内容");
}
// 结尾合法性校验
char last = input.charAt(input.length() - 1);
if (last != UNIT_JIAO && last != UNIT_FEN && last != CHN_ZHENG && last != CHN_ZHENG_ALT) {
fail(ERR_FORMAT, "金额结尾必须为分/角/整/正:{last}", "last", String.valueOf(last));
}
// 移除整/正后缀
if (last == CHN_ZHENG || last == CHN_ZHENG_ALT) {
if (input.length() < 2) {
fail(ERR_FORMAT, "金额格式非法,缺少有效主体:{last}", "last", String.valueOf(last));
}
char secondLast = input.charAt(input.length() - 2);
if (secondLast != UNIT_YUAN && secondLast != UNIT_JIAO) {
fail(ERR_FORMAT, "整字前必须为元或角:{chars}", "chars", String.valueOf(secondLast) + last);
}
input = input.substring(0, input.length() - 1);
}
// 提取分位:分位数字必须非零(零分无需写出)
BigDecimal decimal = BigDecimal.ZERO;
boolean hasFen = false;
if (input.length() >= 2 && input.charAt(input.length() - 1) == UNIT_FEN) {
char digitChar = input.charAt(input.length() - 2);
if (!DIGIT_VALUE_MAP.containsKey(digitChar) || digitChar == CHN_ZERO) {
fail(ERR_FORMAT, "分位缺少有效数字:{digit}", "digit", String.valueOf(digitChar));
}
int digit = DIGIT_VALUE_MAP.get(digitChar);
decimal = decimal.add(BigDecimal.valueOf(digit).multiply(RATE_FEN));
input = input.substring(0, input.length() - 2);
hasFen = true;
}
// 提取角位:角位数字必须非零(零角需用连接零替代)
boolean hasJiao = false;
if (input.length() >= 2 && input.charAt(input.length() - 1) == UNIT_JIAO) {
char digitChar = input.charAt(input.length() - 2);
if (!DIGIT_VALUE_MAP.containsKey(digitChar) || digitChar == CHN_ZERO) {
fail(ERR_FORMAT, "角位缺少有效数字:{digit}", "digit", String.valueOf(digitChar));
}
int digit = DIGIT_VALUE_MAP.get(digitChar);
decimal = decimal.add(BigDecimal.valueOf(digit).multiply(RATE_JIAO));
input = input.substring(0, input.length() - 2);
hasJiao = true;
}
// 有分无角且有整数部分时,分前必须有连接零
if (hasFen && !hasJiao && !input.isEmpty()) {
if (input.charAt(input.length() - 1) != CHN_ZERO) {
fail(ERR_FORMAT, "非相邻数位间缺少连接零:{unit}", "unit", "分");
}
input = input.substring(0, input.length() - 1);
}
// 移除末尾元单位,剩余为纯整数部分
if (input.endsWith(String.valueOf(UNIT_YUAN))) {
input = input.substring(0, input.length() - 1);
}
return new PreprocessResult(input, decimal, negative);
}
// endregion
// region ========== 正向转换:数字转大写 ==========
/**
* 数字转人民币大写(默认配置)
* @param amount 金额数值
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount) {
return toText(amount, Config.DEFAULT);
}
/**
* 数字转人民币大写(可配置零元前缀)
* @param amount 金额数值
* @param stripZeroYuan 纯小数是否省略零元前缀
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, boolean stripZeroYuan) {
Config config = new Config(stripZeroYuan, false, true, false);
return toText(amount, config);
}
/**
* 数字转人民币大写(可配置零元前缀+拓展模式)
* @param amount 金额数值
* @param stripZeroYuan 纯小数是否省略零元前缀
* @param extended 是否启用拓展大额单位
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, boolean stripZeroYuan, boolean extended) {
Config config = new Config(stripZeroYuan, extended, true, false);
return toText(amount, config);
}
/**
* 数字转人民币大写(全参数配置)
* @param amount 金额数值
* @param stripZeroYuan 纯小数是否省略零元前缀
* @param extended 是否启用拓展大额单位
* @param wanTolerant 是否启用万位零宽容
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, boolean stripZeroYuan,
boolean extended, boolean wanTolerant) {
Config config = new Config(stripZeroYuan, extended, wanTolerant, false);
return toText(amount, config);
}
/**
* 数字转人民币大写(配置驱动核心入口)
* <p>所有配置项严格生效:拓展模式控制范围、分段模式控制进位体系、
* 万位零宽容控制万位零的省略、零元前缀控制纯小数写法
* @param amount 金额数值
* @param config 转换配置
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, Config config) {
if (amount == null) { fail(ERR_PARAM, "金额不能为空"); }
if (BigDecimal.ZERO.compareTo(amount) == 0) { return STR_ZERO_YUAN_ZHENG; }
boolean negative = amount.signum() < 0;
BigDecimal abs = amount.abs().setScale(2, RoundingMode.HALF_UP);
BigDecimal max = config.isExtendedMode() ? EXTENDED_MAX : STANDARD_MAX;
if (abs.compareTo(max) >= 0) {
fail(ERR_RANGE, "金额超出上限:{max}", "max", max.stripTrailingZeros().toPlainString());
}
String result = config.isEightSegmentMode()
? buildText8Seg(abs, config)
: buildText4Seg(abs, config);
return negative ? CHN_NEGATIVE + result : result;
}
/**
* 4位分段模式大写生成
* @param abs 绝对值金额
* @param config 转换配置
* @return 整数部分大写字符串
*/
private static String buildText4Seg(BigDecimal abs, Config config) {
String numStr = abs.toPlainString().replace(".", "");
int totalLen = numStr.length();
int intLen = totalLen - 2;
StringBuilder sb = new StringBuilder(totalLen * 2);
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 = {' ', '万', '亿', '兆', '京', '垓'};
char[] posUnits = {'仟', '佰', '拾', ' '};
boolean zeroPending = false;
boolean intHasValue = false;
boolean prevSegEndWithUnit = false;
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 && prevSegEndWithUnit)) sb.append(CHN_ZERO);
zeroPending = false;
}
sb.append(DIGITS[digit]);
if (p < 3) sb.append(posUnits[p]);
} else {
if (intHasValue) zeroPending = true;
}
}
if (segHasValue && s < segCount - 1) {
sb.append(segUnits[segCount - 1 - s]);
}
// 万位零宽容:万段且开启宽容时,万位为0也视为段尾有单位,抵消连接零
boolean isWanSeg = (segCount - 1 - s) == 1;
prevSegEndWithUnit = segHasValue
&& (intChars[s * 4 + 3] != '0' || (isWanSeg && config.isWanZeroTolerant()));
}
return appendDecimal(sb, numStr, intLen, intHasValue, config.isStripLeadingZeroYuan());
}
/**
* 8位分段模式大写生成
* <p>段内采用双子段结构:高4位万子段 + 万分隔符 + 低4位原子段;
* 万位零宽容规则与4位分段完全一致,受配置统一控制
* @param abs 绝对值金额
* @param config 转换配置
* @return 整数部分大写字符串
*/
private static String buildText8Seg(BigDecimal abs, Config config) {
String numStr = abs.toPlainString().replace(".", "");
int totalLen = numStr.length();
int intLen = totalLen - 2;
StringBuilder sb = new StringBuilder(totalLen * 2);
int padLen = (8 - intLen % 8) % 8;
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 / 8;
char[] largeUnits = {'京', '亿', '元'};
int unitOffset = largeUnits.length - segCount;
boolean zeroPending = false;
boolean intHasValue = false;
boolean prevSegEndWithUnit = false;
for (int s = 0; s < segCount; s++) {
boolean segHasValue = false;
boolean highPartHasValue = false;
for (int p = 0; p < 8; p++) {
int digit = intChars[s * 8 + p] - '0';
if (digit != 0) {
segHasValue = true;
if (p < 4) highPartHasValue = true;
intHasValue = true;
if (zeroPending) {
if (!(p == 0 && prevSegEndWithUnit)) sb.append(CHN_ZERO);
zeroPending = false;
}
sb.append(DIGITS[digit]);
if (p != 3 && p < 7) {
int idx = p < 3 ? p : p - 4;
sb.append("仟佰拾".charAt(idx));
}
} else {
if (intHasValue) zeroPending = true;
}
// 万子段分隔符处理:高子段有值则加万单位
if (p == 3 && highPartHasValue) {
sb.append(UNIT_WAN);
// 万位零宽容:万位为0且开启宽容时,抵消末尾零
boolean wanIsZero = (intChars[s * 8 + 3] == '0');
if (wanIsZero && config.isWanZeroTolerant()) {
zeroPending = false;
}
}
}
if (s < segCount - 1 && segHasValue) {
sb.append(largeUnits[unitOffset + s]);
}
prevSegEndWithUnit = segHasValue && (intChars[s * 8 + 7] != '0');
}
return appendDecimal(sb, numStr, intLen, intHasValue, config.isStripLeadingZeroYuan());
}
/**
* 追加角分部分与整字
* @param sb 整数部分构建器
* @param numStr 全量数字字符串
* @param intLen 整数部分长度
* @param intHasValue 整数部分是否有非零值
* @param stripZeroYuan 是否省略零元前缀
* @return 完整大写字符串
*/
private static String appendDecimal(StringBuilder sb, String numStr,
int intLen, boolean intHasValue, boolean stripZeroYuan) {
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) {
sb.append(CHN_ZERO).append(DIGITS[fen]).append(UNIT_FEN);
} else if (fen == 0) {
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) {
sb.append(STR_ZERO_YUAN_ZHENG);
} else if (jiao == 0) {
if (!stripZeroYuan) sb.append("零元").append(CHN_ZERO);
sb.append(DIGITS[fen]).append(UNIT_FEN);
} else if (fen == 0) {
if (!stripZeroYuan) sb.append("零元");
sb.append(DIGITS[jiao]).append(UNIT_JIAO);
} else {
if (!stripZeroYuan) sb.append("零元");
sb.append(DIGITS[jiao]).append(UNIT_JIAO)
.append(DIGITS[fen]).append(UNIT_FEN);
}
}
return sb.toString();
}
// endregion
// region ========== 反向转换:大写转数字 ==========
/**
* 人民币大写转数值(默认配置)
* @param chinese 大写金额字符串
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
*/
public static BigDecimal toAmount(String chinese) {
return toAmount(chinese, Config.DEFAULT);
}
/**
* 人民币大写转数值(可配置拓展模式)
* @param chinese 大写金额字符串
* @param extendedMode 是否启用拓展大额单位
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
*/
public static BigDecimal toAmount(String chinese, boolean extendedMode) {
Config config = new Config(true, extendedMode, true, false);
return toAmount(chinese, config);
}
/**
* 人民币大写转数值(配置驱动核心入口)
* <p>采用倒序扫描 + 分层校验架构:外层调度大段,内核负责段内语法解析;
* 所有校验规则严格跟随配置:拓展单位、分段体系、万位零宽容均同步生效
* @param chinese 大写金额字符串
* @param config 转换配置
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
*/
public static BigDecimal toAmount(String chinese, Config config) {
PreprocessResult pre = preprocess(chinese, config);
String intPart = pre.intPart;
BigDecimal total = pre.decimalValue;
boolean negative = pre.negative;
if (!intPart.isEmpty()) {
int threshold = config.getLargeSegmentThreshold();
int subSegBase = UNIT_LEVEL_MAP.get(UNIT_WAN);
SegmentParser parser = new SegmentParser(subSegBase, config.isWanZeroTolerant());
parser.reset(UNIT_YUAN);
char lastLargeUnit = UNIT_YUAN;
int lastLargeUnitLevel = UNIT_LEVEL_MAP.get(UNIT_YUAN);
final int len = intPart.length();
for (int i = len - 1; i >= 0; i--) {
char ch = intPart.charAt(i);
char leftChar = i > 0 ? intPart.charAt(i - 1) : Character.MIN_VALUE;
boolean isUnit = UNIT_LEVEL_MAP.containsKey(ch);
if (isUnit && isLargeSegmentUnit(ch, threshold)) {
// 模式兼容性校验
if (config.isEightSegmentMode() && (ch == UNIT_ZHAO || ch == UNIT_GAI)) {
fail(ERR_UNIT, "8位模式下不支持该单位:{unit}", "unit", String.valueOf(ch));
}
if (!config.isExtendedMode() && isExtendedUnit(ch)) {
fail(ERR_UNIT, "标准模式不支持拓展大额单位:{unit}", "unit", String.valueOf(ch));
}
// 全局大段单调性校验(严格递增 = 正序严格递减)
int curLargeUnitLevel = UNIT_LEVEL_MAP.get(ch);
if (curLargeUnitLevel <= lastLargeUnitLevel) {
Map<String, String> entities = new HashMap<String, String>(4);
entities.put("curr", String.valueOf(ch));
entities.put("prev", String.valueOf(lastLargeUnit));
fail(ERR_UNIT, "大段单位语序非法,{curr} 不能出现在 {prev} 前面", i, entities);
}
lastLargeUnitLevel = curLargeUnitLevel;
// 大段单位左侧合法性校验
boolean leftIllegal = (i == 0)
|| leftChar == CHN_ZERO
|| isLargeSegmentUnit(leftChar, threshold);
if (leftIllegal) {
Map<String, String> entities = new HashMap<String, String>(2);
entities.put("unit", String.valueOf(ch));
fail(ERR_UNIT, "大段单位左侧语法错误:{unit}", i, entities);
}
// 大段间连接零校验(基于幂次差判断数位相邻性)
if (parser.hasNonZero()) {
int lowMaxPower = getBasePower(lastLargeUnit) + parser.getMaxPower();
int highMinPower = calcHighSegMinNonZeroPower(ch, i, intPart);
boolean adjacent = (highMinPower - lowMaxPower == 1);
boolean hasBetweenZero = (i + 1 < len) && (intPart.charAt(i + 1) == CHN_ZERO);
if (!adjacent && !hasBetweenZero) {
// 万位零宽容:严格对齐《支付结算办法》
// 仅万位为0、千位非0、且开启宽容时,方可豁免连接零
boolean wanTolerant = config.isWanZeroTolerant()
&& ch == UNIT_WAN
&& lastLargeUnit == UNIT_YUAN
&& highMinPower - lowMaxPower == 2
&& parser.getMaxPower() == 3;
if (!wanTolerant) {
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(ch));
fail(ERR_FORMAT, "非相邻大段间缺少连接零:{unit}", i, entities);
}
}
}
// 结算当前低位段
BigDecimal segVal = BigDecimal.valueOf(parser.getValue());
total = total.add(segVal.multiply(getLargeSegmentRate(lastLargeUnit)));
// 状态重置,进入高位段解析
lastLargeUnit = ch;
parser.reset(ch);
continue;
}
// 兜底低位单位校验:整数段禁止出现元/角/分
if (ch == UNIT_YUAN || ch == UNIT_JIAO || ch == UNIT_FEN) {
String msg = ch == UNIT_YUAN
? "元单位仅可出现在金额末尾,禁止在整数段中间重复出现"
: "整数部分禁止包含角/分小数单位,角分必须位于金额末尾";
Map<String, String> entities = new HashMap<>(2);
entities.put("unit", String.valueOf(ch));
fail(ERR_UNIT, msg, i, entities);
}
// 普通字符交由段内解析器处理
parser.processChar(ch, leftChar, i);
}
// 结算最高位大段
BigDecimal topSegVal = BigDecimal.valueOf(parser.getValue());
total = total.add(topSegVal.multiply(getLargeSegmentRate(lastLargeUnit)));
}
// 范围校验
BigDecimal max = config.isExtendedMode() ? EXTENDED_MAX : STANDARD_MAX;
if (total.compareTo(max) >= 0) {
fail(ERR_RANGE, "解析金额超出上限:{max}", "max", max.stripTrailingZeros().toPlainString());
}
if (negative) total = total.negate();
return total.setScale(2, RoundingMode.HALF_UP);
}
// endregion
// region ========== 本地测试入口 ==========
/**
* 本地测试入口
* @param args 命令行参数
*/
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("1_0000_2000.34: " + toText(new BigDecimal("100002000.34"), Config.EIGHT_SEG));
System.out.println("100_1000_0100_2000.34: " + toText(new BigDecimal("100100001002000.34"), Config.EIGHT_SEG.withExtendedMode(true)));
System.out.println("\n===== 角分零校验 =====");
System.out.println("壹元零壹分(合法): " + toAmount("壹元零壹分"));
testIllegal("壹元壹分", "壹元壹分(非法,缺零)");
testIllegal("零元零角零分", "零元零角零分(非法,零角零分冗余)");
System.out.println("\n===== 大段连接零校验 =====");
System.out.println("壹亿壹仟万元整(合法,相邻): " + toAmount("壹亿壹仟万元整"));
System.out.println("壹亿零壹万元整(合法,有零): " + toAmount("壹亿零壹万元整"));
testIllegal("壹亿壹万元整", "壹亿壹万元整(非法,缺零)");
System.out.println("\n===== 单位单调性校验 =====");
testIllegal("壹角零壹万元整", "壹角零壹万元整(非法,低位单位混入整数段)");
testIllegal("壹万壹万零壹亿零壹万元整", "壹万壹万零壹亿零壹万元整(非法,段内万重复)", Config.EIGHT_SEG);
System.out.println("\n===== 万位零宽容校验 =====");
System.out.println("壹拾万壹仟元整(宽容模式合法): " + toAmount("壹拾万壹仟元整", Config.DEFAULT));
testIllegal("壹拾万零壹仟元整", "壹拾万零壹仟元整(严格模式非法)", Config.STRICT);
System.out.println("\n===== 8位模式测试 =====");
System.out.println("8位单段: " + toAmount("壹仟贰佰叁拾肆万伍仟陆佰柒拾捌元玖角", Config.EIGHT_SEG));
System.out.println("8位跨段合法: " + toAmount("壹亿零壹元整", Config.EIGHT_SEG));
System.out.println("===== 正向生成校验 =====");
System.out.println("107000.53 默认宽容: " + toText(new BigDecimal("107000.53")));
System.out.println("107000.53 严格模式: " + toText(new BigDecimal("107000.53"), Config.STRICT));
System.out.println("10001.00 默认模式: " + toText(new BigDecimal("10001.00")));
System.out.println("\n===== 反向解析校验 =====");
System.out.println("壹拾万柒仟元伍角叁分(宽容合法): " + toAmount("壹拾万柒仟元伍角叁分"));
testIllegal("壹拾万柒仟元伍角叁分", "严格模式-壹拾万柒仟(应拦截)", Config.STRICT);
testIllegal("壹万壹元整", "宽容模式-壹万壹元(应拦截)", Config.DEFAULT);
System.out.println("\n===== 8位分段万位零 =====");
System.out.println("107000.53 8位宽容: " + toText(new BigDecimal("107000.53"), Config.EIGHT_SEG));
System.out.println("107000.53 8位严格: " + toText(new BigDecimal("107000.53"), Config.EIGHT_SEG.withWanZeroTolerant(false)));
}
private static void testIllegal(String input, String label) {
testIllegal(input, label, Config.DEFAULT);
}
private static void testIllegal(String input, String label, Config config) {
try {
toAmount(input, config);
System.out.println(label + ": 未拦截,异常!");
} catch (AmountParseException e) {
System.out.println(label + ": 拦截成功 - " + e.getMessage());
}
}
// endregion
}
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;
/**
* 人民币金额大写增强包装器
* <p>采用 Wrapper 模式,严格遵循开闭原则,仅做兼容增强,不侵入核心转换逻辑。
*
* <h3>增强能力</h3>
* <ul>
* <li>小写汉字归一化:支持一/二/三等小写数字、十/百/千等小写单位自动转为大写</li>
* <li>复合单位归一化:万亿/万万亿自动归一为兆/京(4位分段模式下)</li>
* <li>友好异常转换:将核心类结构化异常转为用户友好的提示文案</li>
* <li>往返校验:解析后重新生成大写,与原输入比对,保障语义一致</li>
* <li>合法性校验:一键判断输入是否为合规的大写金额</li>
* </ul>
*
* <p>线程安全:全静态无状态设计,可并发调用。
*
* @since JDK 1.8
*/
public final class ChineseCurrencyEnhancerV2 {
/**
* 私有构造:禁止实例化工具类
*/
private ChineseCurrencyEnhancerV2() {
throw new UnsupportedOperationException("工具类禁止实例化");
}
// region ========== 兼容映射表 ==========
/**
* 单字符替换映射:小写数字、单位转大写
*/
private static final char[][] CHAR_REPLACE_MAPPING = {
{'圆', '元'},
{'〇', '零'}, {'一', '壹'}, {'二', '贰'}, {'三', '叁'},
{'四', '肆'}, {'五', '伍'}, {'六', '陆'}, {'七', '柒'},
{'八', '捌'}, {'九', '玖'},
{'十', '拾'}, {'百', '佰'}, {'千', '仟'}
};
/**
* 正则替换规则:复杂句式归一化
*/
private static final String[][] REGEX_REPLACE_RULES = {
{"^拾(?=[万亿兆京兆垓元角分])", "壹拾"},
{"两(?=[拾佰仟万亿元角分])", "贰"},
{"(?<=[壹贰叁肆伍陆柒捌玖拾佰仟])万万亿", "京"},
{"(?<=[壹贰叁肆伍陆柒捌玖拾佰仟])万亿", "兆"}
};
/**
* 单位异常口语化映射:异常提示中大额单位转常用表述
*/
private static final String[][] UNIT_EXCEPTION_MAPPING = {
{"兆", "万亿"}, {"京", "万万亿"}, {"垓", "万京"}
};
/** 错误码:与核心类严格对应 */
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";
/**
* 友好异常模板:命名占位符与核心类实体键名严格对齐
*/
private static final Map<String, String> FRIENDLY_TEMPLATES;
static {
Map<String, String> map = new HashMap<>();
map.put(ERR_PARAM, "金额不能为空");
map.put(ERR_FORMAT, "输入格式不符合规范");
map.put(ERR_UNIT, "单位格式不符合规范");
map.put(ERR_RANGE, "金额超出支持范围");
FRIENDLY_TEMPLATES = Collections.unmodifiableMap(map);
}
// endregion
// region ========== 预编译常量 ==========
/**
* 预编译正则规则列表
*/
private static final List<Map.Entry<Pattern, String>> REGEX_RULES;
static {
List<Map.Entry<Pattern, String>> list = new ArrayList<>(REGEX_REPLACE_RULES.length);
for (String[] rule : REGEX_REPLACE_RULES) {
list.add(new AbstractMap.SimpleEntry<>(
Pattern.compile(rule[0]), rule[1]));
}
REGEX_RULES = Collections.unmodifiableList(list);
}
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final Pattern CMP_PREFIX_PATTERN = Pattern.compile("^人民币");
private static final Pattern CMP_SUFFIX_PATTERN = Pattern.compile("[整正]$");
// endregion
// region ========== 正向转换透传 ==========
/**
* 数字转人民币大写(默认配置)
* @param amount 金额数值
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount) {
return ChineseCurrencyConverterV2.toText(amount);
}
/**
* 数字转人民币大写(可配置零元前缀)
* @param amount 金额数值
* @param stripZeroYuan 纯小数是否省略零元前缀
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, boolean stripZeroYuan) {
return ChineseCurrencyConverterV2.toText(amount, stripZeroYuan);
}
/**
* 数字转人民币大写(可配置零元前缀+拓展模式)
* @param amount 金额数值
* @param stripZeroYuan 纯小数是否省略零元前缀
* @param extended 是否启用拓展大额单位
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, boolean stripZeroYuan, boolean extended) {
return ChineseCurrencyConverterV2.toText(amount, stripZeroYuan, extended);
}
/**
* 数字转人民币大写(全参数配置)
* @param amount 金额数值
* @param stripZeroYuan 纯小数是否省略零元前缀
* @param extended 是否启用拓展大额单位
* @param wanTolerant 是否启用万位零宽容
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, boolean stripZeroYuan,
boolean extended, boolean wanTolerant) {
return ChineseCurrencyConverterV2.toText(amount, stripZeroYuan, extended, wanTolerant);
}
/**
* 数字转人民币大写(配置驱动入口)
* @param amount 金额数值
* @param config 转换配置
* @return 大写金额字符串
* @throws IllegalArgumentException 参数为空或超出范围时抛出
*/
public static String toText(BigDecimal amount, ChineseCurrencyConverterV2.Config config) {
return ChineseCurrencyConverterV2.toText(amount, config);
}
// endregion
// region ========== 反向增强转换 ==========
/**
* 大写金额转数值(默认配置,无往返校验)
* @param chinese 大写金额字符串
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
*/
public static BigDecimal toAmount(String chinese) {
return toAmount(chinese, false, false);
}
/**
* 大写金额转数值(可配置拓展模式,无往返校验)
* @param chinese 大写金额字符串
* @param extendedMode 是否启用拓展大额单位
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、超出范围时抛出
*/
public static BigDecimal toAmount(String chinese, boolean extendedMode) {
return toAmount(chinese, extendedMode, false);
}
/**
* 大写金额转数值(可配置拓展模式与往返校验)
* @param chinese 大写金额字符串
* @param extendedMode 是否启用拓展大额单位
* @param roundTripVerify 是否开启往返语义校验
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、校验不通过时抛出
*/
public static BigDecimal toAmount(String chinese, boolean extendedMode, boolean roundTripVerify) {
return toAmount(chinese, ChineseCurrencyConverterV2.Config.DEFAULT
.withExtendedMode(extendedMode), roundTripVerify);
}
/**
* 大写金额转数值(配置驱动核心入口)
* @param chinese 大写金额字符串
* @param config 转换配置
* @param roundTripVerify 是否开启往返语义校验
* @return 金额数值(保留2位小数)
* @throws IllegalArgumentException 格式错误、单位非法、校验不通过时抛出
*/
public static BigDecimal toAmount(String chinese,
ChineseCurrencyConverterV2.Config config,
boolean roundTripVerify) {
String normalized = normalizeInput(chinese, config);
BigDecimal result;
try {
result = ChineseCurrencyConverterV2.toAmount(normalized, config);
} catch (IllegalArgumentException e) {
String friendlyMsg = translateException(e, config);
throw new IllegalArgumentException(friendlyMsg, e);
}
if (roundTripVerify) {
verifyRoundTrip(normalized, result, config);
}
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) {
return isValid(chinese, ChineseCurrencyConverterV2.Config.DEFAULT
.withExtendedMode(extendedMode));
}
/**
* 校验大写金额是否合法(配置驱动)
* @param chinese 大写金额字符串
* @param config 转换配置
* @return 合法返回true,非法返回false
*/
public static boolean isValid(String chinese, ChineseCurrencyConverterV2.Config config) {
try {
toAmount(chinese, config, true);
return true;
} catch (Exception e) {
return false;
}
}
// endregion
// region ========== 内部核心方法 ==========
/**
* 输入归一化:去空格、字符替换、复合单位归一
* @param raw 原始输入
* @param config 转换配置
* @return 归一化后的标准大写字符串
*/
private static String normalizeInput(String raw, ChineseCurrencyConverterV2.Config config) {
if (raw == null) return null;
String result = raw;
result = WHITESPACE_PATTERN.matcher(result).replaceAll("");
for (char[] mapping : CHAR_REPLACE_MAPPING) {
result = result.replace(mapping[0], mapping[1]);
}
// 8位模式下禁用复合单位归一(万亿是合法段内写法)
if (!config.isEightSegmentMode()) {
for (Map.Entry<Pattern, String> rule : REGEX_RULES) {
result = rule.getKey().matcher(result).replaceAll(rule.getValue());
}
}
return result;
}
/**
* 异常消息转换:结构化异常转友好提示
* @param e 原始异常
* @param config 转换配置项
* @return 友好异常消息
*/
private static String translateException(Exception e, ChineseCurrencyConverterV2.Config config) {
// 优先读取结构化异常,零正则,高可靠
if (e instanceof ChineseCurrencyConverterV2.AmountParseException) {
ChineseCurrencyConverterV2.AmountParseException pe = (ChineseCurrencyConverterV2.AmountParseException) e;
String code = pe.getErrorCode();
Map<String, String> entities = new HashMap<>(pe.getEntities());
// 单位名称口语化转换:8位模式下禁用复合单位归一(万亿是合法段内写法)
if (!config.isEightSegmentMode() && ERR_UNIT.equals(code)) {
for (Map.Entry<String, String> entry : entities.entrySet()) {
String value = entry.getValue();
for (String[] mapping : UNIT_EXCEPTION_MAPPING) {
value = value.replace(mapping[0], mapping[1]);
}
entry.setValue(value);
}
}
// 匹配友好模板
String template = FRIENDLY_TEMPLATES.get(code);
String friendlyMsg = template != null ? template : pe.getMessage();
// 填充命名实体
for (Map.Entry<String, String> entry : entities.entrySet()) {
friendlyMsg = friendlyMsg.replace("{" + entry.getKey() + "}", entry.getValue());
}
// 追加位置信息
if (pe.getPosition() >= 0) {
friendlyMsg += "(第" + (pe.getPosition() + 1) + "个字附近)";
}
return friendlyMsg;
}
// Fallback:非结构化异常直接返回原消息
return e.getMessage();
}
/**
* 往返语义校验:解析后重新生成大写,与原输入比对
* @param original 原始归一化输入
* @param value 解析出的数值
* @param config 转换配置
* @throws IllegalArgumentException 校验不通过时抛出
*/
private static void verifyRoundTrip(String original, BigDecimal value,
ChineseCurrencyConverterV2.Config config) {
String roundTrip = ChineseCurrencyConverterV2.toText(value,
config.withStripLeadingZeroYuan(false));
String src = normalizeForCompare(original);
String target = normalizeForCompare(roundTrip);
if (!src.equals(target)) {
throw new IllegalArgumentException("金额语义校验不通过,解析结果与原输入不一致");
}
}
/**
* 归一化对比字符串:移除前缀、后缀,统一对比基准
* @param text 原始文本
* @return 归一化后的对比文本
*/
private static String normalizeForCompare(String text) {
String result = text;
result = CMP_PREFIX_PATTERN.matcher(result).replaceAll("");
result = CMP_SUFFIX_PATTERN.matcher(result).replaceAll("");
return result;
}
// endregion
/**
* 本地测试入口
* @param args 命令行参数
*/
public static void main(String[] args) {
System.out.println("===== 小写兼容测试 =====");
System.out.println("一千二百三十四万五千六百七十八元五角: "
+ toAmount("一千二百三十四万五千六百七十八元五角",
ChineseCurrencyConverterV2.Config.EIGHT_SEG, false));
System.out.println("一千二百三十四万五千六百七十八元〇五分: "
+ toAmount("一千二百三十四万五千六百七十八元〇五分",
ChineseCurrencyConverterV2.Config.EIGHT_SEG, false));
}
}