[Home] [Downloads] [Search] [Help/forum]


Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to "verify" your details, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.
 Entire forum ➜ MUSHclient ➜ Lua ➜ Converting numbers from text and vice-versa

Converting numbers from text and vice-versa

It is now over 60 days since the last post. This thread is closed.     Refresh page


Pages: 1  2  3  4 

Posted by Nick Gammon   Australia  (23,046 posts)  Bio   Forum Administrator
Date Reply #45 on Thu 18 Mar 2010 03:57 AM (UTC)
Message
It seems to work OK now, excepting zero.

Your code (for 5000 iterations): 4.011 seconds
My code (for 5000 iteration, however different): 3.723 seconds

So they are comparable in speed.

I had to cut the size of the test number down:


  for i = 1, math.floor (MtRand () * 14 + 1) do
    s = s .. math.floor (MtRand () * 10)
  end -- for


Otherwise you get:


seven hundred thirteen trillion six hundred thirty one billion five hundred fifty four million six hundred five thousand eight hundred ninety two

LPEG result was: 7.1363155460589e+014


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by WillFa   USA  (525 posts)  Bio
Date Reply #46 on Thu 18 Mar 2010 03:59 AM (UTC)
Message
Nick Gammon said:

Try my test bed:


print (string.rep ("-", 75))

for i = 1, 5000 do
  local s = ""
  for i = 1, math.floor (MtRand () * 10 + 1) do
    s = s .. math.floor (MtRand () * 10)
  end -- for
  if tonumber(s) ~= 0 then
  print ("Converting: '" .. s .. "'")
  local words = assert (convert_numbers_to_words (s))
  print ("Result =", words)

  n2 = WordtoNum (words)
  print ("LPEG result was:", n2)

  assert (tostring (n2) == string.gsub (s, "^0+", ""), "LPEG conversion failed!")
  end -- if
end -- for




slightly modified since my LPEG doesn't like "zero", and your test strings can be "000" which fail the assert.
Top

Posted by WillFa   USA  (525 posts)  Bio
Date Reply #47 on Thu 18 Mar 2010 04:05 AM (UTC)

Amended on Thu 18 Mar 2010 04:11 AM (UTC) by WillFa

Message
Nick Gammon said:


LPEG result was: 7.1363155460589e+014



Do you lose precision (the final 2 ones), or is it the limitation of display in scientific notation?




print(string.format("%i", WordtoNum("seven hundred thirteen trillion six hundred thirty one billion five hundred fifty four million six hundred five thousand eight hundred ninety two")))


shows full precision.
Top

Posted by Nick Gammon   Australia  (23,046 posts)  Bio   Forum Administrator
Date Reply #48 on Thu 18 Mar 2010 04:07 AM (UTC)
Message
It's the scientific notation - I am checking for an exact string match. Anyway, this is your version amended to work with the bc library:


function WordtoNum (str)
  local P, V, Cf, C, Cg, Cc = lpeg.P, lpeg.V, lpeg.Cf, lpeg.C, lpeg.Cg, lpeg.Cc
  lpeg.locale(lpeg)
  local numvalues = {  one = 1, two = 2, three = 3, four = 4, five = 5,
        six = 6, seven = 7, eight = 8, nine= 9, ten = 10,
        eleven = 11, twelve = 12, thirteen = 13, fourteen = 14, fifteen = 15,
        sixteen = 16, seventeen = 17, eighteen = 18, nineteen = 19, twenty = 20,
        thirty = 30, forty = 40, fifty = 50, sixty = 60, seventy = 70, eighty = 80,
        ninety = 90, hundred = 10^2,
        thousand     = bc.number ("1000"),
        million      = bc.number ("1000000"),
        billion      = bc.number ("1000000000"),
        trillion     = bc.number ("1000000000000"),
        quadrillion  = bc.number ("1000000000000000"),
        quintillion  = bc.number ("1000000000000000000"),
        sextillion   = bc.number ("1000000000000000000000"),
        septillion   = bc.number ("1000000000000000000000000"),
        octillion    = bc.number ("1000000000000000000000000000"),
        nonillion    = bc.number ("1000000000000000000000000000000"),
        decillion    = bc.number ("1000000000000000000000000000000000"),
        undecillion  = bc.number ("1000000000000000000000000000000000000"),
        duodecillion = bc.number ("1000000000000000000000000000000000000000"),


        --blah blah
        }

  local ndigit = P"twenty" + P"thirty" + P"forty" + P"fifty" + P"sixty" + P"seventy" + P"eighty" + P"ninety" +
      P'hundred' +
      P"ten" + P"eleven" + P"twelve" + P"thirteen" + P"fourteen" + P"fifteen" +
      P"sixteen" + P"seventeen" + P"eighteen" + P"nineteen" +
      P"one" + P"two" + P"three" + P"four" + P"five" + P"six" + P"seven" + P"eight" + P"nine"


  local powers =   P'vigintillion' + P'novemdecillion' + P'octodecillion' + P'septendecillion' +
        P'sexdecillion' + P'quindecillion' + P'quattuordecillion' + P'tredecillion' +
        P'duodecillion' + P'undecillion' + P'decillion' + P'nonillion' + P'octillion' +
        P'septillion' + P'sextillion' + P'quintillion' + P'quadrillion' + P'trillion' +
        P'billion' + P'million' + P'thousand'

  local function SumVals (seed, ...)
    local cypher = {...}
    local tmp = bc.number (0)

    for x = 1, #cypher do
      if numvalues[cypher[x]] == 100 then
        if tmp == 0 then
          tmp = 100
        else
          tmp = tmp * 100
        end
      elseif powers:match(cypher[x] or "") then
        tmp = tmp * numvalues[cypher[x]]
      else
        tmp = tmp + numvalues[cypher[x]]
      end
    end
    return seed + tmp
  end

  local cypher = Cg( (C(ndigit) * lpeg.space^-1)^1 * ((C(powers) * lpeg.space^-1) + P(0)) )
  local number = Cf(Cc(0) * cypher^1, SumVals)

  return number:match(str)
