diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index d837dd4cbf..e09f7fbea4 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { alphabetPad, baseToString, stringToBase } from "matrix-js-sdk/src/utils"; + import { reorder } from "./arrays"; export const ALPHABET_START = 0x20; @@ -23,23 +25,6 @@ export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) .map((_, i) => String.fromCharCode(ALPHABET_START + i)) .join(""); -export const baseToString = (base: bigint, alphabet = ALPHABET): string => { - const len = BigInt(alphabet.length); - if (base < len) return alphabet[Number(base)]; - return baseToString(base / len, alphabet) + alphabet[Number(base % len)]; -}; - -export const stringToBase = (str: string, alphabet = ALPHABET): bigint => { - let result = BigInt(0); - const len = BigInt(alphabet.length); - for (let i = str.length - 1, j = BigInt(0); i >= 0; i--, j++) { - result += BigInt(str.charCodeAt(i) - alphabet.charCodeAt(0)) * (len ** j); - } - return result; -}; - -const pad = (str: string, length: number, alphabet = ALPHABET): string => str.padEnd(length, alphabet[0]); - export const midPointsBetweenStrings = ( a: string, b: string, @@ -47,26 +32,28 @@ export const midPointsBetweenStrings = ( maxLen: number, alphabet = ALPHABET, ): string[] => { - const n = Math.min(maxLen, Math.max(a.length, b.length)); - const aPadded = pad(a, n, alphabet); - const bPadded = pad(b, n, alphabet); - const aBase = stringToBase(aPadded, alphabet); - const bBase = stringToBase(bPadded, alphabet); - if (bBase - aBase - BigInt(1) < count) { - if (n < maxLen) { + const padN = Math.min(Math.max(a.length, b.length), maxLen); + const padA = alphabetPad(a, padN, alphabet); + const padB = alphabetPad(b, padN, alphabet); + const baseA = stringToBase(padA, alphabet); + const baseB = stringToBase(padB, alphabet); + + if (baseB - baseA - BigInt(1) < count) { + if (padN < maxLen) { // this recurses once at most due to the new limit of n+1 return midPointsBetweenStrings( - pad(aPadded, n + 1, alphabet), - pad(bPadded, n + 1, alphabet), + alphabetPad(padA, padN + 1, alphabet), + alphabetPad(padB, padN + 1, alphabet), count, - n + 1, + padN + 1, alphabet, ); } return []; } - const step = (bBase - aBase) / BigInt(count + 1); - const start = BigInt(aBase + step); + + const step = (baseB - baseA) / BigInt(count + 1); + const start = BigInt(baseA + step); return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet)); }; @@ -95,6 +82,7 @@ export const reorderLexicographically = ( // apply the fundamental order update to the zipped array const newOrder = reorder(ordersWithIndices, fromIndex, toIndex); + // check if we have to fill undefined orders to complete placement const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; let leftBoundIdx = toIndex; @@ -105,14 +93,19 @@ export const reorderLexicographically = ( ? stringToBase(newOrder[toIndex + 1].order) : BigInt(Number.MAX_VALUE); + // check how far left we would have to mutate to fit in that direction for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; leftBoundIdx = i; } + // verify the left move would be sufficient + const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order); + const bigToIndex = BigInt(toIndex); if (leftBoundIdx === 0 && - newOrder[0].order !== undefined && - nextBase - stringToBase(newOrder[0].order) < toIndex + firstOrderBase !== undefined && + nextBase - firstOrderBase <= bigToIndex && + firstOrderBase <= bigToIndex ) { canMoveLeft = false; } @@ -124,11 +117,13 @@ export const reorderLexicographically = ( ? stringToBase(newOrder[toIndex - 1]?.order) : BigInt(Number.MIN_VALUE); + // check how far right we would have to mutate to fit in that direction for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) { - if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; // TODO verify + if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; rightBoundIdx = i; } + // verify the right move would be sufficient if (rightBoundIdx === newOrder.length - 1 && (newOrder[rightBoundIdx] ? stringToBase(newOrder[rightBoundIdx].order) @@ -138,27 +133,23 @@ export const reorderLexicographically = ( } } + // pick the cheaper direction const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER; const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER; - if (orderToLeftUndefined || leftDiff < rightDiff) { rightBoundIdx = toIndex; } else { leftBoundIdx = toIndex; } - const prevOrder = newOrder[leftBoundIdx - 1]?.order - ?? String.fromCharCode(ALPHABET_START).repeat(5); + const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? ""; const nextOrder = newOrder[rightBoundIdx + 1]?.order - ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length); + ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length || 1); const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); - console.log("@@ test", { canMoveLeft, canMoveRight, prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined, leftDiff, rightDiff }); - - return changes.map((order, i) => { - const index = newOrder[leftBoundIdx + i].index; - - return { index, order }; - }); + return changes.map((order, i) => ({ + index: newOrder[leftBoundIdx + i].index, + order, + })); }; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index 9f92774acb..d5671ebe76 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -15,13 +15,12 @@ limitations under the License. */ import { sortBy } from "lodash"; +import { stringToBase, baseToString, averageBetweenStrings } from "matrix-js-sdk/src/utils"; import { ALPHABET, - baseToString, midPointsBetweenStrings, reorderLexicographically, - stringToBase, } from "../../src/utils/stringOrderField"; const moveLexicographicallyTest = ( @@ -39,43 +38,58 @@ const moveLexicographicallyTest = ( }); const newOrders = sortBy(zipped, i => i[1]); - console.log("@@ moveLexicographicallyTest", {orders, zipped, newOrders, fromIndex, toIndex, ops}); expect(newOrders[toIndex][0]).toBe(fromIndex); expect(ops).toHaveLength(expectedChanges); }; describe("stringOrderField", () => { it("stringToBase", () => { - expect(Number(stringToBase(" "))).toBe(0); - expect(Number(stringToBase("a"))).toBe(65); - expect(Number(stringToBase("aa"))).toBe(6240); - expect(Number(stringToBase("cat"))).toBe(610934); - expect(Number(stringToBase("doggo"))).toBe(5607022724); - expect(Number(stringToBase(" "))).toEqual(0); - expect(Number(stringToBase("a", "abcdefghijklmnopqrstuvwxyz"))).toEqual(0); - expect(Number(stringToBase("a"))).toEqual(65); - expect(Number(stringToBase("c", "abcdefghijklmnopqrstuvwxyz"))).toEqual(2); - expect(Number(stringToBase("ab"))).toEqual(6241); - expect(Number(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz"))).toEqual(53); - expect(Number(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))).toEqual(4.511596985796182e+78); - expect(Number(stringToBase("~".repeat(50)))).toEqual(7.694497527671333e+98); - // expect(typeof stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual("bigint"); + expect(Number(stringToBase(""))).toBe(0); + expect(Number(stringToBase(" "))).toBe(1); + expect(Number(stringToBase("a"))).toBe(66); + expect(Number(stringToBase(" !"))).toBe(97); + expect(Number(stringToBase("aa"))).toBe(6336); + expect(Number(stringToBase("cat"))).toBe(620055); + expect(Number(stringToBase("doggo"))).toBe(5689339845); + expect(Number(stringToBase("a", "abcdefghijklmnopqrstuvwxyz"))).toEqual(1); + expect(Number(stringToBase("a"))).toEqual(66); + expect(Number(stringToBase("c", "abcdefghijklmnopqrstuvwxyz"))).toEqual(3); + expect(Number(stringToBase("ab"))).toEqual(6337); + expect(Number(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz"))).toEqual(80); + expect(Number(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))).toEqual(4.648312045971824e+78); + expect(Number(stringToBase("~".repeat(50)))).toEqual(7.776353884348688e+98); + expect(Number(stringToBase(" "))).toEqual(7820126496); + expect(Number(stringToBase(" "))).toEqual(96); + expect(Number(stringToBase(" !"))).toEqual(97); + expect(Number(stringToBase("S:J\\~"))).toEqual(4258975590); + expect(Number(stringToBase("!'Tu:}"))).toEqual(16173443434); }); it("baseToString", () => { - expect(baseToString(BigInt(10))).toBe(ALPHABET[10]); - expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("k"); - expect(baseToString(BigInt(6241))).toEqual("ab"); - expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("cb"); - expect(baseToString(BigInt(1234))).toBe(",~"); + expect(baseToString(BigInt(10))).toBe(ALPHABET[9]); + expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("j"); + expect(baseToString(BigInt(6241))).toEqual("`a"); + expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("ba"); + expect(baseToString(BigInt(1234))).toBe("+}"); + expect(baseToString(BigInt(0))).toBe(""); // TODO + expect(baseToString(BigInt(1))).toBe(" "); + expect(baseToString(BigInt(95))).toBe("~"); + expect(baseToString(BigInt(96))).toBe(" "); + expect(baseToString(BigInt(97))).toBe(" !"); + expect(baseToString(BigInt(98))).toBe(' "'); + expect(baseToString(BigInt(1))).toBe(" "); }); it("midPointsBetweenStrings", () => { + expect(averageBetweenStrings("!!", "##")).toBe('""'); const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort(); expect(midpoints[0]).toBe("a"); expect(midpoints[4]).toBe("e"); expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]); expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]); + expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]); + expect(averageBetweenStrings(" ", "!!")).toBe(" P"); + expect(averageBetweenStrings("! ", "!!")).toBe("! "); }); it("moveLexicographically left", () => { @@ -221,7 +235,7 @@ describe("stringOrderField", () => { ); }); - it.skip("test moving left into no left space", () => { + it("test moving left into no left space", () => { moveLexicographicallyTest( ["11", "12", "13", "14", "19"], 3, @@ -229,41 +243,11 @@ describe("stringOrderField", () => { 2, 2, ); - }); - it("test moving right into no right space", () => { - moveLexicographicallyTest( - ["15", "16", "17", "18", "19"], - 1, - 3, - 3, - 2, - ); - }); - - it("test moving right into no left space", () => { - moveLexicographicallyTest( - ["11", "12", "13", "14", "15", "16", undefined], - 1, - 3, - 3, - ); - }); - - it("test moving left into no right space", () => { - moveLexicographicallyTest( - ["15", "16", "17", "18", "19"], - 4, - 3, - 4, - 2, - ); - }); - - it("test moving left into no left space", () => { moveLexicographicallyTest( [ ALPHABET.charAt(0), + // Target ALPHABET.charAt(1), ALPHABET.charAt(2), ALPHABET.charAt(3), @@ -278,6 +262,14 @@ describe("stringOrderField", () => { }); it("test moving right into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 1, + 3, + 3, + 2, + ); + moveLexicographicallyTest( [ ALPHABET.charAt(ALPHABET.length - 5), @@ -294,6 +286,13 @@ describe("stringOrderField", () => { }); it("test moving right into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "15", "16", undefined], + 1, + 3, + 3, + ); + moveLexicographicallyTest( ["0", "1", "2", "3", "4", "5"], 1, @@ -304,6 +303,14 @@ describe("stringOrderField", () => { }); it("test moving left into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 4, + 3, + 4, + 2, + ); + moveLexicographicallyTest( [ ALPHABET.charAt(ALPHABET.length - 5), @@ -329,11 +336,11 @@ describe("stringOrderField", () => { it("rolls over sanely", () => { const maxSpaceValue = "~".repeat(50); - const fiftyFirstChar = "!" + " ".repeat(50); + const fiftyFirstChar = " ".repeat(51); expect(next(maxSpaceValue)).toBe(fiftyFirstChar); expect(prev(fiftyFirstChar)).toBe(maxSpaceValue); - expect(stringToBase(ALPHABET[0])).toEqual(BigInt(0)); - expect(stringToBase(ALPHABET[1])).toEqual(BigInt(1)); + expect(Number(stringToBase(ALPHABET[0]))).toEqual(1); + expect(Number(stringToBase(ALPHABET[1]))).toEqual(2); expect(ALPHABET[ALPHABET.length - 1]).toBe("~"); expect(ALPHABET[0]).toBe(" "); });