Coding Challenge #1: Numbers to French

Coding Challenge #1

Welcome everyone to the Coding Challenge! This is a series of challenges for interested developers, that I will be releasing every once in a while. The main goal behind doing them is testing yourself and your mind, to see how well you can get around problems. I’m mainly gonna be introducing algorithms and problems where you have to think hard, which is why I think these challenges are great, because they don’t just help you improve your inner Roblox developer skills, but also the outer computer science skills, which would be super useful in the future if you’re ever going to switch careers. Experience and massive knowledge of what Roblox Studio is capable of isn’t really needed to finish a challenge (at least most of them), you just need to know the basics, and work that brains of yours. That’s the introduction done, let’s get right to it!

These might not be considered #resources:community-tutorials really, but since there is no other suitable place for them, let’s just say they are a way to learn and practice more.

First of all, a bunch of info that you would need to know. Each challenge would be well-described, giving examples of possible inputs, and required outputs. Once you think you have finished the challenge, dominating every possible case, or you have managed to do a part of it but couldn’t finish, you can reply with what you made whenever you want! Even if it’s a failed attempt, don’t be shy to share it! I never said these challenges were gonna be easy, or hard, the difficulty varies. Also if you have any feedback, or correction, feel free to post a reply! Then expect a DM from me, congratulating you or giving you some notices. The official answer to the challenge would be replied to this topic at maximum before the next 24 hours (sorry if I’m late). The submissions needs some requirements:

  • Has to be written in Lua 5.1 (the version roblox uses)
  • The program has to be compatible with Roblox Studio, meaning you can’t use features vanilla Lua has but rbxlua disabled

Now for today’s challenge, the first one ever, I want you to write a function, that takes a number as an input, and outputs the French representation of that number. Ah oui oui! :croissant:

Basically, an algorithm to turn numbers into french words. For example, 51 would be cinquante-cinq, 168 would be cent soixante-huit, and even crazy stuff like 89564686 which is quatre-vingt-neuf million cinq cent soixante-quatre mille six cent quatre-vingt-six (jeez).

print(toFrench(125)) --prints 'cent vingt-cinq'

codingchallenge1

This algorithm has to work with any given number, no matter how many digits it has (you can limit it to a certain number, 12 digits is enough, meaning billions) and it has to respect all the rules the french numbering system has, such as:

  • Adding a hyphen - when needed
  • 80 has an s at the end quatre-vingts unlike 81 and 82… ect. without the s (quatre-vingt-deux)
  • Any number with 1 at the end being written às vingt et un, trente et un ect. while 81 is written as quatre-vingt-un and not quatre-vingt et un
  • Invaribale numbers, such as un, deuxonze, douze…/vingt, trentequatre-vingt-dix. (Basically numbers from 0 to 16; and all tens, like 10, 20, 30… to 90)
  • Numbers between 90 and 99, this is the trickest part
  • 1 000 000 and 1 000 written as un million and un mille but 100 written as just cent without a un.
  • mille not having an s when there are multiple thousands (ex: cinq mille), but million and milliard ect. having an s if there are multiple. (ex: cinq millions)

And many other special cases that I didn’t mention! For a first challenge, this is deffintely tough, which is why you can even share a part of the challenge done, even if it doesn’t fully work. But don’t worry, this is supposed to be a tutorial, we’ll see how to do it from start to finish. Also, you may wanna learn more about the french numbering system, from resources such as this and this. Et voilà!

Goodluck on your quest! And I hope you had fun doing this! You can find a list of all the challenges here or a github version here.

Capture

Here is the answer! Waiting for you, don't spoil yourself!

Now! Time for the answer :trumpet:!

First thing, we need to setup certain keywords, invariable words, that we will always use throughout the entire program, such as numbers from 0 to 16, and tens like 10, 20 … all the way to 90, also multiples of ten, such as 1000, 10000000, 10000000000…
So, we would have this

local invariable = {"zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix", "onze", "douze",
"treize", "quatorze", "quinze", "seize"}
local dozens = {"dix", "vingt", "trente", "quarante", "cinquante", "soixante", "soixante-dix", "quatre-vingt"}
local multiples = {"", "mille", "million", "milliard"}

