diff --git a/src/Converter.ts b/src/Converter.ts index 37d13ce..62d4cce 100644 --- a/src/Converter.ts +++ b/src/Converter.ts @@ -5,41 +5,88 @@ import { ReadStreamOptions, ConvertOptions, NumberSystem, Digit } from './common export default class Converter { constructor(private readonly system: NumberSystem, private options: ConvertOptions) {} - public convert = (value: string | number | bigint): string => { + private convertString = (value: string): string => { let converted: string[] = [] - const valueStr = value as string - switch (typeof value!) { - case 'string': - // TODO bail out when exponent is too large! - try { - return this.convert(BigInt(new Big(valueStr).toFixed())) - } catch (e) {} - return this.convert(BigInt(valueStr)) - case 'number': - return this.convert(BigInt(value as number)) - case 'bigint': - let current = value as bigint - let thousandPower = 0n - while (current > 0n) { - const hundreds = Number(((current % 1000n) / 100n) % 10n) as Digit - const tens = Number(((current % 100n) / 10n) % 10n) as Digit - const ones = Number(current % 10n) as Digit + try { + const big = new Big(value) + const { e: exponent, c: coefficients } = big + const isExponentSmall = exponent < 1000 + if (isExponentSmall) { + return this.convert(BigInt(big.toFixed())) + } + let currentPlace = exponent % 3 + let thousandPower = BigInt(exponent) / 3n + coefficients.reduce( + ([place, hundredsRaw = 0, tensRaw = 0, onesRaw = 0], c) => { + let thePlace = (place + 3) % 3 + const nextPlace = (place + 3 - 1) % 3 + + switch (thePlace) { + case 2: + return [nextPlace, c, tensRaw, onesRaw] + case 1: + return [nextPlace, hundredsRaw, c, onesRaw] + default: + case 0: + break + } let kiloName: string | undefined let kiloCount: string + const hundreds = hundredsRaw as Digit + const tens = tensRaw as Digit + const ones = c as Digit if (!(ones === 0 && tens === 0 && hundreds === 0)) { if (thousandPower > 0n) { const { scale = 'short' } = this.options kiloName = this.system.getKiloName[scale!]!(thousandPower) } kiloCount = this.system.getKiloCount(hundreds, tens, ones) - converted.unshift(this.system.joinKilo(kiloCount, kiloName)) + converted.push(this.system.joinKilo(kiloCount, kiloName)) } + thousandPower = thousandPower - 1n + return [nextPlace, 0, 0, 0] + }, + [currentPlace], + ) + } catch {} + return converted.join(' ') + } + + private convertBigInt = (value: bigint): string => { + let converted: string[] = [] + let current = value as bigint + let thousandPower = 0n + while (current > 0n) { + const hundreds = Number(((current % 1000n) / 100n) % 10n) as Digit + const tens = Number(((current % 100n) / 10n) % 10n) as Digit + const ones = Number(current % 10n) as Digit - thousandPower = thousandPower + 1n - current = current / 1000n + let kiloName: string | undefined + let kiloCount: string + if (!(ones === 0 && tens === 0 && hundreds === 0)) { + if (thousandPower > 0n) { + const { scale = 'short' } = this.options + kiloName = this.system.getKiloName[scale!]!(thousandPower) } - return converted.join(' ') + kiloCount = this.system.getKiloCount(hundreds, tens, ones) + converted.unshift(this.system.joinKilo(kiloCount, kiloName)) + } + + thousandPower = thousandPower + 1n + current = current / 1000n + } + return converted.join(' ') + } + + public convert = (value: string | number | bigint): string => { + switch (typeof value!) { + case 'string': + return this.convertString(value as string) + case 'number': + return this.convertString(value.toString(10)) + case 'bigint': + return this.convertBigInt(value as bigint) default: break } diff --git a/src/systems/de/base/kilo/combiningPrefix.ts b/src/systems/de/base/kilo/combiningPrefix.ts index f724c81..be3f4cb 100644 --- a/src/systems/de/base/kilo/combiningPrefix.ts +++ b/src/systems/de/base/kilo/combiningPrefix.ts @@ -67,7 +67,7 @@ const combiningPrefix: CombiningPrefix = (kiloHundreds, kiloTens, kiloOnes, kilo if ( (currentKiloHundreds === 0n && kiloTensNumber > 1) || (currentKiloHundreds > 0n && kiloTensNumber !== 1) || - kiloHundreds >= 10 + (kiloHundreds >= 10n && kiloTensNumber !== 1) ) { prefix += 't' } diff --git a/src/systems/en/base/kilo/combiningPrefix.ts b/src/systems/en/base/kilo/combiningPrefix.ts index f724c81..ba0fe15 100644 --- a/src/systems/en/base/kilo/combiningPrefix.ts +++ b/src/systems/en/base/kilo/combiningPrefix.ts @@ -67,10 +67,11 @@ const combiningPrefix: CombiningPrefix = (kiloHundreds, kiloTens, kiloOnes, kilo if ( (currentKiloHundreds === 0n && kiloTensNumber > 1) || (currentKiloHundreds > 0n && kiloTensNumber !== 1) || - kiloHundreds >= 10 + (kiloHundreds >= 10n && kiloTensNumber !== 1) ) { prefix += 't' } + return prefix + 'i' } diff --git a/src/systems/tl/base/kilo/combiningPrefix.ts b/src/systems/tl/base/kilo/combiningPrefix.ts index f724c81..be3f4cb 100644 --- a/src/systems/tl/base/kilo/combiningPrefix.ts +++ b/src/systems/tl/base/kilo/combiningPrefix.ts @@ -67,7 +67,7 @@ const combiningPrefix: CombiningPrefix = (kiloHundreds, kiloTens, kiloOnes, kilo if ( (currentKiloHundreds === 0n && kiloTensNumber > 1) || (currentKiloHundreds > 0n && kiloTensNumber !== 1) || - kiloHundreds >= 10 + (kiloHundreds >= 10n && kiloTensNumber !== 1) ) { prefix += 't' } diff --git a/tests/Converter.test.ts b/tests/Converter.test.ts index e416427..83ca858 100644 --- a/tests/Converter.test.ts +++ b/tests/Converter.test.ts @@ -8,7 +8,7 @@ describe.each` ${'en'} | ${'English'} | ${English} | ${'short'} ${'en'} | ${'English'} | ${English} | ${'long'} ${'en'} | ${'English'} | ${English} | ${'europeanLong'} -`('$name ($scale count)', ({ locale, system, scaleRaw, }) => { +`('$name ($scaleRaw count)', ({ locale, system, scaleRaw, }) => { const scale = scaleRaw as Scale let converter: Converter @@ -17,18 +17,62 @@ describe.each` }) describe('on values', () => { + const expected = [ + { + value: 1000000, + conversions: { + short: 'one million', + long: 'one million', + europeanLong: 'one million', + }, + }, + { + value: 123456789, + conversions: { + short: 'one hundred twenty three million four hundred fifty six thousand seven hundred eighty nine', + long: 'one hundred twenty three million four hundred fifty six thousand seven hundred eighty nine', + europeanLong: 'one hundred twenty three million four hundred fifty six thousand seven hundred eighty nine', + }, + }, + { + value: '1e+6', + conversions: { + short: 'one million', + long: 'one million', + europeanLong: 'one million', + }, + }, + { + value: '123e+3000003', + conversions: { + short: 'one hundred twenty three milliamilliatillion', + long: 'one hundred twenty three thousand quingenmilliatillion', + europeanLong: 'one hundred twenty three quingenmilliatilliard', + }, + }, + { + value: '123e+3003003', + conversions: { + short: 'one hundred twenty three milliamilliaunmilliatillion', + long: 'one hundred twenty three thousand quingenmilliaquingentillion', + europeanLong: 'one hundred twenty three quingenmilliaquingentilliard', + }, + }, + ] test.each` - value | name - ${1000000} | ${'one million'} - ${123456789} | ${'one hundred twenty three million four hundred fifty six thousand seven hundred eighty nine'} - ${'1e+6'} | ${'one million'} - `('should parse $value as $name', ({ value, name, }) => { - expect(converter.convert(value)).toBe(name) + value | conversions + ${expected[0].value} | ${expected[0].conversions} + ${expected[1].value} | ${expected[1].conversions} + ${expected[2].value} | ${expected[2].conversions} + ${expected[3].value} | ${expected[3].conversions} + ${expected[4].value} | ${expected[4].conversions} + `('should parse $value', ({ value, conversions, }) => { + expect(converter.convert(value)).toBe(conversions[scale]) }) }) describe('on streams', () => { - const ENCODING = 'utf-8' + const ENCODING = 'utf8' const expected: Record> = { 'million': { @@ -71,6 +115,56 @@ describe.each` long: 'one hundred twenty three thousand milliasextrigintillion four hundred fifty six milliasextrigintillion seven hundred eighty nine thousand milliaquintrigintillion two hundred forty six milliaquintrigintillion eight hundred one thousand milliaquattuortrigintillion three hundred fifty seven milliaquattuortrigintillion', europeanLong: 'one hundred twenty three milliasextrigintilliard four hundred fifty six milliasextrigintillion seven hundred eighty nine milliaquintrigintilliard two hundred forty six milliaquintrigintillion eight hundred one milliaquattuortrigintilliard three hundred fifty seven milliaquattuortrigintillion', }, + 'example8': { + short: 'one hundred twenty three nongenseptenoctoginmilliaduoseptuagintillion four hundred fifty six nongenseptenoctoginmilliaunseptuagintillion seven hundred eighty nine nongenseptenoctoginmilliaseptuagintillion two hundred forty six nongenseptenoctoginmillianovemsexagintillion eight hundred one nongenseptenoctoginmilliaoctosexagintillion three hundred fifty seven nongenseptenoctoginmilliaseptensexagintillion', + long: 'one hundred twenty three thousand quadringentrenonaginmilliaquingensextrigintillion four hundred fifty six quadringentrenonaginmilliaquingensextrigintillion seven hundred eighty nine thousand quadringentrenonaginmilliaquingenquintrigintillion two hundred forty six quadringentrenonaginmilliaquingenquintrigintillion eight hundred one thousand quadringentrenonaginmilliaquingenquattuortrigintillion three hundred fifty seven quadringentrenonaginmilliaquingenquattuortrigintillion', + europeanLong: 'one hundred twenty three quadringentrenonaginmilliaquingensextrigintilliard four hundred fifty six quadringentrenonaginmilliaquingensextrigintillion seven hundred eighty nine quadringentrenonaginmilliaquingenquintrigintilliard two hundred forty six quadringentrenonaginmilliaquingenquintrigintillion eight hundred one quadringentrenonaginmilliaquingenquattuortrigintilliard three hundred fifty seven quadringentrenonaginmilliaquingenquattuortrigintillion', + }, + 'example9': { + short: 'one hundred twenty three nongenseptenoctoginmilliamilliamilliaduoseptuagintillion four hundred fifty six nongenseptenoctoginmilliamilliamilliaunseptuagintillion seven hundred eighty nine nongenseptenoctoginmilliamilliamilliaseptuagintillion two hundred forty six nongenseptenoctoginmilliamilliamillianovemsexagintillion eight hundred one nongenseptenoctoginmilliamilliamilliaoctosexagintillion three hundred fifty seven nongenseptenoctoginmilliamilliamilliaseptensexagintillion', + long: 'one hundred twenty three thousand quadringentrenonaginmilliamilliamilliaquingenmilliamilliasextrigintillion four hundred fifty six quadringentrenonaginmilliamilliamilliaquingenmilliamilliasextrigintillion seven hundred eighty nine thousand quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquintrigintillion two hundred forty six quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquintrigintillion eight hundred one thousand quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquattuortrigintillion three hundred fifty seven quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquattuortrigintillion', + europeanLong: 'one hundred twenty three quadringentrenonaginmilliamilliamilliaquingenmilliamilliasextrigintilliard four hundred fifty six quadringentrenonaginmilliamilliamilliaquingenmilliamilliasextrigintillion seven hundred eighty nine quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquintrigintilliard two hundred forty six quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquintrigintillion eight hundred one quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquattuortrigintilliard three hundred fifty seven quadringentrenonaginmilliamilliamilliaquingenmilliamilliaquattuortrigintillion', + }, + 'example10': { + short: 'one milliatillion', + long: 'one thousand quingentillion', + europeanLong: 'one quingentilliard', + }, + 'example11': { + short: 'one duotriginmilliasescenquattuorseptuagintillion', + long: 'one thousand sexdecmilliatrecenseptentrigintillion', + europeanLong: 'one sexdecmilliatrecenseptentrigintilliard', + }, + 'example12': { + short: 'one milliamilliatillion', + long: 'one thousand quingenmilliatillion', + europeanLong: 'one quingenmilliatilliard', + }, + 'example13': { + short: 'one octingensexquinquaginmilliamilliaseptingensexseptuaginmilliaquingenundecillion', + long: 'one quadringenoctoviginmilliamilliatrecenoctooctoginmilliaduocensexquinquagintillion', + europeanLong: 'one quadringenoctoviginmilliamilliatrecenoctooctoginmilliaduocensexquinquagintillion', + }, + 'example14': { + short: 'one milliamilliamilliatillion', + long: 'one thousand quingenmilliamilliatillion', + europeanLong: 'one quingenmilliamilliatilliard', + }, + 'example15': { + short: 'one trecenunnonaginmilliamilliamilliaquingenunoctoginmilliamilliaduocensexdecmilliacenduononagintillion', + long: 'one thousand cenquinnonaginmilliamilliamilliaseptingennonaginmilliamilliasescenoctomilliasexnonagintillion', + europeanLong: 'one cenquinnonaginmilliamilliamilliaseptingennonaginmilliamilliasescenoctomilliasexnonagintilliard', + }, + 'example16': { + short: 'one milliamilliamilliamilliatillion', + long: 'one thousand quingenmilliamilliamilliatillion', + europeanLong: 'one quingenmilliamilliamilliatilliard', + }, + 'example17': { + short: 'one duocenduotriginmilliamilliamilliamilliaduononaginmilliamilliamilliacenseptuaginmilliamilliacennovemdecmillianongensextrigintillion', + long: 'one thousand censexdecmilliamilliamilliamilliasexquadraginmilliamilliamilliaquinoctoginmilliamillianovemquinquaginmillianongenoctosexagintillion', + europeanLong: 'one censexdecmilliamilliamilliamilliasexquadraginmilliamilliamilliaquinoctoginmilliamillianovemquinquaginmillianongenoctosexagintilliard', + }, } beforeAll(() => { @@ -78,7 +172,7 @@ describe.each` try { fs.mkdirSync(`./tests/output/${locale}`) } catch {} }) - test.each(Object.keys(expected))('should correctly parse %s.txt', async (filename) => { + test.each(Object.keys(expected))(`should correctly parse %s.txt`, async (filename) => { const inputPath = `./tests/input/${filename}.txt` const outputPath = `./tests/output/${locale}/${filename}.${scale}.txt` diff --git a/tests/input/example10.txt b/tests/input/example10.txt new file mode 100644 index 0000000..5eae2b5 --- /dev/null +++ b/tests/input/example10.txt @@ -0,0 +1 @@ +1e+3003 diff --git a/tests/input/example11.txt b/tests/input/example11.txt new file mode 100644 index 0000000..573beb6 --- /dev/null +++ b/tests/input/example11.txt @@ -0,0 +1 @@ +1e+98025 diff --git a/tests/input/example12.txt b/tests/input/example12.txt new file mode 100644 index 0000000..348bd5a --- /dev/null +++ b/tests/input/example12.txt @@ -0,0 +1 @@ +1e+3000003 diff --git a/tests/input/example13.txt b/tests/input/example13.txt new file mode 100644 index 0000000..b81101a --- /dev/null +++ b/tests/input/example13.txt @@ -0,0 +1 @@ +1e+2570329536 diff --git a/tests/input/example14.txt b/tests/input/example14.txt new file mode 100644 index 0000000..9aec112 --- /dev/null +++ b/tests/input/example14.txt @@ -0,0 +1 @@ +1e+3000000003 diff --git a/tests/input/example15.txt b/tests/input/example15.txt new file mode 100644 index 0000000..886c7d6 --- /dev/null +++ b/tests/input/example15.txt @@ -0,0 +1 @@ +1e+1174743648579 diff --git a/tests/input/example16.txt b/tests/input/example16.txt new file mode 100644 index 0000000..c302ae3 --- /dev/null +++ b/tests/input/example16.txt @@ -0,0 +1 @@ +1e+3000000000003 diff --git a/tests/input/example17.txt b/tests/input/example17.txt new file mode 100644 index 0000000..f394a3b --- /dev/null +++ b/tests/input/example17.txt @@ -0,0 +1 @@ +1e+696276510359811 diff --git a/tests/input/example8.txt b/tests/input/example8.txt new file mode 100644 index 0000000..c31471b --- /dev/null +++ b/tests/input/example8.txt @@ -0,0 +1 @@ +1.23456789246801357e+2961221 diff --git a/tests/input/example9.txt b/tests/input/example9.txt new file mode 100644 index 0000000..781a1fc --- /dev/null +++ b/tests/input/example9.txt @@ -0,0 +1 @@ +123.456789246801357e+2961000000219