Skip to content

Instantly share code, notes, and snippets.

@timblair
Last active December 29, 2015 20:09
Show Gist options
  • Save timblair/7722251 to your computer and use it in GitHub Desktop.
Save timblair/7722251 to your computer and use it in GitHub Desktop.
Counting from 1 to 1,000,000 in words. Actually caters for numbers up to 10^36-1 (using the short scale) if you really need to...
class NumberConverter
UNITS = [nil] + %w{ one two three four five six seven eight nine ten eleven
twelve thirteen fourteen fifteen sixteen seventeen eightteen nineteen }
TENS = %w{ twenty thirty forty fifty sixty seventy eighty ninety }
SCALE = %w{ hundred thousand } +
%w{ m b tr quadr quint sext sept oct non dec }.map { |i| i + "illion" }
# cache some lookups for a ~25% speed increase
@@cache ||= {}
def initialize(num)
@num = num.to_i
end
def to_words
words = case @num
when -Float::INFINITY..0; nil # anything less than 1
when 1..19; UNITS[@num] # just pick the correct unit
when 20..99; tens
when 100..999; hundreds
else thousands_or_more
end
# squash the array and join the words (using implicit string conversion)
[words].flatten.compact.join(" ").strip
end
# implicit string conversion instead of using .to_words everywhere
alias :to_str :to_words
private
def tens
# grab the correct "ten", plus the unit
@@cache[@num] ||= [TENS[(@num / 10) - 2], self.class.new(@num % 10)]
end
def hundreds
@@cache[@num] ||= [
UNITS[@num / 100], # how many hundreds?
SCALE.first, # the actual "hundred"
@num % 100 > 0 ? "and" : "", # an "and" if necessary
self.class.new(@num % 100) # the tens, teens and units
]
end
def thousands_or_more
# split the number in to chunks of 3, e.g. 1234567 => [1, 234, 567]
seg = @num.to_s.reverse.scan(/(.{1,3})/).flatten.map(&:reverse).reverse
# we'll deal with all but the first segment later
first = seg.first
rest = seg.drop(1).join.to_i
# we might need an "and" and a comma for readability
sep = (1..99).include?(rest) ? " and" : (rest > 0 ? "," : "")
[
@@cache[first] || self.class.new(first), # the first segment
SCALE[seg.length-1] + sep, # the scale and separator
self.class.new(rest) # the other segments
]
end
end
if $0 == __FILE__
if ARGV.first.to_i > 0
puts NumberConverter.new(ARGV.first).to_words
else
(1..1_000_000).each { |i| puts "#{i}: " + NumberConverter.new(i).to_words }
end
end
require "minitest/autorun"
require_relative "number_converter"
class TestNumberConverter < MiniTest::Unit::TestCase
NUMBERS_TO_WORDS = {
1 => "one",
2 => "two",
9 => "nine",
10 => "ten",
11 => "eleven",
19 => "nineteen",
20 => "twenty",
21 => "twenty one",
99 => "ninety nine",
100 => "one hundred",
101 => "one hundred and one",
110 => "one hundred and ten",
199 => "one hundred and ninety nine",
200 => "two hundred",
999 => "nine hundred and ninety nine",
1_000 => "one thousand",
1_001 => "one thousand and one",
1_100 => "one thousand, one hundred",
1_101 => "one thousand, one hundred and one",
1_999 => "one thousand, nine hundred and ninety nine",
2_000 => "two thousand",
5_678 => "five thousand, six hundred and seventy eight",
9_999 => "nine thousand, nine hundred and ninety nine",
10_000 => "ten thousand",
10_001 => "ten thousand and one",
19_000 => "nineteen thousand",
19_999 => "nineteen thousand, nine hundred and ninety nine",
20_000 => "twenty thousand",
20_001 => "twenty thousand and one",
56_789 => "fifty six thousand, seven hundred and eighty nine",
99_999 => "ninety nine thousand, nine hundred and ninety nine",
100_000 => "one hundred thousand",
100_001 => "one hundred thousand and one",
100_123 => "one hundred thousand, one hundred and twenty three",
119_123 => "one hundred and nineteen thousand, one hundred and twenty three",
567_890 => "five hundred and sixty seven thousand, eight hundred and ninety",
999_999 => "nine hundred and ninety nine thousand, nine hundred and ninety nine",
1_000_000 => "one million",
1_000_001 => "one million and one",
1_234_567 => "one million, two hundred and thirty four thousand, five hundred and sixty seven",
4_567_890 => "four million, five hundred and sixty seven thousand, eight hundred and ninety",
9_999_999 => "nine million, nine hundred and ninety nine thousand, nine hundred and ninety nine",
100_000_000 => "one hundred million",
1_000_000_000 => "one billion"
}
NUMBERS_TO_WORDS.each do |number, words|
define_method("test_#{words.gsub(/[^a-z]+/, '_')}") do
assert_equal words, NumberConverter.new(number).to_words
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment