一、前言
在《算法的乐趣》中看到阿拉伯数字与中文数字的转换算法(C语言实现),感觉不太清晰。这里用Racket语言重新做了一个实现,并进一步完善为到兆分节并可进行大写转换。
这里是接前一篇内容(详见:https://blog.csdn.net/chinazhangyong/article/details/80588160)。为了独自成篇,前一篇内容提到的内容这里如有需要,依然保留。
源代码可见:https://github.com/OnRoadZy/ConvertNumberAndChinese.git
先对中文数字转阿拉伯数字算法做一个解释。
二、中文数字转阿拉伯数字的算法实现
根据中文数字分节特点,可分为千、万、亿、兆四种片段进行处理。而万分节片段中包含千分节片段,同样,亿、兆分节也包括低一级的分节片段。每一个分节里边通常是“数+量词”的组对(个位及零位除外)。
这样,可以针对各分节情况分别编程进行模块化处理。我们就可以把中文字串当成列表处理,而每一个分节可以通过检查其显式表达的“万”、“亿”、“兆”等字符进行分节划分,将每一个分节里的“数+量词”对及零、个位数转化成阿拉伯数字(通过数×量级),然后分别加起来即是所需数值。把对各分节的处理模块连接成管道,将中文字串输入管道,完成分节、转化、组合这个流程,输出即为我们需要的数字值。
阿拉伯数字转中文数字采用由高位到低位的顺序解析,而中文数字转阿拉伯数字采用由低位到高位的顺序解析。
三、具体实现
1、准备数据列表
要完成转化,需要准备中文数字及阿拉伯数字的对应值,这里通过列表(数组)来存储相应内容。
由于各列表都是已知内容并固定,这里用vector实现。
;定义数据列表:
(define chinese-number (vector "零" "一" "二" "三" "四" "五" "六" "七" "八" "九" "十" "百" "千" "万" "亿" "兆"))
(define chinese-t-number (vector "零" "壹" "贰" "叁" "肆" "伍" "陆" "柒" "捌" "玖" "拾" "佰" "仟" "万" "亿" "兆"))
(define number-vector (vector 0 1 2 3 4 5 6 7 8 9 10 100 1000 10000 100000000 10000000000000000))
2、统一处理大小写中文
为了简化编程,这里将大写中文和普通中文进行统一。通过一个全局变量chinese-vector实现,以函数关键字参数进行选择。
;初始化全局chinese-vector:
(define chinese-vector chinese-number)
实现选择的程序片段:
;根据参数情况设置全局chinese-vector值:
(set! chinese-vector
(case tag
['normal chinese-number]
['capitalization chinese-t-number]
[else (display "#:style参数错误。")]))
3、关于数字代替
为了增加程序可读性,将可能用到的数字用符号代替。有两种方式,一是采用全局变量指定数据,二是采用Racket的宏实现。显然第二种方式对程序开销和运行速度(虽然这里不会有太大影响)都是有利的。这里采用第二种方式。
;定义量级对应列表位置宏:
(define-syntax-rule (wan-id) 13);万
(define-syntax-rule (yi-id) 14);亿
(define-syntax-rule (zhao-id) 15);万
在接下来的编程中,根据量级宏位置名称取得相应的分节名,通过节名称划分分节。
4、最低级分节的解析
在中文字串里,最低级分节就是千分节。我们通过递归完成片段解析并求取相应数值。
;解析千以内分节解析并求值:
(define (parse-section-qian ch-str)
(cond
[(equal? ch-str "") 0]
[(= (string-length ch-str) 1)
;仅有一个值,必然是数字,直接返回该对应值:
(vector-ref number-vector
(vector-member ch-str chinese-vector))]
;取得相邻两个数子单字串,
;非特殊情况,应为数值和量词:
[else
(let* [;取得字串长度: (len (string-length ch-str)) ;取得数值(先当做是数值): (num (vector-ref number-vector (vector-member (substring ch-str (- len 2) (- len 1)) chinese-vector))) ;取得量词的位置: (unit-pos (vector-member (substring ch-str (- len 1) len) chinese-vector)) ;取得量词对应的两级数字: (level (vector-ref number-vector unit-pos))]
(cond ;原定量词位置为数字,则忽略量词直接作为数字加上: [(and (>= unit-pos 0) (<= unit-pos 9)) (+ level (parse-section-qian (substring ch-str 0 (- len 1))))] ;通常情况,数字和量词组合: [else (+ (* level num) (parse-section-qian (substring ch-str 0 (- len 2))))]))]))
5、解析管道的入口
做好了最低级的解析,就可以创建解析管道了。我们先来做一个入口,这个入口将在每个片段都要用到。
;对中文字串千分节解析:
;从低位到高位顺序对中文字串解析求值:
(define (parse-chinese-section ch-str section-str)
(if (= (string-length ch-str) 1)
;进行千小节解析计算:
(parse-section-qian (string-append ch-str section-str))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (wan-id)));万 (+ (parse-section-qian section-str) (parse-section-wan (substring ch-str 0 (- len 1)) ""))] ;分节字串清空。
[(equal? ch (vector-ref chinese-vector (yi-id)));亿 (+ (parse-section-qian section-str) (parse-section-yi (substring ch-str 0 (- len 1)) ""))] ;分节字串清空。
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆 (+ (parse-section-qian section-str) (parse-section-zhao (substring ch-str 0 (- len 1)) ""))] ;分节字串清空。
[else (parse-chinese-section (substring ch-str 0 (- len 1)) (string-append ch section-str))]))))
如要对这个函数做测试,可暂时关闭各后续管道节点(万、亿、兆),可参考前一篇进行。
这里的一个重要事情是:这个函数里包含两个参数,一个是接受的中文数字字串,另一个参数是指向分节片段内容的中文数字字串的引用变量,而分节片段内容这里采用尾递归来收集组装。
6、解析万分节
;对中文字串万分节解析:
(define (parse-section-wan ch-str section-str)
(if (= (string-length ch-str) 1)
;进行千分节解析:
(* (vector-ref number-vector (wan-id)) ;万对应的数量级。
(parse-chinese-section
(string-append ch-str section-str) ""))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (yi-id)));亿 (+ (parse-section-wan section-str "") (parse-section-yi (substring ch-str 0 (- len 1)) ""))] ;分节字串清空。
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆 (+ (parse-section-wan section-str "") (parse-section-zhao (substring ch-str 0 (- len 1)) ""))] ;分节字串清空。
[else (parse-section-wan (substring ch-str 0 (- len 1)) (string-append ch section-str))]))))
解析万分节时,除了解析字符串以组成分节字符串外并生成分解数字外,就是继续向高一级分节连接管道(亿、兆)。
7、解析亿分节
和万分节类似。
;对中文字串亿分节解析:
(define (parse-section-yi ch-str section-str)
(if (= (string-length ch-str) 1)
;进行亿分节解析:
(* (vector-ref number-vector (yi-id)) ;亿对应的数量级。
(parse-chinese-section
(string-append ch-str section-str) ""))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆 (+ (parse-section-yi section-str "") (parse-section-zhao (substring ch-str 0 (- len 1)) ""))] ;分节字串清空。
[else (parse-section-yi (substring ch-str 0 (- len 1)) (string-append ch section-str))]))))
8、解析兆分节
要注意的是,在兆之上就再没有更高的分节,如有更大数数字,必然是兆分节的重复。因此以下程序中更高一级分节为兆分节管道的重复。
;对中文字串兆分节解析:
(define (parse-section-zhao ch-str section-str [zhao-times 1])
(if (= (string-length ch-str) 1)
;进行兆分节解析:
(* (expt (vector-ref number-vector (zhao-id)) ;兆对应的数量级。
zhao-times)
(parse-chinese-section
(string-append ch-str section-str) ""))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆 (+ (parse-section-zhao section-str "" zhao-times) (parse-section-zhao (substring ch-str 0 (- len 1)) "" ;分节字串清空。 (+ zhao-times 1)))]
[else (parse-section-zhao (substring ch-str 0 (- len 1)) (string-append ch section-str) zhao-times)]))))
从以上代码可以看出,在组装阿拉伯数值时,兆分节的更高一级任然是兆分节,那么其对应的数量级该怎么计算呢?这里采用了(兆数量级^n),n为兆分节出现的次数(程序中的zhao-times变量)。为了保存这个n值,在函数参数中再增加了一个zhao-times参数,同样采用尾递归的方式计算并保存兆分节出现的次数。同时对parse-section-zhao函数的zhao-times参数设置初始值1(每次进入兆分节时必然要计算一个,此时就不用带这个参数),简化了程序。
9、完善程序
至此,整个解析管道全部完成并连通,可以做一个简单测试:
(parse-chinese-section "一千二百三十四兆五千零六万七千八百九十亿零一百二十三万四千零六十九" "")
12345006789001234069
做一个接口程序,以便使用:
;将中文字串转化为数字:
(define (chinese->number ch-str #:style [tag 'normal])
;根据参数情况设置全局chinese-vector值:
(set! chinese-vector
(case tag
['normal chinese-number]
['capitalization chinese-t-number]
[else (display "#:style参数错误。")]))
(if (equal? ch-str "")
0
(parse-chinese-section ch-str "")))
这个程序里边包含了关键字参数的检测(前边已经提到)以及基本的数据检测。而关键字参数的缺省值为符号’normal,在实际使用中函数不带#:style也能按缺省方式(普通中文转换)执行。
10、导出以供使用
为了其它程序使用(即用require导入),需要进行provide导出:
(provide chinese->number)
在测试文件中,就是通过
(require "chinese->number.rkt")
导入使用的。
11、添加合约
为确保导入的使用者(与写这个转换程序的不一定是同一个人)能够不出差错的使用,需要加入合约。
(provide
(contract-out
[chinese->number (->* (string?)
(#:style number-chinese-symbol?)
integer?)]))
这样对输入数字进行string?检测,对#:style的tag进行number-chinese-symbol?检测,输出指定做integer?检测,确保安全可靠,同时在编写程序时不用每个去编写限定代码,提高了编写速度。
这里合约考虑了必须参数与可选参数合约,以便默认情况下可不带关键字参数#:style tag。
这里number-chinese-symbol?为自定义合约,是专门针对tag进行检测用的,及要检测确保为符号类型的参数,又要检测确保为指定内容的符号(’normal或’capitalization)。
;定义number-chinese-symbol?合约:
(define (number-chinese-symbol? flag)
(if (symbol? flag)
(case flag
[('normal 'capitalization) flag]
[else '参数值错误。])
'不是symbol类型。))
至此,完整实现了中文数字对阿拉伯数字的转化。
五、完整源代码
;chinese-number.rkt
;中文数字与阿拉伯数字的转换。
#lang racket
(provide
(contract-out
[chinese->number (->* (string?)
(#:style number-chinese-symbol?)
integer?)]))
;定义number-chinese-symbol?合约:
(define (number-chinese-symbol? flag)
(if (symbol? flag)
(case flag
[('normal 'capitalization) flag]
[else '参数值错误。])
'不是symbol类型。))
;定义数据列表:
(define chinese-number (vector "零" "一" "二" "三" "四" "五" "六" "七" "八" "九" "十" "百" "千" "万" "亿" "兆"))
(define chinese-t-number (vector "零" "壹" "贰" "叁" "肆" "伍" "陆" "柒" "捌" "玖" "拾" "佰" "仟" "万" "亿" "兆"))
(define number-vector (vector 0 1 2 3 4 5 6 7 8 9 10 100 1000 10000 100000000 10000000000000000))
;初始化全局chinese-vector:
(define chinese-vector chinese-number)
;定义量级对应列表位置宏:
(define-syntax-rule (wan-id) 13);万
(define-syntax-rule (yi-id) 14);亿
(define-syntax-rule (zhao-id) 15);万
;-----------------------------------------
;将中文字串转化为数字:
(define (chinese->number ch-str #:style [tag 'normal])
;根据参数情况设置全局chinese-vector值:
(set! chinese-vector
(case tag
['normal chinese-number]
['capitalization chinese-t-number]
[else (display "#:style参数错误。")]))
(if (equal? ch-str "")
0
(parse-chinese-section ch-str "")))
;对中文字串千分节解析:
;从低位到高位顺序对中文字串解析求值:
(define (parse-chinese-section ch-str section-str)
(if (= (string-length ch-str) 1)
;进行千小节解析计算:
(parse-section-qian (string-append ch-str section-str))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (wan-id)));万
(+ (parse-section-qian section-str)
(parse-section-wan
(substring ch-str 0 (- len 1))
""))] ;分节字串清空。
[(equal? ch (vector-ref chinese-vector (yi-id)));亿
(+ (parse-section-qian section-str)
(parse-section-yi
(substring ch-str 0 (- len 1))
""))] ;分节字串清空。
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆
(+ (parse-section-qian section-str)
(parse-section-zhao
(substring ch-str 0 (- len 1))
""))] ;分节字串清空。
[else
(parse-chinese-section
(substring ch-str 0 (- len 1))
(string-append ch section-str))]))))
;对中文字串万分节解析:
(define (parse-section-wan ch-str section-str)
(if (= (string-length ch-str) 1)
;进行千分节解析:
(* (vector-ref number-vector (wan-id)) ;万对应的数量级。
(parse-chinese-section
(string-append ch-str section-str) ""))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (yi-id)));亿
(+ (parse-section-wan section-str "")
(parse-section-yi
(substring ch-str 0 (- len 1))
""))] ;分节字串清空。
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆
(+ (parse-section-wan section-str "")
(parse-section-zhao
(substring ch-str 0 (- len 1))
""))] ;分节字串清空。
[else
(parse-section-wan
(substring ch-str 0 (- len 1))
(string-append ch section-str))]))))
;对中文字串亿分节解析:
(define (parse-section-yi ch-str section-str)
(if (= (string-length ch-str) 1)
;进行亿分节解析:
(* (vector-ref number-vector (yi-id)) ;亿对应的数量级。
(parse-chinese-section
(string-append ch-str section-str) ""))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆
(+ (parse-section-yi section-str "")
(parse-section-zhao
(substring ch-str 0 (- len 1))
""))] ;分节字串清空。
[else
(parse-section-yi
(substring ch-str 0 (- len 1))
(string-append ch section-str))]))))
;对中文字串兆分节解析:
(define (parse-section-zhao ch-str section-str [zhao-times 1])
(if (= (string-length ch-str) 1)
;进行兆分节解析:
(* (expt (vector-ref number-vector (zhao-id)) ;兆对应的数量级。
zhao-times)
(parse-chinese-section
(string-append ch-str section-str) ""))
;顺序进行字串解析:
(let* [(len (string-length ch-str));字串长度。
(ch (substring ch-str (- len 1)))];提取字串最后一个字符。
(cond
[(equal? ch (vector-ref chinese-vector (zhao-id)));兆
(+ (parse-section-zhao section-str "" zhao-times)
(parse-section-zhao
(substring ch-str 0 (- len 1))
"" ;分节字串清空。
(+ zhao-times 1)))]
[else
(parse-section-zhao
(substring ch-str 0 (- len 1))
(string-append ch section-str)
zhao-times)]))))
;解析千以内分节解析并求值:
(define (parse-section-qian ch-str)
(cond
[(equal? ch-str "") 0]
[(= (string-length ch-str) 1)
;仅有一个值,必然是数字,直接返回该对应值:
(vector-ref number-vector
(vector-member ch-str chinese-vector))]
;取得相邻两个数子单字串,
;非特殊情况,应为数值和量词:
[else
(let* [;取得字串长度:
(len (string-length ch-str))
;取得数值(先当做是数值):
(num (vector-ref
number-vector
(vector-member
(substring ch-str (- len 2) (- len 1))
chinese-vector)))
;取得量词的位置:
(unit-pos (vector-member
(substring ch-str (- len 1) len)
chinese-vector))
;取得量词对应的两级数字:
(level (vector-ref number-vector unit-pos))]
(cond
;原定量词位置为数字,则忽略量词直接作为数字加上:
[(and (>= unit-pos 0) (<= unit-pos 9))
(+ level
(parse-section-qian
(substring ch-str 0 (- len 1))))]
;通常情况,数字和量词组合:
[else
(+ (* level num)
(parse-section-qian
(substring ch-str 0 (- len 2))))]))]))
到这里,完成了阿拉伯数字转中文数字,中文数字转阿拉伯数字。是不是一定对所有的数字都能够正确转换呢?要做完整测试后才能确保。程序测试是消除Bug的重要一环。后边将再列一篇介绍测试程序。