Notice how each number is correspondant to an index. For 1 digit numbers (the tables invariable) each number is positioned corresponding to itself plus one, if we wanted 5 (cinq) we would do 5+1, because cinq is at the 6th index. We have to add one because 0 (zéro) is at the first index, and arrays in lua start 1. For tens (the table dozens), each number is at its corresponding position as well, for example 40 (quarante) is at 4th index. And for multiples of 10, (the table multiples), basically thousands (mille) millions (million) billions (milliard) ect., they’re positioned relative to the part of the number they’re in. You’ll see later that we’re dividing each inputed number every 3 numbers, to make things easier. Thousands for example, is always at the second part, (the second 4th, 5th, 6th numbers), millions at third ect. The algorithm I’m writing will only support up to billions (milliard) but you can add more if you want, it’s just a matter of adding them into the table.

Next, let’s start writing the function

function toFrench(num)
	local n = tostring(num) --n is a string represenation of the number, which we would use a lot
	local space = " " --a space that we will use when concatenating
 	local parts = {} --this table would hold each 3 numbers together
	local result = "" --this variable is the output value, that we will constanly change
end

To make things easier, we divide each inputed number to different parts each 3 characters, which you might know

codingchallenge1-3

This makes things easier, because we would always be dealing with 3-digit numbers, and each time just add something from multiples according to which part you’re currently in. In the upper picture for example, we just need to convert 546, add nothing, then convert 385, add “mille”, then convert 1, and add “million”. I hope you got the picture.

codingchallenge1-4

To divide the number to parts each 3 characters

for part in string.gmatch(string.reverse(n),".?.?.?") do --i'm reversing the string for a reason i'll explain later, also this part might be confusing if you don't know about string patterns and iterators, but just know it splits a string every 3 characters, you can easily come up with another way
    table.insert(parts, part)
end

Now, it’s time to get into the conversion! When I was writing my algorithm, I wrote a list of possible inputs, and started dominating possibility by possibility. I suggest you do that to break down the algorithm to smaller chunks to make things easier.
First, let’s eliminate numbers between 0-16. If a person inputed a number inputed between 0 and 16, I can instanly give him the answer, without having to do the parts thing.

if invariable[num+1] then return invariable[num+1] end --if it's a number between 0 and 16 (the invariable ones), return what's in that number + 1

Now for the most important part, we will be looping through each part (each 3 characters), and converting them. I’m gonna set 3 different variables, one containing the ones, the tens and the hundreds.

for i = #parts, 1, -1 do --notice how i'm looping through the parts backwards, this is because when we're concatenating, we want them to be in order
	local part = parts[i] --each part each time
	if part ~= "" then --an empty string might be in the table, so I check that off
		local ones, tens, hundreds = tonumber(string.sub(part,1,1)), tonumber(string.sub(part,2,2)), 	tonumber(string.sub(part,3,3))
		local double = tostring(tens)..tostring(ones) --contains the number version of tens and ones together, basically 41, 52, 98 ect.
		local second = "" --contains the french version of tens and ones together, that we will add to the hundreds later
		local prefix = "-" --remember we're gonna need this
	end
end

Notice how ones is the first index, tens is the second, hundreds is third. Logically, if we had a part like "569" for example, ones is the third, tens is the second, hundreds is the first. Why is it backwards then? Well if you remember, in the upper part when I told you i’ll explain why I was reversing the string. This is why, the string is reversed, "569" becomes "965", ones becomes first, tens stays second, hunderds becomes third. The reason I’m doing this is, what if a part had just 1 number or 2, instead of 3. Like, "1" for example, if I didn’t reverse the string, and change the orders, hundreds would be 1, when ones is supposed to be. But if we flip the order, we can make ones be 1, and fix things. See! You alawys gotta find a solution to problems like these!

Now, to the toughest part: the conversion! As I said above, second will contain ones and tens together, then we add them to hundreds, yet again, a way to make things easier. So first, we start to get to the conversion of the first two numbers (ones and tens). As I said earlier, I eleminate case by case! We’ll be constanly using the varibale double

