From a4fa2779d4a40bd8a2eeeb65d96fa61cacd836e5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Jun 2021 10:33:00 +0100 Subject: [PATCH] Iterate lexicographic ordering implementation --- src/stores/SpaceStore.tsx | 69 ++++--------------------- src/utils/arrays.ts | 17 ++++++- src/utils/stringOrderField.ts | 79 +++++++++++++++++++++++++++++ test/utils/stringOrderField-test.ts | 76 +++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 61 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 47c735285c..d0ec573306 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -34,12 +34,7 @@ import {setHasDiff} from "../utils/sets"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; import { arrayHasOrderChange } from "../utils/arrays"; -import { - ALPHABET_END, - ALPHABET_START, - averageBetweenStrings, - midPointsBetweenStrings, -} from "../utils/stringOrderField"; +import { reorderLexicographically } from "../utils/stringOrderField"; interface IState {} @@ -645,64 +640,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public moveRootSpace(fromIndex: number, toIndex: number): void { - if ( - fromIndex < 0 || toIndex < 0 || - fromIndex > this.rootSpaces.length || toIndex > this.rootSpaces.length || - fromIndex === toIndex - ) { - return; - } - const space = this.rootSpaces[fromIndex]; - const orders = this.rootSpaces.map(this.getSpaceTagOrdering); + const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering); + const changes = reorderLexicographically(currentOrders, fromIndex, toIndex); - let prevOrder: string; - let nextOrder: string; + changes.forEach(({ index, order }) => { + this.setRootSpaceOrder(this.rootSpaces[index], order); + }); - if (toIndex > fromIndex) { - // moving down - prevOrder = orders[toIndex]; - nextOrder = orders[toIndex + 1]; + if (changes.length) { + this.notifyIfOrderChanged(); } else { - // accounts for downwards displacement of existing inhabitant of this index - prevOrder = toIndex > 0 ? orders[toIndex - 1] : String.fromCharCode(ALPHABET_START).repeat(5); // TODO - nextOrder = orders[toIndex]; + // TODO } - console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder}); - - if (prevOrder === undefined) { - // to be able to move to this toIndex we will first need to insert a bunch of orders for earlier elements - const firstUndefinedIndex = orders.indexOf(undefined); - const numUndefined = orders.length - firstUndefinedIndex; - const lastOrder = orders[firstUndefinedIndex - 1] ?? String.fromCharCode(ALPHABET_START); // TODO - nextOrder = String.fromCharCode(ALPHABET_END).repeat(lastOrder.length + 1); - const newOrders = midPointsBetweenStrings(lastOrder, nextOrder, numUndefined); - - if (newOrders.length === numUndefined) { - console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder, newOrders}); - for (let i = firstUndefinedIndex, j = 0; i <= toIndex; i++, j++) { - if (i === toIndex && toIndex < fromIndex) continue; - if (i === fromIndex) continue; - const newOrder = newOrders[j]; - console.log("@@ preset", {i, j, newOrder}); - this.setRootSpaceOrder(this.rootSpaces[i], newOrder); - } - - prevOrder = newOrders[newOrders.length - 1]; - } else { - prevOrder = nextOrder; // rebuild - } - } - - if (prevOrder !== nextOrder) { - const order = averageBetweenStrings(prevOrder, nextOrder ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1)); - console.log("@@ set", {prevOrder, nextOrder, order}); - this.setRootSpaceOrder(space, order); - } else { - // TODO REBUILD - } - - this.notifyIfOrderChanged(); - console.log("@@ done", this.rootSpaces.map(this.getSpaceTagOrdering)); } } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index e527f43c29..d319631d93 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {percentageOf, percentageWithin} from "./numbers"; +import { percentageOf, percentageWithin } from "./numbers"; /** * Quickly resample an array to have less/more data points. If an input which is larger @@ -223,6 +223,21 @@ export function arrayMerge(...a: T[][]): T[] { }, new Set())); } +/** + * Moves a single element from fromIndex to toIndex. + * @param list the list from which to construct the new list. + * @param fromIndex the index of the element to move. + * @param toIndex the index of where to put the element. + * @returns A new array with the requested value moved. + */ +export function reorder(list: T[], fromIndex: number, toIndex: number): T[] { + const result = Array.from(list); + const [removed] = result.splice(fromIndex, 1); + result.splice(toIndex, 0, removed); + + return result; +} + /** * Helper functions to perform LINQ-like queries on arrays. */ diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index fce859ddb8..ab65a46cb2 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 { reorder } from "./arrays"; + export const ALPHABET_START = 0x20; export const ALPHABET_END = 0x7E; export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) @@ -54,3 +56,80 @@ export const midPointsBetweenStrings = (a: string, b: string, count: number, alp } return Array(count).fill(undefined).map((_, i) => baseToString(aBase + step + (i * step), alphabet)); }; + +interface IEntry { + index: number; + order: string; +} + +export const reorderLexicographically = ( + orders: Array, + fromIndex: number, + toIndex: number, +): IEntry[] => { + if ( + fromIndex < 0 || toIndex < 0 || + fromIndex > orders.length || toIndex > orders.length || + fromIndex === toIndex + ) { + return []; + } + + const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order })); + const newOrder = reorder(ordersWithIndices, fromIndex, toIndex); + + const isMoveTowardsRight = toIndex > fromIndex; + const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; + + let leftBoundIdx = toIndex; + let rightBoundIdx = toIndex; + + const canDisplaceLeft = isMoveTowardsRight || orderToLeftUndefined || true; // TODO + if (canDisplaceLeft) { + const nextBase = newOrder[toIndex + 1]?.order !== undefined + ? stringToBase(newOrder[toIndex + 1].order) + : Number.MAX_VALUE; + for (let i = toIndex - 1, j = 0; i >= 0; i--, j++) { + if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; + leftBoundIdx = i; + } + } + + const canDisplaceRight = !orderToLeftUndefined; + // TODO check if there is enough space on the right hand side at all, + // I guess find the last set order and then compare it to prevBase + $requiredGap + if (canDisplaceRight) { + const prevBase = newOrder[toIndex - 1]?.order !== undefined + ? stringToBase(newOrder[toIndex - 1]?.order) + : Number.MIN_VALUE; + for (let i = toIndex + 1, j = 0; i < newOrder.length; i++, j++) { + if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; // TODO verify + rightBoundIdx = i; + } + } + + const leftDiff = toIndex - leftBoundIdx; + const rightDiff = rightBoundIdx - toIndex; + + if (orderToLeftUndefined || leftDiff < rightDiff) { + rightBoundIdx = toIndex; + } else { + leftBoundIdx = toIndex; + } + + const prevOrder = newOrder[leftBoundIdx - 1]?.order + ?? String.fromCharCode(ALPHABET_START).repeat(5); // TODO + const nextOrder = newOrder[rightBoundIdx + 1]?.order + ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1); // TODO + + const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx); + // TODO If we exceed maxLen then reorder EVERYTHING + + console.log("@@ test", { prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined }); + + return changes.map((order, i) => { + const index = newOrder[leftBoundIdx + i].index; + + return { index, order }; + }); +}; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index 5b8c2f3feb..8e3ae06b79 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -14,14 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { sortBy } from "lodash"; + import { ALPHABET, averageBetweenStrings, baseToString, midPointsBetweenStrings, + reorderLexicographically, stringToBase, } from "../../src/utils/stringOrderField"; +const moveLexicographicallyTest = ( + orders: Array, + fromIndex: number, + toIndex: number, + expectedIndices: number[], +): void => { + const ops = reorderLexicographically(orders, fromIndex, toIndex); + expect(ops.map(o => o.index).sort()).toStrictEqual(expectedIndices.sort()); + + const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]); + ops.forEach(({ index, order }) => { + zipped[index][1] = order; + }); + + const newOrders = sortBy(zipped, i => i[1]); + console.log("@@ moveLexicographicallyTest", {orders, zipped, newOrders, fromIndex, toIndex, ops}); + expect(newOrders[toIndex][0]).toBe(fromIndex); +}; + describe("stringOrderField", () => { it("stringToBase", () => { expect(stringToBase(" ")).toBe(0); @@ -35,6 +57,9 @@ describe("stringOrderField", () => { expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(2); expect(stringToBase("ab")).toEqual(6241); expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(53); + expect(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual(4.5115969857961825e+78); + expect(stringToBase("~".repeat(50))).toEqual(7.694497527671333e+98); + // expect(typeof stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual("bigint"); }); it("baseToString", () => { @@ -57,11 +82,18 @@ describe("stringOrderField", () => { { a: "a", b: "z", output: "m", alphabet: "abcdefghijklmnopqrstuvwxyz" }, { a: "AA", b: "zz", output: "^." }, { a: "A", b: "z", output: "]" }, + { + a: "A".repeat(50), + b: "Z".repeat(50), + output: "M}M}M}N ba`54Qpt\\\\Z+kNA#O(9}z>@2jJm]%Y^$m<8lRzz/2[Y", + }, ].forEach((c) => { // assert that the output string falls lexicographically between `a` and `b` expect([c.a, c.b, c.output].sort()[1]).toBe(c.output); expect(averageBetweenStrings(c.a, c.b, c.alphabet)).toBe(c.output); }); + + expect(averageBetweenStrings("Q#!x+k", "V6yr>L")).toBe("S\\Mu5,"); }); it("midPointsBetweenStrings", () => { @@ -69,5 +101,49 @@ describe("stringOrderField", () => { expect(midPointsBetweenStrings("a", "e", 0)).toStrictEqual([]); expect(midPointsBetweenStrings("a", "e", 4)).toStrictEqual([]); }); + + it("moveLexicographically left", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, [2]); + }); + + it("moveLexicographically right", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, [1]); + }); + + it("moveLexicographically all undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + [0, 4], + ); + }); + + it("moveLexicographically all undefined to end", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 1, + 4, + [0, 1, 2, 3, 4], + ); + }); + + it("moveLexicographically some undefined move left", () => { + moveLexicographicallyTest( + ["a", "c", "e", undefined, undefined, undefined], + 5, + 2, + [5], + ); + }); + + it("moveLexicographically some undefined move left close", () => { + moveLexicographicallyTest( + ["a", "a", "e", undefined, undefined, undefined], + 5, + 1, + [1, 5], + ); + }); });