emacs-利用elisp进行大小写中文和数字的转换
有的时候,我们想快速进行数字与大小写中文的转换。
1. 将小写中文转换为阿拉伯数字
支持范围:零到九百九十九
(defun org-smart-get-chn-time-chn2num (chn-str)
"将中文数字'零'到'九百九十九'转换为整数,支持'两'作为'二'的变体。"
(save-match-data
(let* ((num 0)
(digits `(("零" . 0) ("一" . 1) ("二" . 2) ("两" . 2) ("三" . 3) ("四" . 4)
("五" . 5) ("六" . 6) ("七" . 7) ("八" . 8) ("九" . 9)))
(hundreds-pos (string-match "百" chn-str))
(tens-pos (string-match "十" chn-str)))
;; 处理百位
(when hundreds-pos
(let* ((char-before (substring chn-str 0 hundreds-pos))
(digit (cdr (assoc char-before digits))))
(setq num (+ num (* digit 100)))))
;; 处理十位
(when tens-pos
(let* ((char-before (if hundreds-pos
(substring chn-str (1+ hundreds-pos) tens-pos)
(substring chn-str 0 tens-pos)))
(digit (cdr (assoc char-before digits))))
(cond
;; "十" 或 "一十" 的情况
((or (string= "" char-before) (string= "一" char-before))
(setq num (+ num 10)))
;; "零" 在十位前,例如 "一百零五" 中的 "零"
((string= "零" char-before))
;; "零" 本身不增加数值,但会影响个位的解析,此处无需额外操作
(t
(setq num (+ num (* (or digit 1) 10)))))))
;; 处理个位
(let* ((last-char (substring chn-str -1))
(last-digit (cdr (assoc last-char digits))))
(when (and last-digit (not (or (string= last-char "百") (string= last-char "十"))))
;; 如果 "零" 出现在末尾,例如 "一百一十",last-char会是"十",所以这里的判断是有效的
(if (and tens-pos (= 0 (length (substring chn-str (1+ tens-pos)))))
;; 类似于 "二十", "三十" 这样的整十数,个位不再处理
()
(setq num (+ num last-digit)))))
;; 处理特殊情况 "十"
(if (string= "十" chn-str)
10
num))))
2. 将阿拉伯数字转化为大写/小写中文
支持范围: 0.01 到 99999999.99 之间的数字,小数最多2位数。
交互式调用 num2chn 默认会提示输入数字并转换为小写中文;如果用 C-u 的前缀调用,则转换为会计的大写中文。
在elisp 中调用时统一转换为小写中文。
(defconst num2chn--lower-digits
'("零" "一" "二" "三" "四" "五" "六" "七" "八" "九"))
(defconst num2chn--upper-digits
'("零" "壹" "贰" "叁" "肆" "伍" "陆" "柒" "捌" "玖"))
;; 依次为:千 百 十 个 万
(defconst num2chn--lower-units '("千" "百" "十" "" "万"))
(defconst num2chn--upper-units '("仟" "佰" "拾" "" "万"))
(defun num2chn--section (n digits units strip)
"把 0-9999 的整数 N 转成中文。
DIGITS 为 0-9 的中文数字表,UNITS 为 (千 百 十 \"\")。
STRIP 非 nil 且结果以“一十/壹拾”开头时,省略开头的数字。"
(let ((result "")
(pending-zero nil))
(dolist (pair '((1000 . 0) (100 . 1) (10 . 2) (1 . 3)))
(let* ((place (car pair))
(idx (cdr pair))
(digit (floor n place)))
(setq n (% n place))
(cond
((zerop digit)
;; 只有后面还有非零数字时才补“零”
(when (and (> n 0) (> (length result) 0))
(setq pending-zero t)))
(t
(when pending-zero
(setq result (concat result (nth 0 digits))))
(setq pending-zero nil)
(setq result (concat result (nth digit digits) (nth idx units)))))))
(if (and strip
(>= (length result) 2)
(string= (substring result 0 1) (nth 1 digits))
(string= (substring result 1 2) (nth 2 units)))
(substring result 1)
result)))
(defun num2chn--integer (n digits units strip)
"把 0-99999999 的整数 N 转成中文。
DIGITS、UNITS 含义同 `num2chn--section';STRIP 只作用于最高位那一段。"
(cond
((zerop n)
(nth 0 digits))
((> n 9999)
(let* ((high (floor n 10000))
(low (mod n 10000))
(result (concat (num2chn--section high digits units strip)
(nth 4 units))))
(if (zerop low)
result
(concat result
(if (< low 1000) (nth 0 digits) "")
(num2chn--section low digits units nil)))))
(t
(num2chn--section n digits units strip))))
(defun num2chn (num &optional uppercase clipboard)
"把 0.01 到 99999999.99 之间的数字转换成中文。
NUM 可以是数字或数字字符串,超过两位小数时四舍五入。
UPPERCASE 非 nil 时返回金额大写(壹元贰角叁分),否则返回普通读数
(一点二三)。CLIPBOARD 非 nil 时把结果复制到剪贴板。
交互调用时读取一个数字,并把结果复制到剪贴板;带前缀参数(C-u)
时返回金额大写。作为函数调用时不碰剪贴板。
(num2chn 1.23) => \"一点二三\"
(num2chn 1.23 t) => \"壹元贰角叁分\"
(num2chn 1.23 nil t) => 返回 \"一点二三\",同时复制到剪贴板"
(interactive
(list (read-number "数字 (0.01~99999999.99): ") current-prefix-arg t))
(let* ((num (if (numberp num) num (string-to-number num)))
(cents (round (* num 100))))
(unless (<= 1 cents 9999999999)
(error "请输入 0.01 到 99999999.99 之间的数字"))
(let* ((int-part (floor cents 100))
(frac (mod cents 100))
(jiao (floor frac 10))
(fen (mod frac 10))
(result (if uppercase
(num2chn--currency int-part jiao fen)
(num2chn--plain int-part jiao fen))))
(when clipboard
(kill-new result)
(message "已复制到剪贴板:%s" result))
result)))
(defun num2chn--plain (int-part jiao fen)
"普通读法:整数部分 + “点” + 小数位。"
(let ((digits num2chn--lower-digits)
(units num2chn--lower-units))
(concat (num2chn--integer int-part digits units t)
(cond
((and (zerop jiao) (zerop fen)) "")
((zerop jiao) (concat "点" (nth 0 digits) (nth fen digits)))
((zerop fen) (concat "点" (nth jiao digits)))
(t (concat "点" (nth jiao digits) (nth fen digits)))))))
(defun num2chn--currency (int-part jiao fen)
"金额大写:元、角、分;无角分时以“整”结尾。"
(let* ((digits num2chn--upper-digits)
(units num2chn--upper-units)
(int-str (if (> int-part 0)
(concat (num2chn--integer int-part digits units nil) "圆")
"")))
(cond
((and (zerop jiao) (zerop fen))
(concat int-str "整"))
(t
(concat int-str
(if (and (> int-part 0) (zerop jiao) (> fen 0))
(nth 0 digits)
"")
(if (> jiao 0) (concat (nth jiao digits) "角") "")
(if (> fen 0) (concat (nth fen digits) "分") ""))))))
3. 方便在正则中调用的别名
这样一来就可以用 \,(str2num \1) 在正则中将匹配到的数字结果转换为数字进行计算后替换回去了。
(defalias 'num2str 'number-to-string)
(defalias 'str2num 'string-to-number)