Racket实现数字与中文的转换算法一(阿拉伯数字转换为中文数字)

51 篇文章 11 订阅

一、前言

在《算法的乐趣》中看到阿拉伯数字与中文数字的转换算法(C语言实现),感觉不太清晰。这里用Racket语言重新做了一个实现,并进一步完善为到兆分节并可进行大写转换。

Racket编程的一大好处是可以随时对所编写的模块进行测试(连接解析管道的模块按顺序测试),安全可靠、简单快速。

通过本文可以了解Racket语言vector、list的应用,简单宏的编程应用,合约及自定义合约的编程,函数关键字参数、可选参数的应用等等。

源代码可见:https://github.com/OnRoadZy/ConvertNumberAndChinese.git

先对阿拉伯数字转中文算法做一个解释。

二、阿拉伯数字与中文数字

阿拉伯数字的特点是每一位都用相应的数字表示出来,其分节(个、十、百、千、万、亿、兆)是隐含的。

中文数字相反,中文数字并不全部表现出每一个数字位,对连续零则省略为隐含表达,而分节则是显式表达。

三、阿拉伯数字转中文数字的算法实现

根据数字分节特点,可分为千、万、亿、兆四种片段进行处理。而万分节片段中包含千分节片段,同样,亿、兆分节也包括低一级的分节片段。

这样,我们可以针对各分节情况分别编程进行模块化处理。把阿拉伯数字转换成数字字串,那么我们就可以把数字串当成列表处理,而每一个分节可以根据其数量级的长度进行简单划分。把对各分节的处理模块连接成管道,将数字串列输入管道,完成分节、转化、组合这个流程,输出即为我们需要的中文数字字串。

据说高德纳编写Tex也是用的这种管道技术,从原理上应该是类似的。

四、具体实现

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 (shi-id) 10);十
(define-syntax-rule (bai-id) 11);百
(define-syntax-rule (qian-id) 12);千
(define-syntax-rule (wan-id) 13);万
(define-syntax-rule (yi-id) 14);亿
(define-syntax-rule (zhao-id) 15);兆

;定义节长度:
(define-syntax-rule (qian-len) 4);千
(define-syntax-rule (wan-len) 8);万
(define-syntax-rule (yi-len) 16);亿

在接下来的编程中,根据量级宏位置名称取得相应的分节名,通过节长度划分分节。

4、最低级分节的解析

在数字字串里,最低级分节就是千分节。我们通过递归完成片段解析。

;解析千分节数片段:
(define (parse-section-qian num-str)
  (cond
    [(= (string->number num-str) 0) ""] ;这个数为0,转化为空值,并不加量词。
    [(= (string-length num-str) 1) ;为最后一个字符,在后边加上量词。
     (generate-num-chinese (substring num-str 0 1)
                           (string-length num-str))]
    [else ;递归解析整段字串片段:
     (let [(cur-ch-str ;当前中文数字。 (generate-num-chinese (substring num-str 0 1) (string-length num-str))) (rest-ch-str ;剩下的中文片段。 (parse-number-section (substring num-str 1))) ;已经生成的中文数字串最后一个字符(用于检查是否有连续的零): (zero-str (vector-ref chinese-vector 0))]
       ;检查当前得到的字符是否和后边的第一个字符都为零(合并多个相邻的零):
       (if (and (equal? (substring cur-ch-str 0 1) zero-str) (equal? (substring rest-ch-str 0 1) zero-str)) rest-ch-str (string-append cur-ch-str rest-ch-str)))]))

其中涉及一个自定义函数:generate-num-chinese,它将每个阿拉伯数字生成为一个数和一个量词的中文数字最小表达单位(中文数字是显式表示的,如:三千)。

其中还涉及到对多个0的转化,需要检测阿拉伯数字字串里的0与已经生成的中文字串零的相邻情况。

;检查当前得到的字符是否和后边的第一个字符都为零(合并多个相邻的零):
(if (and (equal? (substring cur-ch-str 0 1) zero-str)
         (equal? (substring rest-ch-str 0 1) zero-str))
    rest-ch-str
    (string-append cur-ch-str rest-ch-str))

