diff --git a/Conversions/ArbitraryBase.js b/Conversions/ArbitraryBase.js index 99e6c1bd5..33c88d5c1 100644 --- a/Conversions/ArbitraryBase.js +++ b/Conversions/ArbitraryBase.js @@ -1,20 +1,36 @@ /** -* Converts a string from one base to other +* 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. 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 * @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 @@ -27,24 +43,57 @@ const convertArbitraryBase = (stringInBaseOne, baseOneCharacters, baseTwoCharact value += (digitNumber * placeValue) placeValue *= stringOneBase } - let stringInBaseTwo = '' + const outputChars = [] const stringTwoBase = baseTwoCharacters.length while (value > 0) { - const remainder = value % stringTwoBase - stringInBaseTwo = baseTwoCharacters.charAt(remainder) + stringInBaseTwo - value /= stringTwoBase + const [divisionResult, remainder] = floorDiv(value, stringTwoBase) + outputChars.push(baseTwoCharacters[remainder]) + value = divisionResult } - const baseTwoZero = baseTwoCharacters.charAt(0) - return stringInBaseTwo.replace(new RegExp(`^${baseTwoZero}+`), '') + 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') + } -// > convertArbitraryBase('98', '0123456789', '01234567') -// '142' + const baseOneCharacters = [...baseOneCharacterString] + const baseTwoCharacters = [...baseTwoCharacterString] -// > convertArbitraryBase('98', '0123456789', 'abcdefgh') -// 'bec' + 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] +} -// > convertArbitraryBase('129', '0123456789', '01234567') -// '201' +export { convertArbitraryBase, convertArbitraryBaseBigIntVersion } diff --git a/Conversions/test/ArbitraryBase.test.js b/Conversions/test/ArbitraryBase.test.js index e552a6fc6..c6e835eb5 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') @@ -34,3 +34,23 @@ 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') +}) + +test('BigInt version with input string of arbitrary length', () => { + const resBigIntVersion = convertArbitraryBaseBigIntVersion( + String(10n ** 100n), + '0123456789', + '0123456789abcdefghijklmnopqrstuvwxyz' + ) + + expect(resBigIntVersion).toBe((10n ** 100n).toString(36)) +})