First case: what if double was a number between 0 and 16, I can instanly return that!

Second case: as I said in the original post in rules that we must respect, 90, 80 and 81 are all special cases, that we can just take care of on their own

Third case: numbers between 91 and 99, which I’ll leave on their own aswell because they are different from others

Fifth case: normal conversion, where there is nothing special

Additional situations that we need to take care of: -if ones was anything bigger than 0, but tens was 0, we would need to take care of that.
-if both ones and tens were 0, I just need to ignore without any conversion
-numbers where tens is > 1, and ones was 1, we would need to add a " et un"
-numbers where tens is > 1, and ones was 0, basically 20, 30, 40 all the way to 80

So as you can see! A LOT to take care of, which is why this challenge is hard, so let’s get to it.

local ones, tens, hundreds = tonumber(string.sub(part,1,1)), tonumber(string.sub(part,2,2)), tonumber(string.sub(part,3,3))
local double = tostring(tens)..tostring(ones)
local second = ""
local prefix = "-"


if double ~= "00" and tens ~= nil then --this is the 2nd additional case, we pass when both ones and tens are not zero, and tens exists, we just move on
	if invariable[tonumber(double)+1] then --that's the first case done!
		second = invariable[tonumber(double)+1]
	elseif double == "90" then --these three down below
		second = "quatre-vingt-dix"
	elseif tens == 9 then --this is the case when we're dealing with 90-99
		--we'll do this later!
	elseif double == "81" then
		second = "quatre-vingt-un"
	elseif double == "80" then --are the third case
		second = "quatre-vingts"
	elseif ones == 1 then --this is the third additional case where if ones is == 1
		second = dozens[tens].." et un"
	elseif ones == 0 then --this is the fourth additional case where if ones is == 0
		second = dozens[tens]
	else --this is the normal case, just normal conversion
		--we'll do this later as well!			
	end
elseif ones ~= 0 and tens == nil then --this is the 1st additional case where we got a 1 digit number, meaning ones is anything but 0, and tens is 0
	second = invariable[ones+1]
end

That’s almost everything covered! The conversion is very self-explainable, which I also explained earlier. Like dozens[tens] for example, or invariable[ones+1].

Now, let’s take care of the rest, I’m gonna start with the normal conversion, which is really straight forward

second = dozens[tens]..prefix..invariable[ones+1]

Find the word for the dozens (vingt, trente, quarante…), add "-" to it, find the word for the number between 1 and 9.

But for 91-99 that’s something else! Because numbers from 91 to 99 are written in the form: 80+11, 80+12 … 80+19, quatre-vingt-onze, quatre-vingt-douzequatre-vingt-dix-neuf. We have to take care of two seperate things: from 91 to 96 they have their own rule, and from 97 to 99 have their own as well, because if you remember, 1 to 16 are invariable words, that can’t be changed, but 17 to 19 have a rule.

second = "quatre-vingt"..(invariable[tonumber(double)-80+1] or dozens[1]..prefix..invariable[ones+1])

I agree! A rather weird notation. invariable[tonumber(double)-80+1] is for the numbers from 1 to 16, dozens[1]..prefix..invariable[ones+1] is for 17 to 19. First, I am doing tonumber(double)-80 because the invariable number is from 1 to 16, what if I get something like 96? 96 - 80= 16 to get the part I want. Then I add one to that again, like I explained in the beginning. Second, dozens[1]..prefix..invariable[ones+1] is very easy to explain, I always do dozens[1] because that gives back dix, and we always need dix (quatre-vingt-dix-sept to dix-neuf). Then we add a prefix, then we add invariable[ones+1], because from 97 and 98 and 99, ones would be either 7 or 8 or 9, which we need to look for. And that’s it! Crazy right? We got this now.

