A code to convert from numbers to English words, any improvements?

I’ve made a function that converts from numbers to English words (e.g. 100 to one hundred). It works but I need a better and more effiencint way of doing this, it supports numbers as strings. How can I improve this code?

function ToWord(value, context)
	    context = context or { financial = false; scale = 'long'; decimal = 0; };
	    context = { scale = context.scale or 'long'; decimal = context.decimal or 0; };
	    local numbers =
	    {
	        ones = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "tweleve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" };
	        tens = { '', "twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninty" };
	        thousands =
	        {
	            short = { "thousand", "million", "billion", "trillion", "quadrillion", "quintillion", "sextillion", "septillion" };
	            long = { "thousand", "million", "thousand million", "billion", "thousand billion", "trillion", "thousand trillion", "quadrillion" };
	        };
	        decimal =
	        {
	            short = { "thousandth", "millionth", "billionth", "trilionth", "quadrillionth", "sextillionth", "septillionth" };
	            long = { "thousandth", "millionth", "thousand-millionth", "billionth", "thousand-billionth", "trilionth", "thousand-trillionth", "quadrillionth" };
	        };
	    };
	
	    local function _raw_convert(n)
	        local r = { };
	        for i = #n, 1, -3 do
	            local val_p = tonumber(n:sub(math.max(i - 2, 1), i));
	            local val_p_r = numbers.ones[val_p];
	            if not val_p_r then
	                if val_p >= 20 then
	                    local h = '';
	                    if val_p >= 100 then
	                        h = numbers.ones[math.floor(val_p / 100)] .. ' hundred and ';
	                    end;
	                    val_p_r = h .. (numbers.tens[math.floor((val_p % 100) / 10)] or '') .. '-' .. (numbers.ones[val_p % 10] or '');
	                end;
	            end;
	            val_p_r = (val_p_r or ''):gsub("[- ]+$", "");
	            if val_p_r ~= '' then
	                val_p_r = val_p_r .. ' ' .. (numbers.thousands[context.scale][(#n - i) / 3] or '');
	                table.insert(r, 1, (val_p_r:gsub("[- ]+$", "")));
	            end;
	        end;
	        return table.concat(r, ', ');
	    end;
	
	    if type(value) == 'number' then
	        value = ("%.99f"):format(value);
	    end;
	    local int_v, frac_v = value:match("(%d*)[.]?(%d*)");
	    local int_r = _raw_convert(int_v:gsub("^0+", ''));
	    local frac_r;
	
	    frac_v = frac_v:gsub("0+$", '');
	    local frac_r = '';
	    if #frac_v > 0 then
	        if context.decimal == 0 and not context.financial then
	            frac_r = { };
	            for n in frac_v:gmatch('%d') do
	                table.insert(frac_r, numbers.ones[tonumber(n)]);
	            end;
	            frac_r = table.concat(frac_r, ' ');
	        elseif context.decimal == 1 and not context.financial then
	            local d = "tenth";
	            if #frac_v == 2 then
	                d = "hundredth";
	            elseif #frac_v > 2 then
	                d = (((#frac_v % 3) == 2 and 'hundred-') or ((#frac_v % 3) == 1 and 'ten-') or '') .. (numbers.decimal[context.scale][math.floor(#frac_v / 3)] or '');
	            end;
	            frac_r = _raw_convert(frac_v)
	            if frac_r ~= '' then
	                frac_r = frac_r .. ' ' .. d .. ((frac_v:gsub("[13579]", '') == frac_v) and '' or 's');
	            end;
	        else
	            frac_r = frac_v:gsub("^0+", '') .. '/1' .. ('0'):rep(#frac_v);
	        end;
	    end;
	
	    if int_r == '' and ((context.decimal == 0) or (#frac_r == 0)) then
	        int_r = 'zero';
	    end;
	
	    if #frac_r == 0 then
	        return int_r;
	    else
	        return ("%s %s %s"):format(int_r, context.decimal == 0 and 'point' or 'and', frac_r);
	    end;
	end;
4 Likes

Can you explain what type the value and context parameters are?

The value paramter could be string or float and if float is converted to string while the parameter context is a table.

Furthermore, what fields do you expect in the context table?

None really, it’s optional. (30 character)

Do you convert any negative numbers or is it strictly 0 and positive numbers?

Also, to what decimal point are you targetting? Is it only integers?

I’m asking since displaying “x millionths” doesn’t sound like a good UX decision.

Only positive numbers and zzro.

and it targets up to 24 decimal places. I originally wanted it to be only positive integers but I added this for fun.

I have created an updates version of this script.
It is now using grammar so the first version would say “one” but this version would say “One.”

function ToWord(value, context)
	context = context or { financial = false; scale = 'long'; decimal = 0; };
	context = { scale = context.scale or 'long'; decimal = context.decimal or 0; };
	local numbers =
		{
			ones = { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" };
			tens = { '', "Twenty", "Thirty", "Fourty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" };
			thousands =
			{
				short = { "Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion" };
				long = { "Thousand", "Tillion", "Thousand million", "Billion", "Thousand billion", "Trillion", "Thousand trillion", "Quadrillion" };
			};
			decimal =
			{
				short = { "Thousandth", "Millionth", "Billionth", "Trilionth", "Wuadrillionth", "Sextillionth", "Septillionth" };
				long = { "Thousandth", "Millionth", "Thousand-millionth", "Billionth", "Thousand-billionth", "Trilionth", "Thousand-trillionth", "Quadrillionth" };
			};
		};

	local function _raw_convert(n)
		local r = { };
		for i = #n, 1, -3 do
			local val_p = tonumber(n:sub(math.max(i - 2, 1), i));
			local val_p_r = numbers.ones[val_p];
			if not val_p_r then
				if val_p >= 20 then
					local h = '';
					if val_p >= 100 then
						h = numbers.ones[math.floor(val_p / 100)] .. ' hundred and ';
					end;
					val_p_r = h .. (numbers.tens[math.floor((val_p % 100) / 10)] or '') .. '-' .. (numbers.ones[val_p % 10] or '');
				end;
			end;
			val_p_r = (val_p_r or ''):gsub("[- ]+$", "");
			if val_p_r ~= '' then
				val_p_r = val_p_r .. ' ' .. (numbers.thousands[context.scale][(#n - i) / 3] or '');
				table.insert(r, 1, (val_p_r:gsub("[- ]+$", "")));
			end;
		end;
		return table.concat(r, ', ');
	end;

	if type(value) == 'number' then
		value = ("%.99f"):format(value);
	end;
	local int_v, frac_v = value:match("(%d*)[.]?(%d*)");
	local int_r = _raw_convert(int_v:gsub("^0+", ''));
	local frac_r;

	frac_v = frac_v:gsub("0+$", '');
	local frac_r = '';
	if #frac_v > 0 then
		if context.decimal == 0 and not context.financial then
			frac_r = { };
			for n in frac_v:gmatch('%d') do
				table.insert(frac_r, numbers.ones[tonumber(n)]);
			end;
			frac_r = table.concat(frac_r, ' ');
		elseif context.decimal == 1 and not context.financial then
			local d = "tenth";
			if #frac_v == 2 then
				d = "hundredth";
			elseif #frac_v > 2 then
				d = (((#frac_v % 3) == 2 and 'hundred-') or ((#frac_v % 3) == 1 and 'ten-') or '') .. (numbers.decimal[context.scale][math.floor(#frac_v / 3)] or '');
			end;
			frac_r = _raw_convert(frac_v)
			if frac_r ~= '' then
				frac_r = frac_r .. ' ' .. d .. ((frac_v:gsub("[13579]", '') == frac_v) and '' or 's');
			end;
		else
			frac_r = frac_v:gsub("^0+", '') .. '/1' .. ('0'):rep(#frac_v);
		end;
	end;

	if int_r == '' and ((context.decimal == 0) or (#frac_r == 0)) then
		int_r = 'zero';
	end;

	if #frac_r == 0 then
		local toSend = int_r.."."
		return(toSend);
	else
		return ("%s %s %s"):format(int_r, context.decimal == 0 and 'point' or 'and', frac_r);
	end;
end;

I have a much more efficient function that can handle small and huge numbers

you might want to look at this

I can’t exactly get the code because I am on mobile right now, but you can easily look at the source code in the Convert module

1 Like

You do realise this is over a year old?

Your implementation only accepts IEEE, no idea why did you say

Your implementation don’t accept Infinity and all 252 NaNs and don’t account for subnormals nor negative zero.
You’re using floating point division which I always avoided because I needed it to be arbitrary precise.

1 Like

that’s impossible, not a number

I decided to post it because @phyouthcenter1 replied a year later

I never even noticed that I missed negative zero, tysm

also it can do any whole number up to 4 nonillion, could you please tell me what doesn’t work so I can improve it, thanks

Actually, I am not using floating point division, did you actually look into what my code does?

anyways I took this as feedback on how to improve it, thanks

EDIT: I tested it and I guess if a decimal started with a 1 then it wouldn’t be right, looks like it was a simple ternary mistake :sweat_smile:

so if you try “0.1…” just wanted to let you know it won’t be right for now because I am not updating the module yet