blob: e43cd389d26fa9f6d078038474893164d1058018 [file] [log] [blame]
/**
* @module modules/sort
* @description
*
* Provides SortHistory, which provides functionality for sorting a a generic
* type <T> on various attributes, and reflecting the state of the sort to/from
* a serialized string, which is useful for passing to/from stateReflector.
*
*/
/** The direction a column is sorted in. */
export type direction = 1 | -1;
export const up: direction = 1;
export const down: direction = -1;
/** Represents a how a single column in the table is to be sorted.
*/
export class SortSelection {
// The column to sort on, the value is an index into a columnSortFunctions
// array.
column: number = 0;
dir: direction = up;
constructor(column: number, dir: direction) {
this.column = column;
this.dir = dir;
}
toggleDirection(): void {
if (this.dir === down) {
this.dir = up;
} else {
this.dir = down;
}
}
/** Returns 1 if sorting in the up direction, and -1 if sorting in the down
* direction. */
directionMultiplier(): number {
return this.dir;
}
/** Encodes the SortSelection as a string. */
encode(): string {
const encodedDir = this.dir === up ? 'u' : 'd';
return `${encodedDir}${this.column}`;
}
/** Decode an encoded SortSelection from a string encoded by
* SortSelection.encode(). */
static decode(s: string): SortSelection {
const dir = s[0] === 'u' ? up : down;
const column = +s.slice(1);
return new SortSelection(column, dir);
}
}
/** Type for a function that can be passed to Array.sort().
*
* It should always sort a column in an ascending direction.
*/
export type compareFunc<T> = (a: T, b: T)=> number;
/** An array of the sort functions for all the columns. Note that the index of a
* sort function does not need to correspond to location of a column on the
* display. Every value for the full length of columnSortFunctions should be
* populated, even if populated with a noop function, e.g. a function that
* return 0 for all inputs.
*/
export type columnSortFunctions<T> = compareFunc<T>[];
/**
* Keeps one SortSelection for each column being displayed. As the user clicks
* on columns the function `selectColumnToSortOn` can be called to keep
* `this.history` up to date.
*
* This enables better sorting behavior, i.e. when you click on col A to sort,
* then on col B to sort, if there are ties in col B they are broken by the
* existing order in col A, just like you would get when sorting by columns in a
* spreadsheet.
*
* This is not technically 'stable sort', while each sort action by the user
* looks like it is doing a stable sort, which is the goal, we are really doing
* an absolute sort based on a memory of all previous sort actions.
*/
export class SortHistory<T> {
/** Columns will be sorted by the first entry in history. If that yields a
* tie, then the second entry in history will be used to break the tie, etc.
*/
history: SortSelection[] = []
sortFunctions: columnSortFunctions<T> = []
constructor(sortFunctions: columnSortFunctions<T>) {
this.sortFunctions = sortFunctions;
this.history = this.sortFunctions.map((_, column) => new SortSelection(column, up));
}
/** Moves the selected column to the front of the list for sorting, and also
* reverses its current direction.
*/
selectColumnToSortOn(column: number): void {
// Remove the matching SortSelection from history.
let removed: SortSelection[] = [];
for (let i = 0; i < this.history.length; i++) {
if (column === this.history[i].column) {
removed = this.history.splice(i, 1);
break;
}
}
// Toggle its direction.
removed[0].toggleDirection();
// Then add back to the beginning of the list.
this.history.unshift(removed[0]);
}
/** compare is a compareFunc that sorts based on the state of all the
* SortSelections in history.
*/
compare(a: T, b: T): number {
let ret = 0;
// Call each compareFunc in `history` until one of them produces a non-zero
// result. If all calls return 0 then this compare function also returns 0.
this.history.some((sel: SortSelection) => {
ret = sel.directionMultiplier() * this.sortFunctions[sel.column](a, b);
return ret;
});
return ret;
}
/** Encodes the SortHistory as a string.
*
* The format is of all the serialized history members joined by
* dashes.
*/
encode(): string {
return this.history.map((sel: SortSelection) => sel.encode()).join('-');
}
/** Decodes a string previously encoded via this.encode() and uses it to set
* the history state. */
decode(s: string): void {
if (s === '') {
return;
}
const oldHistory = [...this.history];
this.history = s.split('-').map((encodedSortSelection: string) => SortSelection.decode(encodedSortSelection));
// Now add in all the members of oldHistory that don't appear in this.history.
oldHistory.forEach((oldSelection: SortSelection) => {
if (!this.history.some((sel: SortSelection) => sel.column === oldSelection.column)) {
this.history.push(oldSelection);
}
});
const isValid = this.history.every((ss: SortSelection): boolean => (ss.column >= 0) && (ss.column < this.sortFunctions.length));
if (!isValid) {
this.history = oldHistory;
}
}
}