if double ~= "00" and tens ~= nil then
	if invariable[tonumber(double)+1] then 
		second = invariable[tonumber(double)+1]
	elseif double == "90" then
		second = "quatre-vingt-dix"
	elseif double == "81" then
		second = "quatre-vingt-un"
	elseif double == "80" then
		second = "quatre-vingts"
	elseif ones == 1 then
		second = dozens[tens].." et un"
	elseif ones == 0 then
		second = dozens[tens]
	elseif tens == 9 then
		second = "quatre-vingt-"..(invariable[tonumber(double)-80+1] or dozens[1]..prefix..invariable[ones+1])
	else
		second = dozens[tens]..prefix..invariable[ones+1]		
	end
elseif tens == nil and ones ~= 0 then
	second = invariable[ones+1]
end

Now! For the hundreds part, which is easy.
Here, there are three cases:

First case: if hundreds didn’t exits, or was 0

Second case: the normal case, where hundreds is > 1

Third case: if hundreds is equal to 1

if hundreds == nil or hundreds == 0 then --first case, where there are no hundreds so we don't add hundreds ("cent")	
	result = result..second..space..multiples[i]..space
else --cases where there are hundreds
	if hundreds > 1 then  --case if hundreds is bigger than one, so we add a number before "cent"
		result = result..invariable[hundreds+1].." cent "..second..space..multiples[i]..space
	else --case where hundreds is 1, so we don't add a number before "cent"
		result = result.."cent "..second..space..multiples[i]..space
	end
end

And that’s it! Notice that I’m putting spaces in the correct placement, and putting second (previous part of converting ones and tens) in the correct place as well, you gotta think about that. And also! We’re using multiples for the first time, which I explained at first, where each part gets its corresponding suffix (“mille”, “million”, “milliard”) according to its index, i. And that’s it! We’re actually done!

And here is the final beautiful code

local invariable = {"zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix", "onze", "douze",
"treize", "quatorze", "quinze", "seize"}
local dozens = {"dix", "vingt", "trente", "quarante", "cinquante", "soixante", "soixante-dix", "quatre-vingt"}
local multiples = {"", "mille", "million", "milliard"}

function toFrench(num)
	local n = tostring(num)
	local parts = {}
	local space = " "
	local result = ""
	

	if invariable[num+1] then return invariable[num+1] end
	
	for part in string.gmatch(string.reverse(n),".?.?.?") do
    		table.insert(parts, part)
	end
	
	for i = #parts, 1, -1 do
		local part = parts[i]
		if part ~= "" then
			local ones, tens, hundreds = tonumber(string.sub(part,1,1)), tonumber(string.sub(part,2,2)), tonumber(string.sub(part,3,3))
			local double = tostring(tens)..tostring(ones)
			local second = ""
			local prefix = "-"
			
			if double ~= "00" and tens ~= nil then
				if invariable[tonumber(double)+1] then 
					second = invariable[tonumber(double)+1]
				elseif double == "90" then
					second = "quatre-vingt-dix"
				elseif tens == 9 then
					second = "quatre-vingt-"..(invariable[tonumber(double)-80+1] or dozens[1]..prefix..invariable[ones+1])
				elseif tens == 7 then
					second = "soixante-"..(invariable[tonumber(double)-60+1] or dozens[1]..prefix..invariable[ones+1])
				elseif double == "81" then
					second = "quatre-vingt-un"
				elseif double == "80" then
					second = "quatre-vingts"
				elseif ones == 1 then
					second = dozens[tens].." et un"
				elseif ones == 0 then
					second = dozens[tens]
				else
					second = dozens[tens]..prefix..invariable[ones+1]		
				end
			elseif tens == nil and ones ~= 0 then
				 second = invariable[ones+1]
			end
							
			if hundreds == nil or hundreds == 0 then
			
				result = result..second..space..multiples[i]..space
			else
				if hundreds > 1 then
					result = result..invariable[hundreds+1].." cent "..second..space..multiples[i]..space
				else
					result = result.."cent "..second..space..multiples[i]..space
				end
			end
		end
	end
	
	return result --return that bad boy
end

AND FINALLY! We can watch our code kicking butts!

while true do
	wait()
	local x = math.random(1, 999999999)
	print(x, toFrench(x))
