1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Offset the alphabet by 1

This commit is contained in:
Travis Ralston
2021-06-14 10:28:26 -06:00
parent d99ea1c6b4
commit d3027e1fe8
2 changed files with 61 additions and 31 deletions

View File

@@ -285,26 +285,49 @@ describe("utils", function() {
describe('baseToString', () => { describe('baseToString', () => {
it('should calculate the appropriate string from numbers', () => { it('should calculate the appropriate string from numbers', () => {
expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[10]); // Verify the whole alphabet
expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual('k'); for (let i = BigInt(1); i <= DEFAULT_ALPHABET.length; i++) {
expect(baseToString(BigInt(6241))).toEqual("ab"); console.log({i}); // for debugging
expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual('cb'); expect(baseToString(i)).toEqual(DEFAULT_ALPHABET[Number(i) - 1]);
}
// Just quickly double check that repeated characters aren't treated as padding, particularly
// at the beginning of the alphabet where they are most vulnerable to this behaviour.
expect(baseToString(BigInt(1))).toEqual(DEFAULT_ALPHABET[0].repeat(1));
expect(baseToString(BigInt(96))).toEqual(DEFAULT_ALPHABET[0].repeat(2));
expect(baseToString(BigInt(9121))).toEqual(DEFAULT_ALPHABET[0].repeat(3));
expect(baseToString(BigInt(866496))).toEqual(DEFAULT_ALPHABET[0].repeat(4));
expect(baseToString(BigInt(82317121))).toEqual(DEFAULT_ALPHABET[0].repeat(5));
expect(baseToString(BigInt(7820126496))).toEqual(DEFAULT_ALPHABET[0].repeat(6));
expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[9]);
expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual('j');
expect(baseToString(BigInt(6337))).toEqual("ab");
expect(baseToString(BigInt(80), "abcdefghijklmnopqrstuvwxyz")).toEqual('cb');
}); });
}); });
describe('stringToBase', () => { describe('stringToBase', () => {
it('should calculate the appropriate number for a string', () => { it('should calculate the appropriate number for a string', () => {
expect(stringToBase(" ")).toEqual(BigInt(0)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(1))).toEqual(BigInt(1));
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(0)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(2))).toEqual(BigInt(96));
expect(stringToBase("a")).toEqual(BigInt(65)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(3))).toEqual(BigInt(9121));
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(2)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(4))).toEqual(BigInt(866496));
expect(stringToBase("ab")).toEqual(BigInt(6241)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(5))).toEqual(BigInt(82317121));
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(53)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(6))).toEqual(BigInt(7820126496));
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(1));
expect(stringToBase("a")).toEqual(BigInt(66));
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(3));
expect(stringToBase("ab")).toEqual(BigInt(6337));
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(80));
}); });
}); });
describe('averageBetweenStrings', () => { describe('averageBetweenStrings', () => {
it('should average appropriately', () => { it('should average appropriately', () => {
console.log(stringToBase(" "));
console.log(stringToBase("!!"));
expect(averageBetweenStrings(" ", "!!")).toEqual(" P");
expect(averageBetweenStrings('A', 'z')).toEqual(']'); expect(averageBetweenStrings('A', 'z')).toEqual(']');
expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m'); expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m');
expect(averageBetweenStrings('AA', 'zz')).toEqual('^.'); expect(averageBetweenStrings('AA', 'zz')).toEqual('^.');
@@ -354,22 +377,8 @@ describe("utils", function() {
it('should roll over', () => { it('should roll over', () => {
const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1]; const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1];
const firstAlpha = DEFAULT_ALPHABET[0]; const firstAlpha = DEFAULT_ALPHABET[0];
const secondAlpha = DEFAULT_ALPHABET[1];
// You may be looking at this and wondering how on this planet we end up with const highRoll = firstAlpha + firstAlpha;
// the next string being +2 on the ASCII table, and this comment is here to tell
// you that you're not insane. Due to a property of the baseN conversion, our
// +1 (and -1 for prevString) turns into +2 because the first character in the
// alphabet is equivalent to zero rather than one. Thus, we're actually adding
// the second character of the alphabet (due to adding a numeric 1) to the
// input string, thus resulting in a human-understandable +2 jump rather than
// a +1 one.
// Let's validate that +1 behaviour with math
expect(stringToBase(DEFAULT_ALPHABET[0])).toEqual(BigInt(0));
expect(stringToBase(DEFAULT_ALPHABET[1])).toEqual(BigInt(1));
const highRoll = secondAlpha + firstAlpha;
const lowRoll = lastAlpha; const lowRoll = lastAlpha;
expect(nextString(lowRoll)).toEqual(highRoll); expect(nextString(lowRoll)).toEqual(highRoll);
@@ -386,9 +395,8 @@ describe("utils", function() {
it('should properly handle rolling over at 50 characters', () => { it('should properly handle rolling over at 50 characters', () => {
// Note: we also test reversibility of large strings here. // Note: we also test reversibility of large strings here.
// See rollover test for why we use weird parts of the alphabet
const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50); const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50);
const fiftyFirstChar = DEFAULT_ALPHABET[1] + DEFAULT_ALPHABET[0].repeat(50); const fiftyFirstChar = DEFAULT_ALPHABET[0].repeat(51);
expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar); expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar);
expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue); expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue);

View File

@@ -497,12 +497,29 @@ export function alphabetPad(s: string, n: number, alphabet = DEFAULT_ALPHABET):
* @returns {string} The baseN number encoded as a string from the alphabet. * @returns {string} The baseN number encoded as a string from the alphabet.
*/ */
export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string { export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string {
// Developer note: the stringToBase() function offsets the character set by 1 so that repeated
// characters (ie: "aaaaaa" in a..z) don't come out as zero. We have to reverse this here as
// otherwise we'll be wrong in our conversion. Undoing a +1 before an exponent isn't very fun
// though, so we rely on a lengthy amount of `x - 1` and integer division rules to reach a
// sane state. This also means we have to do rollover detection: see below.
const len = BigInt(alphabet.length); const len = BigInt(alphabet.length);
if (n < len) { if (n <= len) {
return alphabet[Number(n)]; return alphabet[Number(n) - 1];
} }
return baseToString(n / len, alphabet) + alphabet[Number(n % len)]; let d = n / len;
let r = Number(n % len) - 1;
// Rollover detection: if the remainder is negative, it means that the string needs
// to roll over by 1 character downwards (ie: in a..z, the previous to "aaa" would be
// "zz").
if (r < 0) {
d -= BigInt(Math.abs(r)); // abs() is just to be clear what we're doing. Could also `+= r`.
r = Number(len) - 1;
}
return baseToString(d, alphabet) + alphabet[r];
} }
/** /**
@@ -527,9 +544,14 @@ export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): bigint {
// Developer caution: we carefully cast to BigInt here to avoid losing precision. We cannot // Developer caution: we carefully cast to BigInt here to avoid losing precision. We cannot
// rely on Math.pow() (for example) to be capable of handling our insane numbers. // rely on Math.pow() (for example) to be capable of handling our insane numbers.
let result = BigInt(0); let result = BigInt(0);
for (let i = s.length - 1, j = BigInt(0); i >= 0; i--, j++) { for (let i = s.length - 1, j = BigInt(0); i >= 0; i--, j++) {
result += BigInt(s.charCodeAt(i) - alphabet.charCodeAt(0)) * (len ** j); const charIndex = s.charCodeAt(i) - alphabet.charCodeAt(0);
// We add 1 to the char index to offset the whole numbering scheme. We unpack this in
// the baseToString() function.
result += BigInt(1 + charIndex) * (len ** j);
} }
return result; return result;
} }