% \iffalse meta-comment
% !TEX program  = XeLaTeX
%<*internal>
\iffalse
%</internal>
%<*readme>
zhnumber
========

The `zhnumber` package provides commands to typeset Chinese representations of
numbers. The main difference between this package and `CJKnumb` is that commands
provided by this package are expandable in the proper way. So, it seems that
zhnumber is a good alternative to `CJKnumb` package.

Basic Usage
-----------
The package provides the following macros:

    \zhnumber{<number>}

Convert `<number>` to a full Chinese representation.

    \zhnum{<counter>}

Similar to `\arabic{<counter>}`, but representation of `<counter>`
as Chinese numerals.

    \zhdigits{<number>}
    \zhdigits*{<number>}

Handle `<number>` as a string of digits and convert each of them into the
corresponding Chinese digit. The unstarred version uses the Chinese circle glyph
(〇) for digit zero; the starred version uses the traditional glyph (零).

    \zhrod{<number>}
    \zhrodbox{<number>}

Represent `<number>` with counting-rod numerals (U+1D360 to U+1D371). `\zhrod`
is expandable and only produces the characters, so it can be used in the table
of contents and PDF bookmarks; `\zhrodbox` is not expandable and also selects the
font and tightens the spacing. Both need a font that covers these code points;
see `\zhrodsetup` in the manual.

You can read the package manual (in Chinese) for more detailed explanations.

Contributing
------------