end

codingchallenge1-5

And just in case this challenge annoyed you brutally, here is a funny video revenging you, by making fun of french numbers.

44 Likes

[Submitted @hexwulf]

You can be sneaky! And use some Http magic instead.

local HttpService = game:GetService("HttpService")

local URL_Config = {
  BaseURL = 'https://www.google.com/search';
  SessionKey = 'sZV_XsmCCNPvgAbQubqIAw&q';
}

local ConvertToFrenchNumber = function(InputString)
   local FormedURL = ("%s?ei=%s=google+translate+english+%s+to+french"):format(URL_Config.BaseURL, URL_Config.SessionKey, InputString)
   local HtmlBodyContent = HttpService:GetAsync(FormedURL)
   return HtmlBodyContent:match([["BNeawe iBp4i AP7Wnd">(.-)</div>]]):gsub('<div><div class="BNeawe iBp4i AP7Wnd">', '')
end

print(ConvertToFrenchNumber(1))
12 Likes

Thank you this is really great for practise, please keep continuing this series!

3 Likes

Thank you! I will. I’m not expecting a lot of submits honestly, especially for this challenge, which was deffintely too hard. I will try to make them more simple and straight forward, so more people can actually participate.

1 Like

I did this a bit differently, using a dictionary and about 10 different special cases:

Code
-- to be run as a modulescript

local dict = {
	[1] = "un",
	[2] = "deux",
	[3] = "trois",
	[4] = "quatre", 
	[5] = "cinq",
	[6] = "six", 
	[7] = "sept", 
	[8] = "huit", 
	[9] = "neuf", 
	[10] = "dix", 
	[11] = "onze", 
	[12] = "douze", 
	[13] = "treize",
	[14] = "quatorze",
	[15] = "quinze",
	[16] = "seize",
	[17] = "dix-sept",
	[18] = "dix-huit",
	[19] = "dix-neuf",
	-- 17 thru 19 are just 10 + 7 but they're special-cased so it doesn't try 16 + 1
	[20] = "vingt",
	[30] = "trente",
	[40] = "quarante",
	[50] = "cinquante",
	[60] = "soixante",
	[80] = "quatre-vingt",
	[100] = "cent",
	[1000] = {"un mille", "mille"},
	[1e6] = {"un million", "millions"},
	[1e9] = {"un milliard", "milliards"},
	-- not sure why trillion is un billion but i'm just gonna go with it 
	-- source: http://www.french-linguistics.co.uk/tutorials/numbers/
	[1e12] = {"un billion", "billions"},
	[1e15] = {"un billiard", "billiards"},
	[1e18] = {"un trillion", "trillions"},
	[1e21] = {"un trilliard", "trilliards"}, -- this is just a guess tbh
}

-- loop backwards through a numeric-keyed table (not strictly arrays)

