// tslint:disable:max-classes-per-file import { Iterable } from 'ix'; import * as moment from 'moment'; export interface Comparable { compareTo(other: T): number; } export function isComparable(obj: any): obj is Comparable { return (obj as Comparable).compareTo instanceof Function; } export type ValueComparison = (a: T, B: T) => number; export interface Comparer { compare: ValueComparison; } export class ValueComparer implements Comparer { public static displayName = 'ValueComparer'; public static readonly Default = new ValueComparer(); public static DefaultComparison(a: any, b: any) { if (a === b || (a == null && b == null)) { // both are null or the same, so equality is zero return 0; } else if (a == null || b == null) { // only one is null, non-null takes higher value return a == null ? -1 : 1; } else if (isComparable(a)) { // implements Comparable return a.compareTo(b); } else if (String.isString(a) && String.isString(b)) { // native string comparison return a.localeCompare(b); } else if (moment.isMoment(a) && moment.isMoment(b)) { return a.valueOf() - b.valueOf(); } else if (moment.isDuration(a) && moment.isDuration(b)) { return a.asMilliseconds() - b.asMilliseconds(); } else if (moment.isDate(a) && moment.isDate(b)) { return moment(a).valueOf() - moment(b).valueOf(); } else if (Object.isObject(a) || Object.isObject(b)) { // if either side is an object then we have failed referencial equality (first compare) return -1; } else { // fallback on a basic equality check const c: number | undefined = a - b; // it's possible that our basic check failed, so default to zero return c == null || isNaN(c) ? 0 : c; } } constructor( public readonly comparison: ValueComparison< T > = ValueComparer.DefaultComparison, ) {} compare(a: T, b: T) { return this.comparison(a, b); } } export function compare(a: any, b: any) { return ValueComparer.DefaultComparison(a, b) === 0; } export enum SortDirection { Ascending = 1, Descending = 2, } export type FieldValueSelector = ( source: TObj, field: string, ) => TValue; export interface FieldSelector { field: string; valueSelector?: FieldValueSelector; } export interface FieldComparer extends FieldSelector, Comparer {} export class ObjectComparer> { public static displayName = 'ObjectComparer'; public static DefaultComparerKey = ''; public static createFieldComparer( field: string, comparison: ValueComparison, valueSelector?: (source: TObj, field: string) => TValue, ) { return { field, compare: comparison, valueSelector, } as FieldComparer; } public readonly comparerMap: StringMap>; public readonly defaultComparer: FieldComparer | undefined; constructor( defaultSortField: string | undefined, ...comparers: Array> ); constructor(...comparers: Array>); constructor(...args: any[]) { const comparers: Array> = args; const defaultSortField = args[0] == null || !String.isNullOrEmpty(args[0]) ? args.shift() : undefined; this.comparerMap = {}; this.comparerMap[ ObjectComparer.DefaultComparerKey ] = ObjectComparer.createFieldComparer( ObjectComparer.DefaultComparerKey, ValueComparer.DefaultComparison, ); comparers.forEach(x => (this.comparerMap[x.field] = x)); if (defaultSortField != null) { this.defaultComparer = this.getComparer(defaultSortField); } } public getComparer(field?: string): FieldComparer { let comparer = this.comparerMap[field || ObjectComparer.DefaultComparerKey] || this.comparerMap[ObjectComparer.DefaultComparerKey]; if (String.isNullOrEmpty(comparer.field) && !String.isNullOrEmpty(field)) { comparer = Object.assign({}, comparer, { field }); } return comparer; } public getCompare(comparer: Comparer) { return comparer.compare || ValueComparer.DefaultComparison; } public getValue(source: T, comparer: FieldComparer) { return comparer.valueSelector == null ? source[comparer.field] : comparer.valueSelector(source, comparer.field); } public sortIterable( source: Iterable, field: string, direction: SortDirection = SortDirection.Ascending, ) { const comparer = this.getComparer(field); const defaultComparer = this.defaultComparer; if (direction === SortDirection.Ascending) { const orderedSource = source.orderBy( x => this.getValue(x, comparer), this.getCompare(comparer), ); source = orderedSource; if (defaultComparer != null) { source = orderedSource.thenBy( x => this.getValue(x, defaultComparer), this.getCompare(defaultComparer), ); } } else if (direction === SortDirection.Descending) { const orderedSource = source.orderByDescending( x => this.getValue(x, comparer), comparer.compare, ); source = orderedSource; if (defaultComparer != null) { source = orderedSource.thenByDescending( x => this.getValue(x, defaultComparer), this.getCompare(defaultComparer), ); } } return source; } }