UNPKG

haystack-core

Version:
1,857 lines (1,846 loc) 503 kB
/* * Copyright (c) 2019, J2 Innovations. All Rights Reserved */ /** * The type of token */ var TokenType; (function (TokenType) { // End of file TokenType[TokenType["eof"] = 0] = "eof"; // Text TokenType[TokenType["text"] = 1] = "text"; // Paths TokenType[TokenType["paths"] = 2] = "paths"; // Value types TokenType[TokenType["string"] = 3] = "string"; TokenType[TokenType["number"] = 4] = "number"; TokenType[TokenType["date"] = 5] = "date"; TokenType[TokenType["time"] = 6] = "time"; TokenType[TokenType["uri"] = 7] = "uri"; TokenType[TokenType["ref"] = 8] = "ref"; TokenType[TokenType["boolean"] = 9] = "boolean"; TokenType[TokenType["symbol"] = 10] = "symbol"; // Operators TokenType[TokenType["equals"] = 11] = "equals"; TokenType[TokenType["notEquals"] = 12] = "notEquals"; TokenType[TokenType["lessThan"] = 13] = "lessThan"; TokenType[TokenType["lessThanOrEqual"] = 14] = "lessThanOrEqual"; TokenType[TokenType["greaterThan"] = 15] = "greaterThan"; TokenType[TokenType["greaterThanOrEqual"] = 16] = "greaterThanOrEqual"; TokenType[TokenType["leftBrace"] = 17] = "leftBrace"; TokenType[TokenType["rightBrace"] = 18] = "rightBrace"; // Relationship TokenType[TokenType["rel"] = 19] = "rel"; // Wildcard equality TokenType[TokenType["wildcardEq"] = 20] = "wildcardEq"; })(TokenType || (TokenType = {})); /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * A token object with some parsed text. */ class TokenObj { /** * The token type. */ type; /** * The token text value. */ text; /** * Flag used to identify a token object. */ _isATokenObj = true; /** * Contructs a new token value. * * @param type The token type. * @param text The token's text. * @param value The tokens value. */ constructor(type, text) { this.type = type; this.text = text; } /** * @returns The text value as a paths array. */ get paths() { return [this.text]; } /** * Returns true if the type matches this token's type. * * @param type The token type. * @return True if the type matches. */ is(type) { return this.type === type; } /** * Returns true if the object matches this one. * * @param type The token type. * @param text The text. * @return True if the objects are equal. */ equals(token) { if (!isTokenObj(token)) { return false; } return this.type === token.type && this.text === token.text; } /** * @returns A string representation of the token. */ toString() { return this.toFilter(); } /** * @returns The encoded value that can be used in a haystack filter. */ toFilter() { return this.text; } /** * @returns A JSON representation of the token. */ toJSON() { return { type: TokenType[this.type], text: this.text, }; } } /** * Test to see if the value is an instance of a token object. */ function isTokenObj(value) { return !!(value && value._isATokenObj); } /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * A collection of constant tokens. */ const tokens = { eof: new TokenObj(TokenType.eof, '<eof>'), equals: new TokenObj(TokenType.equals, '=='), notEquals: new TokenObj(TokenType.notEquals, '!='), lessThan: new TokenObj(TokenType.lessThan, '<'), lessThanOrEqual: new TokenObj(TokenType.lessThanOrEqual, '<='), greaterThan: new TokenObj(TokenType.greaterThan, '>'), greaterThanOrEqual: new TokenObj(TokenType.greaterThanOrEqual, '>='), leftBrace: new TokenObj(TokenType.leftBrace, '('), rightBrace: new TokenObj(TokenType.rightBrace, ')'), and: new TokenObj(TokenType.text, 'and'), or: new TokenObj(TokenType.text, 'or'), not: new TokenObj(TokenType.text, 'not'), wildcardEq: new TokenObj(TokenType.wildcardEq, '*=='), }; /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * Haystack kind enumeration. */ var Kind; (function (Kind) { Kind["Str"] = "str"; Kind["Number"] = "number"; Kind["Date"] = "date"; Kind["Time"] = "time"; Kind["Uri"] = "uri"; Kind["Ref"] = "ref"; Kind["Bool"] = "bool"; Kind["Dict"] = "dict"; Kind["DateTime"] = "dateTime"; Kind["Marker"] = "marker"; Kind["Remove"] = "remove"; Kind["NA"] = "na"; Kind["Coord"] = "coord"; Kind["XStr"] = "xstr"; Kind["Bin"] = "bin"; Kind["Symbol"] = "symbol"; Kind["List"] = "list"; Kind["Grid"] = "grid"; })(Kind || (Kind = {})); /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /* eslint @typescript-eslint/no-unused-vars: "off", @typescript-eslint/no-empty-function: "off" */ /** * An immutable JSON value. */ const JSON_MARKER = { _kind: Kind.Marker, }; let marker; /** * Haystack marker value. */ class HMarker { /** * Constructs a new haystack marker. */ constructor() { } /** * Makes a marker. * * @returns A marker instance. */ static make() { return marker ?? (marker = Object.freeze(new HMarker())); } /** * @returns The value's kind. */ getKind() { return Kind.Marker; } /** * Compares the value's kind. * * @param kind The kind to compare against. * @returns True if the kind matches. */ isKind(kind) { return valueIsKind(this, kind); } /** * Returns true if the haystack filter matches the value. * * @param filter The filter to test. * @param cx Optional haystack filter evaluation context. * @returns True if the filter matches ok. */ matches(filter, cx) { return valueMatches(this, filter, cx); } /** * Dump the value to the local console output. * * @param message An optional message to display before the value. * @returns The value instance. */ inspect(message) { return valueInspect(this, message); } /** * @returns A string representation of the value. */ toString() { return '✔'; } /** * @returns The zinc encoded string. */ valueOf() { return this.toZinc(); } /** * Encodes to an encoding zinc value. * * @returns The encoded zinc string. */ toZinc() { return 'M'; } /** * Encodes to an encoded zinc value that can be used * in a haystack filter string. * * A dict isn't supported in filter so throw an error. * * @returns The encoded value that can be used in a haystack filter. */ toFilter() { throw new Error(NOT_SUPPORTED_IN_FILTER_MSG); } /** * Value equality check. * * @param value The marker to compare. * @returns True if the value is the same. */ equals(value) { return valueIsKind(value, Kind.Marker); } /** * Compares two values. * * @param value The value to compare against. * @returns The sort order as negative, 0, or positive */ compareTo(value) { return valueIsKind(value, Kind.Marker) ? 0 : -1; } /** * @returns A JSON reprentation of the object. */ toJSON() { return JSON_MARKER; } /** * @returns A string containing the JSON representation of the object. */ toJSONString() { return JSON.stringify(this); } /** * @returns A byte buffer that has an encoded JSON string representation of the object. */ toJSONUint8Array() { return TEXT_ENCODER.encode(this.toJSONString()); } /** * @returns A JSON v3 representation of the object. */ toJSONv3() { return 'm:'; } /** * @returns An Axon encoded string of the value. */ toAxon() { return 'marker()'; } /** * @returns Returns the value instance. */ newCopy() { return this; } /** * @returns The value as a grid. */ toGrid() { return HGrid.make(this); } /** * @returns The value as a list. */ toList() { return HList.make([this]); } /** * @returns The value as a dict. */ toDict() { return HDict.make(this); } } /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * Haystack date. */ class HDate { /** * Interval value. */ #value; /** * Internal implementation. */ #date; /** * Constructs a new haystack date. * * @param value The value as a string, date object or an object * literal with year, month and day values. * @throws An error if the date is invalid. */ constructor(value) { if (value instanceof Date) { this.#value = HDate.getDateFromDateObj(value); this.#date = value; } else if (typeof value === 'string') { this.#value = value; this.#date = HDate.toJsDate(this.#value); } else if (value.val) { this.#value = value.val; this.#date = HDate.toJsDate(this.#value); } else { let year = 0; let month = 0; let day = 0; if (value && value.year) { const dateObj = value; year = dateObj.year; month = dateObj.month; day = dateObj.day; } let date = String(year); date += '-' + (month < 10 ? '0' : '') + month; date += '-' + (day < 10 ? '0' : '') + day; this.#value = date; this.#date = HDate.toJsDate(this.#value); this.validate(); } } static toJsDate(value) { const date = Date.parse(value + 'T00:00:00Z'); if (isNaN(date)) { throw new Error(`Invalid Date format ${value}`); } return new Date(date); } /** * Factory method for a haystack date. * * @param value The value as a string, date object or an object * literal with year, month and day values. * @returns The haystack date. */ static make(value) { if (valueIsKind(value, Kind.Date)) { return value; } else { return new HDate(value); } } /** * @returns The date value. */ get value() { return this.#value; } set value(value) { throw new Error(CANNOT_CHANGE_READONLY_VALUE); } /** * @returns The value's kind. */ getKind() { return Kind.Date; } /** * Compares the value's kind. * * @param kind The kind to compare against. * @returns True if the kind matches. */ isKind(kind) { return valueIsKind(this, kind); } /** * Returns true if the haystack filter matches the value. * * @param filter The filter to test. * @param cx Optional haystack filter evaluation context. * @returns True if the filter matches ok. */ matches(filter, cx) { return valueMatches(this, filter, cx); } /** * Dump the value to the local console output. * * @param message An optional message to display before the value. * @returns The value instance. */ inspect(message) { return valueInspect(this, message); } /** * Value equality check. * * @param value The value property. * @returns True if the value is the same. */ equals(value) { return (valueIsKind(value, Kind.Date) && value.#value === this.#value); } /** * Compares two dates. * * @param value The value to compare against. * @returns The sort order as negative, 0, or positive */ compareTo(value) { if (!valueIsKind(value, Kind.Date)) { return -1; } if (this.#value < value.#value) { return -1; } if (this.#value === value.#value) { return 0; } return 1; } /** * @returns A string representation of the value. */ toString() { return this.date.toLocaleDateString(); } /** * Encodes to an encoded zinc value that can be used * in a haystack filter string. * * The encoding for a haystack filter is mostly zinc but contains * some exceptions. * * @returns The encoded value that can be used in a haystack filter. */ toFilter() { return this.toZinc(); } /** * @returns The date as a string. */ valueOf() { return this.value; } /** * @returns A JS date object. */ get date() { return this.#date; } /** * @return Today's date. */ static now() { return HDate.make(new Date()); } /** * @returns The year for the date. */ get year() { return this.date.getUTCFullYear(); } set year(year) { throw new Error(CANNOT_CHANGE_READONLY_VALUE); } /** * @returns The month for the date. */ get month() { return this.date.getUTCMonth() + 1; } set month(month) { throw new Error(CANNOT_CHANGE_READONLY_VALUE); } /** * @returns The day for the date. */ get day() { return this.date.getUTCDate(); } set day(day) { throw new Error(CANNOT_CHANGE_READONLY_VALUE); } /** * Validate the internal year, month and day variables. */ validate() { let year; let month; let day; const res = /^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$/.exec(this.#value); if (res) { year = Number(res[1]); month = Number(res[2]); day = Number(res[3]); } if (!year || year < 1900) { throw new Error('Invalid year'); } if (!month || month < 1 || month > 12) { throw new Error('Invalid month'); } if (!day || day < 1 || day > 31) throw new Error('Invalid day'); } /** * Encodes to an encoding zinc value. * * @returns The encoded zinc string. */ toZinc() { return this.#value; } /** * Return the date from the JS date object. * * @param date The JS date object. * @returns The date string. * @throws An error if the date can't be found. */ static getDateFromDateObj(date) { return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`; } /** * @returns A JSON reprentation of the object. */ toJSON() { return { _kind: this.getKind(), val: this.#value, }; } /** * @returns A string containing the JSON representation of the object. */ toJSONString() { return JSON.stringify(this); } /** * @returns A byte buffer that has an encoded JSON string representation of the object. */ toJSONUint8Array() { return TEXT_ENCODER.encode(this.toJSONString()); } /** * @returns A JSON v3 representation of the object. */ toJSONv3() { return `d:${this.toZinc()}`; } /** * @returns An Axon encoded string of the value. */ toAxon() { return this.toZinc(); } /** * @returns Returns the value instance. */ newCopy() { return this; } /** * @returns The value as a grid. */ toGrid() { return HGrid.make(this); } /** * @returns The value as a list. */ toList() { return HList.make([this]); } /** * @returns The value as a dict. */ toDict() { return HDict.make(this); } } /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /* eslint @typescript-eslint/no-explicit-any: "off" */ /** * Holds all memoized cached values via a weakly referenced map. * * This enables cached values to persist against the lifetime of an object * until they're garbaged collected. This can be achieved without mutating the * original object that could possibly be frozen/immutable. */ const MEMOIZE_CACHES = new WeakMap(); /** * Memoization Cache. */ class MemoizeCache { /** * The internal cache map. */ #cache = new Map(); /** * Get an item from the cache. * * ```typescript * const myAge = getMemoizeCache(obj)?.get('age') * ``` * * @param name name of item to retrieve. * @returns item or undefined. */ get(name) { return this.#cache.get(name); } /** * Set an item in the cache. * * ```typescript * getMemoizeCache(obj)?.set('age', 123) * ``` * * @param name name of cached item. * @param value cached item. * @returns The cache item. */ set(name, value) { this.#cache.set(name, value); return value; } /** * Return true if the item exists in the cache. * ```typescript * const isInCache = !!getMemoizeCache(obj)?.has('age') * ``` * * @param name name of cached item to lookup. * @returns true if an item is in cache. */ has(name) { return this.#cache.has(name); } /** * Clear all entries from the cache. * * ```typescript * getMemoizeCache(obj)?.clear() * ``` */ clear() { this.#cache.clear(); } /** * Removes an item from the cache by name. * * ```typescript * getMemoizeCache(obj).delete('memoizedElementName') * ``` * * @param name cached item name to remove * @returns true if an item is successfully removed. */ remove(name) { return this.#cache.delete(name); } /** * Return the size of the cache. * * ```typescript * const size = getMemoizeCache(obj).?size ?? 0 * ``` * * @returns The size of the cache. */ get size() { return this.#cache.size; } /** * Returns if the cache is empty. * * @returns True if the cache is empty. */ isEmpty() { return this.size === 0; } /** * Return all of the cached keys. * * * ```typescript * const keys = getMemoizeCache(obj)?.keys() * ``` * * @returns The cache's keys. */ get keys() { return [...this.#cache.keys()]; } /** * Return a copy of the cache as an object. * * ```typescript * const obj = getMemoizeCache(obj)?.toObj() * ``` * * @returns The cached values. */ toObj() { const obj = {}; for (const key of this.keys) { obj[key] = this.get(key); } return obj; } /** * Dump the value to the local console output. * * @param message An optional message to display before the value. * @returns The value instance. */ inspect(message) { if (message) { console.log(String(message)); } const obj = {}; for (const key of this.keys) { obj[key] = String(this.get(key)); } console.table(obj); return this; } } /** * Return the memoize cache. * * @param obj The object to return the cache from. * @returns The memoize cache. */ function getCache(obj) { let cache = MEMOIZE_CACHES.get(obj); if (!cache) { MEMOIZE_CACHES.set(obj, (cache = new MemoizeCache())); } return cache; } /** * Return the memoized cache to use or undefined if it can't be found. * * @param obj The object to look up the cache from. * @returns The memoize cache or undefined if not found. */ function getMemoizeCache(obj) { return MEMOIZE_CACHES.get(obj); } /** * A property accessor decorator used for memoization of getters and methods. */ function memoize() { return function (target, context, descriptor) { let propKey = ''; let get; let value; let tc39 = false; if (typeof context === 'string') { propKey = context; get = descriptor?.get; value = descriptor?.value; } else if (context?.kind && context?.name && typeof context.name === 'string') { tc39 = true; // Support newer decorator standard (TC39). Found some issues // with certain build processes where we need to dynamically // support both standards. // https://github.com/tc39/proposal-decorators propKey = context.name; switch (context.kind) { case 'getter': get = target; break; case 'method': value = target; break; } } if (typeof get === 'function') { const getter = function () { const cache = getCache(this); return cache.has(propKey) ? cache.get(propKey) : cache.set(propKey, get.call(this)); }; return tc39 ? getter : { get: getter, }; } else if (typeof value === 'function') { const method = function (...args) { const cache = getCache(this); const key = JSON.stringify({ propKey, args }); return cache.has(key) ? cache.get(key) : cache.set(key, value.apply(this, args)); }; return tc39 ? method : { value: method, }; } else { throw new Error('Only class methods and getters can be memoized'); } }; } /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ var __decorate$3 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; /** * The central database where all units are registered and cached. */ class UnitDatabase { /** * All units keyed by their ids. */ #byId = new Map(); /** * All units organized by quantity. */ #quantities = new Map(); /** * All the registered units. */ #units = new Set(); /** * Define a unit within the database. * * @param unit The unit to define. */ define(unit) { for (const id of unit.ids) { this.#byId.set(id, unit); } const quantity = unit.quantity; if (quantity) { let units = this.#quantities.get(quantity); if (!units) { units = new Map(); this.#quantities.set(quantity, units); } units.set(unit.name, unit); } this.#units.add(unit); } /** * Get a unit via its id or undefined if it can't be found. * * @param id The unit's id. * @returns The unit or undefined. */ get(id) { return this.#byId.get(id); } /** * @returns All the registered units in the database. */ get units() { return Object.freeze([...this.#units.values()]); } /** * @returns A list of all the quantities. */ get quantities() { return [...this.#quantities.keys()]; } /** * Returns all the units for the specified quantity. * * @param quanity The quantity to search for. * @returns An array of units. */ getUnitsForQuantity(quanity) { const units = this.#quantities.get(quanity); return Object.freeze(units ? [...units.values()] : []); } /** * Return a unit that can be used when multiplying two numbers together. * * @param unit0 The first unit to multiply by. * @param unit1 The second unit to multiply by. * @return The unit to multiply by. * @throws An error if the units can't be multiplied. */ multiply(unit0, unit1) { // If either is dimensionless give up immediately. if (!unit0.dimensions || !unit1.dimensions) { throw new Error(`Cannot compute dimensionless ${unit0.name} * ${unit1.name}`); } // Compute dim/scale of a * b. const dim = unit0.dimensions.add(unit1.dimensions); const scale = unit0.scale * unit1.scale; // Find all matches. const matches = this.match(dim, scale); if (matches.length === 1) { return matches[0]; } // Right how our technique for resolving multiple matches is lame. const expectedName = `${unit0.name}_${unit1.name}`; for (const match of matches) { if (match.name === expectedName) { return match; } } // For now just give up throw new Error(`Cannot match to db ${unit0.name} * ${unit1.name}`); } /** * Return a unit that can be used when dividing two numbers together. * * @param unit0 The first unit to divide by. * @param unit1 The second unit to divide by. * @return The unit to divide by. * @throws An error if the units can't be divided. */ divide(unit0, unit1) { // If either is dimensionless give up immediately. if (!unit0.dimensions || !unit1.dimensions) { throw new Error(`Cannot compute dimensionless ${unit0.name} / ${unit1.name}`); } // Compute dim/scale of a / b. const dim = unit0.dimensions.subtract(unit1.dimensions); const scale = unit0.scale / unit1.scale; // Find all matches. const matches = this.match(dim, scale); if (matches.length === 1) { return matches[0]; } // Right how our technique for resolving multiple matches is lame. const expectedName = `${unit0.name}_${unit1.name}`; for (const match of matches) { if (match.name === expectedName) { return match; } } // For now just give up throw new Error(`Cannot match to db ${unit1.name} / ${unit1.name}`); } /** * Match the unit dimensions and scale against what's already registered * in the database. * * @param dim The dimension. * @param scale The scale. * @returns A list of matching units. */ match(dim, scale) { const units = []; for (const unit of this.#units) { if (unit.dimensions?.equals(dim) && HUnit.isApproximate(unit.scale, scale)) { units.push(unit); } } return units; } } __decorate$3([ memoize() ], UnitDatabase.prototype, "match", null); /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * A unit's dimension. * * https://fantom.org/doc/sys/Unit */ class UnitDimensions { kg; m; sec; K; A; mol; cd; constructor({ kg, m, sec, K, A, mol, cd }) { this.kg = kg ?? 0; this.m = m ?? 0; this.sec = sec ?? 0; this.K = K ?? 0; this.A = A ?? 0; this.mol = mol ?? 0; this.cd = cd ?? 0; } /** * Add this unit to another one and return the result. * * @param dim The dimension to add to this one. * @returns The new dimension. */ add(dim) { return new UnitDimensions({ kg: this.kg + dim.kg, m: this.m + dim.m, sec: this.sec + dim.sec, K: this.K + dim.K, A: this.A + dim.A, mol: this.mol + dim.mol, cd: this.cd + dim.cd, }); } /** * Subtract the units from one another and return the result. * * @param dim The dimension to subtract from this one. * @returns The new dimension. */ subtract(dim) { return new UnitDimensions({ kg: this.kg - dim.kg, m: this.m - dim.m, sec: this.sec - dim.sec, K: this.K - dim.K, A: this.A - dim.A, mol: this.mol - dim.mol, cd: this.cd - dim.cd, }); } /** * Dimension equality. * * @param dim The dimension to test for equality. * @returns True if the dimensions match. */ equals(dim) { if (!dim) { return false; } return (this.kg === dim.kg && this.m === dim.m && this.sec === dim.sec && this.K === dim.K && this.A === dim.A && this.mol === dim.mol && this.cd === dim.cd); } /** * @returns A JSON representation of the dimension. */ toJSON() { return { kg: this.kg, m: this.m, sec: this.sec, K: this.K, A: this.A, mol: this.mol, cd: this.cd, }; } } /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * The module cached unit database. */ let db; /** * @returns The unit database. */ function getDb() { // With two-way dependencies, it's always far safer to lazily create resources in TS/JS. return db ?? (db = new UnitDatabase()); } /** * Returns true if the unit is for bytes. * * @param unit The unit test. * @returns True if the unit is for bytes. */ function isByteUnit(unit) { return (unit.name === 'byte' || unit.name === 'kilobyte' || unit.name === 'megabyte' || unit.name === 'gigabyte' || unit.name === 'terabyte' || unit.name === 'petabyte'); } /** * A type guard for testing whether a value is an `HUnit`. * * @param value The value to test. * @returns True if the value is an `HUnit` instance. */ function valueIsHUnit(value) { return !!value?._isHUnit; } /** * A haystack unit. * * https://project-haystack.org/doc/Units */ class HUnit { #quantity; #ids; #dimensions; #scale; #offset; /** * Used for a type guard check. */ _isHUnit = true; constructor({ quantity, ids, dimensions, scale, offset }) { this.#quantity = quantity; this.#ids = ids; this.#dimensions = dimensions && new UnitDimensions(dimensions); this.#scale = scale; this.#offset = offset; } /** * Defines a unit in the database. * * @param data The unit instance or data. * @return The unit instance. * @throws An error if the unit is already registered. */ static define(data) { const unit = valueIsHUnit(data) ? data : new HUnit(data); getDb().define(unit); return unit; } /** * Return a unit from the database via its id or undefined if it can't be found. * * @param id The id of the unit to look up. * @returns The unit or undefined. */ static get(id) { return getDb().get(id); } /** * Clear the underlying unit database. */ static clearDatabase() { // Completely overwrite the database so we also // wipe out any memoization. db = undefined; } /** * Parse the database and return all of the units. * * @param text The text from `units.txt` to parse. * @returns All parsed units. */ static parseDatabase(text) { const units = []; let lastQuantity = ''; for (const line of text.split('\n')) { const quantity = this.parseQuantity(line); if (quantity) { lastQuantity = quantity; } else { const unit = this.parseUnit(line); if (unit) { // Record the last quantity if we have one. if (lastQuantity) { unit.quantity = lastQuantity; } units.push(unit); } } } return units; } /** * Parse some text to see if contains some quantity information. * Return the quantity or undefined if it can't be found. * * @param text The text to parse. * @returns The quantity or undefined if it can't be found. */ static parseQuantity(text) { const res = /^ *-- *([^(]+) */.exec(text); return ((res && res[1]) ?? '').trim(); } /** * Parse a line from `units.txt` as follows... * * unit := <ids> [";" <dim> [";" <scale> [";" <offset>]]] * names := <ids> ("," <id>)* * id := <idChar>* * idChar := 'a'-'z' | 'A'-'Z' | '_' | '%' | '/' | any char > 128 * dim := <ratio> ["*" <ratio>]* // no whitespace allowed * ratio := <base> <exp> * base := "kg" | "m" | "sec" | "K" | "A" | "mol" | "cd" * exp := <int> * scale := <float> * offset := <float> * * @param text The text to parse. * @return The unit data or undefined if no unit data is found. * @throws An error if unit data is found but the format is invalid. */ static parseUnit(text) { const [, ids, dimensions, scale, offset] = /^ *([a-zA-Z][^;]*) *;? *([^;]+)? *;? *([^;]+)? *;? *([^;]+)?/.exec(text) ?? []; if (!ids) { return undefined; } const data = { ids: this.parseIds(ids), scale: 1, offset: 0, }; if (dimensions) { data.dimensions = this.parseDimensions(dimensions); } if (scale) { data.scale = this.parseNumber(scale); } if (offset) { data.offset = this.parseNumber(offset); } return data; } static parseIds(ids) { return ids.split(',').map((id) => id.trim()); } static parseDimensions(dimensions) { const dimension = {}; dimensions.split('*').forEach((dim) => { const [, ratio, val] = /^ *(kg|sec|mol|m|K|A|cd) *([-+]?\d+)/.exec(dim) ?? []; switch (ratio) { case 'kg': dimension.kg = Number(val); break; case 'sec': dimension.sec = Number(val); break; case 'mol': dimension.mol = Number(val); break; case 'm': dimension.m = Number(val); break; case 'K': dimension.K = Number(val); break; case 'A': dimension.A = Number(val); break; case 'cd': dimension.cd = Number(val); break; } }); return Object.keys(dimension).length ? new UnitDimensions(dimension) : undefined; } static parseNumber(str) { const num = Number(str.trim()); if (isNaN(num) || typeof num !== 'number') { throw new Error(`Unable to convert '${str}' to a number`); } return num; } /** * @returns The ids for the units. */ get ids() { return this.#ids; } /** * @returns The unit's name. */ get name() { return this.#ids[0] ?? ''; } /** * @returns The unit's symbol. */ get symbol() { return this.#ids[this.#ids.length - 1] ?? ''; } /** * @returns The unit's scale. */ get scale() { return this.#scale; } /** * @returns The unit's offset. */ get offset() { return this.#offset; } /** * @returns The unit's quantity or undefined if none available. */ get quantity() { return this.#quantity; } /** * @returns A list of all the quantities available. */ static get quantities() { return getDb().quantities; } /** * Return the units for the quantity. * * @param quanity The quantity to search for. * @returns An array of quantities. An empty array is returned * if the quantity can't be found. */ static getUnitsForQuantity(quanity) { return getDb().getUnitsForQuantity(quanity); } /** * @returns All the registered units. */ static get units() { return getDb().units; } /** * @returns The unit's dimensions or undefined if dimensionless. */ get dimensions() { return this.#dimensions; } /** * Return a unit that can be used when multiplying two numbers together. * * @param unit The unit to multiply. * @return The unit to multiply by. * @throws An error if the units can't be multiplied. */ multiply(unit) { return getDb().multiply(this, unit); } /** * Return a unit that can be used when dividing two numbers together. * * @param unit The unit to divide. * @return The unit to divide by. * @throws An error if the units can't be divided. */ divide(unit) { return getDb().divide(this, unit); } /** * @returns The unit as a JSON object. */ toJSON() { const data = { ids: this.#ids, scale: this.#scale, offset: this.#offset, }; if (this.#quantity) { data.quantity = this.#quantity; } if (this.#dimensions) { data.dimensions = this.#dimensions?.toJSON(); } return data; } /** * Equality for units. * * @param unit The value to compare against. * @returns True if the units are equal. */ equals(unit) { if (!unit) { return false; } // Don't take into account `quantity` as this used // merely for organization. if (this.ids.length !== unit.ids.length) { return false; } for (let i = 0; i < this.ids.length; ++i) { if (this.ids[i] !== unit.ids[i]) { return false; } } if (this.#dimensions ? !this.#dimensions.equals(unit.dimensions) : unit.dimensions) { return false; } if (!HUnit.isApproximate(this.scale, unit.scale)) { return false; } if (this.offset !== unit.offset) { return false; } return true; } /** * @returns A string representation of a unit. */ toString() { return this.symbol; } /** * Dump the current state of the unit database to the console output. */ static inspectDb() { console.table(getDb().units.map((unit) => unit.toJSON())); } /** * Convert the unit to the new scalar. * * @param scalar The new scalar value. * @param to The new unit to convert too. * @returns The new scalar value. */ convertTo(scalar, to) { // Bytes have no dimension so handle as a special case. if (!(isByteUnit(this) && isByteUnit(to)) && !this.#dimensions?.equals(to.dimensions)) { throw new Error(`Inconvertible units: ${this} and ${to}`); } return (scalar * this.scale + this.offset - to.offset) / to.scale; } /** * Returns true if the two numbers approximately match. * * @param a The first number. * @param b The second number. * @returns True if the two numbers match. */ static isApproximate(a, b) { if (a === b) { return true; } // Pretty loose with our approximation because the database // doesn't have super great resolution for some normalizations. const t = Math.min(Math.abs(a / 1e3), Math.abs(b / 1e3)); return Math.abs(a - b) <= t; } } /* * Copyright (c) 2023, J2 Innovations. All Rights Reserved */ /** * @module * * Defines all the time units required for number comparison. * * Please note these units were originally created in `haystack-units` whereby * all the units are dynamically created from the Fantom unit database at * build time. */ HUnit.define({ ids: ['nanosecond', 'ns'], scale: 1e-9, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['microsecond', 'µs'], scale: 0.000001, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); const millisecond = HUnit.define({ ids: ['millisecond', 'ms'], scale: 0.001, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['hundredths_second', 'cs'], scale: 0.01, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['tenths_second', 'ds'], scale: 0.1, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['second', 'sec', 's'], scale: 1, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['minute', 'min'], scale: 60, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['hour', 'hr', 'h'], scale: 3600, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['day'], scale: 86400, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['week', 'wk'], scale: 604800, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['julian_month', 'mo'], scale: 2629800, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); HUnit.define({ ids: ['year', 'yr'], scale: 31536000, offset: 0, dimensions: { kg: 0, m: 0, sec: 1, K: 0, A: 0, mol: 0, cd: 0 }, quantity: 'time', }); /* * Copyright (c) 2020, J2 Innovations. All Rights Reserved */ /** * The default numeric precision. */ const DEFAULT_PRECISION = 1; let zeroNoUnitsNum; const POSITIVE_INFINITY_ZINC = 'INF'; const NEGATIVE_INFINITY_ZINC = '-INF'; const NOT_A_NUMBER_ZINC = 'NaN'; /** * Haystack number with units. */ class HNum { /** * The numerical value. */ #value; /** * The unit symbol for the number. */ #unitSymbol; /** * Constructs a new haystack number. * * @param value The value. * @param unit Optional units. */ constructor(value, unit) { this.#value = value; if (unit) { if (typeof unit === 'string') { this.#unitSymbol = unit; } else { const symbol = unit.symbol; if (!HUnit.get(symbol)) { HUnit.define(unit); } this.#unitSymbol = symbol; } } else { this.#unitSymbol = ''; } } /** * Makes a haystack number. * * @param value The value or a hayson number object. * @param unit Optional units. * @returns A haystack number. */ static make(value, unit) { let val = 0; if (typeof value === 'number') { val = value; } else if (valueIsKind(value, Kind.Number)) { return value; } else { const obj = value; switch (typeof obj.val) { case 'string': if (obj.val === POSITIVE_INFINITY_ZINC) { val = Number.POSITIVE_INFINITY; } else if (obj.val === NEGATIVE_INFINITY_ZINC) { val = Number.NEGATIVE_INFINITY; } else if (obj.val === NOT_A_NUMBER_ZINC) { val = Number.NaN; } else { throw new Error('Invalid hayson number string value'); } break; case 'number': val = obj.val; break; default: throw new Error('Invalid hayson number value'); } unit = obj.unit; } return val === 0 && !unit ? zeroNoUnitsNum ?? Object.freeze((zeroNoUnitsNum = new HNum(val, unit))) : new HNum(val, unit); } /** * @returns The numeric value. */ get value() { return this.#value; } set value(value) { throw new Error(CANNOT_CHANGE_READONLY_VALUE); } /** * @returns Optional unit value for a number. */ get unit() { // Always lazily look up the unit from the unit database // just in case the unit was defined after the number was loaded. // This can happen as the loading of the unit database is decoupled // from the core haystack value type system. if (!this.#unitSymbol) { return undefined; } let unit = HUnit.get(this.#unitSymbol); if (unit) { return unit; } unit = new HUnit({ ids: [this.#unitSymbol], scale: 1, offset: 0 }); HUnit.define(unit); return unit; } set unit(unit) { throw new Error(CANNOT_CHANGE_READONLY_VALUE); } /** * @returns The value's kind. */ getKind() { return Kind.Number; } /** * Compares the value's kind. * * @param kind The kind to compare against. * @returns True if the kind matches. */ isKind(kind) { return valueIsKind(this, kind); } /** * Returns true if the haystack filter matches the value. * * @param filter The filter to test. * @param cx Optional haystack filter evaluation context. * @returns True if the filter matches ok. */ matches(filter, cx) { return valueMatches(this, filter, cx); } /** * Dump the value to the local console output. * * @param message An optional message to display before the value. * @returns The value instance. */ inspect(message) { return valueInspect(this, message); } /** * @returns The object's value as a number. */ valueOf() { return this.#value; } /** * Return the number as a readable string. * * @param params The number format options. Can also be a number for precision * to support backwards compatibility. * @returns A string representation of the value. */ toString(options) { let precision = DEFAULT_PRECISION; let locale; if (typeof options === 'number') { precision = options; } else if (options) { precision = options.precision; locale = options.locale; } if (this.value === Number.POSITIVE_INFINITY) { return POSITIVE_INFINITY_ZINC; } else if (this.value === Number.NEGATIVE_INFINITY) { return NEGATIVE_INFINITY_ZINC; } else if (isNaN(this.value)) { return NOT_A_NUMBER_ZINC; } else { const value = this.value.toLocaleString(locale, { style: 'decimal', maximumFractionDigits: precision, minimumFractionDigits: 0, }); return this.#unitSymbol ? value + this.#unitSymbol : value; } } /** * Encodes to an encoded zinc value that can be used * in a haystack filter string. * * The encoding for a haystack filter is mostly zinc but contains * some exceptions. * * @returns The encoded value that can be used in a haystack filter. */ toFilter() { if (this.value === Number.POSITIVE_INFINITY || this.value === Number.NEGATIVE_INFINITY || isNaN(this.value)) { throw new Error('Numeric INF, -INF and NaN not supported in filter'); } return this.toZinc(); } /** * Encodes to an encoding zinc value. * * @returns The encoded zinc string. */ toZinc() { return this.encodeToZinc(/*unitSeparator*/ ''); } /** * Returns a number encoded as zinc. * * @param unitSeparator The separator to use between the unit and the number when encoding. * @returns The encoded number. */ encodeToZinc(separator) { if (this.value === Number.POSITIVE_INFINITY) { return POSITIVE_INFINITY_ZINC; } else if (this.value === Number.NEGATIVE_INFINITY) { return NEGATIVE_