haystack-core
Version:
Project Haystack Core
1,857 lines (1,846 loc) • 503 kB
JavaScript
/*
* 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_