local function reverseIPairs(t)
	local indexes = {}
	
	-- get indexes and sort them largest -> smallest
	for index, _ in pairs(t) do
		indexes[#indexes + 1] = index
	end
	
	local i = 0
	
	table.sort(indexes, function(index1, index2)
		return index1 > index2
	end)
	
	-- return a func that grabs from the indexes table to get key+value
	return function()
		i = i + 1
		
		if i <= #indexes then
			return indexes[i], t[indexes[i]]
		end
	end
end

-- take a whole number* and give its written out french equivalent
local function toFrench(original)
	assert(
		
		type(original) == "number" and -- it's a number and
		original >= 0 and  -- it's over 0 and
		original < 1e24 and  -- it's under a septillion and
		0 == select(2, math.modf(original)), -- it's an integer
		
		"`num` must be a valid integer between 0 and 1 septillion"
	)
	local final = ""
	local num = original

	-- loop backwards (start at 1e21, go back)
	for i, v in reverseIPairs(dict) do
		local j = math.floor(num / i)
		local prefix = ""

		if num >= i then
			-- if it's like 200 or whatever, we want to stick the prefix (2) in front of the thing in table (100)
			if j > 1 then
				prefix = toFrench(j) .. " "
			end

			-- every "x and y or z" is a different special case

			if type(dict[i]) == "string" then
				-- oh boy special casing
				if num == 1 and final:sub(#final) == "-" then
					final = final:match("quatre%-vingt%-$") and final .. "un-" or final:sub(1, #final - 1) .. " et un-"
					break
				elseif num == 11 and final:sub(#final) == "-" then
					final = final:match("quatre%-vingt%-$") and final .. "onze-" or final:sub(1, #final - 1) .. " et onze-"
					break
				end
				-- oh boy MORE special casing
				final = final..prefix..dict[i].. (num==80 and"s-"or(num>=100 and" "or"-"))
				num = num - i * j
			else
				-- oh boy EVEN MORE special casing (for 1 mil+)
				final = final..prefix..dict[i][prefix==""and 1 or 2]..(num >= 100 and" "or"-")
				num = num - i * j
			end
		elseif 1 > num then
			break
		end
	end

	final = final:sub(1, #final - 1) -- cut off the last " " or "-"

	return original > 0 and final or "zéro"
end

return toFrench

After some benchmarking, it looks like yours is much faster (about 9x faster), but I’m still fine with my solution the way it is.

Edit: It looks like the Google Translate method doesn’t always work too well:
image

(Another edit to fix all numbers ending in one, as well as 1000-1999 being “thousand” instead of “a thousand”.)

6 Likes

Very good! I like the approach you took. What you did is really great, and you respected almost all the proposed the rules. As I can see you have experienced the pain of special cases hehe

3 Likes

What did I miss? I thought I respected them all.

Edit: Nevermind, I found a few bugs. Fixing them now.

It looks like we both got 71-76 wrong? I found a site saying it’s “soixante et onze”, whereas you had “soixante-dix et un” and I had “soixante-onze”. I’m not really sure which is right (you or the site, I know I’m wrong), but I’m going with what the site’s got since that’s what Google Translate says as well.

4 Likes

Oh yeah, nice noticing that. I did A LOT of testing after finishing this, I guess this one slipped through.
The thing is, 71 to 76 are all special cases, just like 91 to 99. I forgot about that, and just carried on using the normal rule of turning 7 to “soixante-dix” and 1 to “un” then connectin them with a “-”. And after exploring your code for a while, it seems that you have this covered, but only got “71” wrong, to make things easy just add to the dictionarry the number “71”, so you can instanly give it back as a result.

But for my code, a simple fix would be to check if the number was a seventy and something (between 71 and 79), and make the same thing I did for ninty and something (between 91 and 99), but instead of doing invariable[tonumber(double)-80+1] I do invariable[tonumber(double)-60+1].

At the moment it’s very late for me, gonna take some rest, but I’ll promise to fix this tomorrow, and also take more time to see how your code functions and test it to find anything wrong!

1 Like

My code’s a bit of a mess, so I’ll try to clean it up in the meantime. Your code was a lot cleaner, so it should be easier for you to add a fix. I look forward to seeing more of these challenges, though, this was fun!

3 Likes

I thought this was a cool challenge idea so I tried creating my own module code for a different language than French and it turned out pretty well. It is much easier though so it probably doesn’t make up for much of a challenge against this.

1 Like

I’m really happy that you thought it was fun! Didn’t get a lot of submits, but your serious contribution deffintely fulfilled my goal, thanks! More challenges are comin’ in, but the wait time might be long due to the current back load post approval team is having.

Sooo! I did more testing, and I examined your code. I managed to fix the 71 to 76 problem in my code, I’m in the process of adding it to the answer.

After running through a lot of numbers, my results and yours are identical! You respected all the rules, in fact you even supported higher numbers going past a billion (and yes it bugged me as well how a billion is “un milliard” and a trillion is “un billion”).

I like your code because it was different from mine, you used other techniques and ideas that I didn’t use. You used some smart string manipulation that I didn’t do. Overall it’s very good! Good job

1 Like

I’m glad you liked it! Don’t be shy to sumbit the module’s code, I’d be happy to anything inspired or related to this challenge.