generate-num-chinese函数:

;生成单个数字的中文带量词:
;num-char单个数字,len单个数字对应的数字串位置。
(define (generate-num-chinese num-char len)
  (if (= (string->number num-char) 0)
      (vector-ref chinese-vector 0)
      (string-append (convert-num-chinese num-char)
                     (convert-level-chinese len))))

其中convert-num-chinese转换数字,convert-level-chinese根据数字长度生成量词。

;将单个数字转化为中文:
(define (convert-num-chinese num-char)
  (vector-ref chinese-vector
              (string->number num-char)))

;根据数字位数生成量词(千以内):
(define (convert-level-chinese len)
  (cond
    [(= len (string-length "1")) ""]
    [(= len (string-length "10"))
     (vector-ref chinese-vector (shi-id))]
    [(= len (string-length "100"))
     (vector-ref chinese-vector (bai-id))]
    [(= len (string-length "1000"))
     (vector-ref chinese-vector (qian-id))]
    [else (display "没有对应长度的分节。")]))

5、解析管道的入口

做好了最低级的解析,就可以创建解析管道了。我们先来做一个入口,这个入口将在每个片段都要用到。

;解析数字分节:
(define (parse-number-section num-str)
  (if (<= (string-length num-str) (qian-len));千级以内的数
      (parse-section-qian num-str)
      (let [(qian-rest-len (- (string-length num-str) (qian-len)))]
        (string-append
         (parse-section-wan (substring num-str 0 qian-rest-len))
         (parse-section-qian (substring num-str qian-rest-len))))))

从程序中能够看出,数字串的解析将从这里由低到高(千 -> 万 -> 亿 -> 兆)逐级完成。

如要对这个函数做测试,可暂时关闭后续管道节点(万),如下:

;解析数字分节:
(define (parse-number-section num-str)
  (if (<= (string-length num-str) (qian-len));千级以内的数
      (parse-section-qian num-str)
      (let [(qian-rest-len (- (string-length num-str) (qian-len)))]
        (string-append
         ;(parse-section-wan (substring num-str 0 qian-rest-len))
         (parse-section-qian (substring num-str qian-rest-len))))))

用(parse-number-section “12345”)做测试,将得到

"二千三百四十五"

只处理了千分节。

6、解析万分节

;解析万分节数:
(define (parse-section-wan num-str)
  (if (<= (string-length num-str) (qian-len));千级以内的数
      (let [(ch-str (parse-number-section num-str))]
        (string-append
         ch-str
         (if (equal? ch-str "") "" (vector-ref chinese-vector (wan-id)))));万
      (let [(wan-rest-len (- (string-length num-str) (qian-len)))]
        (string-append
         (parse-section-yi (substring num-str 0 wan-rest-len))
         (parse-section-wan (substring num-str wan-rest-len))))))

解析万分节时,除了处理“万”这个量词外,就是重复进行千分节内容的处理,因为万分节内包括千分节(程序在前面已经准备好)。另外就是继续向高一级分节连接管道(亿)。

7、解析亿分节

和万分节类似。

;解析亿分节数:
(define (parse-section-yi num-str)
  (if (<= (string-length num-str) (wan-len));万级以内的数
      (let [(ch-str (parse-number-section num-str))]
        (string-append
         ch-str
         (if (equal? ch-str "") "" (vector-ref chinese-vector (yi-id)))));亿
      (let [(yi-rest-len (- (string-length num-str) (wan-len)))]
        (string-append
         (parse-section-zhao (substring num-str 0 yi-rest-len))
         (parse-section-yi (substring num-str yi-rest-len))))))

8、解析兆分节

要注意的是,在兆之上就再没有更高的分节,如有更大数数字,必然是兆分节的重复。因此以下程序中更高一级分节为兆分节管道的重复。

