|
1 |
| -#lang racket/base |
2 |
| - |
3 |
| -;; say : Convert integers to English-language descriptions |
4 |
| - |
5 |
| -;; Implements the basic algorithm. |
6 |
| -;; - Does not use the OSX "say" command to speak the number |
7 |
| -;; - Does not insert "and" between chunks |
8 |
| - |
9 |
| -(require |
10 |
| - racket/contract |
11 |
| - (only-in racket/match match-define) |
12 |
| - (only-in racket/string string-trim)) |
13 |
| - |
14 |
| -(define SCALE '#(END thousand million billion trillion)) |
15 |
| -;; Supported size classifiers |
16 |
| - |
17 |
| -(define UPPER-BOUND (sub1 (expt 10 (* (vector-length SCALE) 3)))) |
18 |
| -;; The largest printable number |
19 |
| - |
20 |
| -(define (scale? v) (for/or ([s (in-vector SCALE)]) (eq? v s))) |
21 |
| -;; Contract for scales |
22 |
| - |
23 |
| -;; Use contracts to enforce all bounds |
24 |
| -(provide (contract-out |
25 |
| - [step1 (-> (integer-in 0 99) string?)] |
26 |
| - ;; Convert a positive, 2-digit number to an English string |
27 |
| - |
28 |
| - [step2 (-> natural-number/c (listof (integer-in 0 999)))] |
29 |
| - ;; Divide a large positive number into a list of 3-digit (or smaller) chunks |
30 |
| - |
31 |
| - [step3 (-> (integer-in (- UPPER-BOUND) UPPER-BOUND) |
32 |
| - (listof (cons/c natural-number/c scale?)))] |
33 |
| - ;; Break a number into chunks and insert scales between the chunks |
34 |
| - |
35 |
| - [step4 (-> (integer-in (- UPPER-BOUND) UPPER-BOUND) |
36 |
| - string?)] |
37 |
| - ;; Convert a number to an English-language string |
38 |
| -)) |
39 |
| - |
40 |
| -;; ============================================================================= |
41 |
| - |
42 |
| -(define N<20 |
43 |
| - '#("zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten" |
44 |
| - "eleven" "twelve" "thirteen" "fourteen" "fifteen" "sixteen" "seventeen" |
45 |
| - "eighteen" "nineteen")) |
46 |
| - |
47 |
| -(define TENS>10 |
48 |
| - '#("twenty" "thirty" "forty" "fifty" "sixty" "seventy" "eighty" "ninety")) |
49 |
| - |
50 |
| -(define (step1 n) |
51 |
| - (cond |
52 |
| - [(< n 20) |
53 |
| - (vector-ref N<20 n)] |
54 |
| - [else |
55 |
| - (define q (quotient n 10)) |
56 |
| - (define r (modulo n 10)) |
57 |
| - (define ten-str (vector-ref TENS>10 (- q 2))) |
58 |
| - (define one-str (and (not (zero? r)) (vector-ref N<20 r))) |
59 |
| - (if one-str |
60 |
| - (string-append ten-str "-" one-str) |
61 |
| - ten-str)])) |
62 |
| - |
63 |
| -(define (step2 N) |
64 |
| - (let loop ([acc '()] |
65 |
| - [n N] ;; Starts as original & we remove 3 digits each step. |
66 |
| - [i 0]) ;; Index used to pick a scale |
67 |
| - (define q (quotient n 1000)) |
68 |
| - (define r (modulo n 1000)) |
| 1 | +#lang racket |
| 2 | + |
| 3 | +(provide say) |
| 4 | + |
| 5 | +(define/contract (say number) |
| 6 | + (-> (and/c exact-nonnegative-integer? (</c 1e12)) string?) |
| 7 | + (if (zero? number) |
| 8 | + "zero" |
| 9 | + (let* ([digits (string->list (number->string number))] |
| 10 | + [chunks (map list->string (chunk-from-right digits))] |
| 11 | + [words (map to-words chunks)] |
| 12 | + [scaled (label-magnitude (reverse words))]) |
| 13 | + (string-join (reverse scaled) " ")))) |
| 14 | + |
| 15 | +(define (chunk-from-right chars) |
| 16 | + (foldr (lambda (char acc) |
| 17 | + (cond |
| 18 | + [(empty? acc) (list (list char))] |
| 19 | + [(< (length (first acc)) 3) (cons (cons char (first acc)) (rest acc))] |
| 20 | + [else (cons (list char) acc)])) |
| 21 | + '() |
| 22 | + chars)) |
| 23 | + |
| 24 | +(define (to-words chunk) |
| 25 | + (let ([number (string->number chunk)]) |
69 | 26 | (cond
|
70 |
| - [(= n r) |
71 |
| - ;; Reached fixpoint, stop iteration |
72 |
| - (cons r acc)] |
73 |
| - [else |
74 |
| - ;; Repeat using the quotient |
75 |
| - (loop (cons r acc) q (add1 i))]))) |
76 |
| - |
77 |
| -(define (step3 n) |
78 |
| - (define (add-scale n acc+i) |
79 |
| - (match-define (cons acc i) acc+i) |
80 |
| - (define s (vector-ref SCALE i)) |
81 |
| - (define n+s (cons n s)) |
82 |
| - (cons (cons n+s acc) (add1 i))) |
83 |
| - (car (foldr add-scale (cons '() 0) (step2 n)))) |
84 |
| - |
85 |
| -(define (step4 N) |
86 |
| - ;; Break N into chunks, convert each chunk+scale to a string |
87 |
| - (define str* |
88 |
| - (for/list ([n+s (in-list (step3 (abs N)))]) |
89 |
| - (match-define (cons n s) n+s) |
90 |
| - (define q (quotient n 100)) |
91 |
| - (define r (modulo n 100)) |
92 |
| - (define n-str |
93 |
| - (cond |
94 |
| - [(zero? n) |
95 |
| - ""] |
96 |
| - [(< n 100) |
97 |
| - (step1 r)] |
98 |
| - [else |
99 |
| - (define hd (vector-ref N<20 q)) |
100 |
| - (define tl (step1 r)) |
101 |
| - (if (equal? "zero" tl) |
102 |
| - (string-append hd " hundred") |
103 |
| - (string-append hd " hundred " tl))])) |
104 |
| - ;; Don't print a scale for zeros or the last chunk |
105 |
| - (if (or (eq? s 'END) (zero? n)) |
106 |
| - n-str |
107 |
| - (string-append n-str (format " ~a " s))))) |
108 |
| - ;; Use `string-trim` to remove trailing whitespace |
109 |
| - (define n-str (string-trim (apply string-append str*))) |
110 |
| - (cond ;; Check for special cases |
111 |
| - [(zero? N) |
112 |
| - "zero"] |
113 |
| - [(negative? N) |
114 |
| - (string-append "negative " n-str)] |
115 |
| - [else |
116 |
| - n-str])) |
117 |
| - |
| 27 | + [(< number 20) (list-ref first-twenty number)] |
| 28 | + [(< number 100) |
| 29 | + (let* ([tens (quotient number 10)] |
| 30 | + [tens-word (list-ref tens-words tens)] |
| 31 | + [ones (remainder number 10)] |
| 32 | + [ones-word (list-ref first-twenty ones)]) |
| 33 | + (if (zero? ones) |
| 34 | + tens-word |
| 35 | + (string-append tens-word "-" ones-word)))] |
| 36 | + [else |
| 37 | + (let* ([hundreds (quotient number 100)] |
| 38 | + [hundreds-word (list-ref first-twenty hundreds)] |
| 39 | + [rest (remainder number 100)] |
| 40 | + [rest-word (to-words (number->string rest))]) |
| 41 | + (if (string=? rest-word "") |
| 42 | + (string-append hundreds-word " hundred") |
| 43 | + (string-append hundreds-word " hundred " rest-word)))]))) |
| 44 | + |
| 45 | +(define (label-magnitude words) |
| 46 | + (for/list ([word words] |
| 47 | + [magnitude '("" " thousand" " million" " billion")] |
| 48 | + #:unless (string=? word "")) |
| 49 | + (string-append word magnitude))) |
| 50 | + |
| 51 | +(define first-twenty |
| 52 | + '("" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten" |
| 53 | + "eleven" "twelve" "thirteen" "fourteen" "fifteen" "sixteen" "seventeen" "eighteen" "nineteen")) |
| 54 | + |
| 55 | +(define tens-words |
| 56 | + '("" "tens" "twenty" "thirty" "forty" "fifty" "sixty" "seventy" "eighty" "ninety")) |
0 commit comments