Tuesday, July 04, 2006

Ruby : English Numbers

Well, it's been almost a month since my last post and what a busy month it's been! I started to write an article about a wrapper I had written for C++ iostreams that automatically closes them on scope exit until I found out that they do that anyway!! Talk about redundant levels of abstraction!

I've also found myself over-using templates recently, I mean they're fun to use but they tend to obfusticate code and generate hard-to-read compiler errors. I had this realisation after discovering Ruby - one of the easiest, most concise and powerful "scripting" languages I've ever had the pleasure to use. Check it out www.rubycentral.com. I've been following a great article called Learn To Program (Ruby), a precursor to the book by the same name.

On Chapter 8 there's a nifty English Number Program example that takes any positive integer and returns the english name of that number. It's quite limited and only works properly between 0-999. The author sets an exersize to make it work for thousands and millions and asks, how high can you go? My answer? Is that British Scale or American Scale? Either? Ok, well, in either case I can go as high as 10^600, which is the largest number-word below googolplex which is too large to include anyway :)

Here's my first Ruby program (based on the original example), enjoy ... for an example of conciseness see the code I wrote to sort the array of name/exponent pairs by exponent ...

def englishNumber number, scale
if number < 0 # No negative numbers.
return 'Please enter a number that isn\'t negative.'
if number == 0
return 'zero'

# check scale:

if scale<0 or scale>1
return 'scale out of range'

# valid scales:
# 0) British (long scale)
# 1) American (short scale)

numString = '' # This is the string we will return.

onesPlace = ['one', 'two', 'three', 'four',
'five', 'six', 'seven', 'eight', 'nine']
tensPlace = ['ten', 'twenty', 'thirty', 'forty', 'fifty',
'sixty', 'seventy', 'eighty', 'ninety']
teenagers = ['eleven', 'twelve', 'thirteen', 'fourteen',
'fifteen', 'sixteen', 'seventeen', 'eighteen',

# short scale words

expTens = [['b'],['tr'],['quadr'],['quint'],['sext'],

# add short scale exponents and append "illion"'s!

exp = 9

expTens.each do |expTen|

expTen[0] = expTen[0] + 'illion';
exp = exp + 3


if scale == 0 # British (long scale)
# not using uncommon "milliard" (10**9)

# convert exponents to long scale

expTens.each do |expTen|

expTen[1] = (expTen[1] - 3) * 2



# add words and exponents common to both scales

expTens = [ ['hundred', 2], ['thousand', 3], ['million', 6] ]
+ expTens
expTens = expTens + [ ['googol', 100], ['centillion', 600] ]

# rational.rb says googolplex i.e. 10**(10**100) => Infinity

# unfortunatly now after the possible conversion to British
# long scale the expTens array is not in order. A googol's
# exponent is 100 which means it should be between
# sexdecillion and septendecillion.

# let's simply sort the array every time in case other such
# ordering errors occur

expTens.sort! { |x, y| x[1]<=>y[1] } # how easy was that! :)

left = number

# handle hundreds and above

expTens.reverse.each do |expTen|

value = 10**expTen[1]
write = left/value
left = left - write*value

if write > 0

exps = englishNumber(write, scale)
numString = numString + exps + ' ' + expTen[0]

if left > 0

if left < 100
numString = numString + ' and '
numString = numString + ', '




# handle teens

write = left/10 # How many tens left to write out?
left = left - write*10 # Subtract off those tens.

if write > 0
if ((write == 1) and (left > 0))
# Since we can't write "tenty-two" instead of "twelve",
# we have to make a special exception for these.
numString = numString + teenagers[left-1]
# The "-1" is because teenagers[3] is 'fourteen',
# not 'thirteen'.

# Since we took care of the digit in the ones place
# already, we have nothing left to write.
left = 0
numString = numString + tensPlace[write-1]
# The "-1" is because tensPlace[3] is 'forty',
# not 'thirty'.

if left > 0
# So we don't write 'sixtyfour'...
numString = numString + '-'

# handle ones

write = left # How many ones left to write out?
left = 0 # Subtract off those ones.

if write > 0
numString = numString + onesPlace[write-1]
# The "-1" is because onesPlace[3] is 'four', not 'three'.

# Now we just return "numString"...

def formatEnglishNumber value, scale
value.to_s+" = "+englishNumber( value, scale)

def formatEnglishNumberExp exp, scale
"10**"+exp.to_s+" = "+englishNumber(10**exp, scale)

scale = -1;
while scale<0 or scale>1
puts "Use 0) British Long Scale 1) American Short Scale?"
scale = gets.chomp.to_i

puts "Some Numbers:"
puts formatEnglishNumber( 0, scale)
puts formatEnglishNumber( 9, scale)
puts formatEnglishNumber( 10, scale)
puts formatEnglishNumber( 11, scale)
puts formatEnglishNumber( 17, scale)
puts formatEnglishNumber( 32, scale)
puts formatEnglishNumber( 88, scale)
puts formatEnglishNumber( 99, scale)
puts formatEnglishNumber(100, scale)
puts formatEnglishNumber(101, scale)
puts formatEnglishNumber(234, scale)
puts formatEnglishNumber(1000, scale)
puts formatEnglishNumber(3211, scale)
puts formatEnglishNumber(10000, scale)
puts formatEnglishNumber(100000, scale)
puts formatEnglishNumber(999999, scale)

puts "Let's test the scale: "

scalemax = 64;
if scale == 0
scalemax = 121

scalemax.times do |exp|
puts formatEnglishNumberExp(exp, scale)

puts "Some bigger numbers!"
puts formatEnglishNumber(100098765432134, scale)
puts formatEnglishNumber(2348723948732948723, scale)
puts formatEnglishNumber(2342342324100098765432134, scale)
puts formatEnglishNumber(124523598054213453230980329480, scale)
puts formatEnglishNumberExp(100, scale)
puts formatEnglishNumberExp(600, scale)
# googolplex is too big!!
#puts formatEnglishNumberExp(10**100, scale)

puts "Press Enter To Quit"

1 comment:

Anonymous said...

I've written similiar class: