You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/common/SortFunc.ts

89 lines
3.5 KiB

/**
* SortFunc class interprets the sortSpec (as saved in viewSection.sortColRefs), exposing a
* compare(rowId1, rowId2) function that can be used to actually sort rows in a view.
*
* TODO: When an operation (such as a paste) would cause rows to jump in the sort order, this
* class should support freezing of row positions until the user chooses to re-sort. This is not
* currently implemented.
*/
import {ColumnGetters} from 'app/common/ColumnGetters';
import {nativeCompare} from 'app/common/gutil';
/**
* Compare two cell values, paying attention to types and values. Note that native JS comparison
* can't be used for sorting because it isn't transitive across types (e.g. both 1 < "2" and "2" <
* "a" are true, but 1 < "a" is false.). In addition, we handle complex values represented in
* Grist as arrays.
*
* Note that we need to handle different types of values regardless of the column type,
* because e.g. a numerical column may contain text (alttext) or null values.
*/
export function typedCompare(val1: any, val2: any): number {
// TODO: We should use Intl.Collator for string comparisons to handle accented strings.
let type1, array1;
return nativeCompare(type1 = typeof val1, typeof val2) ||
// We need to worry about Array comparisons because formulas returing Any may return null or
// object values represented as arrays (e.g. ['D', ...] for dates). Comparing those without
// distinguishing types would break the sort. Also, arrays need a special comparator.
(type1 === 'object' &&
(nativeCompare(array1 = val1 instanceof Array, val2 instanceof Array) ||
(array1 && _arrayCompare(val1, val2)))) ||
nativeCompare(val1, val2);
}
function _arrayCompare(val1: any[], val2: any[]): number {
for (let i = 0; i < val1.length; i++) {
if (i >= val2.length) {
return 1;
}
const value = nativeCompare(val1[i], val2[i]);
if (value) {
return value;
}
}
return val1.length === val2.length ? 0 : -1;
}
type ColumnGetter = (rowId: number) => any;
/**
* getters is an implementation of app.common.ColumnGetters
*/
export class SortFunc {
// updateSpec() or updateGetters() can populate these fields, used by the compare() method.
private _colGetters: ColumnGetter[] = []; // Array of column getters (mapping rowId to column value)
private _ascFlags: number[] = []; // Array of 1 (ascending) or -1 (descending) flags.
constructor(private _getters: ColumnGetters) {}
public updateSpec(sortSpec: number[]): void {
// Prepare an array of column getters for each column in sortSpec.
this._colGetters = sortSpec.map(colRef => {
return this._getters.getColGetter(Math.abs(colRef));
}).filter(getter => getter) as ColumnGetter[];
// Collect "ascending" flags as an array of 1 or -1, one for each column.
this._ascFlags = sortSpec.map(colRef => (colRef >= 0 ? 1 : -1));
const manualSortGetter = this._getters.getManualSortGetter();
if (manualSortGetter) {
this._colGetters.push(manualSortGetter);
this._ascFlags.push(1);
}
}
/**
* Returns 1 or -1 depending on whether rowId1 should be shown before rowId2.
*/
public compare(rowId1: number, rowId2: number): number {
for (let i = 0, len = this._colGetters.length; i < len; i++) {
const getter = this._colGetters[i];
const value = typedCompare(getter(rowId1), getter(rowId2));
if (value) {
return value * this._ascFlags[i];
}
}
return nativeCompare(rowId1, rowId2);
}
}