end


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Tiopon   USA  (71 posts)  Bio
Date Reply #49 on Thu 18 Mar 2010 04:09 AM (UTC)
Message
Nick Gammon said:

Tiopon said:

I just put zero at the top of the listed numbers, above one... so the top section looks like:
...
and that seems to work properly for those cases... any possible caveats on that?


With that in it accepts "zero", and also constructs like "zero thousand", "zero hundred and twenty two" or "five hundred and zero" which I suppose is OK. It isn't wrong, it just looks strange.
Yeah... doing stuff like eight million zero thousand two hundred and five... does convert to 8000205. Odd, but it may end up getting parsed at some point.
Top

Posted by Nick Gammon   Australia  (23,046 posts)  Bio   Forum Administrator
Date Reply #50 on Thu 18 Mar 2010 04:14 AM (UTC)

Amended on Thu 18 Mar 2010 04:17 AM (UTC) by Nick Gammon

Message
I'm hoping that duodecillion is high enough for the use you are planning to put it to in a MUD game.

Something like:


The large, black wolf brushes you.
You _maim_ the large, black wolf!
The large, black wolf brushes you.
You dodge the large, black wolf's attack.
You _demolish_ the large, black wolf!
The large, black wolf is DEAD!!
You receive six octillion two hundred and twenty-four septillion two hundred and forty-six sextillion seven hundred and thirty-two quintillion five hundred and twenty-six quadrillion eight hundred and ninety-one trillion nine hundred and sixty-two billion seven hundred and thirty million five hundred and thirty-five thousand one hundred and forty experience points.
The large, black wolf's guts spill grotesquely from its torso.
You gain a level!!!

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #51 on Thu 18 Mar 2010 08:23 AM (UTC)
Message
Par for the course for DBZ games. :-P
I'll have to read over the code in more detail soon, at the moment it's late and I'm pissed off because somebody stole my gym bag from a bar. Sigh.

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,046 posts)  Bio   Forum Administrator
Date Reply #52 on Thu 18 Mar 2010 09:56 PM (UTC)

Amended on Fri 19 Mar 2010 03:17 AM (UTC) by Nick Gammon

Message
This is my final, cleaned up version. It looks a bit neater in places, and handles numbers up to 10^66.


-- Convert a number to words
-- Author: Nick Gammon
-- Date: 18th March 2010

-- Examples of use:
--    words  = convert_numbers_to_words ("94921277802687490518")
--    number = convert_words_to_numbers ("one hundred eight thousand three hundred nine")

-- Both functions return nil and an error message so you can check for failure,
-- or assert, eg. words = assert (convert_numbers_to_words ("2687490518"))

-- Units, must be in inverse order!
-- The trailing space is required as the space between words

