Implemented sorting
Summary: For context see https://fb.workplace.com/notes/470523670998369 This diff adds the capability to apply a sorting, and inserts item in a sorted way using binary search in a temporarily intermediate collection. (That collection is optimized away in later diffs, so it is mostly the idea and the tests that are interesting) Reviewed By: nikoant Differential Revision: D25953336 fbshipit-source-id: a51b05e25242f0835280ada99798676311511ef0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
0dc1abdac4
commit
dfda71c350
@@ -12,6 +12,7 @@
|
||||
"@emotion/css": "^11.0.0",
|
||||
"@emotion/react": "^11.1.1",
|
||||
"immer": "^8.0.1",
|
||||
"lodash": "^4.17.20",
|
||||
"react-element-to-jsx-string": "^14.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {sortedIndexBy, sortedLastIndexBy, property} from 'lodash';
|
||||
|
||||
// TODO: support better minification
|
||||
// TODO: separate views from datasource to be able to support multiple transformation simultanously
|
||||
// TODO: expose interface with public members only
|
||||
|
||||
type ExtractKeyType<
|
||||
T extends object,
|
||||
@@ -22,6 +25,7 @@ type AppendEvent<T> = {
|
||||
type UpdateEvent<T> = {
|
||||
type: 'update';
|
||||
value: T;
|
||||
oldValue: T;
|
||||
index: number;
|
||||
};
|
||||
|
||||
@@ -37,7 +41,9 @@ class DataSource<
|
||||
private keyAttribute: undefined | keyof T;
|
||||
private idToIndex: Map<KEY_TYPE, number> = new Map();
|
||||
dataUpdateQueue: DataEvent<T>[] = [];
|
||||
// viewUpdateQueue;
|
||||
|
||||
private sortBy: undefined | ((a: T) => number | string);
|
||||
private _sortedRecords: T[] | undefined;
|
||||
|
||||
viewRecords: T[] = [];
|
||||
nextViewRecords: T[] = []; // for double buffering
|
||||
@@ -64,8 +70,17 @@ class DataSource<
|
||||
return this._recordsById;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing only.
|
||||
* Returns the set of records after applying sorting
|
||||
*/
|
||||
get sortedRecords(): readonly T[] {
|
||||
return this.sortBy ? this._sortedRecords! : this._records;
|
||||
}
|
||||
|
||||
constructor(keyAttribute: KEY | undefined) {
|
||||
this.keyAttribute = keyAttribute;
|
||||
this.setSortBy(undefined);
|
||||
}
|
||||
|
||||
private assertKeySet() {
|
||||
@@ -127,7 +142,15 @@ class DataSource<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an item in the base data collection.
|
||||
* Note that the index is based on the insertion order, and not based on the current view
|
||||
*/
|
||||
update(index: number, value: T) {
|
||||
const oldValue = this._records[index];
|
||||
if (value === oldValue) {
|
||||
return;
|
||||
}
|
||||
if (this.keyAttribute) {
|
||||
const key = this.getKey(value);
|
||||
const currentKey = this.getKey(this._records[index]);
|
||||
@@ -142,6 +165,7 @@ class DataSource<
|
||||
this.emitDataEvent({
|
||||
type: 'update',
|
||||
value,
|
||||
oldValue,
|
||||
index,
|
||||
});
|
||||
}
|
||||
@@ -156,6 +180,25 @@ class DataSource<
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
setSortBy(sortBy: undefined | keyof T | ((a: T) => number | string)) {
|
||||
if (this.sortBy === sortBy) {
|
||||
return;
|
||||
}
|
||||
if (typeof sortBy === 'string') {
|
||||
sortBy = property(sortBy); // TODO: it'd be great to recycle those if sortBy didn't change!
|
||||
}
|
||||
this.sortBy = sortBy as any;
|
||||
if (sortBy === undefined) {
|
||||
this._sortedRecords = undefined;
|
||||
} else {
|
||||
this._sortedRecords = [];
|
||||
// TODO: using .sort will be faster?
|
||||
this._records.forEach((value) => {
|
||||
this.insertSorted(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
emitDataEvent(event: DataEvent<T>) {
|
||||
this.dataUpdateQueue.push(event);
|
||||
// TODO: schedule
|
||||
@@ -164,9 +207,69 @@ class DataSource<
|
||||
|
||||
processEvents() {
|
||||
const events = this.dataUpdateQueue.splice(0);
|
||||
events.forEach((_event) => {
|
||||
// TODO:
|
||||
});
|
||||
events.forEach(this.processEvent);
|
||||
}
|
||||
|
||||
processEvent = (event: DataEvent<T>) => {
|
||||
const {value} = event;
|
||||
switch (event.type) {
|
||||
case 'append':
|
||||
// sort
|
||||
if (this.sortBy) {
|
||||
this.insertSorted(value);
|
||||
}
|
||||
// reverse
|
||||
|
||||
// filter
|
||||
|
||||
// notify
|
||||
break;
|
||||
case 'update':
|
||||
// sort
|
||||
if (this.sortBy) {
|
||||
// find old entry
|
||||
const oldIndex = this.getSortedIndex(event.oldValue);
|
||||
if (
|
||||
this.sortBy(this._sortedRecords![oldIndex]) === this.sortBy(value)
|
||||
) {
|
||||
// sort value is the same? just swap the item
|
||||
this._sortedRecords![oldIndex] = value;
|
||||
} else {
|
||||
// sort value is different? remove and add
|
||||
this._sortedRecords!.splice(oldIndex, 1);
|
||||
this.insertSorted(value);
|
||||
}
|
||||
// reverse
|
||||
|
||||
// filter
|
||||
|
||||
// notify
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('unknown event type');
|
||||
}
|
||||
};
|
||||
|
||||
private getSortedIndex(value: T) {
|
||||
let index = sortedIndexBy(this._sortedRecords, value, this.sortBy);
|
||||
// the item we are looking for is not necessarily the first one at the insertion index
|
||||
while (this._sortedRecords![index] !== value) {
|
||||
index++;
|
||||
if (index >= this._sortedRecords!.length) {
|
||||
throw new Error('illegal state: sortedIndex not found'); // sanity check to avoid browser freeze if people mess up with internals
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private insertSorted(value: T) {
|
||||
const insertionIndex = sortedLastIndexBy(
|
||||
this._sortedRecords,
|
||||
value,
|
||||
this.sortBy!,
|
||||
);
|
||||
this._sortedRecords!.splice(insertionIndex, 0, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,3 +94,74 @@ test('throws on invalid keys', () => {
|
||||
ds.append({id: 'cookie', title: 'test'});
|
||||
}).toThrow(`Duplicate key: 'cookie'`);
|
||||
});
|
||||
|
||||
test('sorting works', () => {
|
||||
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
|
||||
ds.setSortBy((todo) => todo.title);
|
||||
expect(ds.sortedRecords).toEqual([drinkCoffee, eatCookie]);
|
||||
|
||||
ds.setSortBy(undefined);
|
||||
ds.setSortBy(undefined);
|
||||
expect(ds.sortedRecords).toEqual([eatCookie, drinkCoffee]);
|
||||
ds.setSortBy((todo) => todo.title);
|
||||
expect(ds.sortedRecords).toEqual([drinkCoffee, eatCookie]);
|
||||
|
||||
const aleph = {
|
||||
id: 'd',
|
||||
title: 'aleph',
|
||||
};
|
||||
ds.append(aleph);
|
||||
expect(ds.records).toEqual([eatCookie, drinkCoffee, aleph]);
|
||||
expect(ds.sortedRecords).toEqual([aleph, drinkCoffee, eatCookie]);
|
||||
});
|
||||
|
||||
test('sorting preserves insertion order with equal keys', () => {
|
||||
type N = {
|
||||
$: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const a = {$: 'a', name: 'a'};
|
||||
const b1 = {$: 'b', name: 'b1'};
|
||||
const b2 = {$: 'b', name: 'b2'};
|
||||
const b3 = {$: 'b', name: 'b3'};
|
||||
const c = {$: 'c', name: 'c'};
|
||||
|
||||
const ds = createDataSource<N>([]);
|
||||
ds.setSortBy('$');
|
||||
ds.append(b1);
|
||||
ds.append(c);
|
||||
ds.append(b2);
|
||||
ds.append(a);
|
||||
ds.append(b3);
|
||||
|
||||
expect(ds.records).toEqual([b1, c, b2, a, b3]);
|
||||
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, c]);
|
||||
|
||||
// if we append a new item with existig item, it should end up in the end
|
||||
const b4 = {
|
||||
$: 'b',
|
||||
name: 'b4',
|
||||
};
|
||||
ds.append(b4);
|
||||
expect(ds.records).toEqual([b1, c, b2, a, b3, b4]);
|
||||
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, b4, c]);
|
||||
|
||||
// if we replace the middle item, it should end up in the middle
|
||||
const b2r = {
|
||||
$: 'b',
|
||||
name: 'b2replacement',
|
||||
};
|
||||
ds.update(2, b2r);
|
||||
expect(ds.records).toEqual([b1, c, b2r, a, b3, b4]);
|
||||
expect(ds.sortedRecords).toEqual([a, b1, b2r, b3, b4, c]);
|
||||
|
||||
// if we replace something with a different sort value, it should be sorted properly, and the old should disappear
|
||||
const b3r = {
|
||||
$: 'aa',
|
||||
name: 'b3replacement',
|
||||
};
|
||||
ds.update(4, b3r);
|
||||
expect(ds.records).toEqual([b1, c, b2r, a, b3r, b4]);
|
||||
expect(ds.sortedRecords).toEqual([a, b3r, b1, b2r, b4, c]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user