From cff0f16c44c662c5a867b14ccb65df4b4310e49a Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Wed, 22 Feb 2023 03:33:48 +0000 Subject: [PATCH 1/3] Make ArbitraryBase Unicode-aware https://mathiasbynens.be/notes/javascript-unicode#counting-symbols --- Conversions/ArbitraryBase.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Conversions/ArbitraryBase.js b/Conversions/ArbitraryBase.js index 99e6c1bd57..3faabc2684 100644 --- a/Conversions/ArbitraryBase.js +++ b/Conversions/ArbitraryBase.js @@ -5,16 +5,19 @@ * @param {string} baseTwoCharacters Character set for the output base * @returns {string} */ -const convertArbitraryBase = (stringInBaseOne, baseOneCharacters, baseTwoCharacters) => { - if ([stringInBaseOne, baseOneCharacters, baseTwoCharacters].map(arg => typeof arg).some(type => type !== 'string')) { +const convertArbitraryBase = (stringInBaseOne, baseOneCharacterString, baseTwoCharacterString) => { + if ([stringInBaseOne, baseOneCharacterString, baseTwoCharacterString].map(arg => typeof arg).some(type => type !== 'string')) { throw new TypeError('Only string arguments are allowed') } - [baseOneCharacters, baseTwoCharacters].forEach(baseString => { - const charactersInBase = [...baseString] + + const baseOneCharacters = [...baseOneCharacterString] + const baseTwoCharacters = [...baseTwoCharacterString] + + for (const charactersInBase of [baseOneCharacters, baseTwoCharacters]) { if (charactersInBase.length !== new Set(charactersInBase).size) { throw new TypeError('Duplicate characters in character set are not allowed') } - }) + } const reversedStringOneChars = [...stringInBaseOne].reverse() const stringOneBase = baseOneCharacters.length let value = 0 @@ -31,11 +34,11 @@ const convertArbitraryBase = (stringInBaseOne, baseOneCharacters, baseTwoCharact const stringTwoBase = baseTwoCharacters.length while (value > 0) { const remainder = value % stringTwoBase - stringInBaseTwo = baseTwoCharacters.charAt(remainder) + stringInBaseTwo + stringInBaseTwo = baseTwoCharacters.at(remainder) + stringInBaseTwo value /= stringTwoBase } - const baseTwoZero = baseTwoCharacters.charAt(0) - return stringInBaseTwo.replace(new RegExp(`^${baseTwoZero}+`), '') + const baseTwoZero = baseTwoCharacters.at(0) + return stringInBaseTwo.replace(new RegExp(`^${baseTwoZero}+`, 'u'), '') } export { convertArbitraryBase } @@ -48,3 +51,6 @@ export { convertArbitraryBase } // > convertArbitraryBase('129', '0123456789', '01234567') // '201' + +// > convertArbitraryBase('98', '0123456789', 'πŸ’πŸŽΈπŸ¦„') +// 'πŸŽΈπŸ’πŸŽΈπŸ¦„πŸ¦„' From 910343505947bc5370514604d51434c77ac55b32 Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Thu, 23 Feb 2023 03:34:47 +0800 Subject: [PATCH 2/3] Fix performance bug and add Unicode test --- Conversions/ArbitraryBase.js | 34 +++++++++++++------------- Conversions/test/ArbitraryBase.test.js | 10 ++++++++ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Conversions/ArbitraryBase.js b/Conversions/ArbitraryBase.js index 3faabc2684..221062ba98 100644 --- a/Conversions/ArbitraryBase.js +++ b/Conversions/ArbitraryBase.js @@ -1,3 +1,16 @@ +/** +* Divide two numbers and get the result of floor division and remainder +* @param {number} dividend +* @param {number} divisor +* @returns {[result: number, remainder: number]} +*/ +const floorDiv = (dividend, divisor) => { + const remainder = dividend % divisor + const result = Math.floor(dividend / divisor) + + return [result, remainder] +} + /** * Converts a string from one base to other * @param {string} stringInBaseOne String in input base @@ -33,24 +46,11 @@ const convertArbitraryBase = (stringInBaseOne, baseOneCharacterString, baseTwoCh let stringInBaseTwo = '' const stringTwoBase = baseTwoCharacters.length while (value > 0) { - const remainder = value % stringTwoBase - stringInBaseTwo = baseTwoCharacters.at(remainder) + stringInBaseTwo - value /= stringTwoBase + const [divisionResult, remainder] = floorDiv(value, stringTwoBase) + stringInBaseTwo = baseTwoCharacters[remainder] + stringInBaseTwo + value = divisionResult } - const baseTwoZero = baseTwoCharacters.at(0) - return stringInBaseTwo.replace(new RegExp(`^${baseTwoZero}+`, 'u'), '') + return stringInBaseTwo || baseTwoCharacters[0] } export { convertArbitraryBase } - -// > convertArbitraryBase('98', '0123456789', '01234567') -// '142' - -// > convertArbitraryBase('98', '0123456789', 'abcdefgh') -// 'bec' - -// > convertArbitraryBase('129', '0123456789', '01234567') -// '201' - -// > convertArbitraryBase('98', '0123456789', 'πŸ’πŸŽΈπŸ¦„') -// 'πŸŽΈπŸ’πŸŽΈπŸ¦„πŸ¦„' diff --git a/Conversions/test/ArbitraryBase.test.js b/Conversions/test/ArbitraryBase.test.js index e552a6fc6a..4f9e7021b2 100644 --- a/Conversions/test/ArbitraryBase.test.js +++ b/Conversions/test/ArbitraryBase.test.js @@ -34,3 +34,13 @@ test('Check the answer of convertArbitraryBase(111, 0123456789, abcdefgh) is bfh const res = convertArbitraryBase('111', '0123456789', 'abcdefgh') expect(res).toBe('bfh') }) + +test('Unicode awareness', () => { + const res = convertArbitraryBase('98', '0123456789', 'πŸ’πŸŽΈπŸ¦„') + expect(res).toBe('πŸŽΈπŸ’πŸŽΈπŸ¦„πŸ¦„') +}) + +test('zero', () => { + const res = convertArbitraryBase('0', '0123456789', 'abc') + expect(res).toBe('a') +}) From 6667e4c227648c3c33f6951195936a695b46c1fb Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Thu, 23 Feb 2023 08:22:19 +0800 Subject: [PATCH 3/3] Add BigInt version and push output chars to array --- Conversions/ArbitraryBase.js | 53 +++++++++++++++++++++++--- Conversions/test/ArbitraryBase.test.js | 12 +++++- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/Conversions/ArbitraryBase.js b/Conversions/ArbitraryBase.js index 221062ba98..33c88d5c1b 100644 --- a/Conversions/ArbitraryBase.js +++ b/Conversions/ArbitraryBase.js @@ -12,7 +12,7 @@ const floorDiv = (dividend, divisor) => { } /** -* Converts a string from one base to other +* Converts a string from one base to other. Loses accuracy above the value of `Number.MAX_SAFE_INTEGER`. * @param {string} stringInBaseOne String in input base * @param {string} baseOneCharacters Character set for the input base * @param {string} baseTwoCharacters Character set for the output base @@ -43,14 +43,57 @@ const convertArbitraryBase = (stringInBaseOne, baseOneCharacterString, baseTwoCh value += (digitNumber * placeValue) placeValue *= stringOneBase } - let stringInBaseTwo = '' + const outputChars = [] const stringTwoBase = baseTwoCharacters.length while (value > 0) { const [divisionResult, remainder] = floorDiv(value, stringTwoBase) - stringInBaseTwo = baseTwoCharacters[remainder] + stringInBaseTwo + outputChars.push(baseTwoCharacters[remainder]) value = divisionResult } - return stringInBaseTwo || baseTwoCharacters[0] + return outputChars.reverse().join('') || baseTwoCharacters[0] } -export { convertArbitraryBase } +/** +* Converts a arbitrary-length string from one base to other. Doesn't lose accuracy. +* @param {string} stringInBaseOne String in input base +* @param {string} baseOneCharacters Character set for the input base +* @param {string} baseTwoCharacters Character set for the output base +* @returns {string} +*/ +const convertArbitraryBaseBigIntVersion = (stringInBaseOne, baseOneCharacterString, baseTwoCharacterString) => { + if ([stringInBaseOne, baseOneCharacterString, baseTwoCharacterString].map(arg => typeof arg).some(type => type !== 'string')) { + throw new TypeError('Only string arguments are allowed') + } + + const baseOneCharacters = [...baseOneCharacterString] + const baseTwoCharacters = [...baseTwoCharacterString] + + for (const charactersInBase of [baseOneCharacters, baseTwoCharacters]) { + if (charactersInBase.length !== new Set(charactersInBase).size) { + throw new TypeError('Duplicate characters in character set are not allowed') + } + } + const reversedStringOneChars = [...stringInBaseOne].reverse() + const stringOneBase = BigInt(baseOneCharacters.length) + let value = 0n + let placeValue = 1n + for (const digit of reversedStringOneChars) { + const digitNumber = BigInt(baseOneCharacters.indexOf(digit)) + if (digitNumber === -1n) { + throw new TypeError(`Not a valid character: ${digit}`) + } + value += (digitNumber * placeValue) + placeValue *= stringOneBase + } + const outputChars = [] + const stringTwoBase = BigInt(baseTwoCharacters.length) + while (value > 0n) { + const divisionResult = value / stringTwoBase + const remainder = value % stringTwoBase + outputChars.push(baseTwoCharacters[remainder]) + value = divisionResult + } + return outputChars.reverse().join('') || baseTwoCharacters[0] +} + +export { convertArbitraryBase, convertArbitraryBaseBigIntVersion } diff --git a/Conversions/test/ArbitraryBase.test.js b/Conversions/test/ArbitraryBase.test.js index 4f9e7021b2..c6e835eb5a 100644 --- a/Conversions/test/ArbitraryBase.test.js +++ b/Conversions/test/ArbitraryBase.test.js @@ -1,4 +1,4 @@ -import { convertArbitraryBase } from '../ArbitraryBase' +import { convertArbitraryBase, convertArbitraryBaseBigIntVersion } from '../ArbitraryBase' test('Check the answer of convertArbitraryBase(98, 0123456789, 01234567) is 142', () => { const res = convertArbitraryBase('98', '0123456789', '01234567') @@ -44,3 +44,13 @@ test('zero', () => { const res = convertArbitraryBase('0', '0123456789', 'abc') expect(res).toBe('a') }) + +test('BigInt version with input string of arbitrary length', () => { + const resBigIntVersion = convertArbitraryBaseBigIntVersion( + String(10n ** 100n), + '0123456789', + '0123456789abcdefghijklmnopqrstuvwxyz' + ) + + expect(resBigIntVersion).toBe((10n ** 100n).toString(36)) +})