local inverse_units = {
    "vigintillion ",     -- 10^63
    "novemdecillion ",   -- 10^60
    "octodecillion ",    -- 10^57
    "septendecillion ",  -- 10^54
    "sexdecillion ",     -- 10^51
    "quindecillion ",    -- 10^48
    "quattuordecillion ",-- 10^45
    "tredecillion ",     -- 10^42
    "duodecillion ",    -- 10^39
    "undecillion ",     -- 10^36
    "decillion ",       -- 10^33
    "nonillion ",       -- 10^30
    "octillion ",       -- 10^27
    "septillion ",      -- 10^24
    "sextillion ",      -- 10^21
    "quintillion ",     -- 10^18
    "quadrillion ",     -- 10^15
    "trillion ",        -- 10^12
    "billion ",         -- 10^9
    "million ",         -- 10^6
    "thousand ",        -- 10^3
  } -- inverse_units
  
local inverse_numbers = {
    "one ",
    "two ",
    "three ",
    "four ",
    "five ",
    "six ",
    "seven ",
    "eight ",
    "nine ",
    "ten ",
    "eleven ",
    "twelve ",
    "thirteen ",
    "fourteen ",
    "fifteen ",
    "sixteen ",
    "seventeen ",
    "eighteen ",
    "nineteen ",
    "twenty ",
    [30] = "thirty ",
    [40] = "forty ",
    [50] = "fifty ",
    [60] = "sixty ",
    [70] = "seventy ",
    [80] = "eighty ",
    [90] = "ninety ",
 }  -- inverse_numbers
 
local function convert_up_to_999 (n)

  if n <= 0 then
    return ""
  end -- if zero
  
  local hundreds = math.floor (n / 100)
  local tens = math.floor (n % 100)
  local result = ""

  -- if over 99 we need to say x hundred  
  if hundreds > 0 then
  
    result = inverse_numbers [hundreds] .. "hundred "
    if tens == 0 then
      return result
    end -- if only a digit in the hundreds column
  
  -- to have "and" between things like "hundred and ten"
  -- uncomment the next line
  --  result = result .. "and "

  end -- if
  
  -- up to twenty it is then just five hundred (and) fifteen
  if tens <= 20 then
    return result .. inverse_numbers [tens] 
  end -- if

  -- otherwise we need: thirty (something)
  result = result .. inverse_numbers [math.floor (tens / 10) * 10] 
  
  -- get final digit (eg. thirty four)
  local digits = math.floor (n % 10)

  -- to put a hyphen between things like "forty-two" 
  -- uncomment the WITH HYPHEN line and 
  -- comment out the NO HYPHEN line

  if digits > 0 then
    result = result ..  inverse_numbers [digits]  -- NO HYPHEN
--    result = string.sub (result, 1, -2) .. "-" ..  inverse_numbers [digits]  -- WITH HYPHEN
  end -- if 

  return result
  
end -- convert_up_to_999

-- convert a number to words
-- See: http://www.gammon.com.au/forum/?id=10155

function convert_numbers_to_words (n)
  local s = tostring (n)
  
  -- preliminary sanity checks
  local c = string.match (s, "%D")
  if c then
    return nil, "Non-numeric digit '" .. c .. "' in number"
  end -- if

  if #s == 0 then
    return nil, "No number supplied"
  elseif #s > 66 then
    return nil, "Number too big to convert to words"
  end -- if
  
  -- make multiple of 3
  while #s % 3 > 0 do
    s = "0" .. s
  end -- while
      
  local result = ""
  local start = #inverse_units - (#s / 3) + 2
  
  for i = start, #inverse_units do
    local group = tonumber (string.sub (s, 1, 3))
    if group > 0 then
      result = result .. convert_up_to_999 (group) .. inverse_units [i]
    end -- if not zero
    s = string.sub (s, 4)    
  end -- for
  
  result = result .. convert_up_to_999 (tonumber (s)) 

  if result == "" then
    result = "zero"
  end -- if
  
  return (string.gsub (result, " +$", ""))  -- trim trailing spaces

end -- convert_numbers_to_words

-- Convert words to a number
-- Author: Nick Gammon
-- Date: 18th March 2010

-- Does NOT handle decimal places (eg. four point six)

