Skip to content

Sync say tests #435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion exercises/practice/say/.meta/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"authors": [
"bennn"
"bennn",
"BNAndras"
],
"contributors": [
"arguello",
Expand Down
171 changes: 55 additions & 116 deletions exercises/practice/say/.meta/example.rkt
Original file line number Diff line number Diff line change
@@ -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? (</c 1e12)) string?)
(if (zero? number)
"zero"
(let* ([digits (string->list (number->string number))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we gain by converting the number into lists of characters here and then turning them back into numbers in to-words? Couldn't we get the groups of three using arithmetic, e.g., with quotient/remainder? But since this method works, there is no need to change anything.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to do things in discrete steps, but you're right. Thinking of efficiency, I think the CI should perhaps track the time taken for each example solution as an approximate estimate of how long it'd take the test runner. The example only needs to prove the test suite is solvable, but it'd be nice to know if that's within the 20 seconds available. I've had a few example solutions I wrote elsewhere that worked great on the CI but timed out when submitted. That didn't feel too great.

[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"))
12 changes: 12 additions & 0 deletions exercises/practice/say/.meta/tests.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
133 changes: 62 additions & 71 deletions exercises/practice/say/say-test.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
47 changes: 4 additions & 43 deletions exercises/practice/say/say.rkt
Original file line number Diff line number Diff line change
@@ -1,45 +1,6 @@
#lang racket/base
#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 "Not implemented yet"))

(define (step2 N)
(error "Not implemented yet"))

(define (step3 n)
(error "Not implemented yet"))

(define (step4 N)
(error "Not implemented yet"))
(define (say number)
(error "Please implement 'say'"))