libs/cdk/virtual-table/src/services/filterable/filter.worker.ts
/* eslint-disable unicorn/consistent-function-scoping */
import { Nullable, PlainObject, PlainObjectOf } from '@angular-ru/cdk/typings';
import { FilterGlobalOptions } from './filter-global-options';
import { FilterableMessage } from './filterable-message';
import { TableFilterType } from './table-filter-type';
type NumericFilterTypes =
| TableFilterType.LESS_OR_EQUAL
| TableFilterType.MORE_OR_EQUAL
| TableFilterType.LESS_THAN
| TableFilterType.MORE_THAN;
type PlainValue = string | number | boolean;
// TODO: should be refactor because duplicate code as sortWorker
// eslint-disable-next-line sonarjs/cognitive-complexity,max-lines-per-function
export function filterAllWorker<T>({ source, global, types, columns }: FilterableMessage<T>): T[] {
const INTERVAL_ARRAY_SIZE: number = 2;
const PLAIN_TYPES: Set<string> = new Set(['number', 'string', 'boolean']);
const { value: globalOperand, type: globalFilterType }: FilterGlobalOptions = global;
let result: T[] = source;
if (isFilled(globalOperand) && isNotNil(globalFilterType)) {
result = result.filter((item: T): boolean => filterColumnsTogether(item, globalOperand, globalFilterType));
}
if (!columns.isEmpty) {
result = result.filter((item: T): boolean => filterColumnsSeparately(item));
}
function filterColumnsTogether(item: T, operand: string, filterType: TableFilterType | string): boolean {
const flattenedItem: PlainObject = flatten(item);
return isSatisfying(Object.values(flattenedItem), operand, filterType);
}
// eslint-disable-next-line complexity
function filterColumnsSeparately(item: T): boolean {
for (const fieldKey of Object.keys(columns.values)) {
const fieldValue: Nullable<PlainObject | PlainValue> = getValueByPath(item, fieldKey);
const fieldOperand: Nullable<string> = columns.values[fieldKey];
const fieldFilterType: Nullable<TableFilterType> = columns.types[fieldKey];
if (isPlainValue(fieldValue) && isFilled(fieldOperand) && isNotNil(fieldFilterType)) {
const satisfies: boolean = isSatisfying(
Array.isArray(fieldValue) ? fieldValue : [fieldValue],
fieldOperand,
fieldFilterType
);
if (!satisfies) {
return false;
}
}
}
return true;
}
// eslint-disable-next-line complexity,max-lines-per-function
function isSatisfying(
valuesSet: Nullable<PlainValue>[],
operand: PlainValue,
filterType: TableFilterType | string
): boolean {
try {
switch (filterType) {
case types.START_WITH:
return valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.some(startsWith(toLowercase(operand)));
case types.END_WITH:
return valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.some(endsWith(toLowercase(operand)));
case types.CONTAINS:
return valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.some(includes(toLowercase(operand)));
case types.CONTAINS_ONE_OF_VALUES: {
const operandsToContain: string[] = String(operand)
.split(',')
.map((value: string): string => value.trim())
.filter((value: string): boolean => checkValueIsFilled(value))
.map((value: string): string => toLowercase(value));
return valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.some(includesOneOf(operandsToContain));
}
case types.DOES_NOT_CONTAIN:
return valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.every(notIncludes(toLowercase(operand)));
case types.EQUALS:
return valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.includes(toLowercase(operand));
case types.EQUALS_DATES_WITHOUT_TIME: {
if (
typeof operand === 'number' ||
typeof operand === 'string' ||
(operand as any) instanceof Date
) {
return valuesSet
.filter(
(element: Nullable<PlainValue>): element is number | string =>
typeof element === 'number' || typeof element === 'string'
)
.map((element: number | string): number => toDateWithoutTime(element).getTime())
.includes(toDateWithoutTime(operand as any).getTime());
} else {
return false;
}
}
case types.DOES_NOT_EQUAL:
return !valuesSet
.map((element: Nullable<PlainValue>): string => toLowercase(element))
.includes(toLowercase(operand));
case types.MORE_THAN:
case types.MORE_OR_EQUAL:
case types.LESS_THAN:
case types.LESS_OR_EQUAL:
return valuesSet
.filter((element: Nullable<PlainValue>): element is PlainValue => isFilled(element))
.some(compareNumber(operand, filterType));
case types.INTERVAL: {
const operandsArray: [PlainValue, PlainValue] = (Array.isArray(operand) &&
operand.length === INTERVAL_ARRAY_SIZE
? operand
: [null, null]) as any as [PlainValue, PlainValue];
return valuesSet
.filter((element: Nullable<PlainValue>): element is PlainValue => isFilled(element))
.some(isInInterval(operandsArray));
}
case types.IS_FILLED:
return valuesSet
.map((element: Nullable<PlainValue>): element is PlainValue => isFilled(element))
.includes(operand as boolean);
default:
return true;
}
} catch {
return false;
}
}
function toDateWithoutTime(value: string | number | Date): Date {
const date: Date = value instanceof Date ? value : new Date(value);
date.setHours(0, 0, 0, 0);
return date;
}
function toLowercase(value: Nullable<PlainValue>): string {
return value?.toString().trim().toLocaleLowerCase() ?? '';
}
function startsWith(prefix: string): (value: string) => boolean {
return (value: string): boolean => value.startsWith(prefix);
}
function endsWith(prefix: string): (value: string) => boolean {
return (value: string): boolean => value.endsWith(prefix);
}
function includes(substring: string): (value: string) => boolean {
return (value: string): boolean => value.includes(substring);
}
function includesOneOf(subStrings: string[]): (value: string) => boolean {
return (value: string): boolean => subStrings.some((substring: string): boolean => value.includes(substring));
}
function notIncludes(substring: string): (value: string) => boolean {
return (value: string): boolean => !value.includes(substring);
}
function checkValueIsFilled(value: string): boolean {
return value.length > 0;
}
function isInInterval(comparing: [PlainValue, PlainValue]): (value: PlainValue) => boolean {
return (value: PlainValue): boolean => comparing[0] <= value && value <= comparing[1];
}
function compareNumber(comparing: PlainValue, type: NumericFilterTypes): (value: PlainValue) => boolean {
const comparingNumber: number = asNumber(comparing);
switch (type) {
case types.MORE_THAN:
return (value: PlainValue): boolean => Number(value) > comparingNumber;
case types.MORE_OR_EQUAL:
return (value: PlainValue): boolean => Number(value) >= comparingNumber;
case types.LESS_THAN:
return (value: PlainValue): boolean => Number(value) < comparingNumber;
case types.LESS_OR_EQUAL:
return (value: PlainValue): boolean => Number(value) <= comparingNumber;
}
}
function asNumber(value: PlainValue): number {
const comparingNumber: number = Number(value);
if (isNaN(comparingNumber)) {
throw new Error('Operand is not a number');
} else {
return comparingNumber;
}
}
function flatten<K = string>(value: PlainObject, excludeKeys: string[] = []): PlainObjectOf<K> {
const depthGraph: PlainObjectOf<K> = {};
for (const key in value) {
if (value.hasOwnProperty(key) && !excludeKeys.includes(key)) {
mutate<K>(value, depthGraph, key);
}
}
return depthGraph;
}
function getValueByPath(object: PlainObject, path: string): Nullable<PlainObject | PlainValue> {
return path
.split('.')
.reduce(
(tempValue: Nullable<PlainObject | PlainValue>, key: string): Nullable<PlainObject | PlainValue> =>
(tempValue as any)?.[key],
object
);
}
function mutate<K>(object: PlainObject, depthGraph: PlainObjectOf<K>, key: string): void {
const isObject: boolean = typeof object[key] === 'object' && object[key] !== null;
if (isObject) {
const flatObject: PlainObject = flatten(object[key]);
for (const path in flatObject) {
if (flatObject.hasOwnProperty(path)) {
depthGraph[`${key}.${path}`] = flatObject[path];
}
}
} else {
depthGraph[key] = object[key];
}
}
function isPlainValue(value?: Nullable<PlainObject> | PlainValue): value is Nullable<PlainValue> {
return (
isNil(value) ||
PLAIN_TYPES.has(typeof value) ||
(Array.isArray(value) && value.every((element: unknown): boolean => PLAIN_TYPES.has(typeof element)))
);
}
function isFilled(value?: Nullable<PlainValue>): value is PlainValue {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return (
isNotNil(value) &&
((Array.isArray(value) && value.length > 0) || (value as PlainValue)?.toString().length > 0)
);
}
function isNil(value: Nullable<unknown>): value is null | undefined {
return !isNotNil(value);
}
function isNotNil<V>(value: Nullable<V>): value is V {
return value !== null && value !== undefined;
}
return result;
}