import {hasOwnProperty, isNumber, isString, splitAccessPath, stringValue, writeConfig, isObject} from 'vega-util'; import {isLogicalAnd, isLogicalNot, isLogicalOr, LogicalComposition} from './logical.js'; export const duplicate = structuredClone; export function never(message: string): never { throw new Error(message); } /** * Creates an object composed of the picked object properties. * * var object = {'a': 1, 'b': '2', 'c': 3}; * pick(object, ['a', 'c']); * // → {'a': 1, 'c': 3} */ export function pick(obj: T, props: readonly K[]): Pick { const copy: any = {}; for (const prop of props) { if (hasOwnProperty(obj, prop)) { copy[prop] = obj[prop]; } } return copy; } /** * The opposite of _.pick; this method creates an object composed of the own * and inherited enumerable string keyed properties of object that are not omitted. */ export function omit(obj: T, props: readonly K[]): Omit { const copy = {...(obj as any)}; for (const prop of props) { delete copy[prop]; } return copy; } /** * Monkey patch Set so that `stringify` produces a string representation of sets. */ (Set.prototype as any)['toJSON'] = function () { return `Set(${[...this].map((x) => stringify(x)).join(',')})`; }; /** * Converts any object to a string of limited size, or a number. */ export function hash(a: any): string | number { if (isNumber(a)) { return a; } const str = isString(a) ? a : stringify(a); // short strings can be used as hash directly, longer strings are hashed to reduce memory usage if (str.length < 250) { return str; } // from http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ let h = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); h = (h << 5) - h + char; h = h & h; // Convert to 32bit integer } return h; } export function isNullOrFalse(x: any): x is false | null { return x === false || x === null; } export function contains(array: readonly T[], item: T) { return array.includes(item); } /** * Returns true if any item returns true. */ export function some(arr: readonly T[], f: (d: T, k?: any, i?: any) => boolean) { let i = 0; for (const [k, a] of arr.entries()) { if (f(a, k, i++)) { return true; } } return false; } /** * Returns true if all items return true. */ export function every(arr: readonly T[], f: (d: T, k?: any, i?: any) => boolean) { let i = 0; for (const [k, a] of arr.entries()) { if (!f(a, k, i++)) { return false; } } return true; } /** * Like TS Partial but applies recursively to all properties. */ export type DeepPartial = {[P in keyof T]?: DeepPartial}; /** * recursively merges src into dest */ export function mergeDeep(dest: T, ...src: readonly DeepPartial[]): T { for (const s of src) { deepMerge_(dest, s ?? {}); } return dest; } function deepMerge_(dest: any, src: any) { for (const property of keys(src)) { writeConfig(dest, property, src[property], true); } } export function unique(values: readonly T[], f: (item: T) => string | number): T[] { const results: T[] = []; const u = {}; let v: string | number; for (const val of values) { v = f(val); if (v in u) { continue; } (u as any)[v] = 1; results.push(val); } return results; } export type Dict = Record; /** * Returns true if the two dictionaries agree. Applies only to defined values. */ export function isEqual(dict: Dict, other: Dict) { const dictKeys = keys(dict); const otherKeys = keys(other); if (dictKeys.length !== otherKeys.length) { return false; } for (const key of dictKeys) { if (dict[key] !== other[key]) { return false; } } return true; } export function setEqual(a: Set, b: Set) { if (a.size !== b.size) { return false; } for (const e of a) { if (!b.has(e)) { return false; } } return true; } export function hasIntersection(a: ReadonlySet, b: ReadonlySet) { for (const key of a) { if (b.has(key)) { return true; } } return false; } export function prefixGenerator(a: ReadonlySet): ReadonlySet { const prefixes = new Set(); for (const x of a) { const splitField = splitAccessPath(x); // Wrap every element other than the first in `[]` const wrappedWithAccessors = splitField.map((y, i) => (i === 0 ? y : `[${y}]`)); const computedPrefixes = wrappedWithAccessors.map((_, i) => wrappedWithAccessors.slice(0, i + 1).join('')); for (const y of computedPrefixes) { prefixes.add(y); } } return prefixes; } /** * Returns true if a and b have an intersection. Also return true if a or b are undefined * since this means we don't know what fields a node produces or depends on. */ export function fieldIntersection(a: ReadonlySet, b: ReadonlySet): boolean { if (a === undefined || b === undefined) { return true; } return hasIntersection(prefixGenerator(a), prefixGenerator(b)); } export function isEmpty(obj: object) { return keys(obj).length === 0; } // This is a stricter version of Object.keys but with better types. See https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208 export const keys = Object.keys as (o: T) => Extract[]; // Stricter version from https://github.com/microsoft/TypeScript/issues/51572#issuecomment-1319153323 export const vals = Object.values as (obj: T) => Array; // Stricter version from https://github.com/microsoft/TypeScript/issues/51572#issuecomment-1319153323 export const entries = Object.entries as (obj: T) => Array<[keyof T, T[keyof T]]>; // Using mapped type to declare a collect of flags for a string literal type S // https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types export type Flag = {[K in S]: 1}; export function isBoolean(b: any): b is boolean { return b === true || b === false; } /** * Convert a string into a valid variable name */ export function varName(s: string): string { // Replace non-alphanumeric characters (anything besides a-zA-Z0-9_) with _ const alphanumericS = s.replace(/\W/g, '_'); // Add _ if the string has leading numbers. return (s.match(/^\d+/) ? '_' : '') + alphanumericS; } export function logicalExpr(op: LogicalComposition, cb: (...args: readonly any[]) => string): string { if (isLogicalNot(op)) { return `!(${logicalExpr(op.not, cb)})`; } else if (isLogicalAnd(op)) { return `(${op.and.map((and: LogicalComposition) => logicalExpr(and, cb)).join(') && (')})`; } else if (isLogicalOr(op)) { return `(${op.or.map((or: LogicalComposition) => logicalExpr(or, cb)).join(') || (')})`; } else { return cb(op); } } /** * Delete nested property of an object, and delete the ancestors of the property if they become empty. */ export function deleteNestedProperty(obj: any, orderedProps: string[]) { if (orderedProps.length === 0) { return true; } const prop = orderedProps.shift()!; if (prop in obj && deleteNestedProperty(obj[prop], orderedProps)) { delete obj[prop]; } return isEmpty(obj); } export function titleCase(s: string) { return s.charAt(0).toUpperCase() + s.substr(1); } /** * Converts a path to an access path with datum. * @param path The field name. * @param datum The string to use for `datum`. */ export function accessPathWithDatum(path: string, datum = 'datum') { const pieces = splitAccessPath(path); const prefixes = []; for (let i = 1; i <= pieces.length; i++) { const prefix = `[${pieces.slice(0, i).map(stringValue).join('][')}]`; prefixes.push(`${datum}${prefix}`); } return prefixes.join(' && '); } /** * Return access with datum to the flattened field. * * @param path The field name. * @param datum The string to use for `datum`. */ export function flatAccessWithDatum(path: string, datum: 'datum' | 'parent' | 'datum.datum' = 'datum') { return `${datum}[${stringValue(splitAccessPath(path).join('.'))}]`; } /** * Return access with datum to **an unescaped path**. * * ```ts * console.log(accessWithDatumToUnescapedPath("vega's favorite")) * // "datum['vega\\'s favorite']" * ``` * * @param path The unescaped path name. E.g., `"a.b"`, `"vega's favorite"`. (Note * that the field defs take escaped strings like `"a\\.b"`, `"vega\\'s favorite"`, * but this function is for the unescaped field/path) */ export function accessWithDatumToUnescapedPath(unescapedPath: string) { const singleQuoteEscapedPath = unescapedPath.replaceAll("'", "\\'"); return `datum['${singleQuoteEscapedPath}']`; } function escapePathAccess(string: string) { return string.replace(/(\[|\]|\.|'|")/g, '\\$1'); } /** * Replaces path accesses with access to non-nested field. * For example, `foo["bar"].baz` becomes `foo\\.bar\\.baz`. */ export function replacePathInField(path: string) { return `${splitAccessPath(path).map(escapePathAccess).join('\\.')}`; } /** * Replace all occurrences of a string with another string. * * @param string the string to replace in * @param find the string to replace * @param replacement the replacement */ export function replaceAll(string: string, find: string, replacement: string) { return string.replace(new RegExp(find.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), replacement); } /** * Remove path accesses with access from field. * For example, `foo["bar"].baz` becomes `foo.bar.baz`. */ export function removePathFromField(path: string) { return `${splitAccessPath(path).join('.')}`; } /** * Count the depth of the path. Returns 1 for fields that are not nested. */ export function accessPathDepth(path: string) { if (!path) { return 0; } return splitAccessPath(path).length; } /** * This is a replacement for chained || for numeric properties or properties that respect null so that 0 will be included. */ export function getFirstDefined(...args: readonly T[]): T | undefined { return args.find((a) => a !== undefined); } // variable used to generate id let idCounter = 42; /** * Returns a new random id every time it gets called. * * Has side effect! */ export function uniqueId(prefix?: string) { const id = ++idCounter; return prefix ? String(prefix) + id : id; } /** * Resets the id counter used in uniqueId. This can be useful for testing. */ export function resetIdCounter() { idCounter = 42; } export function internalField(name: string) { return isInternalField(name) ? name : `__${name}`; } export function isInternalField(name: string) { return name.startsWith('__'); } /** * Normalize angle to be within [0,360). */ export function normalizeAngle(angle: number) { if (angle === undefined) { return undefined; } return ((angle % 360) + 360) % 360; } /** * Returns whether the passed in value is a valid number. */ export function isNumeric(value: number | string): boolean { if (isNumber(value)) { return true; } return !isNaN(value as any) && !isNaN(parseFloat(value)); } const clonedProto = Object.getPrototypeOf(structuredClone({})); /** * Compares two values for equality, including arrays and objects. * * Adapted from https://github.com/epoberezkin/fast-deep-equal. */ export function deepEqual(a: any, b: any) { if (a === b) return true; if (a && b && typeof a == 'object' && typeof b == 'object') { // compare names to avoid issues with structured clone if (a.constructor.name !== b.constructor.name) return false; let length; let i: number; if (Array.isArray(a)) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0; ) if (!deepEqual(a[i], b[i])) return false; return true; } if (a instanceof Map && b instanceof Map) { if (a.size !== b.size) return false; for (const e of a.entries()) if (!b.has(e[0])) return false; for (const e of a.entries()) if (!deepEqual(e[1], b.get(e[0]))) return false; return true; } if (a instanceof Set && b instanceof Set) { if (a.size !== b.size) return false; for (const e of a.entries()) if (!b.has(e[0])) return false; return true; } if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { length = (a as any).length; if (length != (b as any).length) return false; for (i = length; i-- !== 0; ) if ((a as any)[i] !== (b as any)[i]) return false; return true; } if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; // also compare to structured clone prototype if (a.valueOf !== Object.prototype.valueOf && a.valueOf !== clonedProto.valueOf) return a.valueOf() === b.valueOf(); if (a.toString !== Object.prototype.toString && a.toString !== clonedProto.toString) return a.toString() === b.toString(); const ks = Object.keys(a); length = ks.length; if (length !== Object.keys(b).length) return false; for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, ks[i])) return false; for (i = length; i-- !== 0; ) { const key = ks[i]; if (!deepEqual(a[key], b[key])) return false; } return true; } // true if both NaN, false otherwise return a !== a && b !== b; } /** * Converts any object to a string representation that can be consumed by humans. * * Adapted from https://github.com/epoberezkin/fast-json-stable-stringify */ export function stringify(data: any) { const seen: any[] = []; return (function _stringify(node: any) { if (node?.toJSON && typeof node.toJSON === 'function') { node = node.toJSON(); } if (node === undefined) return undefined; if (typeof node == 'number') return isFinite(node) ? `${node}` : 'null'; if (typeof node !== 'object') return JSON.stringify(node); let i; let out; if (Array.isArray(node)) { out = '['; for (i = 0; i < node.length; i++) { if (i) out += ','; out += _stringify(node[i]) || 'null'; } return `${out}]`; } if (node === null) return 'null'; if (seen.includes(node)) { throw new TypeError('Converting circular structure to JSON'); } const seenIndex = seen.push(node) - 1; const ks = Object.keys(node).sort(); out = ''; for (i = 0; i < ks.length; i++) { const key = ks[i]; const value = _stringify(node[key]); if (!value) continue; if (out) out += ','; out += `${JSON.stringify(key)}:${value}`; } seen.splice(seenIndex, 1); return `{${out}}`; })(data); } /** * Check if the input object has the property and it's not undefined. * * @param object the object * @param property the property to search * @returns if the object has the property and it's not undefined. */ export function hasProperty(obj: T, key: string | number | symbol): key is keyof T { return isObject(obj) && hasOwnProperty(obj, key) && (obj as any)[key] !== undefined; }