local numbers = {
         zero       = bc.number (0),
         one        = bc.number (1),
         two        = bc.number (2),
         three      = bc.number (3),
         four       = bc.number (4),
         five       = bc.number (5),
         six        = bc.number (6),
         seven      = bc.number (7),
         eight      = bc.number (8),
         nine       = bc.number (9),
         ten        = bc.number (10),
         eleven     = bc.number (11),
         twelve     = bc.number (12),
         thirteen   = bc.number (13),
         fourteen   = bc.number (14),
         fifteen    = bc.number (15),
         sixteen    = bc.number (16),
         seventeen  = bc.number (17),
         eighteen   = bc.number (18),
         nineteen   = bc.number (19),
         twenty     = bc.number (20),
         thirty     = bc.number (30),
         forty      = bc.number (40),
         fifty      = bc.number (50),
         sixty      = bc.number (60),
         seventy    = bc.number (70),
         eighty     = bc.number (80),
         ninety     = bc.number (90),
} -- numbers 

local units = {
          hundred             = bc.number ("100"),
          thousand            = bc.number ("1" .. string.rep ("0",  3)),
          million             = bc.number ("1" .. string.rep ("0",  6)),
          billion             = bc.number ("1" .. string.rep ("0",  9)),
          trillion            = bc.number ("1" .. string.rep ("0", 12)),
          quadrillion         = bc.number ("1" .. string.rep ("0", 15)),
          quintillion         = bc.number ("1" .. string.rep ("0", 18)),
          sextillion          = bc.number ("1" .. string.rep ("0", 21)),
          septillion          = bc.number ("1" .. string.rep ("0", 24)),
          octillion           = bc.number ("1" .. string.rep ("0", 27)),
          nonillion           = bc.number ("1" .. string.rep ("0", 30)),
          decillion           = bc.number ("1" .. string.rep ("0", 33)),
          undecillion         = bc.number ("1" .. string.rep ("0", 36)),
          duodecillion        = bc.number ("1" .. string.rep ("0", 39)),
          tredecillion        = bc.number ("1" .. string.rep ("0", 42)),     
          quattuordecillion   = bc.number ("1" .. string.rep ("0", 45)),
          quindecillion       = bc.number ("1" .. string.rep ("0", 48)),    
          sexdecillion        = bc.number ("1" .. string.rep ("0", 51)),     
          septendecillion     = bc.number ("1" .. string.rep ("0", 54)),  
          octodecillion       = bc.number ("1" .. string.rep ("0", 57)),    
          novemdecillion      = bc.number ("1" .. string.rep ("0", 60)),   
          vigintillion        = bc.number ("1" .. string.rep ("0", 63)),     
  } -- units
  
-- convert a number in words to a numeric form
-- See: http://www.gammon.com.au/forum/?id=10155
-- Thanks to David Haley

function convert_words_to_numbers (s)

  local stack = {}
  local previous_type
 
  for word in string.gmatch (s:lower (), "[%a%d]+") do
    if word ~= "and" then  -- skip "and" (like "hundred and fifty two")
      local top = #stack
      
      -- If the current word is a number (English or numeric), 
      -- and the previous word was also a number, pop the previous number 
      -- from the stack and push the addition of the two numbers. 
      -- Otherwise, push the new number.

      local number = tonumber (word)  -- try for numeric (eg. 22 thousand)

      if number then
        number = bc.number (number)   -- turn into "big number"
      else
        number = numbers [word]
      end -- if a number-word "like: twenty"

      if number then
        if previous_type == "number" then   -- eg. forty three
          local previous_number = table.remove (stack, top)  -- get the forty
          number = number + previous_number  -- add three
        end -- if 
        table.insert (stack, number)   
        previous_type = "number"
      else
      
        -- If the current word is a unit, multiply the number on the top of the stack by the unit's magnitude. 
        local unit = units [word]
        if not unit then
          return nil, "Unexpected word: " .. word
        end -- not unit
        previous_type = "unit"
        
        -- It is an error to get a unit before a number.
        
        if top == 0 then
          return nil, "Cannot have unit before a number: " .. word
        end -- starts of with something like "thousand"

        -- pop until we get something larger on the stack
        local interim_result = bc.number (0)
        while top > 0 and stack [top] < unit do
          interim_result = interim_result + table.remove (stack, top)
          top = #stack
        end -- while
        table.insert (stack, interim_result * unit)
               
      end -- if number or not
    end -- if 'and'

  end -- for each word
  
  if #stack == 0 then
    return nil, "No number found"
  end -- nothing
  
  -- When the input has been parsed, sum all numbers on the stack.
  
  local result = bc.number (0)
  for _, item in ipairs (stack) do
    result = result + item
  end -- for
  
  return result
end -- function convert_words_to_numbers


The test bed you can use to confirm it works is this (just put underneath and run it):


print (string.rep ("-", 75))

local start_time = GetInfo (232)

