Move over to new lexicographic string sorting

pull/21833/head
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 RoomViewStore from "./RoomViewStore";
import { arrayHasOrderChange } from "../utils/arrays";
import {
ALPHABET_END,
ALPHABET_START,
averageBetweenStrings,
midPointsBetweenStrings,
} from "../utils/stringOrderField";
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`
export const getChildOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
let validatedOrder: string = null;
if (typeof order === "string" && Array.from(order).every((c: string) => {
const validOrder = (order: string): string | null => {
if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0);
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) => {
@ -625,8 +632,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private getSpaceTagOrdering = (space: Room): string | undefined => {
if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
const order = space.getAccountData(EventType.SpaceOrder)?.getContent()?.order;
return typeof order === "string" ? order : undefined;
return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
};
private sortRootSpaces(spaces: Room[]): Room[] {
@ -635,7 +641,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private setRootSpaceOrder(space: Room, order: string): void {
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 {
@ -653,32 +659,42 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
let nextOrder: string;
if (toIndex > fromIndex) {
prevOrder = toIndex >= 0 ? orders[toIndex] : "aaaaa";
nextOrder = toIndex <= orders.length ? orders[toIndex + 1] : "zzzzz";
// moving down
prevOrder = orders[toIndex];
nextOrder = orders[toIndex + 1];
} else {
// accounts for downwards displacement of existing inhabitant of this index
prevOrder = toIndex > 0 ? orders[toIndex - 1] : "aaaaa";
nextOrder = toIndex < orders.length ? orders[toIndex] : "zzzzz";
prevOrder = toIndex > 0 ? orders[toIndex - 1] : String.fromCharCode(ALPHABET_START).repeat(5); // TODO
nextOrder = orders[toIndex];
}
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];
console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder});
nextOrder = lastOrder + step;
for (let i = firstUndefinedIndex; i < toIndex; i++, nextOrder += step) {
console.log("@@ preset", {i, nextOrder});
this.setRootSpaceOrder(this.rootSpaces[i], nextOrder);
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 = nextOrder;
nextOrder += step;
prevOrder = newOrders[newOrders.length - 1];
} else {
prevOrder = nextOrder; // rebuild
}
}
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});
this.setRootSpaceOrder(space, order);
} else {
@ -686,6 +702,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
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([]);
});
});