;解析兆分节数:
;兆分节数会形成反复出现。
(define (parse-section-zhao num-str)
  (if (<= (string-length num-str) (yi-len));亿级以内的数
      (let [(ch-str (parse-number-section num-str))]
        (string-append
         ch-str
         (if (equal? ch-str "") "" (vector-ref chinese-vector (zhao-id)))));兆
      (let [(zao-rest-len (- (string-length num-str) (yi-len)))]
        (string-append
         (parse-section-zhao (substring num-str 0 zao-rest-len))
         ;重复兆分节
         (parse-section-zhao (substring num-str zao-rest-len))))))

9、完善程序

至此,整个解析管道全部完成并连通,可以做一个简单测试:

(parse-number-section "12345006789001234069")

"一千二百三十四兆五千零六万七千八百九十亿零一百二十三万四千零六十九"

做一个接口程序,以便使用:

;将数字转化为中文字串:
(define (number->chinese num #:style [tag 'normal])
  ;根据参数情况设置全局chinese-vector值:
  (set! chinese-vector
        (case tag
          ['normal chinese-number]
          ['capitalization chinese-t-number]
          [else (display "#:style参数错误。")]))
  (if (= num 0) ;检测是否为0。
      (vector-ref chinese-vector 0);零
      (parse-number-section (number->string num))))

这个程序里边包含了关键字参数的检测(前边已经提到)以及基本的数据检测。而关键字参数的缺省值为符号’normal,在实际使用中函数不带#:style也能按缺省方式(转换为普通中文)执行。

10、导出以供使用

为了其它程序使用(即用require导入),需要进行provide导出:

(provide number->chinese)

在测试文件中,就是通过

(require "number-chinese.rkt")

导入使用的。

11、添加合约

为确保导入的使用者(与写这个转换程序的不一定是同一个人)能够不出差错的使用,需要加入合约。

(provide
 (contract-out
  [number->chinese (->* (integer?)
                        (#:style number-chinese-symbol?)
                        string?)]))

这样对输入数字进行integer?检测,对#:style的tag进行number-chinese-symbol?检测,输出指定做string?检测,确保安全可靠,同时在编写程序时不用每个去编写限定代码,提高了编写速度。

这里number-chinese-symbol?为自定义合约,是专门针对tag进行检测用的,及要检测确保为符号类型的参数,又要检测确保为指定内容的符号(’normal或’capitalization)。

;定义number-chinese-symbol?合约:
(define (number-chinese-symbol? flag)
  (if (symbol? flag)
      (case flag
        [('normal 'capitalization) flag]
        [else '参数值错误。])
      '不是symbol类型。))

至此,完整实现了阿拉伯数字对中文数字的转化。

五、完整源代码

;number-chinese.rkt
;阿拉伯数字与中文的转换。
#lang racket

(provide
 (contract-out
  [number->chinese (->* (integer?)
                        (#:style number-chinese-symbol?)
                        string?)]))

;定义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 (shi-id) 10);十
(define-syntax-rule (bai-id) 11);百
(define-syntax-rule (qian-id) 12);千
(define-syntax-rule (wan-id) 13);万
(define-syntax-rule (yi-id) 14);亿
(define-syntax-rule (zhao-id) 15);兆

;定义节长度:
(define-syntax-rule (qian-len) 4);千
(define-syntax-rule (wan-len) 8);万
(define-syntax-rule (yi-len) 16);亿

;--------------------------------------------------------
;将数字转化为中文字串:
(define (number->chinese num #:style [tag 'normal])
  ;根据参数情况设置全局chinese-vector值:
  (set! chinese-vector
        (case tag
          ['normal chinese-number]
          ['capitalization chinese-t-number]
          [else (display "#:style参数错误。")]))
  (if (= num 0) ;检测是否为0。
      (vector-ref chinese-vector 0);零
      (parse-number-section (number->string num))))

;解析数字分节:
(define (parse-number-section num-str)
  (if (<= (string-length num-str) (qian-len));千级以内的数
      (parse-section-qian num-str)
      (let [(qian-rest-len
             (- (string-length num-str) (qian-len)))]
        (string-append
         (parse-section-wan (substring num-str 0 qian-rest-len))
         (parse-section-qian (substring num-str qian-rest-len))))))

;解析万分节数:
(define (parse-section-wan num-str)
  (if (<= (string-length num-str) (qian-len));千级以内的数
      (let [(ch-str (parse-number-section num-str))]
        (string-append
         ch-str
         (if (equal? ch-str "")
             ""
             (vector-ref chinese-vector (wan-id)))));万
      (let [(wan-rest-len
             (- (string-length num-str) (qian-len)))]
        (string-append
         (parse-section-yi (substring num-str 0 wan-rest-len))
         (parse-section-wan (substring num-str wan-rest-len))))))

;解析亿分节数:
(define (parse-section-yi num-str)
  (if (<= (string-length num-str) (wan-len));万级以内的数
      (let [(ch-str (parse-number-section num-str))]
        (string-append
         ch-str
         (if (equal? ch-str "")
             ""
             (vector-ref chinese-vector (yi-id)))));亿
      (let [(yi-rest-len
             (- (string-length num-str) (wan-len)))]
        (string-append
         (parse-section-zhao (substring num-str 0 yi-rest-len))
         (parse-section-yi (substring num-str yi-rest-len))))))

;解析兆分节数:
;兆分节数会形成反复出现。
(define (parse-section-zhao num-str)
  (if (<= (string-length num-str) (yi-len));亿级以内的数
      (let [(ch-str (parse-number-section num-str))]
        (string-append
         ch-str
         (if (equal? ch-str "")
             ""
             (vector-ref chinese-vector (zhao-id)))));兆
      (let [(zao-rest-len
             (- (string-length num-str) (yi-len)))]
        (string-append
         (parse-section-zhao (substring num-str 0 zao-rest-len))
         ;重复兆分节
         (parse-section-zhao (substring num-str zao-rest-len))))))

;解析千分节数片段:
(define (parse-section-qian num-str)
  (cond
    [(= (string->number num-str) 0) ""] ;这个数为0,转化为空值,并不加量词。
    [(= (string-length num-str) 1) ;为最后一个字符,在后边加上量词。
     (generate-num-chinese (substring num-str 0 1)
                           (string-length num-str))]
    [else ;递归解析整段字串片段:
     (let [(cur-ch-str ;当前中文数字。
            (generate-num-chinese (substring num-str 0 1)
                                  (string-length num-str)))
           (rest-ch-str ;剩下的中文片段。
            (parse-number-section (substring num-str 1)))
           ;已经生成的中文数字串最后一个字符(用于检查是否有连续的零):
           (zero-str (vector-ref chinese-vector 0))]
       ;检查当前得到的字符是否和后边的第一个字符都为零(合并多个相邻的零):
       (if (and (equal? (substring cur-ch-str 0 1) zero-str)
                (equal? (substring rest-ch-str 0 1) zero-str))
           rest-ch-str
           (string-append cur-ch-str rest-ch-str)))]))

;生成单个数字的中文带量词:
;num-char单个数字,len单个数字对应的数字串位置。
(define (generate-num-chinese num-char len)
  (if (= (string->number num-char) 0)
      (vector-ref chinese-vector 0)
      (string-append (convert-num-chinese num-char)
                     (convert-level-chinese len))))

;将单个数字转化为中文:
(define (convert-num-chinese num-char)
  (vector-ref chinese-vector
              (string->number num-char)))

;根据数字位数生成量词(千以内):
(define (convert-level-chinese len)
  (cond
    [(= len (string-length "1")) ""]
    [(= len (string-length "10"))
     (vector-ref chinese-vector (shi-id))]
    [(= len (string-length "100"))
     (vector-ref chinese-vector (bai-id))]
    [(= len (string-length "1000"))
     (vector-ref chinese-vector (qian-id))]
    [else (display "没有对应长度的分节。")]))

后边将再列一篇介绍如何实现中文数字转换阿拉伯数字的算法。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值