for i = 1, 5000 do
  local s = ""
  for i = 1, math.floor (MtRand () * 66 + 1) do
    s = s .. math.floor (MtRand () * 10)
  end -- for
  
  print ("Converting: '" .. s .. "'")
  local words = assert (convert_numbers_to_words (s))
  print ("Result =", words)
  n = assert (convert_words_to_numbers (words))
  print ("Result    : '" .. n:tostring () .. "'", "length =", #bc.tostring (n))

  if not bc.iszero (n) then
    assert (string.gsub (s, "^0+", "") == n:tostring (), "Conversion failed!")
  end -- if

end -- for

local end_time = GetInfo (232)

print (string.format ("Time taken = %0.3f seconds", end_time - start_time))


The code handles up to 999 vigintillion, and has a few sanity checks on the supplied number when doing convert_numbers_to_words (eg. is it a number, is it too long).

Example output:


Converting: '221854371162103637052189353391241767991981161850420402320763181735'
Result = two hundred twenty one vigintillion eight hundred fifty four novemdecillion three hundred seventy one octodecillion one hundred sixty two septendecillion one hundred three sexdecillion six hundred thirty seven quindecillion fifty two quattuordecillion one hundred eighty nine tredecillion three hundred fifty three duodecillion three hundred ninety one undecillion two hundred forty one decillion seven hundred sixty seven nonillion nine hundred ninety one octillion nine hundred eighty one septillion one hundred sixty one sextillion eight hundred fifty quintillion four hundred twenty quadrillion four hundred two trillion three hundred twenty billion seven hundred sixty three million one hundred eighty one thousand seven hundred thirty five
Result : '221854371162103637052189353391241767991981161850420402320763181735' length = 66


The comments in the code indicate the two changes needed if you want hyphens in generated words (eg. twenty-two) or the word "and" (eg. hundred and five).

The code does not handle negative numbers or decimals. You could easily add a wrapper to do that.

The code to convert words to numbers just looks for words and digits (thus skipping hyphens). It would also skip other non-letter/non-digit things (like commas) so if you are processing untrusted input you may want to check for those yourself.




Examples of use:


print (convert_numbers_to_words ("94921277802687490518"))


(Large numbers need to be quoted, otherwise Lua turns them into scientific notation, like "9.4921277802687e+019", which it won't handle).

Result:


ninety four quintillion nine hundred twenty one quadrillion two hundred seventy seven trillion eight hundred two billion six hundred eighty seven million four hundred ninety thousand five hundred eighteen


And converting back:


print (convert_words_to_numbers ("sixty five quintillion five hundred fifty nine quadrillion eight hundred eighty seven trillion seven hundred seventy one billion one hundred sixty six million one hundred eight thousand three hundred nine"))


Result:


65559887771166108309


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Tiopon   USA  (71 posts)  Bio
Date Reply #53 on Thu 18 Mar 2010 10:32 PM (UTC)
Message
Beautiful and far beyond what I dreamed when I asked. Thanks so much... convert.lua is making me very happy. :)
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #54 on Fri 19 Mar 2010 03:03 AM (UTC)

Amended on Fri 19 Mar 2010 03:04 AM (UTC) by David Haley

Message
Nick Gammon said:
      if number then
        if previous_type == "number" then   -- eg. forty three
          local previous_number = table.remove (stack, top)  -- get the three
          number = number + previous_number  -- add forty
        end -- if 
        table.insert (stack, number)   
        previous_type = "number"
      else

The comments here are reversed: table.remove will be getting 'forty', because it was previously pushed onto the stack, and we're adding 'three' to the 'forty'.

Nice implementation though. It seems like a generally useful library to have! And certainly much easier to follow, IMHO, than the SQL thing we saw earlier... ;)

I might reword my comment here too:
      -- If the current word is a number (English or numeric), 
      -- and the previous word was also a number, pop the stack and push the addition. 
      -- Otherwise, push the number.

to something like:
      -- If the current word is a number (English or numeric), 
      -- and the previous word was also a number, pop the previous number from the stack and push the addition of the two numbers. 
      -- Otherwise, push the new number.

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,046 posts)  Bio   Forum Administrator
Date Reply #55 on Fri 19 Mar 2010 03:18 AM (UTC)
Message
Thanks David. Amended post to revise your comments.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


143,151 views.

This is page 4, subject is 4 pages long:  [Previous page]  1  2  3  4 

It is now over 60 days since the last post. This thread is closed.     Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.

[Home]