This package is a part of the [ctex-kit](https://github.com/CTeX-org/ctex-kit) project.

Issues and pull requests are welcome.

Copyright and Licence
---------------------

    Copyright (C) 2012, 2014-2020, 2022 by Qing Lee <sobenlee@gmail.com>
    Copyright (C) 2026 by Liam Huang <liamhuang0205@gmail.com>
    ----------------------------------------------------------------------

    This work may be distributed and/or modified under the
    conditions of the LaTeX Project Public License, either
    version 1.3c of this license or (at your option) any later
    version. This version of this license is in
       http://www.latex-project.org/lppl/lppl-1-3c.txt
    and the latest version of this license is in
       http://www.latex-project.org/lppl.txt
    and version 1.3 or later is part of all distributions of
    LaTeX version 2005/12/01 or later.

    This work has the LPPL maintenance status "maintained".

    The Current Maintainer of this work is Liam Huang.

    This package consists of the file  zhnumber.dtx,
                 and the derived files zhnumber.pdf,
                                       zhnumber.sty,
                                       zhnumber-utf8.cfg,
                                       zhnumber-gbk.cfg,
                                       zhnumber-big5.cfg,
                                       zhnumber.ins and
                                       README.md (this file).
%</readme>
%<*internal>
\fi
\begingroup
  \def\temp{LaTeX2e}
\expandafter\endgroup\ifx\temp\fmtname\else
\csname fi\endcsname
%</internal>
%<*install>

\input ctxdocstrip %

\preamble

    Copyright (C) 2012, 2014-2020, 2022 by Qing Lee <sobenlee@gmail.com>
    Copyright (C) 2026 by Liam Huang <liamhuang0205@gmail.com>
--------------------------------------------------------------------------

    This work may be distributed and/or modified under the
    conditions of the LaTeX Project Public License, either
    version 1.3c of this license or (at your option) any later
    version. This version of this license is in
       http://www.latex-project.org/lppl/lppl-1-3c.txt
    and the latest version of this license is in
       http://www.latex-project.org/lppl.txt
    and version 1.3 or later is part of all distributions of
    LaTeX version 2005/12/01 or later.

    This work has the LPPL maintenance status "maintained".

    The Current Maintainer of this work is Liam Huang.

--------------------------------------------------------------------------

\endpreamble
\postamble

    This package consists of the file  zhnumber.dtx,
                 and the derived files zhnumber.pdf,
                                       zhnumber.sty,
                                       zhnumber-utf8.cfg,
                                       zhnumber-gbk.cfg,
                                       zhnumber-big5.cfg,
                                       zhnumber.ins and
                                       README.md.
\endpostamble

\generate
  {
%</install>
%<*internal>
    \usedir{source/latex/zhnumber}
    \file{zhnumber.ins}             {\from{\jobname.dtx}{install}}
%</internal>
%<*install>
    \usedir{tex/latex/zhnumber}
    \file{zhnumber.sty}             {\from{\jobname.dtx}{package}}
    \usedir{tex/latex/zhnumber/config}
    \file{zhnumber-utf8.cfg}        {\from{\jobname.dtx}{config,utf8}}
 \ctxfile{Big5}{zhnumber-big5.cfg}  {\from{\jobname.dtx}{config,big5}}
 \ctxfile{GBK}{zhnumber-gbk.cfg}    {\from{\jobname.dtx}{config,gbk}}
    \nopreamble\nopostamble
    \usedir{doc/latex/zhnumber}
    \file{README.md}                {\from{\jobname.dtx}{readme}}
  }

\endbatchfile
%</install>
%<*internal>
\fi
%</internal>
%<package>\NeedsTeXFormat{LaTeX2e}
%<package>\RequirePackage{expl3}
%<+package|config>\GetIdInfo$Id: zhnumber.dtx f6b3a4f4 2026-09-27 17:13:57 +0800 Liam <liamhuang0205@gmail.com> $
%<package>  {Typesetting numbers with Chinese glyphs}
%<config&utf8>  {Chinese numerals with UTF8 encoding}
%<config&big5>  {Chinese numerals with Big5 encoding}
%<config&gbk>  {Chinese numerals with GBK encoding}
%<package>\ProvidesExplPackage{\ExplFileName}
%<config&utf8>\ProvidesExplFile{\ExplFileName-utf8.cfg}
%<config&big5>\ProvidesExplFile{\ExplFileName-big5.cfg}
%<config&gbk>\ProvidesExplFile{\ExplFileName-gbk.cfg}
%<package|config>  {\ExplFileDate}{3.1}{\ExplFileDescription}
%<*driver>
\documentclass{ctxdoc}
\setCJKmainfont{Noto Serif CJK SC}[
  Language   = Chinese Simplified,
  ItalicFont = LXGWWenKaiGBLite-Regular.ttf
]
\setCJKsansfont{Noto Sans CJK SC}[Language=Chinese Simplified]
\setCJKmonofont{LXGWZhuqueFangsong-Regular.ttf}[Language=Chinese Simplified]
%% 朱雀仿宋没有「卌」「叄」（实现部分代码清单里的古体 40 与繁体大写 3），缺字时改用
%% 文楷补上；只给等宽字族配后备字体，正文与无衬线字体不受 AutoFallBack 影响。
\xeCJKsetup{AutoFallBack=true}
\setCJKfallbackfamilyfont{\CJKttdefault}{LXGWWenKaiGBLite-Regular.ttf}
%% 手册正文用的 Noto Serif CJK 没有算筹字形，所以另声明一个字体族给算筹的例子用。
%% 这也正是手册里推荐给用户的做法（font= 选项），见 \zhrodsetup 的 font 一节。
\newfontfamily\RodFont{LXGWWenKaiGBLite-Regular.ttf}
\SideBySideExampleSet{xrightmargin=.6\linewidth}
\begin{document}
  \DocInput{\jobname.dtx}
  \IndexLayout
  \PrintIndex
\end{document}
%</driver>
% \fi
%
% \GetFileId{zhnumber.sty}
%
% \title{\bfseries\pkg{zhnumber} 宏包}
% \author{李清\thanks{\href{mailto:sobenlee@gmail.com}{sobenlee@gmail.com}}\quad 黄晨成\thanks{\href{mailto:liamhuang0205@gmail.com}{liamhuang0205@gmail.com}}}
% \date{\filedate\qquad\fileversion\thanks{\ctexkitrev{\ExplFileVersion}.}}
% \maketitle
%
% \begin{documentation}
%
% \section{简介}
% \pkg{zhnumber} 宏包用于将阿拉伯数字按照中文格式输出。相比于 \pkg{CJKnumb}，它提供
% 的四个格式转换命令 \tn{zhnumber}，\tn{zhdigits}、\tn{zhnum} 和 \tn{zhdig}
% 都是可以适当展开的，可以正常使用于 |PDF| 书签和交叉引用。
%
% \pkg{zhnumber} 支持 |GBK|，|Big5| 和 |UTF8| 编码，依赖 \LaTeXiii{} 项目的
% \pkg{expl3}，\pkg{xparse} 和 \pkg{l3keys2e} 宏包。
%
% 宏包最初由李清编写和维护，并纳入 ctex-kit 项目管理。
% 自 2022 年至 2026 年，社区已与李清失联逾四年。
% 考虑到这段时间陆续有相关缺陷反馈和功能请求，
% 与 CTAN 联络后，社区决定接手维护该宏包。
% 自 2026 年 6 月起，由黄晨成实际维护。
%
% \section{使用方法}
%
% \begin{function}[updated=2022-07-10]{encoding}
%   \begin{syntax}
%     encoding = <GBK|Big5|(UTF8)>
%   \end{syntax}
%   用于指定编码的宏包选项，可以在调用宏包的时候设定，也可以用 \tn{zhnumsetup}
%   在导言区内设定。对于 \upLaTeX、\XeLaTeX{} 和 \LuaLaTeX，不用指定编码，宏包将
%   自动使用 |UTF8| 编码。只有 \LaTeX{} 和 \pdfLaTeX{} 需要指定编码，如果没有指定，
%   默认也使用 |UTF8|。
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhnumber}
%   \begin{syntax}
%     \tn{zhnumber} \Arg{number}
%   \end{syntax}
%   以中文格式输出数字。这里的数字可以是整数、小数和分数。例如
%   \begin{SideBySideExample}
%     \zhnumber{2012020120}\\
%     \zhnumber{2 012 020 120}\\
%     \zhnumber{2,012,020,120}\\
%     \zhnumber{2012.020120}\\
%     \zhnumber{2012.}\\
%     \zhnumber{.2012}\\
%     \zhnumber{20120/20120}\\
%     \zhnumber{/2012}\\
%     \zhnumber{2012/}\\
%     \zhnumber{201;2020/120}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhdigits}
%   \begin{syntax}
%     \tn{zhdigits}   \Arg{number}
%     \tn{zhdigits} * \Arg{number}
%   \end{syntax}
%   将阿拉伯数字转换为中文数字串。缺省状态下，\tn{zhdigits} 将 0 映射为〇，如果需要
%   将其映射为零，可以使用带星号的形式。例如
%   \begin{SideBySideExample}
%     \zhdigits{2012020120}\\
%     \zhdigits*{2012020120}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, added=2026-08-06]{\zhrod}
%   \begin{syntax}
%     \tn{zhrod} \Arg{number}
%   \end{syntax}
%   将阿拉伯数字转换为\emph{算筹}数字。算筹是中国古代的记数符号，用纵横两式的筹码
%   交替表示各位数字：个位一式、十位另一式、百位又回到第一式，依此交替。
%
%   \tn{zhrod} 只产出字符，不切换字体、不调整字距，因此它是可展开的，可以写进
%   \file{.toc} 一类辅助文件，也可以放进 PDF 书签。需要压紧字距时用
%   \tn{zhrodbox}（见下）。
%
%   下面的例子假定当前字体含算筹字形，做法见下文 \texttt{font} 选项。
%   \begin{SideBySideExample}
%     \RodFont
%     \zhrod{12345}\\
%     \zhrod{-12030.405}
%   \end{SideBySideExample}
%
%   小数部分继续交替：分位与个位异式，厘位与个位同式。负数默认在最前排一个＼，
%   理由见下文 \texttt{minus} 选项。
%
%   \tn{zhrod} 只认数字、一个前导正负号和一个小数点。其余字符原样输出，第二个小数点
%   之后的部分会被丢掉（\verb|\zhrod{1.2.3}| 只排出 \texttt{1.2} 那部分）——它是数字
%   排印命令，不做输入校验，请自己保证传进来的是数字。
%
%   \textbf{参数里不能带分组。}\tn{zhrod} 要按每一位数字距个位有多远来选纵式还是横式，
%   而一个花括号分组在计数时算作一位，于是各位的纵横会整体错开。传进去的必须是不带
%   花括号的数字串：\verb|\zhrod{1{23}4}| 会把花括号原样排出来，而
%   \verb|\zhrod{12\ver}|（其中 \verb|\ver| 展开为 \verb|{2026}|）会得到纵横\emph{全部
%   相反}的一串筹码，看上去仍像正常的算筹却是错的。若数字来自宏，请让它展开为不带分组
%   的数字串（\verb|\def\ver{2026}| 而不是 \verb|\def\ver{{2026}}|）。
%
%   这一点与 \tn{zhdigits} 不同：后者逐位取字、不需要知道位置，所以分组对它无害。
%
%   分组里若还含非数字，后果更严重：\verb|\zhrod{1{ab}4}| 会报多条错误并以非零状态退出
%   （不只是排出多余字符）。实测 \TeX{} 错误为四条（两条 \texttt{Missing number}、一条
%   \texttt{Missing = inserted for \cs{ifnum}}、一条 \texttt{You can't use \cs{numexpr}
%   in horizontal mode}），此外还会有若干条缺字警告，条数随字体与引擎而变，因此这里不写
%   死总数。同属上面那条限制，处置方式一样：不要给它带分组的参数。
% \end{function}
%
% \begin{function}[added=2026-08-06]{\zhrodbox}
%   \begin{syntax}
%     \tn{zhrodbox} \Arg{number}
%   \end{syntax}
%   与 \tn{zhrod} 输出相同的筹码，但会切换到 \texttt{font} 指定的字体，并在相邻筹码
%   之间压紧字距。
%
%   它\emph{不可展开}，因此\textbf{不能}写进辅助文件或 PDF 书签——写进去会在下次编译
%   读回时留下命令名而不是筹码。要用在标题一类会写进目录的位置，请改用 \tn{zhrod}。
%
%   \begin{SideBySideExample}
%     \zhrodsetup{font=\RodFont}
%     \zhrodbox{12345}\\
%     {\RodFont\zhrod{12345}}
%   \end{SideBySideExample}
%
%   上例中两行内容相同，上行压紧了字距。实测 \texttt{12345} 未压缩时宽 50.0\,pt，
%   压缩后 44.4\,pt——五位筹码之间有四处字距，每处 $-1.4$\,pt。
% \end{function}
%
% \begin{function}[added=2026-08-06]{\zhrodsetup}
%   \begin{syntax}
%     \tn{zhrodsetup} \Arg{key-value list}
%   \end{syntax}
%   设置算筹相关选项。算筹的选项独立于 \tn{zhnumsetup}：后者管的是「每个数字写成哪个
%   汉字」（字形与编码），算筹管的是「按位值选哪一式筹码」，两者不通用。
%
%   与 \tn{zhnumsetup} 一样，它设置的是\emph{局部}变量，因此受 \TeX{} 分组约束：写在
%   分组或环境里只在其中有效，写在导言区或分组外则此后一直有效。上面各例都靠这一点
%   连续切换设置。
% \end{function}
%
% \begin{function}[added=2026-08-06]{units}
%   \begin{syntax}
%     units = <(vertical)|horizontal>
%   \end{syntax}
%   指定\emph{个位}用哪一式，其余各位由交替推出。\texttt{vertical} 是个位用纵式
%   （即传统所说的「一纵十横」，较常见），\texttt{horizontal} 是个位用横式。
%
%   \begin{SideBySideExample}
%     \RodFont
%     \zhrodsetup{units=vertical}
%     \zhrod{123456789}\\
%     \zhrodsetup{units=horizontal}
%     \zhrod{123456789}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[added=2026-08-06]{zero}
%   \begin{syntax}
%     zero = <(fill)|omit>
%   \end{syntax}
%   算筹本无零的筹码。默认（\texttt{fill}）在零的位置排一个〇占位；\texttt{omit}
%   则跳过零，靠纵横交替体现空位。
%
%   \begin{SideBySideExample}
%     \RodFont
%     \zhrodsetup{zero=fill}
%     \zhrod{12030}\\
%     \zhrodsetup{zero=omit}
%     \zhrod{12030}
%   \end{SideBySideExample}
%
%   \textbf{\texttt{zero=omit} 会产生歧义，不要用在需要还原数值的场合。}
%   交替只能区分相邻两位，不能表示数量级：省去零之后，\texttt{1}、\texttt{100}、
%   \texttt{10000} 的筹码写法完全相同（都是单独一个纵式的一）。穷举核对过，
%   $1$ 到 $99999$ 之间有 990 组数字因此无法区分。
%
%   史料中确有省零的写法，靠上下文和算筹板上的位置定位数量级；排印这类内容时可以用
%   \texttt{omit}，但要自己保证读者能确定位数。
%
%   还有一个更极端的情形：输入本身就是 0 时，\texttt{omit} 不产出\emph{任何}字符
%   （\verb|\zhrod{0}| 得到空串，\verb|\zhrod{0.0}| 只得到一个小数点）。这不是
%   「读者需要自己定位数量级」，而是整个数字都没有输出。
% \end{function}
%
% \begin{function}[added=2026-08-06]{zerochar}
%   \begin{syntax}
%     zerochar = <token list>
%   \end{syntax}
%   \texttt{zero=fill} 时占位用的字符，默认是〇。
% \end{function}
%
% \begin{function}[added=2026-08-06]{dot}
%   \begin{syntax}
%     dot = <token list>
%   \end{syntax}
%   小数点用的字符，默认是半角句点。算筹在史料里不写小数点（小数位靠算筹板上的位置
%   区分），所以这里\emph{不}沿用 \tn{zhnumsetup} 那套把小数点排成「点」字的做法；
%   若要排成别的形式，用这个选项。
% \end{function}
%
% \begin{function}[added=2026-08-06]{minus}
%   \begin{syntax}
%     minus = <(slash)|overlay>
%   \end{syntax}
%   负数的记法。默认（\texttt{slash}）在筹码之前排一个独立的＼。
%
%   Unicode 建议的记法是 \texttt{overlay}：用组合字符 U+20E5 叠加在最右一位筹码上。
%
%   \textbf{两种记法各自依赖的字符互不重合，需要按字体选择。}
%   \texttt{slash} 用的是 U+FF3C（全角反斜线），\texttt{overlay} 用的是 U+20E5。
%   \textbf{已知限制：}末位是 0 时，\texttt{overlay} 会叠到零的位置上，而不是叠在一个
%   筹码上——\texttt{zero=fill} 时叠在占位的〇上，\texttt{zero=omit} 时叠在末位之前那个
%   筹码上（实测 \verb|\zhrod{-10}| 在 \texttt{fill} 下得到「筹码 + 〇 + U+20E5」）。
%   这是「叠在最右一个字符上」这条规则本身带来的：末位没有筹码可叠。此时建议改用
%   \texttt{slash}。\texttt{zero=omit} 或 \texttt{zerochar} 为空时，整串全是 0 的负数
%   （如 \verb|-0|、\verb|-0.0|）没有任何字符可叠，\texttt{overlay} 不输出负号，结果与
%   相应的正数相同；只有负号的 \verb|-| 也不输出组合字符。
%
%   把 \TeX\ Live 里含全部算筹字形的字体逐一检查（共 19 款），结果是两者\emph{恰好互补}：
%   \pkg{lxgw-fonts} 的 5 款只有 U+FF3C，\pkg{juliamono} 的 14 款只有 U+20E5，
%   \textbf{没有一款同时具备}。所以用 \pkg{lxgw-fonts} 时要用 \texttt{slash}（默认），
%   用 \pkg{juliamono} 时要改成 \texttt{overlay}；用错那一个会得到缺字警告，
%   且负号占了宽度却什么都不显示。
% \end{function}
%
% \begin{function}[added=2026-08-06]{font}
%   \begin{syntax}
%     font = <font switch>
%   \end{syntax}
%   \tn{zhrodbox} 使用的字体切换命令，默认为空（即用当前字体）。
%
%   \pkg{zhnumber} 本身不加载任何字体，也不指定用哪一款——含算筹字形的字体会随时间
%   变化，写死一个名字会过期。请自己声明一个字体族再交给这个选项：
%   \begin{Verbatim}
%     \newfontfamily\RodFont{LXGWWenKaiGBLite-Regular.ttf}
%     \zhrodsetup{font=\RodFont}
%   \end{Verbatim}
%
%   截至 2026 年 8 月，\TeX\ Live 的 \pkg{lxgw-fonts} 里有五款字体覆盖算筹的全部
%   码位（U+1D360 至 U+1D371）：\file{LXGWWenKaiGBLite-Regular.ttf}、
%   \file{LXGWNeoZhiSong.ttf}、\file{LXGWNeoXiHei.ttf} 及后两者的 \texttt{Screen}
%   变体。用它们的好处是筹码与正文汉字可以出自同一款字体。
%
%   注意同一字体包里的臻楷（\file{LXGWZhenKaiGB-Regular.ttf}）与朱雀仿宋
%   （\file{LXGWZhuqueFangsong-Regular.ttf}）\emph{没有}算筹字形。臻楷是独立的字形，
%   不是文楷的粗体版本，但 \texttt{fontset=lxgw} 把它配作 \texttt{zhkai} 的
%   \texttt{BoldFont}；因此在那套字体配置下给算筹加粗会丢字。
%
%   另外，\pkg{xeCJK} 不把算筹码位当作汉字（\tn{XeTeXcharclass} 为 0，而〇与一
%   为 1），所以在 \cls{ctexart} 一类文档里靠 \tn{CJKfamily} 切换取不到筹码字形，
%   必须像上面那样显式声明字体族。这也意味着筹码与相邻汉字之间会插入
%   \tn{CJKecglue} 的间距——那是 glue，因而是可断点，所以 \tn{zhrodbox} 的字距虽然用
%   \tn{kern}，整串筹码在 \pkg{xeCJK} 文档里仍可能断行（\texttt{zero=fill} 时每个〇两侧
%   各有一处，实测确实会断；\texttt{zero=omit} 时没有〇，也就没有这些断点）。
%   要整串一定不断，把它放进 \tn{mbox}。
% \end{function}
%
% \begin{function}[added=2026-08-06]{kern}
%   \begin{syntax}
%     kern = <dimension expression>
%   \end{syntax}
%   \tn{zhrodbox} 在相邻筹码之间插入的字距，默认 \verb|-.14em|。
%
%   这个默认值是照排版效果试出来的\emph{临时值}，没有按史料核定过；若你有可靠的样张，
%   请自行调整。
% \end{function}
%
% \medskip
% \noindent
% \textbf{引擎要求。}算筹只能在 \XeTeX{} 与 \LuaTeX{} 下使用。\pdfTeX{} 与 \upTeX{}
% 是 8-bit 引擎，无法表示算筹所在的码位，在这两个引擎下调用 \tn{zhrod} 或
% \tn{zhrodbox} 会报错而\emph{不会}退化成别的写法——算筹没有 8-bit 的替代记法，
% 静默退化只会让人以为自己排出了算筹。
%
% \begin{function}[EXP, updated=2022-07-10]{\zhnum}
%   \begin{syntax}
%     \tn{zhnum} \Arg{counter}
%     \tn{pagenumbering} \{zhnum\}
%   \end{syntax}
%   与 |\roman| 等类似，用于将 \LaTeX 计数器的值转换为中文数字。例如
%   \begin{SideBySideExample}
%     \zhnum{section}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhdig}
%   \begin{syntax}
%     \tn{zhdig} \Arg{counter}
%     \tn{pagenumbering} \{zhdig\}
%   \end{syntax}
%   与 |\roman| 等类似，用于将 \LaTeX 计数器的值转换为中文数字串。例如
%   \begin{SideBySideExample}
%     \zhdig{section}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhweekday}
%   \begin{syntax}
%     \tn{zhweekday} \Arg{yyyy/mm/dd}
%   \end{syntax}
%   输出日期当天的星期。例如
%   \begin{SideBySideExample}
%     \zhweekday{2012/5/20}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2026-07-09]{\zhdate}
%   \begin{syntax}
%     \tn{zhdate}   \{<yyyy/mm/dd> | <yyyy/mm> | <yyyy>\}
%     \tn{zhdate} * \Arg{yyyy/mm/dd}
%   \end{syntax}
%   以中文格式输出日期，其中带 |*| 的命令还输出星期。例如
%   \begin{SideBySideExample}
%     \zhdate {2026/7/9}\\
%     \zhdate*{2026/7/9}\\
%     \zhdate {2026}, \zhdate {2026/7}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhtoday}
%   与 |\today| 类似，以中文输出当天的日期。例如
%   \begin{SideBySideExample}
%     \zhtoday
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhtime}
%   \begin{syntax}
%     \tn{zhtime} \Arg{hh:mm}
%   \end{syntax}
%   以中文格式输出时间。例如
%   \begin{SideBySideExample}
%     \zhtime{23:56}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhcurrtime}
%   输出当前的时间。例如
%   \begin{SideBySideExample}
%     \zhcurrtime
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhtiangan}
%   \begin{syntax}
%     \tn{zhtiangan} \Arg{number}
%   \end{syntax}
%   输出对应的天干计数。\meta{number} 的正常范围是 1--10，超出范围的数字将输出空值。例如
%   \begin{SideBySideExample}[xrightmargin=.4\linewidth]
%     \zhtiangan{1} \zhtiangan{2} \zhtiangan{3}
%     \zhtiangan{4} \zhtiangan{5} \zhtiangan{10}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhdizhi}
%   \begin{syntax}
%     \tn{zhdizhi} \Arg{number}
%   \end{syntax}
%   输出对应的地支计数。\meta{number} 的正常范围是 1--12，超出范围的数字将输出空值。例如
%   \begin{SideBySideExample}[xrightmargin=.4\linewidth]
%     \zhdizhi{1} \zhdizhi{2} \zhdizhi{3}
%     \zhdizhi{4} \zhdizhi{5} \zhdizhi{12}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhganzhi}
%   \begin{syntax}
%     \tn{zhganzhi} \Arg{number}
%   \end{syntax}
%   输出对应的干支计数。\meta{number} 的正常范围是 1--60，超出范围的数字将输出空值。例如
%   \begin{SideBySideExample}[xrightmargin=.4\linewidth]
%     \zhganzhi{1} \zhganzhi{2} \zhganzhi{3} \\
%     \zhganzhi{4} \zhganzhi{5} \zhganzhi{60}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[EXP, updated=2022-07-10]{\zhganzhinian}
%   \begin{syntax}
%     \tn{zhganzhinian} \Arg{year}
%   \end{syntax}
%   输出公元纪年 \meta{year} 对应的干支纪年。公元前的年份用负数表示。例如
%   \begin{SideBySideExample}[xrightmargin=.4\linewidth]
%     \zhganzhinian{1898}  \zhganzhinian{-246} \\
%     \zhganzhinian{-2697} \zhganzhinian{\year}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[added=2012-05-25]{\zhnumExtendScaleMap}
%   \begin{syntax}
%     \tn{zhnumExtendScaleMap} \oarg{character} \{<character_1>, <character_2>, ..., <character_n>\}
%   \end{syntax}
%   缺省状态下 \tn{zhnumber} 能正确中文格式化的最大整数是 $10^{48}-1$，\tn{zhdigits} 不受
%   这个大小的限制。可以通过 \tn{zhnumExtendScaleMap} 来扩展 \tn{zhnumber}。
%   \meta{character_i} 设置 $10^{4(i+11)}$。若给出可选项 \meta{character}，则当
%   数字大于 $10^{4(n+12)}-1$ 时，统一用 \meta{character} 设置输出数字的进位。
% \end{function}
%
% \begin{function}[updated=2022-07-10]{\zhnumsetup}
%   \begin{syntax}
%     \tn{zhnumsetup} \{<key_1>=<val_1>, <key_2>=<val_2>, ...\}
%   \end{syntax}
%   用于在导言区或文档中，设置中文数字的输出格式。目前可以设置的 \meta{key} 如下介绍。
%   以\textbf{粗体}表示选项的默认值。
% \end{function}
%
% \begin{function}[added=2012-05-25]{time}
%   \begin{syntax}
%     time = <(Arabic)|Chinese>
%   \end{syntax}
%   设置日期和时间的数字格式，\meta{Arabic} 为阿拉伯数字，而 \meta{Chinese} 为中文数字。
%   例如
%   \begin{SideBySideExample}
%     \zhnumsetup{time=Chinese}
%     \zhtoday\zhcurrtime
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}[added=2016-05-01]{arabicsep}
%   \begin{syntax}
%     arabicsep = \Arg{sep}
%   \end{syntax}
%   设置日期和时间的数字格式为阿拉伯数字时，阿拉伯数字与汉字的间隔内容。默认为一个空格。
% \end{function}
%
% \begin{function}[updated=2012-05-25]{style}
%   \begin{syntax}
%     style = <(Simplified)|Traditional|(Normal)|Financial|Ancient>
%   \end{syntax}
%   意义分别为
%
%   \begin{description}[font=\mdseries\ttfamily\small,align=right,leftmargin=*,
%                       labelsep=\marginparsep,labelindent=-\marginparsep]
%     \item[Simplified]  以简体中文输出数字（对 |Big5| 编码无效）；
%     \item[Traditional] 以繁体中文输出数字（对 |Big5| 编码无效）；
%     \item[Normal] 以小写形式输出中文数字；
%     \item[Financial]  以大写形式输出中文数字；
%     \item[Ancient] 以廿输出 20，以卅输出 30，以卌输出 40，以皕输出 200。
%   \end{description}
%
%   可以设置 |style| 为其中一个，也可以是前两个与后三个的适当组合，默认是简体小写。例如
%   \begin{SideBySideExample}[xrightmargin=.4\linewidth]
%     \zhnumsetup{style={Traditional,Financial}}
%     \zhnumber{62012.3}\\
%     \zhnumsetup{style=Ancient}
%     \zhnumber{21}
%   \end{SideBySideExample}
% \end{function}
%
% \begin{function}{null}
%   \begin{syntax}
%     null = <\TFF>
%   \end{syntax}
%   缺省状态下，除了 \tn{zhdigits} 外，其他的格式转换命令，将 0 映射成零，如果需要将 0 映射
%   成〇，可以使用这个选项。
% \end{function}
%
% \begin{function}[added=2015-05-20]{ganzhi-cyclic}
%   \begin{syntax}
%     ganzhi-cyclic = <\TFF>
%   \end{syntax}
%   天干、地支和干支的数字都有一定范围。若参数大于这个范围，\tn{tiangan} 等将输出空值。
%   可以将本选项设置为 |true|，对超出范围的数字取相应的模。
%   请注意，数字 |0| 的结果总是为空值。例如
%   \begin{SideBySideExample}[xrightmargin=.3\linewidth]
%     \zhnumsetup{ganzhi-cyclic}
%     \zhtiangan{11} \zhtiangan{12} \zhtiangan{209}
%     \zhtiangan{-1} \zhtiangan{-2} \zhtiangan{-683} \\
%     \zhdizhi{13}   \zhdizhi{24}   \zhdizhi{1211}
%     \zhdizhi{-1}   \zhdizhi{-2}   \zhdizhi{-8199}  \\
%     \zhganzhi{61}  \zhganzhi{72}  \zhganzhi{2158}  \\
%     \zhganzhi{-1}  \zhganzhi{-2}  \zhganzhi{-789}
%   \end{SideBySideExample}
% \end{function}
%
% \pkg{zhnumber} 提供下列选项来控制阿拉伯数字的中文映射。
% \begin{frameverb}
%   - -0 0 1 2 3 4 5 6 7 8 9 10 20 30 40 100 200 1000
%   E2 E3 E4 E8 E12 E16 E20 E24 E28 E32 E36 E40 E44
%   F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F100 F1000 FE2 FE3
%   T1 T2 T3 T4 T5 T6 T7 T8 T9 T10
%   D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 D11 D12
%   GZ1 GZ2 GZ3 GZ4 GZ5 GZ6 GZ7 GZ8 GZ9 GZ10 ... GZ60
%   dot and parts
%   year month day hour minute weekday mon tue wed thu fri sat sun
% \end{frameverb}
% 其中 |-| 设置负，|-0| 设置〇，|dot| 设置小数的点，|and| 和 |parts| 分别设置分数
% 的“又”和“分之”，|E|$n$ 设置 $10^n$，|F|$n$ 设置数字 $n$ 的大写，|T|$n$ 设置
% 数字 $n$ 的天干，|D|$n$ 设置数字 $n$ 的地支，而 |GZ|$n$ 设置数字 $n$ 的干支。
% 其他的选项同字面意思，不再赘述。例如
% \begin{frameverb}
%   \zhnumsetup{2={两}}
% \end{frameverb}
% 可以将 2 映射成两。需要说明的是，\pkg{zhnumber} 将优先使用这里的设置，所以可能会影响
% 到 |style| 选项。如果要恢复 |style| 的功能，可以使用 |reset| 选项。
%
% \begin{function}[updated=2022-07-10]{reset}
%   \begin{syntax}
%     reset
%   \end{syntax}
%   用于恢复 \pkg{zhnumber} 对阿拉伯数字的初始化映射。\pkg{zhnumber} 的中文数字初始化
%   设置见源代码（第 \ref{sec:zhnum-map}~节）。
% \end{function}
%
% \begin{function}[added=2014-09-09]{activechar}
%   \begin{syntax}
%     activechar = <\TTF>
%   \end{syntax}
%   在 \LaTeX{} 或者 \pdfLaTeX{} 下面输出汉字，传统的办法需要将汉字的首字节设置为
%   活动字符，然后再通过特殊的宏技巧来实现。因此，\pkg{zhnumber} 在载入配置文件的
%   时候，默认会将汉字的首字节设置为活动字符。禁用本选项将不会改变汉字首字节的
%   类代码。需要在本选项之后，使用 \texttt{encoding} 或者 \texttt{reset} 选项
%   才会有效果。
% \end{function}
%
% \begin{function}[updated=2022-07-10]{\zhnumber,\zhdigits,\zhnum,\zhdig}
%   \begin{syntax}
%     \tn{zhnumber}   \oarg{options} \Arg{number}
%     \tn{zhdigits} * \oarg{options} \Arg{number}
%     \tn{zhnum}      \oarg{options} \Arg{counter}
%     \tn{zhdig}      \oarg{options} \Arg{counter}
%   \end{syntax}
%   如果只改变当前数字的中文输出格式，可以使用带选项的格式转换命令，其中 \meta{options}
%   与 \tn{zhnumsetup} 的参数相同，如上所介绍。这些带了选项的命令是不可展开的，在某些场合使
%   用时要小心。
%
%   \changes{v3.1}{2026/08/05}{说明带选项形式在写入辅助文件时的行为：计数器值在写入时
%     即已固定，样式留待读回时套用（\#1008）。}
%
%   不可展开并不妨碍把它们用在会写入辅助文件的位置（例如 \tn{thesection} 进而进入
%   \file{.toc}）。自 v3.1 起，\meta{counter} 在写入的那一刻就被取成数值固定下来，
%   写进辅助文件的形如 \verb|\zhnumberwithoptions{style=Financial}{7}|；样式则留待读回时
%   套用。因此下面这种按层级混搭样式的写法是可行的：
%   \begin{quote}
%     \small\ttfamily
%     \string\renewcommand\string\thesection\Arg{\string\zhnum[style=Normal]\Arg{section}}\\
%     \string\renewcommand\string\thesubsection\Arg{\string\thesection.\string\zhnum[style=Financial]\Arg{subsection}}
%   \end{quote}
%   v3.0 及以前写进辅助文件的是计数器\emph{名}，读回时计数器早已归零或停在别的值上，
%   目录里的编号会与正文不一致。
%
%   \textbf{仍有一处限制}：辅助文件能容纳不可展开的命令，而 \pkg{hyperref} 的
%   \tn{pdfstringdef}（PDF 书签、文档信息一类纯文本字段）不能——它要求内容\emph{完全}
%   可展开。所以把带选项的形式放进 \tn{thesection} 后，若同时用了
%   \opt{bookmarksnumbered}，书签里的编号会退化成
%   \texttt{style=Normal1} 这样的字面文本，并伴随一条
%   \texttt{Token not allowed in a PDF string} 警告。这不是 v3.1 引入的：已发布的 v3.0 在
%   同样写法下退化成 \texttt{style=Normalsection}，警告条数相同。要让书签也正确，请对相应
%   层级改用不带选项的 \tn{zhnum}／\tn{zhdig}（先用 \tn{zhnumsetup} 设定样式），或在
%   \tn{pdfstringdefPreHook} 里自行处理。「不可展开」「不能写进辅助文件」「不能进
%   \tn{pdfstringdef}」是三件不同的事，只有中间那件在 v3.1 里不再成立。
% \end{function}
%
% \end{documentation}
%
% \StopEventually{}
%
% \begin{implementation}
%
% \section{\pkg{zhnumber} 宏包代码实现}
%
%    \begin{macrocode}
%<*package>
%    \end{macrocode}
%
%    \begin{macrocode}
%<@@=zhnum>
%    \end{macrocode}
%
% \changes{v3.1}{2026/05/03}{提升 \LaTeXiii{} 最低版本要求至 2025/10/09。}
%    \begin{macrocode}
\msg_new:nnn { zhnumber } { l3-too-old }
  {
    Support~package~'expl3'~too~old. \\\\
    Please~update~an~up~to~date~version~of~the~bundles\\\\
    'l3kernel'~and~'l3packages'\\\\
    using~your~TeX~package~manager~or~from~CTAN.
  }
\@ifpackagelater { expl3 } { 2025/10/09 } { }
  { \msg_error:nn  { zhnumber }  { l3-too-old } }
%    \end{macrocode}
%
%    \begin{macrocode}
\cs_if_exist:NF \NewDocumentCommand
  { \RequirePackage { xparse } }
%    \end{macrocode}
%
% \begin{macro}{\zhnum_output:n,\zhnum_expand_wrap:wn}
% 受益于 \tn{tex_expanded:D}，我们使用如下的结构简单实现 |f| 展开。
% \begin{verbatim}
%   \exp_not:e
%     {
%       ...
%       \exp_not:o { <tl_var> }
%       ...
%       \exp_not:o { <tl_var> }
%       ...
%       \exp_not:o { <tl_var> }
%       ...
%     }
% \end{verbatim}
% 并且可以将 \tn{zhnumber} 等无风险地使用于诸如 \tn{edef} 定义等完全展开的场合。
% 从 \TeXLive\ 2019 开始，各个主要引擎都已经支持 \tn{tex_expanded:D}，
% 对于较早的版本，\LaTeXiii\ 也实现了一个模拟。
%    \begin{macrocode}
\cs_new:Npn \zhnum_output:n #1
  { \exp_args:Nc \zhnum_exp_not:o { l_@@_ #1 _tl } }
\cs_new_protected:Npn \zhnum_expand_wrap:wn #1#
  { \@@_expand_wrap_aux:nn {#1} }
\cs_new_protected:Npn \@@_expand_wrap_aux:nn #1#2
  { #1 { \exp_not:e {#2} } }
\cs_new_eq:NN \zhnum_exp_not:e \exp_not:e
\cs_new_eq:NN \zhnum_exp_not:o \exp_not:o
%    \end{macrocode}
% 但这种方式也有一定的缺点，比如在 \pdfTeX\ 引擎下，\pkg{hyperref} 包的
% \tn{pdfstringdef} 就需要把汉字首字节的活动字符展开，
% 并作适当的重定义后才能得到预期的结果。为此，我们定义一个清除命令，不使用以上机制。
%    \begin{macrocode}
\cs_new_protected:Npn \zhnumClearWrapper
  {
    \cs_set_eq:NN \zhnum_exp_not:e \use:n
    \cs_set_eq:NN \zhnum_exp_not:o \use:n
  }
\cs_new_protected:Npn \zhnumResetWrapper
  {
    \cs_set_eq:NN \zhnum_exp_not:e \exp_not:e
    \cs_set_eq:NN \zhnum_exp_not:o \exp_not:o
  }
\hook_gput_code:nnn { package/CJKutf8/after } { zhnumber }
  { \g@addto@macro \pdfstringdefPreHook { \zhnumClearWrapper } }
\hook_gput_code:nnn { package/xCJK2uni/after } { zhnumber }
  { \g@addto@macro \pdfstringdefPreHook { \zhnumClearWrapper } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhnumber}
% 用于将输入的数字按照中文格式输出。
%    \begin{macrocode}
\NewExpandableDocumentCommand \zhnumber { +o +m }
  {
    \tl_if_novalue:nTF {#1}
      { \zhnum_number:e }
      { \zhnumberwithoptions {#1} }
    {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnumberwithoptions}
% 带选项的用户函数。
%    \begin{macrocode}
 \NewDocumentCommand \zhnumberwithoptions { +m +m }
  {
    \group_begin:
      \zhnum_set:n {#1}
      \zhnum_number:e {#2}
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_number:n}
% \begin{macro}{\@@_number:www}
% 先判断输入的是小数还是分数。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhnum_number:n #1
  { \@@_number:www #1 . \q_nil . \q_stop }
\cs_new:Npn \@@_number:www #1 . #2 . #3 \q_stop
  {
    \quark_if_nil:nTF {#2}
      { \@@_integer_or_fraction:www #1 / \q_nil / \q_stop }
      { \zhnum_decimal:nn {#1} {#2} }
  }
\cs_generate_variant:Nn \zhnum_number:n { e }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_integer_or_fraction:www}
% 判断是否输入的是分数。
%    \begin{macrocode}
\cs_new:Npn \@@_integer_or_fraction:www #1 / #2 / #3 \q_stop
  {
    \quark_if_nil:nTF {#2}
      { \zhnum_integer:n {#1} }
      { \@@_fraction:wwww #2 \q_mark #1 ; \q_nil ; \q_stop }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_fraction:wwww}
% 对分数进行预处理。
%    \begin{macrocode}
\cs_new:Npn \@@_fraction:wwww #1 \q_mark #2 ; #3 ; #4 \q_stop
  {
    \quark_if_nil:nTF {#3}
      {
        \zhnum_blank_to_zero:n {#1}
        \zhnum_output:n { parts }
        \zhnum_blank_to_zero:n {#2}
      }
      {
        \tl_if_blank:nF {#2}
          {
            \zhnum_number:n {#2}
            \zhnum_output:n { and }
          }
        \zhnum_blank_to_zero:n {#1}
        \zhnum_output:n { parts }
        \zhnum_blank_to_zero:n {#3}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_decimal:nn}
% 对小数进行预处理。
%    \begin{macrocode}
\cs_new:Npn \zhnum_decimal:nn #1#2
  {
    \zhnum_blank_to_zero:n {#1}
    \zhnum_output:n { dot }
    \tl_if_blank:nTF {#2}
      { \zhnum_output:n { 0 } }
      { \zhnum_digits_zero:n {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_blank_to_zero:n}
% 输出小数的整数位。
%    \begin{macrocode}
\cs_new:Npn \zhnum_blank_to_zero:n #1
  {
    \tl_if_blank:nTF {#1}
      { \zhnum_output:n { 0 } }
      { \zhnum_number:n {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhnum}
% 用于将 \LaTeX{} 计数器按中文格式输出。
%
% \changes{v3.1}{2026/08/05}{带选项的 \tn{zhnum} 与 \tn{zhdig} 先把计数器展开为
%   数值再交给处理数值的实现，使它们在写入 \file{.toc} 一类辅助文件时固定为当时的
%   计数器值（\#1008）。}
%
% 带选项的形式仍然不可完全展开（它需要一个分组来局部改样式），但\emph{计数器名不应该
% 被原样写进辅助文件}。原先 \tn{zhnum} 把 \meta{counter} 直接交给
% \tn{zhnumwithoptions}，写进 \file{.toc} 的是 \verb|\zhnumwithoptions{...}{section}|；
% 该文件在下一次编译的 \tn{tableofcontents} 处被读回，此时计数器已经归零或停在
% 别的值上，于是目录里的编号与正文不一致（\#1008 实测：正文「一／一.壹」而目录
% 「零／零.零」）。
%
% 修正办法是在 \tn{zhnum} 这一层先用 \texttt{f} 展开把计数器取成数值，再交给处理\emph{数值}
% 的 \tn{zhnumberwithoptions}：写进辅助文件的成为
% \verb|\zhnumberwithoptions{style=Financial}{7}|（第二个参数是写入那一刻的计数器值，
% 与前面「用法」一节举的例子是同一个），数值已经固定，样式则留待读回时套用。
% 不改成完全可展开是有意的——样式靠 \tn{tl_set_eq:NN} 一类赋值实现（见
% \tn{zhnum_reset_style:}），赋值无法在 \tn{edef} 的展开中生效，强行这样做只会把
% 「不可展开」换成「静默用错样式」。
%    \begin{macrocode}
\NewExpandableDocumentCommand \zhnum { +o +m }
  {
    \tl_if_novalue:nTF {#1}
      { \zhnum_counter:n {#2} }
      { \@@_counter_with_options:nn {#1} {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_counter_with_options:nn}
% 先确认计数器存在（不存在时给出与无选项形式一致的报错），再把它展开成数值。
% \tn{int_use:c} 只在计数器存在时才可用，所以存在性判断必须在展开之前。
%    \begin{macrocode}
\cs_new:Npn \@@_counter_with_options:nn #1#2
  {
    \int_if_exist:cTF { c@#2 }
      {
        \exp_args:Nnf \zhnumberwithoptions {#1}
          { \exp_args:Nc \int_use:N { c@#2 } }
      }
      { \@@_counter_error:n {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnumwithoptions}
% 为兼容而保留：v3.0 及以前写进辅助文件的 \file{.toc} 里可能还留着这个名字，
% 删掉会让旧文件读回时报 undefined control sequence。它现在只是转发。
%    \begin{macrocode}
\NewDocumentCommand \zhnumwithoptions { +m +m }
  {
    \group_begin:
      \zhnum_set:n {#1}
      \zhnum_counter:n {#2}
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_counter:n, \zhnum_int:n}
% 可以直接通过比较 \LaTeX{} 计数器的值来得到符号和绝对值。
%    \begin{macrocode}
\cs_new:Npn \zhnum_counter:n #1
  {
    \int_if_exist:cTF { c@#1 }
      { \exp_args:Nc \zhnum_int:n { c@#1 } }
      { \@@_counter_error:n {#1} }
  }
\cs_new:Npn \@@_counter_error:n #1
  { \msg_expandable_error:nnn { zhnumber } { not-counter } {#1} }
\msg_new:nnn  { zhnumber } { not-counter }
  { `#1'~is~not~a~LaTeX~counter. }
\zhnum_expand_wrap:wn
\cs_new:Npn \zhnum_int:n #1
  {
    \int_compare:nNnTF {#1} > \c_zero_int
      { \zhnum_parse_number:f { \int_eval:n {#1} } }
      {
        \int_compare:nNnTF {#1} < \c_zero_int
          {
            \zhnum_output:n { minus }
            \zhnum_parse_number:f { \int_eval:n { - #1 } }
          }
          { \zhnum_output:n { 0 } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@zhnum}
% 用于支持 |\pagenumbering{zhnum}|。
%    \begin{macrocode}
\cs_new:Npn \@zhnum { \zhnum_int:n }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_integer:n}
% 对整数的处理。这个函数基本抄录自 \pkg{l3bigint} 的 \cs{__bingint_read_do:nn}。它可以
% 正确取得符号，去掉多余的零，还可以循环展开数字。但它在遇到非数字的时候就停止了
% 循环，我们可能需要非数字（例如逗号）来作为分隔符号。因此对它略作修改，跳过非数字。
%    \begin{macrocode}
\cs_new:Npn \zhnum_integer:n #1
  {
    \exp_after:wN \@@_read_integer:www
    \int_value:w
      \exp_after:wN \@@_read_sign_loop:N
      \exp:w \exp_end_continue_f:w \use:n
      #1 \exp_stop_f: \q_recursion_tail \q_recursion_stop
         \@@_result:nn { \c_zero_int } { } ;
  }
\cs_new:Npn \@@_read_sign_loop:N #1
  {
    \if:w + \if:w - \exp_not:N #1 + \fi: \exp_not:N #1
      \exp_after:wN \@@_read_sign_loop:N
      \exp:w \exp_end_continue_f:w \exp_after:wN \use:n
    \else:
      1 \exp_after:wN ;
      \exp:w \exp_end_continue_f:w
        \exp_after:wN \@@_read_zeros_loop:N
        \exp_after:wN #1
    \fi:
  }
\cs_new:Npn \@@_read_zeros_loop:N #1
  {
    \if:w 0 \exp_not:N #1
      \exp_after:wN \@@_read_zeros_loop:N
      \exp:w \exp_end_continue_f:w \exp_after:wN \use:n
    \else:
      \exp_after:wN \@@_read_abs_loop:Nw
      \exp_after:wN #1
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_read_abs_loop:Nw}
% 当数字很大时，\pkg{l3bigint} 的实现会造成 \TeX{} 内存溢出：
% \begin{verbatim}
%   ! TeX capacity exceeded, sorry [expansion depth=10000].
% \end{verbatim}
% 我们在这里参考 \cs{__tl_act:NNNnn} 的实现对它进行了改进。
%    \begin{macrocode}
\cs_new:Npn \@@_read_abs_loop:Nw #1#2 \q_recursion_stop
  {
    \zhnum_if_digit:NTF #1
      { \@@_output:nnwnn { + 1 } #1 }
      { \quark_if_recursion_tail_stop_do:Nn #1 { \@@_loop_end:wnn } }
    \exp_after:wN \@@_read_abs_loop:Nw
      \exp:w \exp_end_continue_f:w \use:n #2 \q_recursion_stop
  }
\cs_new:Npn \@@_output:nnwnn #1#2#3 \@@_result:nn #4#5
  { #3 \@@_result:nn { #4#1 } { #5#2 } }
\cs_new:Npn \@@_loop_end:wnn #1 \@@_result:nn #2#3
  { \int_eval:n {#2} ; #3 }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_read_integer:www}
% |#1| 符号，|#3| 是绝对值，|#2| 是绝对值的长度。
%    \begin{macrocode}
\cs_new:Npn \@@_read_integer:www #1 ; #2 ; #3 ;
  {
    \int_compare:nNnTF {#2} = \c_zero_int
      { \zhnum_output:n { 0 } }
      {
        \int_compare:nNnF {#1} = \c_one_int
          { \zhnum_output:n { minus } }
        \zhnum_parse_number:nn {#2} {#3}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_if_digit:NTF}
% 判断 |#1| 是否为数字位。
%    \begin{macrocode}
\cs_new:Npn \zhnum_if_digit:NTF #1
  {
    \if_int_compare:w 9 < 1 \exp_not:N #1 \exp_stop_f:
      \exp_after:wN \use_i:nn
    \else:
      \exp_after:wN \use_ii:nn
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \zhnum_parse_number:n,
%     \zhnum_parse_number:nn
%   }
%    \begin{macrocode}
\cs_new:Npn \zhnum_parse_number:n #1
  { \exp_args:Nf \zhnum_parse_number:nn { \tl_count:n {#1} } {#1} }
\cs_new:Npn \zhnum_parse_number:nn #1
  { \exp_args:Nf \@@_parse_number:nnn { \int_mod:nn {#1} { 4 } } {#1} }
\cs_new:Npn \@@_parse_number:nnn #1#2
  {
    \int_compare:nNnTF {#2} < 2
      { \zhnum_output:n }
      {
        \int_compare:nNnTF {#1} = \c_zero_int
          { \zhnum_split_number:fn { \int_eval:n { #2 / 4 - 1 } } }
          { \@@_split_number_aux:nnn {#1} {#2} }
      }
  }
\cs_generate_variant:Nn \zhnum_parse_number:n { f }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_split_number_aux:nnn}
% 为了处理的方便，在整数前面补上适当的 $0$，使其位数可以被 $4$ 整除。
%    \begin{macrocode}
\cs_new:Npn \@@_split_number_aux:nnn #1#2
  {
    \exp_after:wN \@@_split_number_aux:wwn
      \int_value:w \int_div_truncate:nn {#2} { 4 }
        \if_case:w #1 \exp_stop_f:
          \or: \exp_after:wN \use:n
          \or: \exp_after:wN \use_i_ii:nnn
          \or: \exp_after:wN \use_i:nnn
        \fi:
        { \exp_stop_f: ; 0 } 0 0 ;
  }
\cs_new:Npn \@@_split_number_aux:wwn #1 ; #2 ; #3
  { \zhnum_split_number:nn {#1} { #2#3 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_split_number:nn}
% 最后加入的 \cs{q_recursion_tail} 是停止递归的标志，而 \cs{q_nil} 用于占位。
%    \begin{macrocode}
\cs_new:Npn \zhnum_split_number:nn #1#2
  {
    \zhnum_split_number:NNnNNNNw \c_true_bool \c_true_bool {#1}
      #2 \q_recursion_tail \q_nil \q_nil \q_nil \q_recursion_stop
  }
\cs_generate_variant:Nn \zhnum_split_number:nn { f }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_split_number:NNnNNNNw}
% 将输入的整数由高位到低位，以四位为一段进行处理。
%    \begin{macrocode}
\cs_new:Npn \zhnum_split_number:NNnNNNNw #1#2#3#4#5#6#7
  {
    \quark_if_recursion_tail_stop:N #4
    \int_compare:nNnTF { #4#5#6#7 } = \c_zero_int
      { \use_i:nn }
      {
        \bool_if:NF #1 { \zhnum_output:n { 0 } }
        \zhnum_process_number:NNNNNN #4#5#6#7#1#2
        \zhnum_scale_map:n {#3}
        \int_compare:nNnTF {#7} = \c_zero_int
      }
      { \zhnum_split_number:NNfNNNNw \c_false_bool \c_true_bool }
      { \zhnum_split_number:NNfNNNNw \c_true_bool  \c_false_bool }
    { \int_eval:n { #3 - 1 } }
  }
\cs_generate_variant:Nn \zhnum_split_number:NNnNNNNw { NNf }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_process_number:NNNNNN}
% 对四位数字按情况进行处理。
%    \begin{macrocode}
\cs_new:Npn \zhnum_process_number:NNNNNN #1#2#3#4#5#6
  {
    \int_compare:nNnTF {#1} = \c_zero_int
      {
        \bool_if:NF #6
          { \zhnum_output:n { 0 } }
      }
      {
        \zhnum_output:n {#1}
        \zhnum_output:n { 1000 }
      }
    \int_compare:nNnTF {#2} = \c_zero_int
      {
        \int_compare:nNnF { #1 * (#3#4) } = \c_zero_int
          { \zhnum_output:n { 0 } }
      }
      {
        \int_compare:nNnTF {#2} = 2
          { \zhnum_output:n { 200 } }
          {
            \zhnum_output:n {#2}
            \zhnum_output:n { 100 }
          }
      }
    \int_compare:nNnTF {#3} = \c_zero_int
      {
        \int_compare:nNnF { #2 * #4 } = \c_zero_int
          { \zhnum_output:n { 0 } }
      }
      {
        \bool_lazy_all:nTF
          {
            { \int_compare_p:nNn {#3}   = \c_one_int }
            { \int_compare_p:nNn {#1#2} = \c_zero_int }
            {#6}
            {#5}
          }
          { \zhnum_output:n { 10 } }
          {
            \int_compare:nTF { 1 < #3 < 5 }
              { \zhnum_output:n { #3 0 } }
              {
                \zhnum_output:n {#3}
                \zhnum_output:n { 10 }
              }
          }
      }
    \int_compare:nNnF {#4} = \c_zero_int
      { \zhnum_output:n {#4} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhdig}
% 用于将 \LaTeX{} 计数器按中文数字串输出。
%    \begin{macrocode}
\NewExpandableDocumentCommand \zhdig { +o +m }
  {
    \tl_if_novalue:nTF {#1}
      { \zhnum_digits_counter:n {#2} }
      { \@@_digits_counter_with_options:nn {#1} {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_digits_counter_with_options:nn}
% 与 \tn{zhnum} 的对应处理一致，先固定数值再套样式，理由见那里。
%
% 星号参数必须写成 \verb|{\BooleanFalse}| 而\emph{不是} \cs{c_false_bool}：
% 这一整串会被写进辅助文件，再在下次编译时重新切分成记号（tokenise），而那时 \verb|_| 不是
% letter（catcode 8），\cs{c_false_bool} 的名字会在写出时就断成
% \verb|\c _false_bool|，读回即出错。具体报什么错取决于写法，实测两种：不带花括号
% 时（初版的 \verb|\exp_args:NNne ... \c_false_bool|）断开的 \verb|_| 被当成键名，
% 报 \texttt{The key `zhnum/options/\_' is unknown} 之类的一串错误；带花括号时报
% \texttt{Missing \$ inserted}。两者都是「名字在写出那一刻断开」的后果。
% \tn{zhdigits} 的无计数器版本本来就用 \verb|{\BooleanFalse}|
% （它是 \pkg{expl3} 为此提供的、名字里没有 \verb|_| 的记号），这里对齐它。
%
% 这一点是盲审查出来的：初版用 \cs{c_false_bool} 时第一遍编译正常，第二遍读回
% \file{.toc} 才出错——等于把 \#1008 那类「跨编译才暴露」的失败换到了另一个命令上。
% 没有为它单独记 \tn{changes}：这条路径本身是 v3.1 新增的（已发布的 v3.0 里
% \tn{zhdig} 的带选项形式根本不可用，见下面 \tn{zhdigwithoptions} 的笔误），
% 所以它不构成对已发布行为的修正，并入核心修复那一条即可。
%    \begin{macrocode}
\cs_new:Npn \@@_digits_counter_with_options:nn #1#2
  {
    \int_if_exist:cTF { c@#2 }
      {
        \exp_args:Nnnf \zhdigitswithoptions { \BooleanFalse }
          {#1} { \exp_args:Nc \int_use:N { c@#2 } }
      }
      { \@@_counter_error:n {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhdigwithoptions}
% 为兼容旧辅助文件而保留，只是转发。
%
% \changes{v3.1}{2026/08/05}{修正 \tn{zhdigwithoptions} 把选项当作额外参数传给
%   \cs{zhnum_digits_counter:n} 的笔误——带选项的 \tn{zhdig} 此前直接报
%   \texttt{Use of \cs{???} doesn't match its definition} 并输出乱码（\#1008）。}
%
% 原先这里写的是 \verb|\zhnum_digits_counter:n #1 {#2}|，多传了一个 \verb|#1|
% （选项本身）。\cs{zhnum_digits_counter:n} 只取一个参数，于是选项被当成计数器名、
% 真正的计数器名溢出到后面。实测 \verb|\zhdig[style=Financial]{section}| 报
% \texttt{Use of \cs{???} doesn't match its definition}，并把
% \texttt{tyle=Financialsection} 印到页面上。
%    \begin{macrocode}
\NewDocumentCommand \zhdigwithoptions { +m +m }
  {
    \group_begin:
      \zhnum_set:n {#1}
      \zhnum_digits_counter:n {#2}
    \group_end:
  }
\cs_new:Npn \zhnum_digits_counter:n #1
  {
    \int_if_exist:cTF { c@#1 }
      { \zhnum_digits_null:v { c@#1 } }
      { \@@_counter_error:n {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@zhdig}
% 用于支持 |\pagenumbering{zhdig}|。
%    \begin{macrocode}
\cs_new:Npn \@zhdig #1
  { \zhnum_digits_null:f { \int_eval:n {#1} } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhdigits,\zhdigitswithoptions}
% 将输入的数字输出为中文数字串输出。
%    \begin{macrocode}
\NewExpandableDocumentCommand \zhdigits { +s +o +m }
  {
    \tl_if_novalue:nTF {#2}
      { \zhnum_digits:Ne #1 }
      { \zhdigitswithoptions {#1} {#2} }
    {#3}
  }
\NewDocumentCommand \zhdigitswithoptions { +m +m +m }
  {
    \group_begin:
      \zhnum_set:n {#2}
      \zhnum_digits:Ne #1 {#3}
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_digits_zero:n,\zhnum_digits_null:n}
% 快捷方式。
%    \begin{macrocode}
\cs_new:Npn \zhnum_digits_zero:n
  { \zhnum_digits:Nn \c_true_bool }
\cs_new:Npn \zhnum_digits_null:n
  { \zhnum_digits:Nn \c_false_bool }
\cs_generate_variant:Nn \zhnum_digits_null:n { v , f }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_digits:Nn}
% 与 \cs{zhnum_integer:n} 类似，但不用去掉多余的零。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhnum_digits:Nn #1#2
  {
    \exp_after:wN \@@_read_digits:w
    \int_value:w
      \exp_after:wN \@@_read_sign_loop:NN \exp_after:wN #1
      \exp:w \exp_end_continue_f:w \use:n
      #2 \exp_stop_f: \q_recursion_tail \q_recursion_stop
  }
\cs_new:Npn \@@_read_sign_loop:NN #1#2
  {
    \if:w + \if:w - \exp_not:N #2 + \fi: \exp_not:N #2
      \exp_after:wN \@@_read_sign_loop:NN \exp_after:wN #1
      \exp:w \exp_end_continue_f:w \exp_after:wN \use:n
    \else:
      1 \exp_after:wN ;
        \exp_after:wN \@@_read_digits_loop:NN
        \exp_after:wN #1
        \exp_after:wN #2
    \fi:
  }
\cs_new:Npn \@@_read_digits_loop:NN #1#2
  {
    \zhnum_if_digit:NTF #2
      { \@@_output_digits:NN #1#2 }
      {
        \quark_if_recursion_tail_stop:N #2
        \token_if_eq_charcode:NNT #2 .
          { \zhnum_output:n { dot } }
      }
    \exp_after:wN \@@_read_digits_loop:NN \exp_after:wN #1
      \exp:w \exp_end_continue_f:w \use:n
  }
\cs_new:Npn \@@_read_digits:w #1 ;
  {
    \int_compare:nNnF {#1} = \c_one_int
      { \zhnum_output:n { minus } }
  }
\cs_new:Npn \@@_output_digits:NN #1#2
  {
    \zhnum_output:n
      {
        \if_int_compare:w #2 = \c_zero_int
          \bool_if:NTF #1 { zero } { null }
        \else:
          #2
        \fi:
      }
  }
\cs_generate_variant:Nn \zhnum_digits:Nn { Ne }
%    \end{macrocode}
% \end{macro}
%
% \changes{v3.1}{2026/07/09}{支持仅输出年或年月。}
%
% \changes{v3.1}{2026/08/06}{新增算筹数字 \tn{zhrod} 与 \tn{zhrodbox}，以及配置命令
%   \tn{zhrodsetup}（\#366）。}
%
% \begin{macro}{\c_@@_rod_engine_bool}
% 算筹码位在 \pdfTeX{} 与 \upTeX{} 下无法表示：这两个引擎是 8-bit 的，
% 实测 \cs{char_generate:nn} 直接报
% \texttt{LaTeX Error: Charcode requested out of engine range}。
% 这是引擎能力缺失，不是缺字体，因此没有退化写法可用。
%
% \emph{不能}复用 \cs{c_@@_unicode_engine_bool}：那个布尔量把 \upTeX{} 也算作真
% （它判的是「是否不必把汉字首字节设为活动字符」），而 \upTeX{} 恰好不能表示算筹。
% 两者是不同粒度的引擎分类，这里需要单独一个只含 \XeTeX{} 与 \LuaTeX{} 的判据。
%    \begin{macrocode}
\bool_const:Nn \c_@@_rod_engine_bool
  {
    \bool_lazy_or_p:nn
      { \sys_if_engine_xetex_p:  }
      { \sys_if_engine_luatex_p: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}
%   {
%     \l_@@_rod_units_vertical_bool,
%     \l_@@_rod_zero_omit_bool,
%     \l_@@_rod_minus_overlay_bool,
%     \l_@@_rod_font_tl,
%     \l_@@_rod_kern_tl,
%     \l_@@_rod_zero_tl,
%     \l_@@_rod_dot_tl
%   }
% 算筹的七个配置变量，对应 \tn{zhrodsetup} 的七个键。
%    \begin{macrocode}
\bool_new:N \l_@@_rod_units_vertical_bool
\bool_set_true:N \l_@@_rod_units_vertical_bool
\bool_new:N \l_@@_rod_zero_omit_bool
\bool_new:N \l_@@_rod_minus_overlay_bool
\tl_new:N \l_@@_rod_font_tl
\tl_new:N \l_@@_rod_kern_tl
\tl_set:Nn \l_@@_rod_kern_tl { -.14 em }
%    \end{macrocode}
% 零、小数点与负号这三个字符\emph{不}走 \tn{zhnumsetup} 的样式与编码体系：
% 算筹只在 Unicode 引擎下可用，没有 GBK／Big5 分支要照顾，而它的零是〇、
% 小数点在史料里也不写汉字「点」。故直接用独立变量，不并入 \cs{zhnum_set_digits_map:nnn}。
%    \begin{macrocode}
\tl_new:N \l_@@_rod_zero_tl
\tl_set:Nn \l_@@_rod_zero_tl { 〇 }
\tl_new:N \l_@@_rod_dot_tl
\tl_set:Nn \l_@@_rod_dot_tl { . }
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}[int]{\@@_rod_vertical:n,\@@_rod_horizontal:n}
% 这两个宏各从一个码区取出一个字符。注意 \emph{区名与字形朝向相反}：
% Unicode 的 \texttt{COUNTING ROD UNIT DIGIT} 区（U+1D360 起）是\textbf{横}画，
% \texttt{TENS DIGIT} 区（U+1D369 起）是\textbf{纵}画。
% 实测 \file{LXGWWenKaiGBLite-Regular.ttf} 下 U+1D360 的 \texttt{wd} 为 10\,pt、
% \texttt{ht} 为 4.05\,pt（扁），U+1D369 的 \texttt{ht} 为 8.03\,pt（高），
% 换 \file{NotoSansSymbols2-Regular.ttf} 复核一致，所以这是 Unicode 的编排而非某款
% 字体的取舍。故\emph{纵}式取 TENS 区、\emph{横}式取 UNIT 区，按区名直译会得到相反的结果。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_vertical:n #1
  { \char_generate:nn { \int_eval:n { "1D368 + #1 } } { 12 } }
\cs_new:Npn \@@_rod_horizontal:n #1
  { \char_generate:nn { \int_eval:n { "1D35F + #1 } } { 12 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_rod_glyph:nn}
% 取一位数字 |#1| 与它的位值 |#2|，产出对应字符。位值奇偶决定朝向：偶位值
% （个位、百位、万位……）用 \tn{zhrodsetup} 的 \texttt{units} 所指定的那一式，
% 奇位值用另一式。
%
% 零单独处理：默认排出 \cs{l_@@_rod_zero_tl}（即〇），\texttt{zero=omit} 时跳过。
% 跳过会产生歧义，理由见用户文档。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_glyph:nn #1#2
  {
    \zhnum_if_digit:NTF #1
      { \@@_rod_digit:nn {#1} {#2} }
      { \exp_not:n {#1} }
  }
%    \end{macrocode}
% 非数字的输入原样输出，不塞进 \cs{int_eval:n} 的算术里。否则
% \verb|\zhrod{1a}| 会把 \texttt{a} 当成数字参与 \verb|"1D368 + a|，
% 把 \cs{__int_eval:w} 一类内部记号漏到页面上（实测在筹码之后多排出 \verb|a=0| 与一个〇；
% 这里不把筹码字形写进 \tn{verb}，等宽字体没有算筹字形，会多出缺字警告）。
% 注意这与 \tn{zhdigits} 的策略\emph{相反}而不是「取齐」：\tn{zhdigits} 把不认识的字符
% \emph{丢掉}（\verb|\zhdigits{1a4}| 得到「一四」），\tn{zhrod} 把它\emph{原样留下}
% （得到筹码、\texttt{a}、筹码）。两者都不报错，但保留原字符更便于发现输入有问题。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_digit:nn #1#2
  {
    \int_compare:nNnTF {#1} = { 0 }
      {
        \bool_if:NF \l_@@_rod_zero_omit_bool
          { \zhnum_exp_not:o \l_@@_rod_zero_tl }
      }
      {
        \@@_rod_kern:
        \int_compare:nNnTF { \int_mod:nn {#2} { 2 } } = { 0 }
          {
            \bool_if:NTF \l_@@_rod_units_vertical_bool
              { \@@_rod_vertical:n   {#1} }
              { \@@_rod_horizontal:n {#1} }
          }
          {
            \bool_if:NTF \l_@@_rod_units_vertical_bool
              { \@@_rod_horizontal:n {#1} }
              { \@@_rod_vertical:n   {#1} }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {\@@_rod_int:n,\@@_rod_int_loop:nn,\@@_rod_int_head:wn}
% 整数部分逐位处理：位值从 \verb|(位数 - 1)| 递减到 0，交给 \cs{@@_rod_glyph:nn}。
%
% 现有的两条读法都不能复用。\cs{zhnum_digits:Nn} 那条逐位循环
% （\cs{@@_read_digits_loop:NN}）\emph{全程不携带「当前是第几位」}：它的布尔参数只是
% 最初传入的 zero/null 开关，小数点靠字符比较识别、负号在循环前一次性剥离。
% \cs{zhnum_integer:n} 那条虽然统计了长度，但那是为了按四位一段做万／亿制分组，
% 不是位值奇偶。所以算筹需要单独写一条带位值的循环。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_int:n #1
  { \@@_rod_int_loop:nn {#1} { \tl_count:n {#1} - 1 } }
%    \end{macrocode}
% 位值用 \cs{tl_count:n} 数，因此参数里的\emph{花括号分组会算作一位}，让各位的纵横整体
% 错开（分组里含非数字时更会直接报错并以非零状态退出）——\verb|\zhrod{12\ver}|（\verb|\ver| 展开为 \verb|{2026}|）与 \verb|\zhrod{122026}|
% 排出的朝向完全相反，且不报错。这是已知限制，手册里写明了，因为修它要把这两条循环改成
% \cs{zhnum_digits:Nn} 那种 \cs{exp:w} \cs{exp_end_continue_f:w} \cs{use:n} 的单记号
% 取参形式（那种形式天然展开一层分组），同时位值计数也要改成先展平再数，改动范围比这个
% 功能本身还大。\tn{zhdigits} 不受影响：它逐位取字、不需要知道位置。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_int_loop:nn #1#2
  { \tl_if_blank:nF {#1} { \@@_rod_int_head:wn #1 \q_stop {#2} } }
\cs_new:Npn \@@_rod_int_head:wn #1#2 \q_stop #3
  {
    \@@_rod_glyph:nn {#1} { \int_eval:n {#3} }
    \@@_rod_int_loop:nn {#2} { \int_eval:n { #3 - 1 } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {\@@_rod_frac:n,\@@_rod_frac_loop:nn,\@@_rod_frac_head:wn}
% 小数部分：位值从 $-1$ 继续递减，交替因此跨小数点自然延续
% （分位与个位异式、厘位与个位同式）。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_frac:n #1
  { \@@_rod_frac_loop:nn {#1} { -1 } }
\cs_new:Npn \@@_rod_frac_loop:nn #1#2
  { \tl_if_blank:nF {#1} { \@@_rod_frac_head:wn #1 \q_stop {#2} } }
\cs_new:Npn \@@_rod_frac_head:wn #1#2 \q_stop #3
  {
    \@@_rod_glyph:nn {#1} { \int_eval:n {#3} }
    \@@_rod_frac_loop:nn {#2} { \int_eval:n { #3 - 1 } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_rod_number:n,\@@_rod_split:www,\@@_rod_sign:w,
%   \@@_rod_overlay_if_base:n,\@@_rod_base_mark:n}
% 拆负号与小数点，再分派给上面两条循环。负号排在哪里取决于 \texttt{minus} 的取值，
% 见下文。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_slash:
  { \char_generate:nn { "FF3C } { 12 } }
\cs_new:Npn \@@_rod_overlay:
  { \char_generate:nn { "20E5 } { 12 } }
%    \end{macrocode}
% 两种负号的位置\emph{不同}，不能共用一个输出点：
%
% \begin{itemize}
%   \item \texttt{slash} 是独立字符，排在筹码\emph{之前}；
%   \item \texttt{overlay} 是组合字符（U+20E5），必须紧跟在\emph{最后一位筹码之后}，
%     才会叠加到那一位上。规范说的正是「叠加在最右一位上」。
% \end{itemize}
%
% 初版两者都排在最前，于是 \texttt{overlay} 叠加到了算筹\emph{前面}那个字符上：
% \verb|X\zhrod{-1234}Y| 会把斜线叠到 \texttt{X} 上，而单独放进盒子时它不叠任何东西，
% 负数与正数的盒子宽度完全相同（实测 JuliaMono 下都是 24\,pt），等于负号没排出来。
% 这个缺陷由本地盲审发现。
%
% \texttt{overlay} 还要求确实有一个基字符可叠。以下输入不排出任何可叠的字符：
% \texttt{zero=omit} 时整串全是 0 的数（\verb|-0|、\verb|-0.0|）、\texttt{zerochar} 置空
% 时同样的输入，以及只有负号的 \verb|-|。若照样追加 U+20E5，它会叠到小数点或算筹前面的
% 正文上。\cs{@@_rod_overlay_if_base:n} 逐个检查输入的记号，只要有一个能排出可叠的字符就
% 追加组合字符，否则不追加：0 只有在 \texttt{zero=fill} 且 \texttt{zerochar} 非空时才
% 排出〇，小数点不算，其余记号（非零数字排出筹码，其他字符原样输出）都可以作基字符。
% 此时数值就是零，负零按零处理，输出与 \verb|0|、\verb|0.0| 相同。\texttt{slash} 的＼
% 是独立字符，不会叠到别的字符上，保持原样。这个缺陷由 PR 审查发现。
%
% 两种记法依赖的字符在现有字体里\emph{恰好互补}：逐一检查 \TeX\ Live 里含全部算筹字形的
% 19 款字体，\pkg{lxgw-fonts} 的 5 款只有 U+FF3C、\pkg{juliamono} 的 14 款只有 U+20E5，
% 没有一款同时具备。因此这两个选项都必须保留，谁也不能当作另一个的替代。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_number:n #1
  { \tl_if_blank:nF {#1} { \@@_rod_sign:w #1 \q_stop } }
\cs_generate_variant:Nn \@@_rod_number:n { e }
\cs_new:Npn \@@_rod_sign:w #1#2 \q_stop
  {
    \str_if_eq:nnTF {#1} { - }
      {
        \bool_if:NF \l_@@_rod_minus_overlay_bool { \@@_rod_slash: }
        \@@_rod_split:www #2 . \q_nil . \q_stop
        \bool_if:NT \l_@@_rod_minus_overlay_bool
          { \@@_rod_overlay_if_base:n {#2} }
      }
      {
        \str_if_eq:nnTF {#1} { + }
          { \@@_rod_split:www #2 . \q_nil . \q_stop }
          { \@@_rod_split:www #1#2 . \q_nil . \q_stop }
      }
  }
\cs_new:Npn \@@_rod_overlay_if_base:n #1
  {
    \tl_if_blank:eF { \tl_map_function:nN {#1} \@@_rod_base_mark:n }
      { \@@_rod_overlay: }
  }
\cs_new:Npn \@@_rod_base_mark:n #1
  {
    \str_if_eq:nnTF {#1} { 0 }
      {
        \bool_lazy_or:nnF
          { \l_@@_rod_zero_omit_bool }
          { \tl_if_blank_p:V \l_@@_rod_zero_tl }
          { * }
      }
      { \str_if_eq:nnF {#1} { . } { * } }
  }
\cs_new:Npn \@@_rod_split:www #1 . #2 . #3 \q_stop
  {
    \@@_rod_int:n {#1}
    \quark_if_nil:nF {#2}
      {
        \zhnum_exp_not:o \l_@@_rod_dot_tl
        \@@_rod_frac:n {#2}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhrod}
% 可展开的算筹输出，与 \tn{zhdigits} 同级。它\emph{只}产出字符，不切字体、不加字距，
% 因此可以进 \file{.toc} 与 PDF 书签：实测这一形式在书签里得到正确的代理对、
% 零条 \texttt{Token not allowed in a PDF string} 警告，而下面 \tn{zhrodbox}
% 那种切字体加字距的形式在同一处会退化并报警告。
%
% \pdfTeX{}／\upTeX{} 下定义成直接报错的版本，不做静默退化：算筹没有 8-bit 的
% 退化写法，退化只会让使用者以为自己排出了算筹。
%    \begin{macrocode}
\msg_new:nnn { zhnumber } { rod-engine-unsupported }
  {
    \token_to_str:N \zhrod \c_space_tl requires ~ XeTeX ~ or ~ LuaTeX. \\
    Counting-rod ~ code ~ points ~ (U+1D360--U+1D371) ~ cannot ~ be ~
    represented ~ by ~ 8-bit ~ engines.
  }
\bool_if:NTF \c_@@_rod_engine_bool
  {
    \zhnum_expand_wrap:wn
    \cs_new:Npn \zhrod #1
      { \@@_rod_number:e {#1} }
  }
  {
    \cs_new_protected:Npn \zhrod #1
      { \msg_error:nn { zhnumber } { rod-engine-unsupported } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhrodbox}
% 排版层：切字体并在相邻字符之间压紧字距。它\emph{不可展开}，不能写进辅助文件。
%
% 字距默认取 \verb|-.14em|，这是个\emph{临时视觉参数}，尚未按史料核对，
% 所以做成可调键而不是写死。
%    \begin{macrocode}
\bool_if:NTF \c_@@_rod_engine_bool
  {
    \cs_new_protected:Npn \zhrodbox #1
      {
        \group_begin:
          \tl_use:N \l_@@_rod_font_tl
          \bool_set_true:N \l_@@_rod_kerning_bool
          \bool_set_false:N \l_@@_rod_kern_seen_bool
          \@@_rod_number:e {#1}
        \group_end:
      }
  }
  {
    \cs_new_protected:Npn \zhrodbox #1
      { \msg_error:nn { zhnumber } { rod-engine-unsupported } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_rod_kern:,\@@_rod_kern_node:}
% 把已经展开成字符序列的算筹逐字排出，字符之间插入字距。
% 先用 \texttt{e} 变体把 \cs{@@_rod_number:n} 展开成纯字符，再逐字处理，
% 这样字距逻辑不必重复位值判断。
%
% 用 \tn{kern} 而\emph{不是} \cs{skip_horizontal:n}：后者排出的是 glue，
% 而 glue 是合法的断行点，于是一串筹码会被拆到两行去
% （实测 \verb|\hsize=60pt| 下 \texttt{123456789} 断成三行）。
% \tn{kern} 不构成断点。这个缺陷由本地盲审发现。
%
% 但这只保证\emph{本命令自己插入的}间距不构成断点，\textbf{并不保证整串筹码一定不断行}：
% 在 \cls{ctexart} 一类加载了 \pkg{xeCJK} 的文档里，筹码本身不是汉字（\tn{XeTeXcharclass}
% 为 0），而占位的〇与 \texttt{minus=slash} 的＼都\emph{是}（为 1），\pkg{xeCJK} 会在这
% 两类字符的交界处插入 \tn{CJKecglue}，那是 glue、因而是可断点。所以 \texttt{zero=fill}
% 时每个〇两侧各有一处，\texttt{minus=slash} 时负号与首位筹码之间也有一处。
%
% 这些位置是否\emph{真的}断，取决于断行的代价：实测 \texttt{fontset=lxgw}、
% \verb|\hsize=60pt| 下，\verb|\zhrodbox{10203040}| 在 \texttt{zero=fill} 时会断开
% （〇提供了多个可断点），而同一串在 \texttt{zero=omit} 时不断（没有〇，也就没有那些
% 可断点）；负号那一处虽然是 glue，实测在 20\,pt 与 60\,pt 下都不会实际断开，因为断在那里
% 会把＼单独留下、代价过高。
%
% 结论是：\tn{kern} 保证的只是\emph{本命令自己插入的}间距不构成断点。要整串筹码一定
% 不断，请自己把它放进 \tn{mbox}，或把 \tn{CJKecglue} 设为 \tn{kern}。
%    \begin{macrocode}
\cs_new:Npn \@@_rod_kern:
  {
    \bool_if:NT \l_@@_rod_kerning_bool
      { \exp_not:V \c_@@_rod_kern_node_tl }
  }
\bool_new:N \l_@@_rod_kerning_bool
\tl_const:Nn \c_@@_rod_kern_node_tl { \@@_rod_kern_node: }
%    \end{macrocode}
% 字距排在每个筹码\emph{之前}，因此第一个筹码前会多出一个。它不能靠「是不是第一位」
% 来判断——\texttt{zero=omit} 时前导零不排任何东西，「第一位」与「第一个实际排出来的
% 筹码」不是一回事（初版按位置插字距，于是 \verb|\zhrodbox{100}| 在 \texttt{omit} 下
% 留下两个多余的字距，把后面的内容拉进筹码里，实测宽度 7.2\,pt 而可展开版是 10.0\,pt）。
%
% 做法是让 \cs{@@_rod_kern_node:} 在盒子里第一次调用时什么都不排、其后才排字距。
% 它只在 \tn{zhrodbox} 的分组内使用，所以这个状态随分组自动复位；\tn{zhrod} 不开
% \cs{l_@@_rod_kerning_bool}，展开结果里连这个记号都不会出现。
%    \begin{macrocode}
\cs_new_protected:Npn \@@_rod_kern_node:
  {
    \bool_if:NTF \l_@@_rod_kern_seen_bool
      { \tex_kern:D \dim_eval:n { \l_@@_rod_kern_tl } \scan_stop: }
      { \bool_set_true:N \l_@@_rod_kern_seen_bool }
  }
\bool_new:N \l_@@_rod_kern_seen_bool
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhdate}
% 输出中文日期。
%    \begin{macrocode}
\msg_new:nnn  { zhnumber } { weekday-unavailable }
  { Weekday ~ is ~ unavailable. ~ Please ~ assign ~ the ~ `day'. }
\NewExpandableDocumentCommand \zhdate { +s +m }
  {
    \@@_date:wwww #2 / / / \q_stop
    \bool_if:NT #1 { \@@_date_star:wwww #2 / / / \q_stop }
  }
%    \end{macrocode}
% 为确保在输入 |yyyy| 或 |yyyy/mm| 的情况下提供足够的 delimiter，
% 辅助函数 \cs{@@_date:wwww} 添加一个额外的 |/#4| 并在调用时添加等量的 delimiter
% \footnote{\url{https://tex.stackexchange.com/a/764551/299948}}。^^A
%    \begin{macrocode}
\cs_new:Npn \@@_date:wwww #1 / #2 / #3 / #4 \q_stop
  { \@@_date_aux:nnn {#1} {#2} {#3} }
\cs_new:Npn \@@_date_star:wwww #1 / #2 / #3 / #4 \q_stop
  {
    \tl_if_blank:eTF {#3}
      { \msg_expandable_error:nnn { zhnumber } { weekday-unavailable } { } }
      { \@@_week_day:www #1/#2/#3 \q_stop }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhtoday}
% 输出当天日期。
%    \begin{macrocode}
\cs_new:Npn \zhtoday
  {
    \@@_date_aux:nnn
      { \int_value:w \tex_year:D }
      { \tex_month:D }
      { \tex_day:D }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_date_aux:nnn}
%    \begin{macrocode}
\cs_new:Npn \@@_date_aux:nnn
  {
    \bool_if:NTF \l_@@_time_bool
      { \@@_date_aux:NNnnnn \zhnum_digits_null:n \zhnum_int:n { } }
      { \@@_date_aux:Nnnnn \int_to_arabic:n { \l_@@_arabic_sep_tl } }
  }
\cs_new:Npn \@@_date_aux:Nnnnn #1#2
  { \@@_date_aux:NNnnnn #1#1 { \zhnum_exp_not:o {#2} } }
\zhnum_expand_wrap:wn
\cs_new:Npn \@@_date_aux:NNnnnn #1#2#3#4#5#6
  {
    \tl_if_empty:eF {#4} {    #1 {#4} #3 \zhnum_output:n { year  } }
    \tl_if_empty:eF {#5} { #3 #2 {#5} #3 \zhnum_output:n { month } }
    \tl_if_empty:eF {#6} { #3 #2 {#6} #3 \zhnum_output:n { day   } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhweekday}
% 输出星期
%    \begin{macrocode}
\cs_new:Npn \zhweekday #1
  { \@@_week_day:www #1 \q_stop }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_week_day:www}
% 用 Zeller 公式计算的结果 $h$ 与实际星期的关系是 $d=h+5\pmod7+1$。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \@@_week_day:www #1/#2/#3 \q_stop
  {
    \if_case:w \zhnum_Zeller:nnn {#1} {#2} {#3} \exp_stop_f:
           \zhnum_output:n { sat }
      \or: \zhnum_output:n { sun }
      \or: \zhnum_output:n { mon }
      \or: \zhnum_output:n { tue }
      \or: \zhnum_output:n { wed }
      \or: \zhnum_output:n { thu }
      \or: \zhnum_output:n { fri }
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_Zeller:nnn,\zhnum_Zeller_aux:Nnnn,\zhnum_two_digits:n}
% 用 Zeller 公式\footnote{\url{http://en.wikipedia.org/wiki/Zeller's_congruence}}
% 计算星期几。
%    \begin{macrocode}
\cs_new:Npn \zhnum_Zeller:nnn #1#2#3
  {
    \int_compare:nNnTF
      { #1 \zhnum_two_digits:n {#2} \zhnum_two_digits:n {#3} } > { 1582 10 04 }
      { \@@_Zeller_aux:Nnnn \zhnum_Zeller_Gregorian:nnn }
      { \@@_Zeller_aux:Nnnn \zhnum_Zeller_Julian:nnn }
    {#1} {#2} {#3}
  }
\cs_new:Npn \@@_Zeller_aux:Nnnn  #1#2#3#4
  {
    \int_compare:nNnTF {#3} < 3
      { #1 { #2 - 1 } { #3 + 12 } {#4} }
      { #1 {#2} {#3} {#4} }
  }
\cs_new:Npn \zhnum_two_digits:n #1
  {
    \int_compare:nNnT {#1} < { 10 } { 0 }
    \int_eval:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_Zeller_Gregorian:nnn}
% 格里历（\zhdate{1582/10/15}及以后）的计算公式
% \[
%   h = \biggl(q + \biggl\lfloor\frac{26(m+1)}{10}\biggr\rfloor + Y +
%   \biggl\lfloor\frac Y4\biggr\rfloor + 6\biggl\lfloor\frac Y{100}\biggr\rfloor
%   + \biggl\lfloor\frac Y{400}\biggr\rfloor\biggr) \pmod 7
% \]
% 其中 $Y$ 为年，$m$ 为月，$q$ 为日；若 $m=1,2$，则令 $m\mathbin{{+}{=}}12$，同时 $Y\mathop{--}{}$。
%    \begin{macrocode}
\cs_new:Npn \zhnum_Zeller_Gregorian:nnn #1#2#3
  {
    \int_mod:nn
      {
          (#3)
        + \int_div_truncate:nn { 26 * ( #2 + 1 ) } { 10 }
        + (#1)
        + \int_div_truncate:nn {#1} { 4 }
        + 6 * \int_div_truncate:nn {#1} { 100 }
        + \int_div_truncate:nn {#1} { 400 }
      }
      { 7 }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_Zeller_Julian:nnn}
% 儒略历（\zhdate{1582/10/04}及以前）的计算公式
% \[
%   h = \biggl(q + \biggl\lfloor\frac{26(m+1)}{10}\biggr\rfloor + Y +
%   \biggl\lfloor\frac Y4\biggr\rfloor + 5\biggr) \pmod 7
% \]
%    \begin{macrocode}
\cs_new:Npn \zhnum_Zeller_Julian:nnn #1#2#3
  {
    \int_mod:nn
      {
          (#3)
        + \int_div_truncate:nn { 26 * ( #2 + 1 ) } { 10 }
        + (#1)
        + \int_div_truncate:nn {#1} { 4 }
        + 5
      }
      { 7 }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhtime}
% 输出时间。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhtime #1
  { \@@_time:ww #1 \q_stop }
\use:e
  {
    \cs_new:Npn \exp_not:N \@@_time:ww
      #1 \c_colon_str #2 \exp_not:N \q_stop
  }
  { \@@_time_aux:nn {#1} {#2} }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhcurrtime}
% 输出当前时间。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhcurrtime
  {
    \@@_time_aux:nn
      { \int_div_truncate:nn \tex_time:D { 60 } }
      { \int_mod:nn \tex_time:D { 60 } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_time_aux:nn,\@@_time_aux:Nnnn}
%    \begin{macrocode}
\cs_new:Npn \@@_time_aux:nn
  {
    \bool_if:NTF \l_@@_time_bool
      { \@@_time_aux:Nnnn \zhnum_int:n { } }
      { \@@_time_aux:Nnnn \int_to_arabic:n { \l_@@_arabic_sep_tl } }
  }
\cs_new:Npn \@@_time_aux:Nnnn #1#2#3#4
  {
    #1 {#3} #2 \zhnum_output:n { hour } #2
    #1 {#4} #2 \zhnum_output:n { minute }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_scale_map:n,\zhnum_scale_map_loop:n}
% 大数系统的映射。
%    \begin{macrocode}
\cs_new:Npn \zhnum_scale_map:n #1
  {
    \tl_if_exist:cTF { l_@@_s #1 _tl }
      { \zhnum_output:n { s #1 } }
      { \zhnum_scale_map_hook:n {#1} }
  }
\cs_new:Npn \zhnum_scale_map_loop:n #1
  { \zhnum_scale_map:n { \int_mod:nn {#1} \l_@@_scale_int } }
\cs_generate_variant:Nn \zhnum_scale_map:n { f }
\int_new:N \l_@@_scale_int
\int_set:Nn \l_@@_scale_int { 11 }
\cs_new_eq:NN \zhnum_scale_map_hook:n \zhnum_scale_map_loop:n
\tl_new:c { l_@@_s 0 _tl }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhnumExtendScaleMap}
% 扩展进位系统。
%    \begin{macrocode}
\NewDocumentCommand \zhnumExtendScaleMap { > { \TrimSpaces } +o +m }
  {
    \int_zero:N \l_@@_tmp_int
    \clist_map_function:nN {#2} \zhnum_set_scale:n
    \tl_if_novalue:nF {#1}
      { \cs_set:Npn \zhnum_scale_map_hook:n ##1 {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_set_scale:n}
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_set_scale:n #1
  {
    \int_incr:N \l_@@_tmp_int
    \exp_args:Nc \@@_set_scale:Nn
      { l_@@_s \int_eval:n { \l_@@_tmp_int + 11 } _tl }
      {#1}
  }
\cs_new_protected:Npn \@@_set_scale:Nn #1
  {
    \tl_if_exist:NF #1
      {
        \tl_new:N #1
        \int_incr:N \l_@@_scale_int
      }
    \tl_set:Nn #1
  }
\int_new:N \l_@@_tmp_int
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_ganzhi_normal:nnn}
% 保证干支的参数为正数。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhnum_ganzhi_normal:nnn #1#2#3
  {
    \int_compare:nNnF {#1} < \c_one_int
      {
        \cs_if_free:cF { l_@@_ #2 _ #1 _tl }
          { \zhnum_output:n { #2 _ #1 } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_ganzhi_cyclic:nnn}
% \begin{macro}{\@@_ganzhi_cyclic_mod:nnnn}
% 对超出范围的数字取模，参数 |0| 的结果是空值。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhnum_ganzhi_cyclic:nnn #1#2#3
  {
    \int_compare:nNnF {#1} = \c_zero_int
      {
        \tl_if_exist:cTF { l_@@_ #2 _ #1 _tl }
          { \zhnum_output:n { #2 _ #1 } }
          {
            \@@_ganzhi_cyclic_mod:fnnn
              { \int_mod:nn {#1} {#3} } {#1} {#2} {#3}
          }
      }
  }
\cs_new:Npn \@@_ganzhi_cyclic_mod:nnnn #1#2#3#4
  {
    \int_compare:nNnTF {#2} > \c_zero_int
      { \zhnum_output:n { #3 _ #1 } }
      {
        \int_compare:nNnTF {#1} = \c_zero_int
          { \zhnum_output:n { #3 _ 1 } }
          { \zhnum_output:n { #3 _ \int_eval:n {  #1 + #4 + 1 } } }
      }
  }
\cs_generate_variant:Nn \@@_ganzhi_cyclic_mod:nnnn { f }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_ganzhi:nnn}
% 默认不对超出范围的数字取模。
%    \begin{macrocode}
\cs_new_eq:NN \zhnum_ganzhi:nnn \zhnum_ganzhi_normal:nnn
\cs_generate_variant:Nn \zhnum_ganzhi:nnn { f }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhtiangan}
% 天干。
%    \begin{macrocode}
\cs_new:Npn \zhtiangan #1
  { \zhnum_ganzhi:fnn { \int_eval:n {#1} } { tiangan } { 10 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhdizhi}
% 地支。
%    \begin{macrocode}
\cs_new:Npn \zhdizhi #1
  { \zhnum_ganzhi:fnn { \int_eval:n {#1} } { dizhi } { 12 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhganzhi}
% 干支。
%    \begin{macrocode}
\cs_new:Npn \zhganzhi #1
  { \zhnum_ganzhi:fnn { \int_eval:n {#1} } { ganzhi } { 60 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhganzhinian}
% 干支纪年。
%    \begin{macrocode}
\cs_new:Npn \zhganzhinian #1
  { \zhnum_ganzhi_nian:f { \int_eval:n {#1} } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_ganzhi_nian:n}
% 干支纪年。公元元年是 |\zhganzhi{58}|。
%    \begin{macrocode}
\zhnum_expand_wrap:wn
\cs_new:Npn \zhnum_ganzhi_nian:n #1
  {
    \int_compare:nNnTF {#1} > \c_zero_int
      { \zhnum_output:n { ganzhi_ \int_mod:nn { #1 + 57 } { 60 } } }
      {
        \int_compare:nNnF {#1} = \c_zero_int
          {
            \zhnum_output:n
              {
                ganzhi_ \int_eval:n
                  { \int_mod:nn { #1 - 2 } { 60 } + 60 }
              }
          }
      }
  }
\cs_generate_variant:Nn \zhnum_ganzhi_nian:n { f }
%    \end{macrocode}
% \end{macro}
%
% 根据需要设置中文阿拉伯数字。
%    \begin{macrocode}
\tl_new:N \l_@@_kv_tl
\tl_new:N \l_@@_tmp_tl
\group_begin:
  \tl_build_begin:N \l_@@_kv_tl
  \int_step_inline:nn { 10 }
    {
      \tl_new:c { l_@@_ #1 _tl }
      \tl_build_put_right:Ne \l_@@_kv_tl
        {
            #1 .tl_set:N = \exp_not:c { l_@@_normal_    #1 _tl } ,
           F#1 .tl_set:N = \exp_not:c { l_@@_financial_ #1 _tl } ,
          E \int_eval:n { #1 * 4 }
               .tl_set:N = \exp_not:c { l_@@_ s#1 _tl } ,
        }
    }
  \clist_map_inline:nn { 0 , 100 , 1000 }
    {
      \tl_new:c { l_@@_ #1 _tl }
      \tl_build_put_right:Ne \l_@@_kv_tl
        {
           #1 .tl_set:N = \exp_not:c { l_@@_normal_    #1 _tl } ,
          F#1 .tl_set:N = \exp_not:c { l_@@_financial_ #1 _tl } ,
        }
    }
  \clist_map_inline:nn
    {
      20 , 30 , 40 , 200 ,
      dot , and , parts , year , month , day , hour , minute
    }
    {
      \tl_build_put_right:Ne \l_@@_kv_tl
        { #1 .tl_set:N = \exp_not:c { l_@@_ #1 _tl } , }
    }
  \tl_build_put_right:Ne \l_@@_kv_tl
    {
      -   .tl_set:N = \exp_not:N \l_@@_minus_tl ,
      -0  .tl_set:N = \exp_not:N \l_@@_null_tl ,
      E2  .tl_set:N = \exp_not:c { l_@@_normal_     100 _tl } ,
      E3  .tl_set:N = \exp_not:c { l_@@_normal_    1000 _tl } ,
      FE2 .tl_set:N = \exp_not:c { l_@@_financial_  100 _tl } ,
      FE3 .tl_set:N = \exp_not:c { l_@@_financial_ 1000 _tl } ,
      E44 .tl_set:N = \exp_not:c { l_@@_ s11 _tl }
    }
%    \end{macrocode}
% \pkg{expl3} 2023-12-14 起，\cs{tl_build_get:NN} 是废弃命令。当 \pkg{l3debug}
% 的 \texttt{deprecation} 开启，废弃命令被重定义为 \tn{outer} 的，无法在其他命令
% 的参数中使用，所以这里用 \cs{use:c} 绕过。
%    \begin{macrocode}
  \cs_if_exist:NTF \tl_build_get_intermediate:NN
    { \tl_build_get_intermediate:NN }
    { \use:c { tl_build_get:NN } }
      \l_@@_kv_tl \l_@@_tmp_tl
  \cs_set:Npn \@@_tmp:w #1 . #2 \q_stop
    { , #1 .groups:n = { user } }
  \clist_map_inline:Nn \l_@@_tmp_tl
    {
      \tl_build_put_right:Ne \l_@@_kv_tl
        { \@@_tmp:w #1 \q_stop }
    }
  \tl_build_put_right:Nn \l_@@_kv_tl
    {
      ,
      weekday .tl_set:N = \l_@@_weekday_tl ,
      weekday .groups:n = { user , pre , weekday } ,
    }
  \clist_map_inline:nn
    { mon , tue , wed , thu , fri , sat , sun }
    {
      \tl_build_put_right:Ne \l_@@_kv_tl
        {
          #1 .tl_set:N = \exp_not:c { l_@@_ #1 _tl } ,
          #1 .groups:n = { user , pos , day } ,
        }
    }
%    \end{macrocode}
% \changes{v3.1}{2026/08/18}{修正 \opt{Tn} 绑定到干支变量而非天干变量的笔误——设置
%   \opt{Tn} 此前会改掉 \tn{zhganzhi} 的输出而对 \tn{zhtiangan} 无效（\#1077）。}
%
% 下面三组键的目标变量必须\emph{各自独立}：\opt{Tn} 写 \cs{l_@@_tiangan_\#1_tl}、
% \opt{Dn} 写 \cs{l_@@_dizhi_\#1_tl}、\opt{GZn} 写 \cs{l_@@_ganzhi_\#1_tl}。
% \tn{zhganzhi} 的输出由天干与地支两个变量组合而成，只有在用户显式设置 \opt{GZn} 时才
% 直接取干支变量，所以三者不能共用同一个目标。
%
% \#1077 之前 \opt{Tn} 抄成了 \cs{l_@@_ganzhi_\#1_tl}，与 \opt{GZn} 指向同一个变量：设置
% \opt{T1} 改掉的是 \tn{zhganzhi}\Arg{1} 而不是 \tn{zhtiangan}\Arg{1}。这处笔误单看
% \opt{Tn} 那两行是看不出来的——步数 \texttt{10}（天干正好十个）与
% \texttt{groups} 里的 \texttt{tiandi} 都是对的，只有变量名错。判断这类问题要把三组键
% 并排比对，而不是逐组检查其内部自洽性。
%    \begin{macrocode}
  \int_step_inline:nn { 10 }
    {
      \tl_build_put_right:Ne \l_@@_kv_tl
        {
          T#1 .tl_set:N = \exp_not:c { l_@@_tiangan_ #1 _tl } ,
          T#1 .groups:n = { user , pre , tiandi } ,
        }
    }
  \int_step_inline:nn { 12 }
    {
      \tl_build_put_right:Ne \l_@@_kv_tl
        {
          D#1 .tl_set:N = \exp_not:c { l_@@_dizhi_ #1 _tl } ,
          D#1 .groups:n = { user , pre , tiandi } ,
        }
    }
  \int_step_inline:nn { 60 }
    {
      \tl_build_put_right:Ne \l_@@_kv_tl
        {
          GZ#1 .tl_set:N = \exp_not:c { l_@@_ganzhi_ #1 _tl } ,
          GZ#1 .groups:n = { user , pos , ganzhi } ,
        }
    }
  \tl_build_end:N \l_@@_kv_tl
  \exp_args:NNno \group_end:
\keys_define:nn
  { zhnum / options }
  { \l_@@_kv_tl }
%    \end{macrocode}
%
% \begin{macro}[int]
%   {
%     \zhnum_set_digits_map:nn,
%     \zhnum_set_digits_map:nnn,
%     \zhnum_set_financial_map:nn,
%     \zhnum_set_financial_map:nnn,
%     \zhnum_set_tiangan_map:nn,
%     \zhnum_set_dizhi_map:nn
%   }
% \begin{variable}
%   {
%     \l_@@_cfg_map_prop,
%     \l_@@_cfg_map_var_prop,
%     \l_@@_cfg_map_finan_prop,
%     \l_@@_cfg_map_ganzhi_prop
%   }
% 将配置文件中的中文数字保存到 \texttt{prop} 变量中。
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_set_digits_map:nn #1
  { \prop_put:Nnn \l_@@_cfg_map_prop {#1} }
\cs_new_protected:Npn \zhnum_set_digits_map:nnn #1#2#3
  {
    \prop_put_if_new:Nnn \l_@@_cfg_map_prop {#1} {#3}
    \prop_put:Nnn \l_@@_cfg_map_var_prop {#1_#2} {#3}
  }
\cs_new_protected:Npn \zhnum_set_financial_map:nn #1
  { \prop_put:Nnn \l_@@_cfg_map_finan_prop {#1} }
\cs_new_protected:Npn \zhnum_set_financial_map:nnn #1#2#3
  {
    \prop_put_if_new:Nnn \l_@@_cfg_map_finan_prop {#1} {#3}
    \prop_put:Nnn \l_@@_cfg_map_var_prop { financial_#1_#2 } {#3}
  }
\cs_new_protected:Npn \zhnum_set_tiangan_map:nn #1
  { \prop_put:Nnn \l_@@_cfg_map_ganzhi_prop { tiangan_#1 } }
\cs_new_protected:Npn \zhnum_set_dizhi_map:nn #1
  { \prop_put:Nnn \l_@@_cfg_map_ganzhi_prop { dizhi_#1 } }
\prop_new:N \l_@@_cfg_map_prop
\prop_new:N \l_@@_cfg_map_var_prop
\prop_new:N \l_@@_cfg_map_finan_prop
\prop_new:N \l_@@_cfg_map_ganzhi_prop
%    \end{macrocode}
% \end{variable}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \zhnum_parse_config:,
%     \zhnum_check_simp:nn,
%     \zhnum_check_financial:nn,
%     \zhnum_set_week_day:
%   }
% 将 \texttt{prop} 表转化到单独的 \texttt{tl} 变量。
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_parse_config:
  {
    \tl_clear_new:N \l_@@_reset_tl
    \tl_clear_new:N \l_@@_reset_simp_tl
    \tl_clear_new:N \l_@@_reset_trad_tl
    \tl_clear_new:N \l_@@_set_ancient_tl
    \tl_clear_new:N \l_@@_set_normal_tl
    \tl_clear_new:N \l_@@_reset_ancient_tl
    \tl_clear_new:N \l_@@_reset_normal_tl
    \tl_clear_new:N \l_@@_reset_financial_tl
    \prop_map_function:NN \l_@@_cfg_map_prop \zhnum_check_simp:nn
    \zhnum_set_ganzhi:
    \zhnum_reset_all:
  }
\cs_new_protected:Npn \zhnum_check_simp:nn #1#2
  {
    \prop_get:NnNTF \l_@@_cfg_map_var_prop
      { #1_ancient } \l_@@_ancient_tl
      { \@@_add_reset_ancient:nN {#1} \l_@@_ancient_tl }
      {
        \@@_check_simp_aux:nn {#2} {#1}
        \prop_get:NnNT \l_@@_cfg_map_finan_prop {#1} \l_@@_tmp_tl
          {
            \exp_args:No \@@_check_simp_aux:nn
              { \l_@@_tmp_tl } { financial_ #1 }
            \@@_add_reset_financial:n {#1}
          }
      }
  }
\cs_new_protected:Npn \@@_check_simp_aux:nn #1#2
  {
    \prop_get:NnNTF \l_@@_cfg_map_var_prop
      { #2 _trad } \l_@@_trad_tl
      {
        \prop_get:NnNF \l_@@_cfg_map_var_prop
          { #2 _simp } \l_@@_simp_tl
          { \tl_set:Nn \l_@@_simp_tl {#1} }
        \@@_add_reset_simp:nNN
          {#2} \l_@@_simp_tl \l_@@_trad_tl
      }
      { \@@_add_reset:nn {#2} {#1} }
  }
\tl_new:N \l_@@_simp_tl
\tl_new:N \l_@@_trad_tl
\tl_new:N \l_@@_ancient_tl
\cs_new_protected:Npn \@@_add_reset:nn #1#2
  {
    \tl_put_right:Ne \l_@@_reset_tl
      {
        \tl_set:Nn \exp_not:c { l_@@_ #1 _tl }
          { \exp_not:n {#2} }
      }
  }
\cs_new_protected:Npn \@@_add_reset_simp:nNN #1#2#3
  {
    \tl_put_right:Ne \l_@@_reset_simp_tl
      {
        \tl_set:Nn \exp_not:c { l_@@_ #1 _tl }
          { \exp_not:o {#2} }
      }
    \tl_put_right:Ne \l_@@_reset_trad_tl
      {
        \tl_set:Nn \exp_not:c { l_@@_ #1 _tl }
          { \exp_not:o {#3} }
      }
  }
\cs_new_protected:Npn \@@_add_reset_financial:n #1
  {
    \tl_put_right:Ne \l_@@_set_normal_tl
      {
        \tl_set_eq:NN
          \exp_not:c { l_@@_normal_ #1 _tl }
          \exp_not:c { l_@@_ #1 _tl }
      }
    \tl_put_right:Ne \l_@@_reset_normal_tl
      {
        \tl_set_eq:NN
          \exp_not:c { l_@@_ #1 _tl }
          \exp_not:c { l_@@_normal_ #1 _tl }
      }
    \tl_put_right:Ne \l_@@_reset_financial_tl
      {
        \tl_set_eq:NN
          \exp_not:c { l_@@_ #1 _tl }
          \exp_not:c { l_@@_financial_ #1 _tl }
      }
  }
\cs_new_protected:Npn \@@_add_reset_ancient:nN #1#2
  {
    \tl_put_right:Ne \l_@@_reset_ancient_tl
      {
        \tl_set:Nn \exp_not:c { l_@@_ #1 _tl }
          { \exp_not:o {#2} }
      }
    \tl_put_right:Ne \l_@@_set_ancient_tl
      {
        \tl_concat:NNN
          \exp_not:c { l_@@_ #1 _tl }
          \exp_not:c { l_@@_   \str_head:n {#1} _tl }
          \exp_not:c { l_@@_ 1 \str_tail:n {#1} _tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \zhnum_set_week_day:,
%     \zhnum_reset_week_day:
%   }
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_set_week_day:
  {
    \cs_set_protected:Npe \zhnum_reset_week_day:
      {
        \@@_set_week_day:nn { mon } { 1 }
        \@@_set_week_day:nn { tue } { 2 }
        \@@_set_week_day:nn { wed } { 3 }
        \@@_set_week_day:nn { thu } { 4 }
        \@@_set_week_day:nn { fri } { 5 }
        \@@_set_week_day:nn { sat } { 6 }
        \@@_set_week_day:nn { sun } { day }
      }
  }
\cs_new_eq:NN \zhnum_reset_week_day: \prg_do_nothing:
\cs_new:Npn \@@_set_week_day:nn #1#2
  {
    \tl_set:Ne \exp_not:c { l_@@_ #1 _tl }
      {
        \exp_not:N \exp_not:o { \exp_not:N \l_@@_weekday_tl }
        \exp_not:N \exp_not:n { \exp_not:v { l_@@_ #2 _tl } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \zhnum_set_ganzhi:,
%     \zhnum_reset_ganzhi:
%   }
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_set_ganzhi:
  {
    \prop_map_function:NN
      \l_@@_cfg_map_ganzhi_prop
      \@@_add_reset:nn
  }
\cs_new_protected:Npn \@@_reset_ganzhi:nn #1#2
  { \tl_set:cn { l_@@_ #1 _tl } {#2} }
\cs_new:Npn \zhnum_zero_mod:nn #1#2
  { \exp_args:Nf \@@_zero_mod_aux:nn { \int_mod:nn {#1} {#2} } {#2} }
\cs_new:Npn \@@_zero_mod_aux:nn #1#2
  { \int_compare:nNnTF {#1} = \c_zero_int {#2} {#1} }
\tl_new:c { l_@@_dizhi_ 0 _tl }
\tl_new:c { l_@@_ganzhi_ 0 _tl }
\tl_new:c { l_@@_tiangan_ 0 _tl }
\group_begin:
\cs_set:Npn \@@_tmp:w #1
  {
    \tl_concat:NNN
      \exp_not:c { l_@@_ganzhi_ #1 _tl }
      \exp_not:c { l_@@_tiangan_ \zhnum_zero_mod:nn {#1} { 10 } _tl }
      \exp_not:c { l_@@_dizhi_   \zhnum_zero_mod:nn {#1} { 12 } _tl }
  }
\cs_new_protected:Npe \zhnum_reset_ganzhi:
  {
    \tl_set_eq:NN
      \exp_not:c { l_@@_dizhi_ 0 _tl }
      \exp_not:c { l_@@_dizhi_ 12 _tl }
    \tl_set_eq:NN
      \exp_not:c { l_@@_tiangan_ 0 _tl }
      \exp_not:c { l_@@_tiangan_ 10 _tl }
    \int_step_function:nN { 60 } \@@_tmp:w
    \tl_set_eq:NN
      \exp_not:c { l_@@_ganzhi_ 0 _tl }
      \exp_not:c { l_@@_ganzhi_ 60 _tl }
  }
\group_end:
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \zhnum_reset_config:,
%     \zhnum_reset_all:,
%     \zhnum_reset_style:
%   }
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_reset_config:
  { \zhnum_load_cfg:o { \l_@@_encoding_str } }
\cs_new_protected:Npn \zhnum_reset_all:
  {
    \zhnum_reset_main:
    \zhnum_reset_simp:
    \zhnum_set_week_day:
    \zhnum_reset_week_day:
    \zhnum_reset_ganzhi:
    \zhnum_reset_normal:
  }
\cs_new_protected:Npn \zhnum_reset_main:
  {
    \tl_use:N \l_@@_reset_tl
    \tl_use:N \l_@@_set_normal_tl
    \tl_concat:NNN
      \l_@@_reset_normal_tl
      \l_@@_reset_normal_tl
      \c_@@_set_zero_tl
    \tl_concat:NNN
      \l_@@_reset_financial_tl
      \l_@@_reset_financial_tl
      \c_@@_set_zero_tl
    \tl_concat:NNN
      \l_@@_reset_financial_tl
      \l_@@_reset_financial_tl
      \l_@@_set_ancient_tl
  }
\tl_const:Ne \c_@@_set_zero_tl
  {
    \tl_set_eq:NN
      \exp_not:N \l_@@_zero_tl
      \exp_not:c { l_@@_0_tl }
  }
\tl_new:N \l_@@_zero_tl
\cs_new_protected:Npn \zhnum_reset_style:
  {
    \zhnum_reset_simp:
    \zhnum_reset_normal:
  }
\cs_new_protected:Npn \zhnum_reset_simp:
  {
    \bool_if:NTF \l_@@_simp_bool
      { \tl_use:N \l_@@_reset_simp_tl }
      { \tl_use:N \l_@@_reset_trad_tl }
  }
\cs_new_protected:Npn \zhnum_reset_normal:
  {
    \bool_if:NTF \l_@@_normal_bool
      {
        \tl_use:N \l_@@_reset_normal_tl
        \@@_reset_ancient:
        \@@_reset_zero:
      }
      { \tl_use:N \l_@@_reset_financial_tl }
  }
\cs_new_protected:Npn \@@_reset_ancient:
  {
    \bool_if:NTF \l_@@_ancient_bool
      { \tl_use:N \l_@@_reset_ancient_tl }
      { \tl_use:N \l_@@_set_ancient_tl }
  }
\cs_new_protected:Npe \@@_reset_zero:
  {
    \exp_not:n { \bool_if:NT \l_@@_null_bool }
      {
        \tl_set_eq:NN
          \exp_not:c { l_@@_0_tl }
          \exp_not:N \l_@@_null_tl
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_load_cfg:n}
% 根据选定编码载入配置文件。
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_load_cfg:n #1
  {
    \zhnum_set_cfg_name:Nn \l_@@_cfg_str {#1}
    \str_if_eq:NNTF \l_@@_cfg_str \l_@@_last_cfg_str
      { \zhnum_reset_all: }
      {
        \zhnum_update_cfg:n {#1}
        \zhnum_parse_config:
      }
  }
\cs_generate_variant:Nn \zhnum_load_cfg:n { o }
\cs_new_protected:Npn \zhnum_update_cfg:n #1
  {
    \prop_if_exist:cTF { g_@@_cfg_ \l_@@_cfg_str _prop }
      { \@@_set_cfg_prop: }
      { \zhnum_input_cfg:n {#1} }
  }
\cs_new_protected:Npn \@@_set_cfg_prop:
  {
    \str_set_eq:NN \l_@@_last_cfg_str \l_@@_cfg_str
    \@@_update_cfg_prop:N \prop_set_eq:Nc
  }
\cs_new_protected:Npn \zhnum_input_cfg:n #1
  {
    \file_get_full_name:nNTF { zhnumber - #1 .cfg } \l_@@_cfg_file_tl
      {
        \bool_set_false:N \l_@@_reset_bool
        \@@_update_cfg_prop:N \@@_prop_initial:Nn
        \group_begin:
          \zhnum_set_catcode:
          \exp_args:No \file_input:n { \l_@@_cfg_file_tl }
          \@@_update_cfg_prop:N \@@_prop_gset_eq:Nn
        \group_end:
        \@@_set_cfg_prop:
      }
      { \msg_error:nne { zhnumber } { file-not-found } {#1} }
  }
\tl_new:N \l_@@_cfg_file_tl
\cs_new_protected:Npn \@@_update_cfg_prop:N
  { \exp_args:No \@@_update_cfg_prop_aux:nN { \l_@@_cfg_str } }
\cs_new_protected:Npn \@@_update_cfg_prop_aux:nN #1#2
  {
    #2 \l_@@_cfg_map_prop        { g_@@_cfg_        #1 _prop }
    #2 \l_@@_cfg_map_var_prop    { g_@@_cfg_var_    #1 _prop }
    #2 \l_@@_cfg_map_finan_prop  { g_@@_cfg_finan_  #1 _prop }
    #2 \l_@@_cfg_map_ganzhi_prop { g_@@_cfg_ganzhi_ #1 _prop }
  }
\cs_new_protected:Npn \@@_prop_initial:Nn #1#2
  {
    \prop_clear:N #1
    \prop_new:c {#2}
  }
\cs_new_protected:Npn \@@_prop_gset_eq:Nn #1#2
  { \prop_gset_eq:cN {#2} #1 }
\str_new:N \l_@@_cfg_str
\str_new:N \l_@@_last_cfg_str
\bool_new:N \l_@@_reset_bool
\msg_new:nnnn  { zhnumber } { file-not-found }
  { File~`#1'~not~found. }
  {
    The~requested~file~could~not~be~found~in~the~current~directory,~
    in~the~TeX~search~path~or~in~the~LaTeX~search~path.
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\c_@@_unicode_engine_bool}
% 使用 \upTeX{} 的时候，也不必将汉字的首字符设置为活动字符。判断 |^^^^0021| 是否为
% 单个记号的办法对 \upTeX{} 不适用。
%    \begin{macrocode}
\bool_const:Nn \c_@@_unicode_engine_bool
  {
    \bool_lazy_any_p:n
      {
        { \sys_if_engine_xetex_p:  }
        { \sys_if_engine_luatex_p: }
        { \sys_if_engine_uptex_p: }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \zhnum_set_catcode:,
%     \zhnum_set_cfg_name:Nn,
%     \zhnum_reset_config:
%   }
% 设置与恢复配置文件前后的 catcode。\pdfLaTeX{} 需要将汉字的首字节设置为活动字符。
%    \begin{macrocode}
\bool_if:NTF \c_@@_unicode_engine_bool
  {
    \cs_new_eq:NN \zhnum_set_catcode: \prg_do_nothing:
    \cs_new_protected:Npn \zhnum_set_cfg_name:Nn #1#2
      {
        \str_set:Ne \l_@@_encoding_str {#2}
        \str_set_eq:NN #1 \l_@@_encoding_str
      }
  }
  {
    \cs_new_protected:Npn \zhnum_set_catcode:
      {
        \bool_if:NTF \l_@@_active_char_bool
          { \zhnum_set_active: }
          { \zhnum_set_other: }
      }
    \cs_new_protected:Npe \zhnum_set_active:
      {
        \int_step_function:nnN
          { 128 } { 255 } \char_set_catcode_active:n
      }
    \cs_new_protected:Npe \zhnum_set_other:
      {
        \int_step_function:nnN
          { 128 } { 255 } \char_set_catcode_other:n
      }
    \cs_new_protected:Npn \zhnum_set_cfg_name:Nn #1#2
      {
        \str_set:Ne \l_@@_encoding_str {#2}
        \str_set:Ne #1
          {
            \l_@@_encoding_str
            \bool_if:NTF \l_@@_active_char_bool
              { / active }
              { / other }
          }
      }
    \bool_new:N \l_@@_active_char_bool
    \bool_set_true:N \l_@@_active_char_bool
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\zhnum_set_encoding:n}
% 设置编码。
%    \begin{macrocode}
\cs_new_protected:Npn \zhnum_set_encoding:n #1
  {
    \str_set:Ne \l_@@_encoding_str
      { \str_lowercase:n {#1} }
    \zhnum_load_cfg:o { \l_@@_encoding_str }
  }
\str_new:N \l_@@_encoding_str
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{encoding,style,null,reset}
% 宏包设置选项。
%    \begin{macrocode}
\keys_define:nn { zhnum / options }
  {
    encoding         .choices:nn =
      { UTF8 , GBK , Big5 }
      { \zhnum_set_encoding:n {#1} } ,
    encoding          .default:n = { GBK } ,
    encoding / Bg5       .meta:n = { encoding = Big5 } ,
    encoding / unknown   .code:n =
      { \msg_error:nnn { zhnumber } { encoding-invalid } {#1} } ,
    style .multichoice: ,
    style / Normal       .code:n =
      {
        \bool_set_false:N \l_@@_ancient_bool
        \bool_set_true:N  \l_@@_normal_bool
      } ,
    style / Financial    .code:n =
      {
        \bool_set_false:N \l_@@_ancient_bool
        \bool_set_false:N \l_@@_normal_bool
      } ,
    style / Ancient      .code:n =
      {
        \bool_set_true:N \l_@@_ancient_bool
        \bool_set_true:N \l_@@_normal_bool
      } ,
    style / Simplified   .code:n = { \bool_set_true:N  \l_@@_simp_bool } ,
    style / Traditional  .code:n = { \bool_set_false:N \l_@@_simp_bool } ,
    style             .default:n = { Normal , Simplified } ,
    style              .groups:n = { style } ,
    null             .bool_set:N = \l_@@_null_bool ,
    null               .groups:n = { style } ,
    time .choice: ,
    time / Chinese       .code:n = { \bool_set_true:N \l_@@_time_bool } ,
    time / Arabic        .code:n = { \bool_set_false:N  \l_@@_time_bool } ,
    time              .default:n = { Arabic } ,
    reset            .bool_set:N = \l_@@_reset_bool ,
    activechar       .bool_set:N = \l_@@_active_char_bool ,
    ganzhi-cyclic .choice: ,
    ganzhi-cyclic / true .code:n =
      { \cs_set_eq:NN \zhnum_ganzhi:nnn \zhnum_ganzhi_cyclic:nnn } ,
    ganzhi-cyclic / false.code:n =
      { \cs_set_eq:NN \zhnum_ganzhi:nnn \zhnum_ganzhi_normal:nnn } ,
    ganzhi-cyclic     .default:n = { true } ,
    arabicsep          .tl_set:N = \l_@@_arabic_sep_tl
  }
%    \end{macrocode}
% 算筹的键单独一个模块。它们与 \tn{zhnumsetup} 那批键作用不同：那批管的是「每个数字
% 换成哪个汉字」（样式与编码），算筹管的是「按位值奇偶选哪个码区」，并且只在
% Unicode 引擎下有意义。混进同一模块会让 \texttt{style} 一类键在算筹下显得可用而实际无效。
%
% \texttt{zero} 与既有的 \texttt{null} 作用也不同：\texttt{null} 管「0 映射成零还是〇」
% （\tn{zhdigits} 默认已用〇），\texttt{zero} 管「写不写」。
%    \begin{macrocode}
\keys_define:nn { zhnum / rod }
  {
    units .choice: ,
    units / vertical    .code:n =
      { \bool_set_true:N  \l_@@_rod_units_vertical_bool } ,
    units / horizontal  .code:n =
      { \bool_set_false:N \l_@@_rod_units_vertical_bool } ,
    units            .default:n = { vertical } ,
    zero .choice: ,
    zero / fill         .code:n =
      { \bool_set_false:N \l_@@_rod_zero_omit_bool } ,
    zero / omit         .code:n =
      { \bool_set_true:N  \l_@@_rod_zero_omit_bool } ,
    zero             .default:n = { fill } ,
    minus .choice: ,
    minus / slash       .code:n =
      { \bool_set_false:N \l_@@_rod_minus_overlay_bool } ,
    minus / overlay     .code:n =
      { \bool_set_true:N  \l_@@_rod_minus_overlay_bool } ,
    minus            .default:n = { slash } ,
    font               .tl_set:N = \l_@@_rod_font_tl ,
    kern               .tl_set:N = \l_@@_rod_kern_tl ,
    zerochar           .tl_set:N = \l_@@_rod_zero_tl ,
    dot                .tl_set:N = \l_@@_rod_dot_tl
  }
\NewDocumentCommand \zhrodsetup { +m }
  {
    \keys_set:nn { zhnum / rod } {#1}
    \tex_ignorespaces:D
  }
%    \end{macrocode}
%    \begin{macrocode}
\bool_new:N \l_@@_simp_bool
\bool_new:N \l_@@_normal_bool
\bool_new:N \l_@@_ancient_bool
\msg_new:nnnn { zhnumber } { encoding-invalid }
  { The~encoding~`#1'~is~invalid. }
  { Available~encodings~are~`UTF8',~`GBK'~and~`Big5'. }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\zhnumsetup}
% 在文档中设置 \pkg{zhnumber} 的接口。
% \changes{v3.1}{2026/09/27}{改用 \cs{keys_set_exclude_groups:nnnN} 代替已弃用的
%   \cs{keys_set_filter:nnnN}，避免 l3kernel 2026-09-09 起在调试模式下报错（\#1095）。}
%    \begin{macrocode}
\NewDocumentCommand \zhnumsetup { +m }
  {
    \zhnum_set:n {#1}
    \tex_ignorespaces:D
  }
\cs_new_protected:Npn \zhnum_set:n #1
  {
    \bool_set_false:N \l_@@_reset_bool
    \keys_set_exclude_groups:nnnN { zhnum / options }
      { style , user } {#1} \l_@@_kv_tl
    \tl_if_empty:NF \l_@@_kv_tl
      { \@@_set_style: }
    \bool_if:NT \l_@@_reset_bool
      { \zhnum_reset_config: }
  }
\cs_new_protected:Npn \@@_set_style:
  {
    \keys_set_exclude_groups:nnoN { zhnum / options }
      { user } { \l_@@_kv_tl } \l_@@_tmp_tl
    \tl_if_empty:NTF \l_@@_tmp_tl
      { \zhnum_reset_style: }
      {
        \tl_if_eq:NNF \l_@@_tmp_tl \l_@@_kv_tl
          { \zhnum_reset_style: }
        \bool_if:NF \l_@@_reset_bool
          { \@@_set_user: }
      }
  }
\cs_new_protected:Npn \@@_set_user:
  {
    \keys_set_exclude_groups:nnoN { zhnum / options }
      { pre , pos } { \l_@@_tmp_tl } \l_@@_kv_tl
    \tl_if_eq:NNF \l_@@_kv_tl \l_@@_tmp_tl
      { \zhnum_reset_normal: }
    \tl_if_empty:NF \l_@@_kv_tl
      { \@@_set_pre_pos: }
  }
\cs_new_protected:Npn \@@_set_pre_pos:
  {
    \keys_set_exclude_groups:nnoN { zhnum / options }
      { pos } { \l_@@_kv_tl } \l_@@_tmp_tl
    \tl_if_eq:NNF \l_@@_tmp_tl \l_@@_kv_tl
      {
        \zhnum_reset_week_day:
        \zhnum_reset_ganzhi:
      }
    \tl_if_empty:NF \l_@@_kv_tl
      { \keys_set:no { zhnum / options } { \l_@@_kv_tl } }
  }
%    \end{macrocode}
% \end{macro}
%
% 初始化设置和执行宏包选项。
%    \begin{macrocode}
\keys_set:nn { zhnum / options }
  { style , time , arabicsep = { ~ } }
\cs_if_exist:NTF \ProcessKeyOptions
  { \ProcessKeyOptions [ zhnum / options ] }
  {
    \RequirePackage { l3keys2e }
    \ProcessKeysOptions { zhnum / options }
  }
%    \end{macrocode}
%
% 如果没有选定编码，则选用 |UTF8|。
%    \begin{macrocode}
\str_if_empty:NTF \l_@@_encoding_str
  { \zhnum_set_encoding:n { UTF8 } }
  { \zhnum_reset_style: }
%    \end{macrocode}
%
%    \begin{macrocode}
%</package>
%    \end{macrocode}
%
% \section{中文数字配置文件}
% \label{sec:zhnum-map}
%
%    \begin{macrocode}
%<*config>
%    \end{macrocode}
%
%    \begin{macrocode}
%<*!big5>
\zhnum_set_digits_map:nnn { minus } { simp } { 负 }
\zhnum_set_digits_map:nnn { minus } { trad } { 負 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { minus } { 負 }
%</big5>
\zhnum_set_digits_map:nn { 0 }     { 零 }
%<*!big5>
\zhnum_set_digits_map:nn { null }  { 〇 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { null }  { ○ }
%</big5>
\zhnum_set_digits_map:nn { 1 }     { 一 }
\zhnum_set_digits_map:nn { 2 }     { 二 }
\zhnum_set_digits_map:nn { 3 }     { 三 }
\zhnum_set_digits_map:nn { 4 }     { 四 }
\zhnum_set_digits_map:nn { 5 }     { 五 }
\zhnum_set_digits_map:nn { 6 }     { 六 }
\zhnum_set_digits_map:nn { 7 }     { 七 }
\zhnum_set_digits_map:nn { 8 }     { 八 }
\zhnum_set_digits_map:nn { 9 }     { 九 }
\zhnum_set_digits_map:nn { 10 }    { 十 }
\zhnum_set_digits_map:nn { 100 }   { 百 }
\zhnum_set_digits_map:nn { 1000 }  { 千 }
\zhnum_set_digits_map:nnn { 20 }   { ancient } { 廿 }
\zhnum_set_digits_map:nnn { 30 }   { ancient } { 卅 }
\zhnum_set_digits_map:nnn { 40 }   { ancient } { 卌 }
\zhnum_set_digits_map:nnn { 200 }  { ancient } { 皕 }
%<*!big5>
\zhnum_set_digits_map:nnn { dot } { simp } { 点 }
\zhnum_set_digits_map:nnn { dot } { trad } { 點 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { dot }   { 點 }
%</big5>
\zhnum_set_digits_map:nn { and }   { 又 }
\zhnum_set_digits_map:nn { parts } { 分之 }
%<*!big5>
\zhnum_set_digits_map:nnn { s1 }    { simp } { 万 }
\zhnum_set_digits_map:nnn { s1 }    { trad } { 萬 }
\zhnum_set_digits_map:nnn { s2 }    { simp } { 亿 }
\zhnum_set_digits_map:nnn { s2 }    { trad } { 億 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { s1 }    { 萬 }
\zhnum_set_digits_map:nn { s2 }    { 億 }
%</big5>
\zhnum_set_digits_map:nn { s3 }    { 兆 }
\zhnum_set_digits_map:nn { s4 }    { 京 }
\zhnum_set_digits_map:nn { s5 }    { 垓 }
\zhnum_set_digits_map:nn { s6 }    { 秭 }
\zhnum_set_digits_map:nn { s7 }    { 穰 }
%<*!big5>
\zhnum_set_digits_map:nnn { s8 }    { simp } { 沟 }
\zhnum_set_digits_map:nnn { s8 }    { trad } { 溝 }
\zhnum_set_digits_map:nnn { s9 }    { simp } { 涧 }
\zhnum_set_digits_map:nnn { s9 }    { trad } { 澗 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { s8 }    { 溝 }
\zhnum_set_digits_map:nn { s9 }    { 澗 }
%</big5>
\zhnum_set_digits_map:nn { s10 }   { 正 }
%<*!big5>
\zhnum_set_digits_map:nnn { s11 }   { simp } { 载 }
\zhnum_set_digits_map:nnn { s11 }   { trad } { 載 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { s11 }   { 載 }
%</big5>
\zhnum_set_digits_map:nn { year }  { 年 }
\zhnum_set_digits_map:nn { month } { 月 }
\zhnum_set_digits_map:nn { day }   { 日 }
%<*!big5>
\zhnum_set_digits_map:nnn { hour }  { simp } { 时 }
\zhnum_set_digits_map:nnn { hour }  { trad } { 時 }
%</!big5>
%<*big5>
\zhnum_set_digits_map:nn { hour }  { 時 }
%</big5>
\zhnum_set_digits_map:nn { minute }  { 分 }
\zhnum_set_digits_map:nn { weekday } { 星期 }
\zhnum_set_financial_map:nn { null } { 零 }
\zhnum_set_financial_map:nn { 0 }    { 零 }
\zhnum_set_financial_map:nn { 1 }    { 壹 }
%<*!big5>
\zhnum_set_financial_map:nnn { 2 }  { simp } { 贰 }
\zhnum_set_financial_map:nnn { 2 }  { trad } { 貳 }
\zhnum_set_financial_map:nnn { 3 }  { simp } { 叁 }
\zhnum_set_financial_map:nnn { 3 }  { trad } { 叄 }
%</!big5>
%<*big5>
\zhnum_set_financial_map:nn { 2 }    { 貳 }
\zhnum_set_financial_map:nn { 3 }    { 參 }
%</big5>
\zhnum_set_financial_map:nn { 4 }    { 肆 }
\zhnum_set_financial_map:nn { 5 }    { 伍 }
%<*!big5>
\zhnum_set_financial_map:nnn { 6 }  { simp } { 陆 }
\zhnum_set_financial_map:nnn { 6 }  { trad } { 陸 }
%</!big5>
%<*big5>
\zhnum_set_financial_map:nn { 6 }    { 陸 }
%</big5>
\zhnum_set_financial_map:nn { 7 }    { 柒 }
\zhnum_set_financial_map:nn { 8 }    { 捌 }
\zhnum_set_financial_map:nn { 9 }    { 玖 }
\zhnum_set_financial_map:nn { 10 }   { 拾 }
\zhnum_set_financial_map:nn { 100 }  { 佰 }
\zhnum_set_financial_map:nn { 1000 } { 仟 }
\zhnum_set_tiangan_map:nn { 1 }  { 甲 }
\zhnum_set_tiangan_map:nn { 2 }  { 乙 }
\zhnum_set_tiangan_map:nn { 3 }  { 丙 }
\zhnum_set_tiangan_map:nn { 4 }  { 丁 }
\zhnum_set_tiangan_map:nn { 5 }  { 戊 }
\zhnum_set_tiangan_map:nn { 6 }  { 己 }
\zhnum_set_tiangan_map:nn { 7 }  { 庚 }
\zhnum_set_tiangan_map:nn { 8 }  { 辛 }
\zhnum_set_tiangan_map:nn { 9 }  { 壬 }
\zhnum_set_tiangan_map:nn { 10 } { 癸 }
\zhnum_set_dizhi_map:nn { 1 }  { 子 }
\zhnum_set_dizhi_map:nn { 2 }  { 丑 }
\zhnum_set_dizhi_map:nn { 3 }  { 寅 }
\zhnum_set_dizhi_map:nn { 4 }  { 卯 }
\zhnum_set_dizhi_map:nn { 5 }  { 辰 }
\zhnum_set_dizhi_map:nn { 6 }  { 巳 }
\zhnum_set_dizhi_map:nn { 7 }  { 午 }
\zhnum_set_dizhi_map:nn { 8 }  { 未 }
\zhnum_set_dizhi_map:nn { 9 }  { 申 }
\zhnum_set_dizhi_map:nn { 10 } { 酉 }
\zhnum_set_dizhi_map:nn { 11 } { 戌 }
\zhnum_set_dizhi_map:nn { 12 } { 亥 }
%    \end{macrocode}
%
%    \begin{macrocode}
%</config>
%    \end{macrocode}
%
% \end{implementation}
%
% \Finale
%
\endinput
