diff --git a/exercises/practice/say/.meta/config.json b/exercises/practice/say/.meta/config.json index 874c003..8a6f0c8 100644 --- a/exercises/practice/say/.meta/config.json +++ b/exercises/practice/say/.meta/config.json @@ -1,6 +1,7 @@ { "authors": [ - "bennn" + "bennn", + "BNAndras" ], "contributors": [ "arguello", diff --git a/exercises/practice/say/.meta/example.rkt b/exercises/practice/say/.meta/example.rkt index 1d83ae0..60bf4fe 100644 --- a/exercises/practice/say/.meta/example.rkt +++ b/exercises/practice/say/.meta/example.rkt @@ -1,117 +1,56 @@ -#lang racket/base - -;; say : Convert integers to English-language descriptions - -;; Implements the basic algorithm. -;; - Does not use the OSX "say" command to speak the number -;; - Does not insert "and" between chunks - -(require - racket/contract - (only-in racket/match match-define) - (only-in racket/string string-trim)) - -(define SCALE '#(END thousand million billion trillion)) -;; Supported size classifiers - -(define UPPER-BOUND (sub1 (expt 10 (* (vector-length SCALE) 3)))) -;; The largest printable number - -(define (scale? v) (for/or ([s (in-vector SCALE)]) (eq? v s))) -;; Contract for scales - -;; Use contracts to enforce all bounds -(provide (contract-out - [step1 (-> (integer-in 0 99) string?)] - ;; Convert a positive, 2-digit number to an English string - - [step2 (-> natural-number/c (listof (integer-in 0 999)))] - ;; Divide a large positive number into a list of 3-digit (or smaller) chunks - - [step3 (-> (integer-in (- UPPER-BOUND) UPPER-BOUND) - (listof (cons/c natural-number/c scale?)))] - ;; Break a number into chunks and insert scales between the chunks - - [step4 (-> (integer-in (- UPPER-BOUND) UPPER-BOUND) - string?)] - ;; Convert a number to an English-language string -)) - -;; ============================================================================= - -(define N<20 - '#("zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten" - "eleven" "twelve" "thirteen" "fourteen" "fifteen" "sixteen" "seventeen" - "eighteen" "nineteen")) - -(define TENS>10 - '#("twenty" "thirty" "forty" "fifty" "sixty" "seventy" "eighty" "ninety")) - -(define (step1 n) - (cond - [(< n 20) - (vector-ref N<20 n)] - [else - (define q (quotient n 10)) - (define r (modulo n 10)) - (define ten-str (vector-ref TENS>10 (- q 2))) - (define one-str (and (not (zero? r)) (vector-ref N<20 r))) - (if one-str - (string-append ten-str "-" one-str) - ten-str)])) - -(define (step2 N) - (let loop ([acc '()] - [n N] ;; Starts as original & we remove 3 digits each step. - [i 0]) ;; Index used to pick a scale - (define q (quotient n 1000)) - (define r (modulo n 1000)) +#lang racket + +(provide say) + +(define/contract (say number) + (-> (and/c exact-nonnegative-integer? (list (number->string number))] + [chunks (map list->string (chunk-from-right digits))] + [words (map to-words chunks)] + [scaled (label-magnitude (reverse words))]) + (string-join (reverse scaled) " ")))) + +(define (chunk-from-right chars) + (foldr (lambda (char acc) + (cond + [(empty? acc) (list (list char))] + [(< (length (first acc)) 3) (cons (cons char (first acc)) (rest acc))] + [else (cons (list char) acc)])) + '() + chars)) + +(define (to-words chunk) + (let ([number (string->number chunk)]) (cond - [(= n r) - ;; Reached fixpoint, stop iteration - (cons r acc)] - [else - ;; Repeat using the quotient - (loop (cons r acc) q (add1 i))]))) - -(define (step3 n) - (define (add-scale n acc+i) - (match-define (cons acc i) acc+i) - (define s (vector-ref SCALE i)) - (define n+s (cons n s)) - (cons (cons n+s acc) (add1 i))) - (car (foldr add-scale (cons '() 0) (step2 n)))) - -(define (step4 N) - ;; Break N into chunks, convert each chunk+scale to a string - (define str* - (for/list ([n+s (in-list (step3 (abs N)))]) - (match-define (cons n s) n+s) - (define q (quotient n 100)) - (define r (modulo n 100)) - (define n-str - (cond - [(zero? n) - ""] - [(< n 100) - (step1 r)] - [else - (define hd (vector-ref N<20 q)) - (define tl (step1 r)) - (if (equal? "zero" tl) - (string-append hd " hundred") - (string-append hd " hundred " tl))])) - ;; Don't print a scale for zeros or the last chunk - (if (or (eq? s 'END) (zero? n)) - n-str - (string-append n-str (format " ~a " s))))) - ;; Use `string-trim` to remove trailing whitespace - (define n-str (string-trim (apply string-append str*))) - (cond ;; Check for special cases - [(zero? N) - "zero"] - [(negative? N) - (string-append "negative " n-str)] - [else - n-str])) - + [(< number 20) (list-ref first-twenty number)] + [(< number 100) + (let* ([tens (quotient number 10)] + [tens-word (list-ref tens-words tens)] + [ones (remainder number 10)] + [ones-word (list-ref first-twenty ones)]) + (if (zero? ones) + tens-word + (string-append tens-word "-" ones-word)))] + [else + (let* ([hundreds (quotient number 100)] + [hundreds-word (list-ref first-twenty hundreds)] + [rest (remainder number 100)] + [rest-word (to-words (number->string rest))]) + (if (string=? rest-word "") + (string-append hundreds-word " hundred") + (string-append hundreds-word " hundred " rest-word)))]))) + +(define (label-magnitude words) + (for/list ([word words] + [magnitude '("" " thousand" " million" " billion")] + #:unless (string=? word "")) + (string-append word magnitude))) + +(define first-twenty + '("" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten" + "eleven" "twelve" "thirteen" "fourteen" "fifteen" "sixteen" "seventeen" "eighteen" "nineteen")) + +(define tens-words + '("" "tens" "twenty" "thirty" "forty" "fifty" "sixty" "seventy" "eighty" "ninety")) diff --git a/exercises/practice/say/.meta/tests.toml b/exercises/practice/say/.meta/tests.toml index 5bc18e1..a5532e9 100644 --- a/exercises/practice/say/.meta/tests.toml +++ b/exercises/practice/say/.meta/tests.toml @@ -24,12 +24,24 @@ description = "twenty" [d78601eb-4a84-4bfa-bf0e-665aeb8abe94] description = "twenty-two" +[f010d4ca-12c9-44e9-803a-27789841adb1] +description = "thirty" + +[738ce12d-ee5c-4dfb-ad26-534753a98327] +description = "ninety-nine" + [e417d452-129e-4056-bd5b-6eb1df334dce] description = "one hundred" [d6924f30-80ba-4597-acf6-ea3f16269da8] description = "one hundred twenty-three" +[2f061132-54bc-4fd4-b5df-0a3b778959b9] +description = "two hundred" + +[feed6627-5387-4d38-9692-87c0dbc55c33] +description = "nine hundred ninety-nine" + [3d83da89-a372-46d3-b10d-de0c792432b3] description = "one thousand" diff --git a/exercises/practice/say/say-test.rkt b/exercises/practice/say/say-test.rkt index 49f3d9d..f1d8258 100644 --- a/exercises/practice/say/say-test.rkt +++ b/exercises/practice/say/say-test.rkt @@ -3,74 +3,65 @@ (require "say.rkt") (module+ test - (require rackunit rackunit/text-ui) - - (define-syntax-rule (check-equal* f [arg == val] ...) - (begin (check-equal? (f arg) val) ...)) - - (define-syntax-rule (check-exn* f pat [arg] ...) - (begin (check-exn exn:fail:contract? (lambda () (f arg))) ...)) - - (define step* - (list - (test-suite "step1" - (check-equal* step1 - [0 == "zero"] - [2 == "two"] - [14 == "fourteen"] - [50 == "fifty"] - [98 == "ninety-eight"] - [99 == "ninety-nine"]) - - (check-exn* step1 - [-1] - [100])) - - (test-suite "step2" - (check-equal* step2 - [1234567890 == '(1 234 567 890)] - [1000000890 == '(1 0 0 890)] - [22 == '(22)] - [3222 == '(3 222)] - [1000231 == '(1 0 231)] - [1000 == '(1 0)])) - - (test-suite "step3" - (check-equal* step3 - [3222 == '((3 . thousand) (222 . END))] - [901003004111 == '((901 . billion) (3 . million) - (4 . thousand) (111 . END))] - [999 == '((999 . END))] - [21 == '((21 . END))] - [19 == '((19 . END))] - [100 == '((100 . END))] - [123 == '((123 . END))] - [1234567890 == '((1 . billion) (234 . million) - (567 . thousand) (890 . END))])) - - (test-suite "step4" - (check-equal* step4 - [10 == "ten"] - [100 == "one hundred"] - [10000 == "ten thousand"] - [10000000 == "ten million"] - [10000000000 == "ten billion"] - [10000000000000 == "ten trillion"] - [999000000000000 == "nine hundred ninety-nine trillion"] - [0 == "zero"] - [16 == "sixteen"] - [300 == "three hundred"] - [440 == "four hundred forty"] - [999 == "nine hundred ninety-nine"] - [-1 == "negative one"] - [22 == "twenty-two"] - [123 == "one hundred twenty-three"] - [22 == "twenty-two"] - [14 == "fourteen"] - [50 == "fifty"] - [98 == "ninety-eight"] - [-432600 == "negative four hundred thirty-two thousand six hundred"] - [12345 == "twelve thousand three hundred forty-five"])))) - - (for ([suite (in-list step*)]) - (run-tests suite))) + (require rackunit + rackunit/text-ui) + + (define suite + (test-suite "say tests" + + (test-equal? "zero" (say 0) "zero") + + (test-equal? "one" (say 1) "one") + + (test-equal? "two" (say 2) "two") + + (test-equal? "fourteen" (say 14) "fourteen") + + (test-equal? "twenty" (say 20) "twenty") + + (test-equal? "twenty-two" (say 22) "twenty-two") + + (test-equal? "thirty" (say 30) "thirty") + + (test-equal? "ninety-nine" (say 99) "ninety-nine") + + (test-equal? "one hundred" (say 100) "one hundred") + + (test-equal? "one hundred twenty-three" + (say 123) + "one hundred twenty-three") + + (test-equal? "two hundred" (say 200) "two hundred") + + (test-equal? "nine hundred ninety-nine" + (say 999) + "nine hundred ninety-nine") + + (test-equal? "one thousand" (say 1000) "one thousand") + + (test-equal? "one thousand two hundred thirty-four" + (say 1234) + "one thousand two hundred thirty-four") + + (test-equal? "one million" (say 1000000) "one million") + + (test-equal? "one million two thousand three hundred forty-five" + (say 1002345) + "one million two thousand three hundred forty-five") + + (test-equal? "one billion" (say 1000000000) "one billion") + + (test-equal? + "a big number" + (say 987654321123) + "nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three") + + (test-exn "numbers below zero are out of range" + exn:fail? + (lambda () (say -1))) + + (test-exn "numbers above 999,999,999,999 are out of range" + exn:fail? + (lambda () (say 1000000000000))))) + + (run-tests suite)) diff --git a/exercises/practice/say/say.rkt b/exercises/practice/say/say.rkt index 34adb39..f92d0ab 100644 --- a/exercises/practice/say/say.rkt +++ b/exercises/practice/say/say.rkt @@ -1,45 +1,6 @@ #lang racket -;; Converts integers to English-language descriptions +(provide say) -;; --- NOTE ------------------------------------------------------------------- -;; The test cases in "say-test.rkt" assume: -;; - Calling a function with an out-of-range argument triggers a contract error -;; - That `step3` returns a list of (number, symbol) pairs -;; -;; We have provided sample contracts so the tests compile, but you -;; will want to edit & strengthen these. -;; -;; (For example, things like 0.333 and 7/8 pass the `number?` contract -;; but these functions expect integers and natural numbers) -;; ---------------------------------------------------------------------------- - -(require racket/contract) - -(provide (contract-out - [step1 (-> number? string?)] - ;; Convert a positive, 2-digit number to an English string - - [step2 (-> number? (listof number?))] - ;; Divide a large positive number into a list of 3-digit (or smaller) chunks - - [step3 (-> number? (listof (cons/c number? symbol?)))] - ;; Break a number into chunks and insert scales between the chunks - - [step4 (-> number? string?)] - ;; Convert a number to an English-language string -)) - -;; ============================================================================= - -(define (step1 n) - (error "Please implement 'step1'")) - -(define (step2 N) - (error "Please implement 'step2'")) - -(define (step3 n) - (error "Please implement 'step3'")) - -(define (step4 N) - (error "Please implement 'step4'")) +(define (say number) + (error "Please implement 'say'"))