Move over to new lexicographic string sorting

This commit is contained in:
Michael Telatynski 2021-06-10 11:40:10 +01:00
parent 271f544637
commit 21fc386317
3 changed files with 170 additions and 24 deletions

View file

@ -34,6 +34,12 @@ import {setHasDiff} from "../utils/sets";
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
import RoomViewStore from "./RoomViewStore"; import RoomViewStore from "./RoomViewStore";
import { arrayHasOrderChange } from "../utils/arrays"; import { arrayHasOrderChange } from "../utils/arrays";
import {
ALPHABET_END,
ALPHABET_START,
averageBetweenStrings,
midPointsBetweenStrings,
} from "../utils/stringOrderField";
interface IState {} interface IState {}
@ -61,18 +67,19 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
}, [[], []]); }, [[], []]);
}; };
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` const validOrder = (order: string): string | null => {
export const getChildOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => { if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
let validatedOrder: string = null;
if (typeof order === "string" && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0); const charCode = c.charCodeAt(0);
return charCode >= 0x20 && charCode <= 0x7E; return charCode >= 0x20 && charCode <= 0x7E;
})) { })) {
validatedOrder = order; return order;
} }
return undefined;
};
return [validatedOrder, creationTs, roomId]; // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
export const getChildOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
return [validOrder(order), creationTs, roomId];
} }
const getRoomFn: FetchRoomFn = (room: Room) => { const getRoomFn: FetchRoomFn = (room: Room) => {
@ -625,8 +632,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private getSpaceTagOrdering = (space: Room): string | undefined => { private getSpaceTagOrdering = (space: Room): string | undefined => {
if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
const order = space.getAccountData(EventType.SpaceOrder)?.getContent()?.order; return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
return typeof order === "string" ? order : undefined;
}; };
private sortRootSpaces(spaces: Room[]): Room[] { private sortRootSpaces(spaces: Room[]): Room[] {
@ -635,7 +641,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private setRootSpaceOrder(space: Room, order: string): void { private setRootSpaceOrder(space: Room, order: string): void {
this.spaceOrderLocalEchoMap.set(space.roomId, order); this.spaceOrderLocalEchoMap.set(space.roomId, order);
this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); // TODO retrying, failure
} }
public moveRootSpace(fromIndex: number, toIndex: number): void { public moveRootSpace(fromIndex: number, toIndex: number): void {
@ -653,32 +659,42 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
let nextOrder: string; let nextOrder: string;
if (toIndex > fromIndex) { if (toIndex > fromIndex) {
prevOrder = toIndex >= 0 ? orders[toIndex] : "aaaaa"; // moving down
nextOrder = toIndex <= orders.length ? orders[toIndex + 1] : "zzzzz"; prevOrder = orders[toIndex];
nextOrder = orders[toIndex + 1];
} else { } else {
// accounts for downwards displacement of existing inhabitant of this index // accounts for downwards displacement of existing inhabitant of this index
prevOrder = toIndex > 0 ? orders[toIndex - 1] : "aaaaa"; prevOrder = toIndex > 0 ? orders[toIndex - 1] : String.fromCharCode(ALPHABET_START).repeat(5); // TODO
nextOrder = toIndex < orders.length ? orders[toIndex] : "zzzzz"; nextOrder = orders[toIndex];
} }
console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder}); console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder});
if (prevOrder === undefined) { 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 firstUndefinedIndex = orders.indexOf(undefined);
const numUndefined = orders.length - firstUndefinedIndex; const numUndefined = orders.length - firstUndefinedIndex;
const lastOrder = orders[firstUndefinedIndex - 1]; const lastOrder = orders[firstUndefinedIndex - 1] ?? String.fromCharCode(ALPHABET_START); // TODO
console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder}); nextOrder = String.fromCharCode(ALPHABET_END).repeat(lastOrder.length + 1);
nextOrder = lastOrder + step; const newOrders = midPointsBetweenStrings(lastOrder, nextOrder, numUndefined);
for (let i = firstUndefinedIndex; i < toIndex; i++, nextOrder += step) {
console.log("@@ preset", {i, nextOrder});
this.setRootSpaceOrder(this.rootSpaces[i], nextOrder);
}
prevOrder = nextOrder; if (newOrders.length === numUndefined) {
nextOrder += step; 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) { if (prevOrder !== nextOrder) {
const order = prevOrder + ((nextOrder - prevOrder) / 2); const order = averageBetweenStrings(prevOrder, nextOrder ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1));
console.log("@@ set", {prevOrder, nextOrder, order}); console.log("@@ set", {prevOrder, nextOrder, order});
this.setRootSpaceOrder(space, order); this.setRootSpaceOrder(space, order);
} else { } else {
@ -686,6 +702,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
this.notifyIfOrderChanged(); this.notifyIfOrderChanged();
console.log("@@ done", this.rootSpaces.map(this.getSpaceTagOrdering));
} }
} }

View file

@ -0,0 +1,56 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const ALPHABET_START = 0x20;
export const ALPHABET_END = 0x7E;
export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START)
.fill(undefined)
.map((_, i) => String.fromCharCode(ALPHABET_START + i))
.join("");
export const baseToString = (base: number, alphabet = ALPHABET): string => {
base = Math.floor(base);
if (base < alphabet.length) return alphabet[base];
return baseToString(Math.floor(base / alphabet.length), alphabet) + alphabet[base % alphabet.length];
};
export const stringToBase = (str: string, alphabet = ALPHABET): number => {
let result = 0;
for (let i = str.length - 1, j = 0; i >= 0; i--, j++) {
result += (str.charCodeAt(i) - alphabet.charCodeAt(0)) * (alphabet.length ** j);
}
return result;
};
const pad = (str: string, length: number, alphabet = ALPHABET): string => str.padEnd(length, alphabet[0]);
export const averageBetweenStrings = (a: string, b: string, alphabet = ALPHABET): string => {
const n = Math.max(a.length, b.length);
const aBase = stringToBase(pad(a, n, alphabet), alphabet);
const bBase = stringToBase(pad(b, n, alphabet), alphabet);
return baseToString((aBase + bBase) / 2, alphabet);
};
export const midPointsBetweenStrings = (a: string, b: string, count: number, alphabet = ALPHABET): string[] => {
const n = Math.max(a.length, b.length);
const aBase = stringToBase(pad(a, n, alphabet), alphabet);
const bBase = stringToBase(pad(b, n, alphabet), alphabet);
const step = (bBase - aBase) / (count + 1);
if (step < 1) {
return [];
}
return Array(count).fill(undefined).map((_, i) => baseToString(aBase + step + (i * step), alphabet));
};

View file

@ -0,0 +1,73 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
ALPHABET,
averageBetweenStrings,
baseToString,
midPointsBetweenStrings,
stringToBase,
} from "../../src/utils/stringOrderField";
describe("stringOrderField", () => {
it("stringToBase", () => {
expect(stringToBase(" ")).toBe(0);
expect(stringToBase("a")).toBe(65);
expect(stringToBase("aa")).toBe(6240);
expect(stringToBase("cat")).toBe(610934);
expect(stringToBase("doggo")).toBe(5607022724);
expect(stringToBase(" ")).toEqual(0);
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(0);
expect(stringToBase("a")).toEqual(65);
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(2);
expect(stringToBase("ab")).toEqual(6241);
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(53);
});
it("baseToString", () => {
expect(baseToString(10)).toBe(ALPHABET[10]);
expect(baseToString(10, "abcdefghijklmnopqrstuvwxyz")).toEqual("k");
expect(baseToString(6241)).toEqual("ab");
expect(baseToString(53, "abcdefghijklmnopqrstuvwxyz")).toEqual("cb");
expect(baseToString(1234)).toBe(",~");
});
it("averageBetweenStrings", () => {
[
{ a: "a", b: "z", output: `m` },
{ a: "ba", b: "z", output: `n@` },
{ a: "z", b: "ba", output: `n@` },
{ a: "# ", b: "$8888", output: `#[[[[` },
{ a: "cat", b: "doggo", output: `d9>Cw` },
{ a: "cat", b: "doggo", output: "cumqh", alphabet: "abcdefghijklmnopqrstuvwxyz" },
{ a: "aa", b: "zz", output: "mz", alphabet: "abcdefghijklmnopqrstuvwxyz" },
{ a: "a", b: "z", output: "m", alphabet: "abcdefghijklmnopqrstuvwxyz" },
{ a: "AA", b: "zz", output: "^." },
{ a: "A", b: "z", output: "]" },
].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);
});
});
it("midPointsBetweenStrings", () => {
expect(midPointsBetweenStrings("a", "e", 3)).toStrictEqual(["b", "c", "d"]);
expect(midPointsBetweenStrings("a", "e", 0)).toStrictEqual([]);
expect(midPointsBetweenStrings("a", "e", 4)).toStrictEqual([]);
});
});