【Java】ChineseCurrencyConverter(货币金额大写转换工具类)

货币金额大写转换工具类

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
}
posted @ 2026-06-26 07:45  XKIND  阅读(23)  评论(0)    收藏  举报