Compressing these into one function?

currently i have a numbers to words converter, but i feel like there’s a pattern in these functions which would allow me to compress them down into one, but how?

function thousandDigitsToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local NLength:number = string.len(tostring(n))
	local FirstDigits:number?
	local TheRest:number?
	if NLength == 4 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 1))
		TheRest = tonumber(string.sub(tostring(n), 2, 4))
	elseif NLength == 5 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 2))
		TheRest = tonumber(string.sub(tostring(n), 3, 5))
	else
		FirstDigits = tonumber(string.sub(tostring(n), 1, 3))
		TheRest = tonumber(string.sub(tostring(n), 4, 6))
	end
	local finalResult:string = belowThreeDigitsToWord(FirstDigits).." thousand "..belowThreeDigitsToWord(TheRest)
	if isNegative then
		finalResult = "negative "..finalResult
	end
	return finalResult
end
 
function millionDigitsToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local NLength:number = string.len(tostring(n))
	local FirstDigits:number?
	local TheRest:number?
	if NLength == 7 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 1))
		TheRest = tonumber(string.sub(tostring(n), 2, 7))
	elseif NLength == 8 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 2))
		TheRest = tonumber(string.sub(tostring(n), 3, 8))
	else
		FirstDigits = tonumber(string.sub(tostring(n), 1, 3))
		TheRest = tonumber(string.sub(tostring(n), 4, 9))
	end
	local finalResult:string = belowThreeDigitsToWord(FirstDigits).." million "..daft:numberToWord(TheRest)
	if isNegative then
		finalResult = "negative "..finalResult
	end
	return finalResult
end
 
function billionDigitsToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local NLength:number = string.len(tostring(n))
	local FirstDigits:number?
	local TheRest:number?
	if NLength == 10 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 1))
		TheRest = tonumber(string.sub(tostring(n), 2, 10))
	elseif NLength == 11 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 2))
		TheRest = tonumber(string.sub(tostring(n), 3, 11))
	else
		FirstDigits = tonumber(string.sub(tostring(n), 1, 3))
		TheRest = tonumber(string.sub(tostring(n), 4, 12))
	end
	local finalResult:string = belowThreeDigitsToWord(FirstDigits).." billion "..daft:numberToWord(TheRest)
	if isNegative then
		finalResult = "negative "..finalResult
	end
	return finalResult
end
 
function trillionDigitsToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local NLength:number = string.len(tostring(n))
	local FirstDigits:number?
	local TheRest:number?
	if NLength == 13 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 1))
		TheRest = tonumber(string.sub(tostring(n), 2, 13))
	elseif NLength == 14 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 2))
		TheRest = tonumber(string.sub(tostring(n), 3, 14))
	else
		FirstDigits = tonumber(string.sub(tostring(n), 1, 3))
		TheRest = tonumber(string.sub(tostring(n), 4, 15))
	end
	local finalResult:string = belowThreeDigitsToWord(FirstDigits).." trillion "..daft:numberToWord(TheRest)
	if isNegative then
		finalResult = "negative "..finalResult
	end
	return finalResult
end
 
function quadrillionDigitsToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local NLength:number = string.len(tostring(n))
	local FirstDigits:number?
	local TheRest:number?
	if NLength == 14 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 1))
		TheRest = tonumber(string.sub(tostring(n), 2, 16))
	elseif NLength == 15 then
		FirstDigits = tonumber(string.sub(tostring(n), 1, 2))
		TheRest = tonumber(string.sub(tostring(n), 3, 17))
	else
		FirstDigits = tonumber(string.sub(tostring(n), 1, 3))
		TheRest = tonumber(string.sub(tostring(n), 4, 18))
	end
	local finalResult:string = belowThreeDigitsToWord(FirstDigits).." quadrillion "..daft:numberToWord(TheRest)
	if isNegative then
		finalResult = "negative "..finalResult
	end
	return finalResult
end

– not the focus of the post, just here if someone asks for it

function numberInRange(n:number, mn:number, mx:number):boolean
	n, mn, mx = tonumber(n), tonumber(mn), tonumber(mx)
	return (n>=mn) and (n<=mx)
end

function singleDigitToWord(n:number):string
	local numbers:{string} = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}
	if n == 0 then
		return "zero"
	elseif n < 0 then
		n = math.abs(n)
		return "negative "..numbers[n]
	else
		return numbers[n]
	end
end

function doubleDigitToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local tenCases:{string} = {"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"}
	local otherNumbers:{string} = {"", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"}
	local FirstDigit:number = tonumber(string.sub(tostring(n), 1, 1))
	local SecondDigit:number = tonumber(string.sub(tostring(n), 2, 2))
	if isNegative then
		for i, v in ipairs(tenCases) do
			tenCases[i] = "negative "..v
		end
		for i, v in ipairs(otherNumbers) do
			otherNumbers[i] = "negative "..v
		end
	end
	-- we got a ten here
	if FirstDigit == 1 then
		-- special case for ten to make the rest of the code cleaner
		if SecondDigit == 0 then
			if isNegative then
				return "negative ten"
			else
				return "ten"
			end
		else
			return tenCases[SecondDigit]
		end
	else
		if SecondDigit == 0 then
			return otherNumbers[FirstDigit]
		else
			return otherNumbers[FirstDigit].."-"..singleDigitToWord(SecondDigit)
		end
	end
end

function tripleDigitToWord(n:number):string
	local isNegative:boolean = (n<0)
	n = math.abs(n)
	local FirstDigit:number = tonumber(string.sub(tostring(n), 1, 1))
	local TheRest:number = tonumber(string.sub(tostring(n), 2, 3))
	local finalResult:string = singleDigitToWord(FirstDigit).." hundred "..daft:numberToWord(TheRest)
	if isNegative then
		finalResult = "negative "..finalResult
	end
	return finalResult
end

function belowThreeDigitsToWord(n:number):string
	-- don't count the negative sign
	local NLength:number = string.len(tostring(math.abs(n)))
	if NLength == 1 then
		return singleDigitToWord(n)
	elseif NLength == 2 then
		return doubleDigitToWord(n)
	else
		return tripleDigitToWord(n)
	end
end
2 Likes

Sadly I think you would need to recode everything from scratch,

there is nothing you can really save, maybe you could make a table for thousand, mill, bill, trill but its really not solving the entire code quality problem.

Is there a little more context on how you want it to work?

1010 = “1 thousand and 10”

2 Likes

There’s gotta be a more optimal way
Or what you could do is classify each one, tenths, hundredths, thousandths, etc so if it detects an amount of zeros it could do something like this

3000 -> three-thousand
2 Likes

lies
(also one thousand ten is proper grammar, just not said often)
anyways i posted my new version as a community resource

3 Likes

You can compress it into one function entirely (devoid of the helper functions), but I think the best case scenario is two functions like so. If you have any questions, I will explain.

local NUMBERS = {
	NEGATIVE_PREFIX = "Negative",
	SINGLES = {
		"Zero", 
		"One", 
		"Two", 
		"Three", 
		"Four", 
		"Five", 
		"Six", 
		"Seven", 
		"Eight", 
		"Nine", 
		"Ten",
		"Eleven", 
		"Twelve", 
		"Thirteen", 
		"Fourteen", 
		"Fifteen", 
		"Sixteen", 
		"Seventeen", 
		"Eighteen", 
		"Nineteen"
	},
	TENS = {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"},
	HUNDRED = "Hundred",
	DEMOMINATIONS = {
		"Thousand", 
		"Million", 
		"Billion",
		"Trillion", 
		"Quadrillion", 
		"Quintillion", 
		"Sextillion", 
		"Septillion", 
		"Octillion", 
		"Nonillion", 
		"Decillion", 
		"Undecillion", 
		"Duodecillion", 
		"Tredecillion", 
		"Quattuordecillion"
	}
}

local MAX_VALUE = 1000 ^ #NUMBERS.DEMOMINATIONS * 999

--[[
	Takes a chunk 1-999 and converts it into words (same as below three digits in your code I think)
]]
local function generateWordsForChunk(chunk: number)
	local words = {}

	if chunk > 0 then
		if chunk >= 100 then
			table.insert(words, NUMBERS.SINGLES[math.floor(chunk / 100) + 1])
			table.insert(words, NUMBERS.HUNDRED)
			chunk = chunk % 100
		end

		if chunk >= 20 then
			table.insert(words, NUMBERS.TENS[math.floor(chunk / 10) - 1])
			chunk = chunk % 10
			if chunk > 0 then
				words[#words] = words[#words] .. "-" .. NUMBERS.SINGLES[chunk + 1]
			end
			
			return words
		end
		
		table.insert(words, NUMBERS.SINGLES[chunk + 1])
	end

	return words
end

local function integerToWords(number: number, titleCase: boolean?)
	assert(typeof(number) == "number", "Expected number, but instead got " .. typeof(number))
	local isNegative = number < 0
	number = math.floor(number)
	number = math.abs(number)
	assert(number < MAX_VALUE, "Number is too big!")

	if number == 0 then
		return NUMBERS.SINGLES[1]
	end


	local words = {}
	if isNegative then
		table.insert(words, NUMBERS.NEGATIVE_PREFIX)
	end

	for i = #NUMBERS.DEMOMINATIONS, 0, -1 do
		local divisor = 1000 ^ i
		if number < divisor then
			continue
		end
		
		local chunk = math.floor(number / divisor)
		local newWords = generateWordsForChunk(chunk)
		
		for _, word in newWords do
			table.insert(words, word)
		end
		if i > 0 then
			table.insert(words, NUMBERS.DEMOMINATIONS[i])
		end

		number %= divisor
	end

	local result = table.concat(words, " "):sub(1, -1)
	return if not titleCase then result:lower() else result
end

return {
	integerToWords = integerToWords,
}