Skip to content

Commit 0c42758

Browse files
authored
Make ArbitraryBase Unicode-aware (#1299)
* Make ArbitraryBase Unicode-aware https://mathiasbynens.be/notes/javascript-unicode#counting-symbols * Fix performance bug and add Unicode test * Add BigInt version and push output chars to array
1 parent 6aa3314 commit 0c42758

File tree

2 files changed

+89
-20
lines changed

2 files changed

+89
-20
lines changed

Conversions/ArbitraryBase.js

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
/**
2-
* Converts a string from one base to other
2+
* Divide two numbers and get the result of floor division and remainder
3+
* @param {number} dividend
4+
* @param {number} divisor
5+
* @returns {[result: number, remainder: number]}
6+
*/
7+
const floorDiv = (dividend, divisor) => {
8+
const remainder = dividend % divisor
9+
const result = Math.floor(dividend / divisor)
10+
11+
return [result, remainder]
12+
}
13+
14+
/**
15+
* Converts a string from one base to other. Loses accuracy above the value of `Number.MAX_SAFE_INTEGER`.
316
* @param {string} stringInBaseOne String in input base
417
* @param {string} baseOneCharacters Character set for the input base
518
* @param {string} baseTwoCharacters Character set for the output base
619
* @returns {string}
720
*/
8-
const convertArbitraryBase = (stringInBaseOne, baseOneCharacters, baseTwoCharacters) => {
9-
if ([stringInBaseOne, baseOneCharacters, baseTwoCharacters].map(arg => typeof arg).some(type => type !== 'string')) {
21+
const convertArbitraryBase = (stringInBaseOne, baseOneCharacterString, baseTwoCharacterString) => {
22+
if ([stringInBaseOne, baseOneCharacterString, baseTwoCharacterString].map(arg => typeof arg).some(type => type !== 'string')) {
1023
throw new TypeError('Only string arguments are allowed')
1124
}
12-
[baseOneCharacters, baseTwoCharacters].forEach(baseString => {
13-
const charactersInBase = [...baseString]
25+
26+
const baseOneCharacters = [...baseOneCharacterString]
27+
const baseTwoCharacters = [...baseTwoCharacterString]
28+
29+
for (const charactersInBase of [baseOneCharacters, baseTwoCharacters]) {
1430
if (charactersInBase.length !== new Set(charactersInBase).size) {
1531
throw new TypeError('Duplicate characters in character set are not allowed')
1632
}
17-
})
33+
}
1834
const reversedStringOneChars = [...stringInBaseOne].reverse()
1935
const stringOneBase = baseOneCharacters.length
2036
let value = 0
@@ -27,24 +43,57 @@ const convertArbitraryBase = (stringInBaseOne, baseOneCharacters, baseTwoCharact
2743
value += (digitNumber * placeValue)
2844
placeValue *= stringOneBase
2945
}
30-
let stringInBaseTwo = ''
46+
const outputChars = []
3147
const stringTwoBase = baseTwoCharacters.length
3248
while (value > 0) {
33-
const remainder = value % stringTwoBase
34-
stringInBaseTwo = baseTwoCharacters.charAt(remainder) + stringInBaseTwo
35-
value /= stringTwoBase
49+
const [divisionResult, remainder] = floorDiv(value, stringTwoBase)
50+
outputChars.push(baseTwoCharacters[remainder])
51+
value = divisionResult
3652
}
37-
const baseTwoZero = baseTwoCharacters.charAt(0)
38-
return stringInBaseTwo.replace(new RegExp(`^${baseTwoZero}+`), '')
53+
return outputChars.reverse().join('') || baseTwoCharacters[0]
3954
}
4055

41-
export { convertArbitraryBase }
56+
/**
57+
* Converts a arbitrary-length string from one base to other. Doesn't lose accuracy.
58+
* @param {string} stringInBaseOne String in input base
59+
* @param {string} baseOneCharacters Character set for the input base
60+
* @param {string} baseTwoCharacters Character set for the output base
61+
* @returns {string}
62+
*/
63+
const convertArbitraryBaseBigIntVersion = (stringInBaseOne, baseOneCharacterString, baseTwoCharacterString) => {
64+
if ([stringInBaseOne, baseOneCharacterString, baseTwoCharacterString].map(arg => typeof arg).some(type => type !== 'string')) {
65+
throw new TypeError('Only string arguments are allowed')
66+
}
4267

43-
// > convertArbitraryBase('98', '0123456789', '01234567')
44-
// '142'
68+
const baseOneCharacters = [...baseOneCharacterString]
69+
const baseTwoCharacters = [...baseTwoCharacterString]
4570

46-
// > convertArbitraryBase('98', '0123456789', 'abcdefgh')
47-
// 'bec'
71+
for (const charactersInBase of [baseOneCharacters, baseTwoCharacters]) {
72+
if (charactersInBase.length !== new Set(charactersInBase).size) {
73+
throw new TypeError('Duplicate characters in character set are not allowed')
74+
}
75+
}
76+
const reversedStringOneChars = [...stringInBaseOne].reverse()
77+
const stringOneBase = BigInt(baseOneCharacters.length)
78+
let value = 0n
79+
let placeValue = 1n
80+
for (const digit of reversedStringOneChars) {
81+
const digitNumber = BigInt(baseOneCharacters.indexOf(digit))
82+
if (digitNumber === -1n) {
83+
throw new TypeError(`Not a valid character: ${digit}`)
84+
}
85+
value += (digitNumber * placeValue)
86+
placeValue *= stringOneBase
87+
}
88+
const outputChars = []
89+
const stringTwoBase = BigInt(baseTwoCharacters.length)
90+
while (value > 0n) {
91+
const divisionResult = value / stringTwoBase
92+
const remainder = value % stringTwoBase
93+
outputChars.push(baseTwoCharacters[remainder])
94+
value = divisionResult
95+
}
96+
return outputChars.reverse().join('') || baseTwoCharacters[0]
97+
}
4898

49-
// > convertArbitraryBase('129', '0123456789', '01234567')
50-
// '201'
99+
export { convertArbitraryBase, convertArbitraryBaseBigIntVersion }

Conversions/test/ArbitraryBase.test.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { convertArbitraryBase } from '../ArbitraryBase'
1+
import { convertArbitraryBase, convertArbitraryBaseBigIntVersion } from '../ArbitraryBase'
22

33
test('Check the answer of convertArbitraryBase(98, 0123456789, 01234567) is 142', () => {
44
const res = convertArbitraryBase('98', '0123456789', '01234567')
@@ -34,3 +34,23 @@ test('Check the answer of convertArbitraryBase(111, 0123456789, abcdefgh) is bfh
3434
const res = convertArbitraryBase('111', '0123456789', 'abcdefgh')
3535
expect(res).toBe('bfh')
3636
})
37+
38+
test('Unicode awareness', () => {
39+
const res = convertArbitraryBase('98', '0123456789', '💝🎸🦄')
40+
expect(res).toBe('🎸💝🎸🦄🦄')
41+
})
42+
43+
test('zero', () => {
44+
const res = convertArbitraryBase('0', '0123456789', 'abc')
45+
expect(res).toBe('a')
46+
})
47+
48+
test('BigInt version with input string of arbitrary length', () => {
49+
const resBigIntVersion = convertArbitraryBaseBigIntVersion(
50+
String(10n ** 100n),
51+
'0123456789',
52+
'0123456789abcdefghijklmnopqrstuvwxyz'
53+
)
54+
55+
expect(resBigIntVersion).toBe((10n ** 100n).toString(36))
56+
})

0 commit comments

Comments
 (0)