|
|
@@ -0,0 +1,45 @@
|
|
|
+export interface ReviewWordOrder<T> {
|
|
|
+ words: T[];
|
|
|
+ history: string[][];
|
|
|
+}
|
|
|
+
|
|
|
+/** Pick an unused order for this word set while enough distinct permutations exist. */
|
|
|
+export function nextReviewWordOrder<T>(
|
|
|
+ words: T[],
|
|
|
+ previous: string[][],
|
|
|
+ idOf: (word: T) => string,
|
|
|
+ random: () => number = Math.random,
|
|
|
+): ReviewWordOrder<T> {
|
|
|
+ const ids = words.map(idOf);
|
|
|
+ const sortedIds = [...ids].sort();
|
|
|
+ const sameWords = (order: string[]) => order.length === ids.length
|
|
|
+ && [...order].sort().every((id, index) => id === sortedIds[index]);
|
|
|
+ const history = previous.filter(sameWords).slice(-10);
|
|
|
+ const used = new Set(history.map((order) => JSON.stringify(order)));
|
|
|
+ if (ids.length > 1 && !history.length) used.add(JSON.stringify(ids));
|
|
|
+ const indices = words.map((_, index) => index);
|
|
|
+ for (let index = indices.length - 1; index > 0; index -= 1) {
|
|
|
+ const target = Math.floor(random() * (index + 1));
|
|
|
+ [indices[index], indices[target]] = [indices[target], indices[index]];
|
|
|
+ }
|
|
|
+
|
|
|
+ const signature = () => indices.map((index) => ids[index]);
|
|
|
+ const advance = () => {
|
|
|
+ let pivot = indices.length - 2;
|
|
|
+ while (pivot >= 0 && indices[pivot] >= indices[pivot + 1]) pivot -= 1;
|
|
|
+ if (pivot < 0) { indices.reverse(); return; }
|
|
|
+ let swap = indices.length - 1;
|
|
|
+ while (indices[swap] <= indices[pivot]) swap -= 1;
|
|
|
+ [indices[pivot], indices[swap]] = [indices[swap], indices[pivot]];
|
|
|
+ indices.splice(pivot + 1, indices.length - pivot - 1, ...indices.slice(pivot + 1).reverse());
|
|
|
+ };
|
|
|
+
|
|
|
+ for (let attempt = 0; attempt <= used.size && used.has(JSON.stringify(signature())); attempt += 1) advance();
|
|
|
+ if (used.has(JSON.stringify(signature())) && history.length) {
|
|
|
+ // Fewer than eleven permutations exist: still change the order from the last entry.
|
|
|
+ const last = JSON.stringify(history[history.length - 1]);
|
|
|
+ for (let attempt = 0; attempt < Math.max(1, history.length) && JSON.stringify(signature()) === last; attempt += 1) advance();
|
|
|
+ }
|
|
|
+ const order = signature();
|
|
|
+ return { words: indices.map((index) => words[index]), history: [...history, order].slice(-10) };
|
|
|
+}
|