Skip to content

Make ArbitraryBase Unicode-aware #1299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 68 additions & 19 deletions Conversions/ArbitraryBase.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 }
22 changes: 21 additions & 1 deletion Conversions/test/ArbitraryBase.test.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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))
})