import { computed, toValue } from 'vue'; import type { ComputedRef, MaybeRefOrGetter } from 'vue'; import { isFunction, isNumber, isObject, isString, isSymbol } from '@robonen/stdlib'; /** * Comparator deciding whether two array elements are considered equal. */ export type UseArrayDifferenceComparatorFn = (value: T, othVal: T) => boolean; export interface UseArrayDifferenceOptions { /** * When `true`, returns the symmetric difference: items present in exactly one * of the two arrays (`list` XOR `values`). When `false`, returns the * asymmetric difference: items in `list` that are not in `values`. * * @see https://en.wikipedia.org/wiki/Symmetric_difference * @default false */ symmetric?: boolean; /** * Custom comparator function, or a key of `T` to compare a single property by. */ comparator?: UseArrayDifferenceComparatorFn | keyof T; } export type UseArrayDifferenceReturn = ComputedRef; function isArrayDifferenceOptions(value: unknown): value is UseArrayDifferenceOptions { // isObject matches PLAIN objects only, so comparator functions/keys never reach here. return isObject(value) && ('symmetric' in value || 'comparator' in value); } /** * @name useArrayDifference * @category Array * @description Reactive difference of two arrays. Returns items in `list` that are not in `values` (asymmetric), or items in exactly one array (symmetric). Both arrays may be reactive (refs or getters). * * @param {MaybeRefOrGetter} list The source array * @param {MaybeRefOrGetter} values The array of values to subtract from `list` * @param {UseArrayDifferenceComparatorFn | keyof T | UseArrayDifferenceOptions} [comparator] A comparator function, a key of `T` to compare by, or an options object with `comparator`/`symmetric` * @param {UseArrayDifferenceOptions} [options] Extra options when `comparator` is a function or key * @returns {UseArrayDifferenceReturn} A computed array of the difference * * @example * const list = ref([1, 2, 3, 4, 5]); * const values = ref([2, 4]); * const diff = useArrayDifference(list, values); // [1, 3, 5] * * @example * const list = ref([{ id: 1 }, { id: 2 }, { id: 3 }]); * const values = ref([{ id: 2 }]); * const diff = useArrayDifference(list, values, 'id'); // [{ id: 1 }, { id: 3 }] * * @example * const a = ref([1, 2, 3]); * const b = ref([2, 3, 4]); * const symmetric = useArrayDifference(a, b, { symmetric: true }); // [1, 4] * * @since 0.0.15 */ export function useArrayDifference( list: MaybeRefOrGetter, values: MaybeRefOrGetter, comparator?: UseArrayDifferenceComparatorFn, options?: UseArrayDifferenceOptions, ): UseArrayDifferenceReturn; export function useArrayDifference( list: MaybeRefOrGetter, values: MaybeRefOrGetter, comparator?: keyof T, options?: UseArrayDifferenceOptions, ): UseArrayDifferenceReturn; export function useArrayDifference( list: MaybeRefOrGetter, values: MaybeRefOrGetter, options?: UseArrayDifferenceOptions, ): UseArrayDifferenceReturn; export function useArrayDifference( list: MaybeRefOrGetter, values: MaybeRefOrGetter, comparator?: UseArrayDifferenceComparatorFn | keyof T | UseArrayDifferenceOptions, options?: UseArrayDifferenceOptions, ): UseArrayDifferenceReturn { let symmetric = false; let resolved: UseArrayDifferenceComparatorFn | keyof T | undefined; if (isArrayDifferenceOptions(comparator)) { symmetric = comparator.symmetric ?? false; resolved = comparator.comparator; } else { resolved = comparator; symmetric = options?.symmetric ?? false; // An explicit comparator/key in `options` wins over the positional argument. if (options?.comparator !== undefined) resolved = options.comparator; } // Resolve the comparator once instead of rebuilding it on every recompute. let compare: UseArrayDifferenceComparatorFn; if (isString(resolved) || isSymbol(resolved) || isNumber(resolved)) { const key = resolved as keyof T; compare = (value, othVal) => value[key] === othVal[key]; } else if (isFunction(resolved)) { compare = resolved; } else { compare = (value, othVal) => value === othVal; } return computed(() => { const source = toValue(list); const other = toValue(values); // Items in `source` absent from `other`. const diff = source.filter(value => !other.some(othVal => compare(value, othVal))); if (!symmetric) return diff; // Items in `other` absent from `source`, appended for the symmetric difference. for (const value of other) { if (!source.some(srcVal => compare(value, srcVal))) diff.push(value); } return diff; }); }