@nullvoxpopuli/ember-composable-helpers
Version:
Composable helpers for Ember
209 lines (172 loc) • 4.9 kB
text/typescript
import { get } from '@ember/object';
import { isEmpty } from '@ember/utils';
import { helper } from '@ember/component/helper';
import EmberArray from '@ember/array';
import asArray from '../utils/as-array.ts';
const collator = new Intl.Collator(undefined, {
sensitivity: 'base',
});
function normalizeToBoolean(val: boolean | number | void): boolean {
if (typeof val === 'boolean') {
return val;
}
if (typeof val === 'number') {
if (val > 0) {
return false;
} else if (val < 0) {
return true;
}
}
return val as unknown as boolean;
}
function safeValueForKey(ctx: unknown, key: string) {
if (ctx === null || ctx === undefined) {
return ctx;
}
return get(ctx, key);
}
function sortDesc<T>(key: string, a: T, b: T) {
if (isEmpty(key)) {
return 0;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const aValue = safeValueForKey(a, key) as any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bValue = safeValueForKey(b, key) as any;
const isANullable = typeof aValue == 'undefined' || aValue === null;
const isBNullable = typeof bValue == 'undefined' || bValue === null;
if (isANullable && isBNullable) {
//both values are nullable
return 0;
}
if (isBNullable) {
// keep bValue last
return -1;
}
if (isANullable) {
// put aValue last
return 1;
}
if (aValue.toLowerCase && bValue.toLowerCase) {
return collator.compare(bValue, aValue);
}
if (aValue < bValue) {
return 1;
} else if (aValue > bValue) {
return -1;
}
return 0;
}
function sortAsc<T>(key: string, a: T, b: T) {
if (isEmpty(key)) {
return 0;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const aValue = safeValueForKey(a, key) as any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bValue = safeValueForKey(b, key) as any;
const isANullable = typeof aValue == 'undefined' || aValue === null;
const isBNullable = typeof bValue == 'undefined' || bValue === null;
if (isANullable && isBNullable) {
//both values are nullable
return 0;
}
if (isBNullable) {
// keep bValue last
return -1;
}
if (isANullable) {
// put aValue last
return 1;
}
if (aValue.toLowerCase && bValue.toLowerCase) {
return collator.compare(aValue, bValue);
}
if (aValue < bValue) {
return -1;
} else if (aValue > bValue) {
return 1;
}
return 0;
}
class SortBy<T> {
array: T[];
constructor(...args: [T[] | EmberArray<T>]) {
let [array] = args;
if (typeof (array as EmberArray<T>).slice === 'function') {
array = (array as EmberArray<T>).slice();
}
this.array = [...(array as T[])];
}
comparator(key: string | ((a: T, b: T) => number)) {
return typeof key === 'function' ? key : this.defaultSort(key);
}
defaultSort(sortKey: string) {
let func = sortAsc;
if (sortKey.match(':desc')) {
func = sortDesc;
}
return (a: T, b: T) => func(sortKey.replace(/:desc|:asc/, ''), a, b);
}
}
/**
* best O(n); worst O(n^2)
* If we feel like swapping with something more performant like QuickSort or MergeSort
* then it should be easy
*
* @class BubbleSort
* @extends SortBy
*/
class BubbleSort<T> extends SortBy<T> {
perform(keys: string[] = []) {
let swapped = false;
const compFuncs = keys.map((key) => this.comparator(key));
const compFunc = (a: T, b: T) => {
for (let i = 0; i < compFuncs.length; i += 1) {
const result = compFuncs[i]?.(a, b);
if (result === 0) {
continue;
}
return result;
}
return 0;
};
for (let i = 1; i < this.array.length; i += 1) {
for (let j = 0; j < this.array.length - i; j += 1) {
const shouldSwap = normalizeToBoolean(
compFunc(this.array[j + 1]!, this.array[j]!),
);
if (shouldSwap) {
[this.array[j], this.array[j + 1]] = [
this.array[j + 1]!,
this.array[j]!,
];
swapped = true;
}
}
// no need to continue sort if not swapped in any inner iteration
if (!swapped) {
return this.array;
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export function sortBy<T, K extends string | `${string}:desc`>(
params: [...K[], T[]],
) {
// slice params to avoid mutating the provided params
const sortParams = params.slice();
const array = asArray(sortParams.pop() as T[]);
let sortKeys = sortParams as K[];
if (!array || !sortKeys || sortKeys.length === 0) {
return [];
}
if (sortKeys.length === 1 && Array.isArray(sortKeys[0])) {
sortKeys = sortKeys[0];
}
const sortKlass = new BubbleSort(array);
sortKlass.perform(sortKeys);
return sortKlass.array;
}
export default helper(sortBy);