I did a second version for my original question:
The original question
The reason I ask this question is I was trying to answer a question from a different user: Figure/number to text converter javascript implementation I found it to be too complex so I ended up completely rewriting it. Then it was already answered in a better way. So I thought to see this as an exercise and ask the question myself.
Figure/number to text converter Javascript implementation: my take in Dutch: v1
I read this question Figure/number to text converter javascript implementation. Like the answer said, that version has two main issues. I tried to solve them.
The main issues with the original:
- The assumption you can switch language by just providing the words for the main numbers is not correct, since numbers are built differently in different languages. My solution: I have one function per language and some global helper functions.
- It uses too many design patterns, especially fluent APIs. My solution: It consists of function calls with an inner helper function for the hundreds and some outer helper functions.
Please give some feedback on the programming itself. You don't have necessarily have to correct my Dutch ;-)
Note: I only support positive integers. It's a linguistic question, but if someone knows: 2.24 can be pronounced as twee komma vierentwintig (two point twenty four), but how do you pronounce decimal fractions starting with zero like 2.024. Is it twee komma nul vierentwintig (two point zero twenty four) or twee komma nul twee vier (two point zero two four)?
The second version
I took into account the answer of J_H. I used strings to represent the numbers now. It works for huge numbers.
I did not pull the transcribe sub-functions (transcribe_xxx
,...) to the global scope, because it seems clearer to me they belong in the scope for Dutch transcription. I tried to use an object with the sub-functions as methods for Dutch. I didn't think the approach was easier to read, because it isn't clear those sub-methods are just used inside transcribe
. I suppose it's a question of being used to this approach in more object-oriented languages.
A question I did have is the naming of variables: I use a lot of similar variable names. e.g. digit_x00
stands for the digit with weight 100, number_xx
stands for a two digit number,... It was easier to write for me then digit_hundreds
or number_last_2_digits
.
One thing I didn't do is validation. The input is required to be a number in the simplest format: no spaces, commas, trailing zeros,... I don't know if it would be necessary. My idea is: when the function is used, it would probably be used with a parameter that comes from number.toString()
, so it would be in the right format. (Now I write this: maybe I could write the global transcribe function to have both types as a parameter and have only the string one be validated.)
const NL = {
code: 'nl',
transcribe(number) {
const NUMBER_0 = 'nul'
const NUMBER_10 = 'tien'
const NUMBERS_FROM_0_TO_9 = [NUMBER_0, 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen']
const NUMBERS_FROM_10_TO_19 = [NUMBER_10, 'elf', 'twaalf', 'dertien', 'veertien', 'vijftien', 'zestien', 'zeventien', 'achttien', 'negentien']
const MULTIPLES_OF_10 = [undefined, NUMBER_10, 'twintig', 'dertig', 'veertig', 'vijftig', 'zestig', 'zeventig', 'tachtig', 'negentig']
const NUMBER_100 = 'honderd'
const NUMBER_1000 = 'duizend'
const POWERS_OF_1000 = [undefined, NUMBER_1000, 'miljoen', 'miljard', 'biljoen', 'biljard', 'triljoen', 'triljard', 'quadriljoen', 'quadriljard', 'quintiljoen', 'quintiljard']
let [num_sign, str_whole, str_fraction] = split_number_in_components(number)
let text_whole = transcribe_whole_number(str_whole)
let text_fraction = transcribe_fraction(str_fraction)
let text_numeric = join_defined_strings(text_whole, ' komma ', text_fraction)
let text_full = text_numeric
if (num_sign === -1) {
text_full = 'min ' + text_full
}
return text_full
function transcribe_whole_number(str_whole) {
if (str_whole === '0') {
return NUMBER_0
}
let text_full = []
let array_str_3_digit_group = [...split_in_digit_groups(str_whole, 3)]
for (let exponent_of_1000 = array_str_3_digit_group.length - 1; exponent_of_1000 >= 0; --exponent_of_1000) {
let str_xxx = array_str_3_digit_group[exponent_of_1000]
let num_xxx = num(str_xxx)
let [digit_x, digit_x0, digit_x00] = split_in_digits(str_xxx, 3)
// transcribe 3 digit group
let text_xxx = transcribe_3_digit_group(str_xxx)
if (text_xxx === undefined) continue
let text_power_of_1000 = POWERS_OF_1000[exponent_of_1000]
let text_xxx_power_of_1000
if (exponent_of_1000 === 0) {
text_xxx_power_of_1000 = text_xxx
} else if (exponent_of_1000 === 1) {
if (num_xxx === 1) {
text_xxx_power_of_1000 = NUMBER_1000
} else {
text_xxx_power_of_1000 = text_xxx + text_power_of_1000
}
} else {
text_xxx_power_of_1000 = text_xxx + ' ' + text_power_of_1000
}
text_full.push(text_xxx_power_of_1000)
}
return text_full.join(' ')
}
function transcribe_fraction(str_fraction) {
if (str_fraction === undefined) {
return undefined
}
let arr_text_fraction = []
for (let str_digit of str_fraction) {
let digit = num(str_digit)
let text_digit = NUMBERS_FROM_0_TO_9[digit]
arr_text_fraction.push(text_digit)
}
return arr_text_fraction.join(' ')
}
function transcribe_3_digit_group(str_xxx) {
let num_xxx = num(str_xxx)
let [digit_x, digit_x0, digit_x00] = split_in_digits(str_xxx, 3)
// Assemble 00-99
let text_xx
let num_xx = num_xxx % 100
if (num_xx === 0) {
text_xx = undefined
} else if ((num_xx % 10) === 0) { // pure multiple of 10
text_xx = MULTIPLES_OF_10[digit_x0]
} else if (num_xx < 10) { // between 1 and 9
text_xx = NUMBERS_FROM_0_TO_9[digit_x]
} else if (num_xx < 20) { // between 11 and 19
text_xx = NUMBERS_FROM_10_TO_19[digit_x]
} else { // non-pure multiple of 10 between 21 and 99
let text_x = NUMBERS_FROM_0_TO_9[digit_x]
let text_x0 = MULTIPLES_OF_10[digit_x0]
let last_letter_text_digit_x = substring_from_end(text_x, 1)
let text_and = (/[aeiou]/gi).test(last_letter_text_digit_x) ? 'ën' : 'en'
text_xx = text_x + text_and + text_x0 // e.g. eenentwintig, tweeëntwintig
}
// Assemble hundreds
let text_x00
if (num_xxx < 100) {
text_x00 = undefined
} else if (100 <= num_xxx && num_xxx < 200) {
text_x00 = NUMBER_100 // honderd
} else {
text_x00 = NUMBERS_FROM_0_TO_9[digit_x00] + NUMBER_100 // e.g. tweehonderd, driehonderd
}
// Combine 00-99 and hundreds
let text_xxx
if (text_x00 === undefined) {
text_xxx = text_xx
} else if (text_xx === undefined) {
text_xxx = text_x00
} else if (num_xx <= 12) {
text_xxx = text_x00 + 'en' + text_xx
} else {
text_xxx = text_x00 + text_xx
}
return text_xxx
}
}
}
const EN = {
code: 'en',
transcribe(number) {
throw new Error("Not implemented")
}
}
// Helper functions
/**
* Returns the sign (1/-1), whole number part (as string) and fraction part (as string) of a number (as string)
* @param {string} str_number
* @returns {[number, string, string]} [sign (=1/-1), whole_number_str, fraction_str]
*/
function split_number_in_components(str_number) {
let sign_num = str_number[0] === '-' ? -1 : 1
let numeric_component_str = sign_num === -1 ? str_number.slice(1) : str_number
let [whole_number_str, fraction_str] = numeric_component_str.split('.')
return [sign_num, whole_number_str, fraction_str]
}
/**
* Take a substring of the last length characters
* @param {string} str
* @param {number} length
* @returns
*/
function substring_from_end(str, length) {
return str.substring(str.length - length)
}
/**
* Shorthand for Number.parseInt
* @param {string} str_number
*/
function num(str_number) {
return Number.parseInt(str_number)
}
/**
* Split a number (as string) into its digits.
* The results go from the least to the most significant digit.
* This makes the indices correspond to the exponents of 10.
* e.g. split_in_digits("123456", 0) -> [6, 5, 4, 3, 2, 1]
* @param {string} number_str a positive integer as string
* @param {number} pad_with_zeros
* @returns {number[]}
*/
function split_in_digits(number_str, pad_with_zeros = 0) {
return Array.from(number_str.padStart(pad_with_zeros, '0')).map(num).reverse()
}
/**
* Split a number (as string) into groups of digits of a given size
* The results go from the least to the most significant group.
* This makes the indices correspond to the exponents of 10**group_size.
* e.g. split_in_digit_groups("1234567890", 3) -> ["890", "567", "234", "1"]
* @param {string} number_str a positive integer as string
* @param {number} group_size the number of digits per group
*/
function * split_in_digit_groups(number_str, group_size) {
while (true) {
let split_index = number_str.length - group_size
let digit_group_str = number_str.substring(split_index)
number_str = number_str.substring(0, split_index)
yield digit_group_str
if (number_str === '') break
}
}
/**
* Returns the result of the join of two strings if both are defined, returns the defined string if only one is defined, returns undefined if both are undefined
* @param {string|undefined} text1
* @param {string|undefined} text2
* @param {string|((t1: string, t2: string) => string|undefined)} join
* @returns {string|undefined}
*/
function join_defined_strings(text1, join, text2) {
if (text1 === undefined && text2 === undefined) {
return undefined
} if (text1 === undefined) {
return text2
} else if (text2 === undefined) {
return text1
} else {
return text1 + join + text2
}
}
let Languages = (function () {
let languages = {}
for (let language of [NL, EN]) {
languages[language.code] = language
}
return languages
})();
/**
* Transcribes a number into text in a given language
* @param {string} string_number
* @param {string} language
*/
function transcribe(string_number, language) {
if (Languages[language]) {
return Languages[language].transcribe(string_number);
} else {
throw new Error("Language not supported" + language);
}
}