import { create as diffCreate } from "jsondiffpatch"; import { type numberFormatOptions, REGEX_ISO8601_LOOSE } from './Atomic.ts'; import { diff, applyChangeset, type Changeset, type Options } from 'json-diff-ts'; // may want to return to this one day // but currently the jsondiffpatch formatter is the best console/ansi diff output for humans :( //import {DiffOptions, DiffOptionsColor, diff as jestDiff} from 'jest-diff'; import clone from "clone"; import ConsoleFormatter from "jsondiffpatch/formatters/console"; import assert from "node:assert"; import dayjs from "dayjs"; import { Traverse } from "neotraverse/modern"; import { serializeError } from "serialize-error"; const console = new ConsoleFormatter(); export const jdiff = diffCreate({ propertyFilter(name, context) { return name !== 'lifecycle'; }, cloneDiffValues: true //omitRemovedValues: true }); const diffOptions: Options = { /*arrayIdentityKeys: {artists: '$value'},*/ keysToSkip: ['playDate','playDateCompleted','listenRanges'] }; export const diffObjects = (a: object, b: object) => { return diff(a, b, diffOptions); } export const patchObject = (a: T, b: Changeset): T => { return applyChangeset(clone(a), b); } // const jestDiffOptions: DiffOptions = { // aAnnotation: 'Old', // bAnnotation: 'New', // aColor: chalk.red, // bColor: chalk.green // } export const diffObjectsConsoleOutput = (a: object, b: object, showUnchanged: boolean = false) => { //return jestDiff(a, b, jestDiffOptions); const left = JSON.parse(JSON.stringify(a)); return console.format(jdiff.diff(left, JSON.parse(JSON.stringify(b))), showUnchanged ? left : undefined); } export const formatNumber = (val: number | string, options?: numberFormatOptions) => { const { toFixed = 2, defaultVal = null, prefix = '', suffix = '', round, } = options || {}; let parsedVal = typeof val === 'number' ? val : Number.parseFloat(val); if (Number.isNaN(parsedVal)) { return defaultVal; } if (!Number.isFinite(val)) { return 'Infinite'; } let prefixStr = prefix; const { enable = false, indicate = true, type = 'round' } = round || {}; if (enable && !Number.isInteger(parsedVal)) { switch (type) { case 'round': parsedVal = Math.round(parsedVal); break; case 'ceil': parsedVal = Math.ceil(parsedVal); break; case 'floor': parsedVal = Math.floor(parsedVal); } if (indicate) { prefixStr = `~${prefix}`; } } const localeString = parsedVal.toLocaleString(undefined, { minimumFractionDigits: toFixed, maximumFractionDigits: toFixed, }); return `${prefixStr}${localeString}${suffix}`; }; export const generateArray = (size: number, gen: (index: number) => T): T[] => { return Array.from(Array(size), (v,k) => gen(k)); } /** Return an array in chunks * * https://stackoverflow.com/a/8495740/1469797 */ export const chunkArray = (chunkSize: number, arr: T[]): T[][] => { assert(chunkSize !== 0, 'chunkSize cannot be 0'); const chunks: T[][] = []; for (let i = 0; i < arr.length; i += chunkSize) { const chunk = arr.slice(i, i + chunkSize); chunks.push(chunk); } return chunks; } export const asDayjsHydratedObject = (obj: T): U => { const cloned = clone(obj); new Traverse(cloned).forEach((ctx, x) => { if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x), true); } }); return cloned as unknown as U; }; export const asErrorSerializedObject = (obj: T): U => { const cloned = clone(obj); new Traverse(cloned).forEach((ctx, x) => { if (x !== null && typeof x === 'object' && x instanceof Error) { ctx.update(serializeError(x), true); } }); return cloned as unknown as U; }; /** * Get indexes of all elements in array that make the function return true * * @see https://stackoverflow.com/a/20798567/1469797 */ export const getAllIndexes = (arr: T[], truthyFunc: (val: T) => boolean) => { const indexes = []; for(let i = 0; i < arr.length; i++) if (truthyFunc(arr[i])) indexes.push(i); return indexes; }; export const removeUndefinedKeys = >(obj: T, returnUndefined: boolean = true): T | undefined => { const newObj: any = {}; Object.keys(obj).forEach((key) => { if (Array.isArray(obj[key])) { newObj[key] = obj[key]; } else if (obj[key] === Object(obj[key])) { // dumb assign nested objects // bc they may be third party library-objects that use prototyping and we don't want to mess with newObj[key] = obj[key]; } else if (obj[key] !== undefined) { newObj[key] = obj[key]; } }); if (Object.keys(newObj).length === 0) { if (returnUndefined) { return undefined; } return newObj; } Object.keys(newObj).forEach(key => { if (newObj[key] === undefined || (null !== newObj[key] && typeof newObj[key] === 'object' && Object.keys(newObj[key]).length === 0)) { delete newObj[key]; } }); //Object.keys(newObj).forEach(key => newObj[key] === undefined || newObj[key] && delete newObj[key]) return newObj; }; export const pick = (obj: T, ...keys: K[]) => ( Object.fromEntries( keys .filter(key => key in obj) .map(key => [key, obj[key]]) ) as Pick );