@qudtlib/core
Version:
Data model for QUDTLib
804 lines (803 loc) • 33 kB
JavaScript
import { Unit } from "./unit.js";
import { arrayContains, arrayDeduplicate, arrayMin, BooleanComparator, compareUsingEquals, findInIterable, NumberComparator, StringComparator, ONE, ZERO, isNullish, } from "./utils.js";
import { Prefix } from "./prefix.js";
import { DerivedUnitSearchMode } from "./derivedUnitSearchMode.js";
import { FactorUnit } from "./factorUnit.js";
import { FactorUnits } from "./factorUnits.js";
import { QuantityValue } from "./quantityValue.js";
import { QudtNamespaces } from "./qudtNamespaces.js";
export class QudtlibConfig {
constructor() {
this.units = new Map();
this.quantityKinds = new Map();
this.prefixes = new Map();
this.systemsOfUnits = new Map();
this.unitsByDimensionVector = new Map();
}
indexUnitsByDimensionVector() {
this.units.forEach((u) => {
const dimVector = u.dimensionVectorIri;
if (!isNullish(dimVector)) {
let unitsWithSameDimVector = this.unitsByDimensionVector.get(dimVector);
if (isNullish(unitsWithSameDimVector)) {
unitsWithSameDimVector = [];
this.unitsByDimensionVector.set(dimVector, unitsWithSameDimVector);
}
unitsWithSameDimVector.push(u);
}
});
}
}
export const config = new QudtlibConfig();
export class Qudt {
/**
* Returns the {@link Unit} identified the specified IRI. For example, <code>
* unit("http://qudt.org/vocab/unit/N-PER-M2")</code> yields `Units.N__PER__M2`,
* if that unit has been loaded;
*
* @param iri the requested unit IRI
* @return the unit or `undefined` if no unit is found
*/
static unit(unitIri) {
return config.units.get(unitIri);
}
/**
* Same as {@link #unit(string)} but throws an exception if no unit is found.
* @param unitIri the unit IRI
* @return the unit
*/
static unitRequired(unitIri) {
const ret = Qudt.unit(unitIri);
if (typeof ret === "undefined") {
throw `Unit ${unitIri} not found`;
}
return ret;
}
/**
* Returns the first unit found whose label matches the specified label after replacing any
* underscore with space and ignoring case (US locale). If more intricate matching is needed,
* clients can use `{@link #allUnits()}.filter(...)`.
*
* @param label the matched label
* @return the first unit found
*/
static unitFromLabel(label) {
const matcher = new CaseInsensitiveUnderscoreIgnoringLabelMatcher(label);
const firstMatch = findInIterable(config.units.values(), (u) => matcher.matchesLangStrings(u.labels) ||
(!!u.currencyCode && matcher.matchesString(u.currencyCode)));
return firstMatch;
}
static unitFromLabelRequired(label) {
const match = this.unitFromLabel(label);
if (!match)
throw `No unit found for label ${label}`;
return match;
}
static unitFromLocalname(localname) {
return Qudt.unit(Qudt.unitIriFromLocalname(localname));
}
static unitFromLocalnameRequired(localname) {
return Qudt.unitRequired(Qudt.unitIriFromLocalname(localname));
}
static unitIriFromLocalname(localname) {
return Qudt.NAMESPACES.unit.makeIriInNamespace(localname);
}
/**
* @deprecated The currencies have been moved back into the units graph. Just use the units methods.
*/
static currencyFromLocalname(localname) {
return this.unitFromLocalname(localname);
}
/**
* @deprecated The currencies have been moved back into the units graph. Just use the units methods.
*/
static currencyFromLocalnameRequired(localname) {
return this.unitFromLocalnameRequired(localname);
}
/**
* @deprecated The currencies have been moved back into the units graph. Just use the units methods.
*/
static currencyIriFromLocalname(localname) {
return this.unitIriFromLocalname(localname);
}
static quantityKind(quantityKindIri) {
return config.quantityKinds.get(quantityKindIri);
}
static quantityKindRequired(quantityKindIri) {
const ret = Qudt.quantityKind(quantityKindIri);
if (typeof ret === "undefined") {
throw `QuantityKind ${quantityKindIri} not found`;
}
return ret;
}
static quantityKindFromLocalname(localname) {
return Qudt.quantityKind(Qudt.quantityKindIriFromLocalname(localname));
}
static quantityKindFromLocalnameRequired(localname) {
return Qudt.quantityKindRequired(Qudt.quantityKindIriFromLocalname(localname));
}
static quantityKindIriFromLocalname(localname) {
return Qudt.NAMESPACES.quantityKind.makeIriInNamespace(localname);
}
static quantityKinds(unit) {
return unit.quantityKindIris.map((iri) => Qudt.quantityKindRequired(iri));
}
static quantityKindsBroad(unit) {
let current = Qudt.quantityKinds(unit);
const result = [];
current.forEach((qk) => result.push(qk));
while (current.length) {
current = current
.flatMap((qk) => qk.broaderQuantityKindIris)
.map((iri) => Qudt.quantityKindRequired(iri));
current.forEach((qk) => result.includes(qk) || result.push(qk));
}
return result;
}
static isBroaderQuantityKind(suspectedBroader, quantityKind) {
const broader = quantityKind.broaderQuantityKindIris;
if (broader.length === 0) {
return false;
}
if (broader.includes(suspectedBroader.iri)) {
return true;
}
return broader.some((b) => Qudt.isBroaderQuantityKind(Qudt.quantityKindRequired(b), suspectedBroader));
}
static prefixFromLabelRequired(label) {
const match = this.prefixFromLabel(label);
if (!match)
throw `No prefix found for label ${label}`;
return match;
}
static prefixFromLabel(label) {
const matcher = new CaseInsensitiveUnderscoreIgnoringLabelMatcher(label);
const firstMatch = findInIterable(config.prefixes.values(), (u) => matcher.matchesLangStrings(u.labels));
return firstMatch;
}
static prefixFromLocalname(localname) {
return Qudt.prefix(Qudt.prefixIriFromLocalname(localname));
}
static prefixFromLocalnameRequired(localname) {
return Qudt.prefixRequired(Qudt.prefixIriFromLocalname(localname));
}
static prefixIriFromLocalname(localname) {
return Qudt.NAMESPACES.prefix.makeIriInNamespace(localname);
}
static prefix(prefixIri) {
return config.prefixes.get(prefixIri);
}
static prefixRequired(prefixIri) {
const ret = Qudt.prefix(prefixIri);
if (typeof ret === "undefined") {
throw `Prefix ${prefixIri} not found`;
}
return ret;
}
static systemOfUnitsFromLabelRequired(label) {
const match = this.systemOfUnitsFromLabel(label);
if (!match)
throw `No systemOfUnits found for label ${label}`;
return match;
}
static systemOfUnitsFromLabel(label) {
const matcher = new CaseInsensitiveUnderscoreIgnoringLabelMatcher(label);
const firstMatch = findInIterable(config.systemsOfUnits.values(), (u) => matcher.matchesLangStrings(u.labels));
return firstMatch;
}
static systemOfUnitsFromLocalname(localname) {
return Qudt.systemOfUnits(Qudt.systemOfUnitsIriFromLocalname(localname));
}
static systemOfUnitsFromLocalnameRequired(localname) {
return Qudt.systemOfUnitsRequired(Qudt.systemOfUnitsIriFromLocalname(localname));
}
static systemOfUnitsIriFromLocalname(localname) {
return Qudt.NAMESPACES.systemOfUnits.makeIriInNamespace(localname);
}
static systemOfUnits(systemOfUnitsIri) {
return config.systemsOfUnits.get(systemOfUnitsIri);
}
static systemOfUnitsRequired(systemOfUnitsIri) {
const ret = Qudt.systemOfUnits(systemOfUnitsIri);
if (typeof ret === "undefined") {
throw `SystemOfUnits ${systemOfUnitsIri} not found`;
}
return ret;
}
/**
* Obtains units based on factor units, using the specified {@link DerivedUnitSearchMode}.
*
* For example,
*
* ```
* const spec = new Map<Unit, number>();
* spec.set(Units.M, 1);
* spec.set(Units.KiloGM, 1);
* spec.set(Units.SEC, -2);
* Qudt.derivedUnitsFrom Map(
* DerivedUnitSearchMode.BEST_MATCH, spec);
* ```
*
* will yield an array containing the Newton Unit ({@code Qudt.Units.N})
*
* @param searchMode the {@link DerivedUnitSearchMode} to use
* @param factorUnits a map containing unit to exponent entries.
* @return the derived units that match the given factor units
*/
static derivedUnitsFromMap(searchMode, factorUnits) {
const flattened = [];
for (const [key, value] of factorUnits.entries()) {
flattened.push(key, value);
}
return this.derivedUnitsFromExponentUnitPairs(searchMode, ...flattened);
}
/**
* Obtains units based on factor units.
*
* @param searchMode the {@link DerivedUnitSearchMode} to use
* @param factorUnits the factor units
* @return the derived unit that match the given factor units
* @see #derivedUnitsFromMap(DerivedUnitSearchMode, Map)
*/
static derivedUnitsFromFactorUnits(searchMode, ...factorUnits) {
return this.derivedUnitsFromFactorUnitSelection(searchMode, new FactorUnits(factorUnits));
}
/**
* Vararg method, must be an even number of arguments, always alternating types of Unit|String
* and Integer.
*
* @param factorUnitSpec alternating Unit (representing a unit IRI) and Decimal|number (the
* exponent)
* @return the units that match
*/
static derivedUnitsFromExponentUnitPairs(searchMode, ...factorUnitSpecs) {
const spec = [];
for (let i = 0; i < factorUnitSpecs.length; i++) {
const specAtI = factorUnitSpecs[i];
if (i % 2 == 0 && specAtI instanceof Unit) {
spec[i] = specAtI;
}
else if (i % 2 == 0 && typeof specAtI === "string") {
const unitString = specAtI;
let unit = Qudt.unit(unitString);
if (typeof unit === "undefined") {
unit = Qudt.unitFromLocalname(unitString);
}
if (typeof unit === "undefined") {
unit = Qudt.unitFromLabel(unitString);
}
if (typeof unit === "undefined") {
throw `Unable to find unit for string ${unitString}, interpreted as iri, label or localname`;
}
spec[i] = unit;
}
else if (i % 2 == 1 && typeof specAtI === "number") {
spec[i] = specAtI;
}
else {
throw `Cannot handle input ${specAtI} at 0-based position ${i}`;
}
}
const initialFactorUnitSelection = FactorUnits.ofFactorUnitSpec(...spec);
return Qudt.derivedUnitsFromFactorUnitSelection(searchMode, initialFactorUnitSelection);
}
/**
* @param searchMode the {@link DerivedUnitSearchMode} to use
* @param selection the factor unit selection
* @return the units that match
* @see #derivedUnitsFromMap(DerivedUnitSearchMode, Map)
*/
static derivedUnitsFromFactorUnitSelection(searchMode, selection) {
const matchingUnits = this.findMatchingUnits(selection);
if (searchMode === DerivedUnitSearchMode.ALL || matchingUnits.length < 2) {
return matchingUnits.sort(Qudt.bestMatchForFactorUnitsComparator(selection));
}
else {
const requestedUnits = selection.contractExponents().factorUnits;
return [
arrayMin(matchingUnits, Qudt.bestMatchForFactorUnitsComparator(selection)),
];
}
}
static bestMatchForFactorUnitsComparator(requestedFactorUnits) {
const reqNorm = requestedFactorUnits.normalize();
const reqNum = requestedFactorUnits.numerator();
const reqNumNorm = reqNum.normalize();
const reqDen = requestedFactorUnits.denominator();
const reqDenNorm = reqDen.normalize();
const reqLocalNamePossibilities = requestedFactorUnits.generateAllLocalnamePossibilities();
return (left, right) => {
if (left.factorUnits.equals(requestedFactorUnits)) {
if (!right.factorUnits.equals(requestedFactorUnits)) {
return -1;
}
}
else {
if (right.factorUnits.equals(requestedFactorUnits)) {
return 1;
}
}
if (right.deprecated) {
if (!left.deprecated) {
return -1;
}
}
else {
if (left.deprecated) {
return 1;
}
}
if (right.isDefinedAsOtherUnit() &&
left.factorUnits.factorUnits?.length == 1 &&
left.factorUnits.factorUnits[0].unit.equals(right) &&
right.factorUnits.factorUnits[0].exponent ===
left.factorUnits.factorUnits[0]?.exponent) {
// if a unit is just another name of another unit with same exponent,
// prefer the other (thus L would be preferred over DeciM)
return -1;
}
if (left.isDefinedAsOtherUnit() &&
right.factorUnits.factorUnits?.length == 1 &&
right.factorUnits.factorUnits[0].unit.equals(left) &&
left.factorUnits.factorUnits[0].exponent ===
right.factorUnits.factorUnits[0]?.exponent) {
return 1;
}
if (left.getIriLocalname().indexOf("-") === -1) {
if (right.getIriLocalname().indexOf("-") > -1) {
return -1; // prefer a derived unit with a new name (such as W, J, N etc.)
}
}
else if (right.getIriLocalname().indexOf("-") === -1) {
return 1;
}
const diffFactorsCountDen = Qudt.expandedFactorsCountDiff(left.factorUnits.denominator(), right.factorUnits.denominator(), reqDen);
if (diffFactorsCountDen != 0) {
return diffFactorsCountDen;
}
const diffFactorsCountNum = Qudt.expandedFactorsCountDiff(left.factorUnits.numerator(), right.factorUnits.numerator(), reqNum);
if (diffFactorsCountNum != 0) {
return diffFactorsCountNum;
}
const factorCountDiff = Qudt.expandedFactorsCountDiff(left.factorUnits, right.factorUnits, requestedFactorUnits);
if (factorCountDiff != 0) {
return factorCountDiff;
}
if (left.dependents >= 10 && left.dependents > 2 * right.dependents) {
return -1; // prefer a unit that has more dependents (other units that refer to
// it as their factor unit or base unit)
}
else if (right.dependents >= 10 &&
right.dependents > 2 * left.dependents) {
return 1;
}
const leftLocalname = left.getIriLocalname();
const rightLocalname = right.getIriLocalname();
if (reqLocalNamePossibilities.includes(leftLocalname)) {
if (!reqLocalNamePossibilities.includes(rightLocalname)) {
return -1;
}
}
else if (reqLocalNamePossibilities.includes(rightLocalname)) {
return 1;
}
const leftUnderscores = Qudt.countUnderscores(leftLocalname);
const rightUnderscores = Qudt.countUnderscores(rightLocalname);
if (leftUnderscores < rightUnderscores) {
return -1; // prefer a unit without modifier in one of its components
}
else if (leftUnderscores > rightUnderscores) {
return 1;
}
if (left.factorUnits.equals(reqNorm)) {
if (!right.factorUnits.equals(reqNorm)) {
return -1; // prefer a unit that matches the normalized factors exactly
}
}
else {
if (right.factorUnits.equals(reqNorm)) {
return 1;
}
}
return StringComparator(left.getIriLocalname(), right.getIriLocalname());
};
}
static countUnderscores(str) {
if (!str)
return 0;
let count = 0;
for (const char of str) {
if (char === "_")
count++;
}
return count;
}
static expandedFactorsCountDiff(leftDen, rightDen, target) {
const leftFactorsDenCnt = leftDen.expand().length;
const rightFactorsDenCnt = rightDen.expand().length;
const reqFactorsDenCnt = target.expand().length;
const diffFactorsCountDen = Math.abs(reqFactorsDenCnt - leftFactorsDenCnt) -
Math.abs(reqFactorsDenCnt - rightFactorsDenCnt);
return diffFactorsCountDen;
}
static findMatchingUnits(initialFactorUnitSelection) {
const matchingUnits = [];
const unitsWithSameDimVector = this.getUnitsByDimensionVector(initialFactorUnitSelection.getDimensionVector());
if (isNullish(unitsWithSameDimVector) ||
unitsWithSameDimVector.length == 0) {
return [];
}
for (const unit of unitsWithSameDimVector) {
if (unit.matches(initialFactorUnitSelection)) {
matchingUnits.push(unit);
}
}
arrayDeduplicate(matchingUnits, compareUsingEquals);
return matchingUnits;
}
static getUnitsByDimensionVector(dimVector) {
let dimVectorIri = undefined;
if (typeof dimVector === "string") {
dimVectorIri = dimVector;
}
else {
dimVectorIri = dimVector.getDimensionVectorIri();
}
const unitsWithSameDimVector = config.unitsByDimensionVector.get(dimVectorIri);
if (isNullish(unitsWithSameDimVector)) {
return [];
}
return unitsWithSameDimVector;
}
static getUnitsWithSameDimensionVector(unit) {
if (isNullish(unit.dimensionVectorIri)) {
throw new Error(`unit ${unit.getIriAbbreviated()} does not have a dimension vector iri`);
}
return this.getUnitsByDimensionVector(unit.dimensionVectorIri);
}
/**
* Returns the unit resulting from scaling the specified `unit` with the specified `prefix`.
* NOTE: if you have unit/prefix labels (not IRIs) - such as "KILO", "NEWTON", use {@link #scaleUnitFromLabels(string, string):Unit}.
*
* @param prefix the prefix to use for scaling or its IRI.
* @param baseUnit the unit to scale or its IRI
* @return the resulting unit
* @throws exception if no such unit is found
*/
static scale(prefix, baseUnit) {
const thePrefix = prefix instanceof Prefix ? prefix : this.prefixRequired(prefix);
const theUnit = baseUnit instanceof Unit ? baseUnit : this.unitRequired(baseUnit);
// special case: KiloGM is not a scaling of GM, it's the other way round. Handle special case here.
if (thePrefix.iri.endsWith("/Kilo") && theUnit.iri.endsWith("/GM")) {
return this.unitFromLocalname("KiloGM");
}
const candidates = this.getUnitsWithSameDimensionVector(theUnit);
for (const u of candidates) {
if (u.prefix?.equals(thePrefix) && u.scalingOf?.equals(theUnit)) {
return u;
}
}
throw `No scaled unit found with base unit ${theUnit.toString()} and prefix ${thePrefix.toString()}`;
}
/**
* Returns the unit resulting from scaling the specified `baseUnitLabel` label
* (such as "METER") with the specified `prefixLabel` (such as "KILO").
*
* @param prefixLabel the label of the prefix, case-insensitive , such as "KILO", or "kilo"
* @param baseUnitLabel the label of the base unit, case-insensitive, such as "Meter"
*/
static scaleUnitFromLabels(prefixLabel, baseUnitLabel) {
return this.scale(Qudt.prefixFromLabelRequired(prefixLabel), Qudt.unitFromLabelRequired(baseUnitLabel));
}
/**
* Returns the list of {@link FactorUnit}s of the specified `unit`.
*
* @param unit the unit to get factors for
* @return the factors of the unit or an empty list if the unit is not a derived unit
*/
static factorUnits(unit) {
return FactorUnit.contractExponents(unit.getLeafFactorUnitsWithCumulativeExponents());
}
/**
* Perform mathematical simplification on factor units. Only simplifies units with exponents of the same sign.
*
* For example,
* ```
* N / M * M -> N per M^2
* ```
*
* @param factorUnits the factor units to simplify
* @return the simplified factor units.
* @deprecated `use contractExponents(FactorUnit[]): FactorUnit[]` instead
*/
static simplifyFactorUnits(factorUnits) {
return FactorUnit.contractExponents(factorUnits);
}
static contractFactorUnits(factorUnits) {
return FactorUnit.contractExponents(factorUnits);
}
static reduceFactorUnits(factorUnits) {
return FactorUnit.reduceExponents(factorUnits);
}
static unscale(unit, treatKiloGmAsUnscaled = true, treatPrefixlessAsUnscaled = true) {
if (!unit.scalingOf) {
return unit;
}
if (treatPrefixlessAsUnscaled && !unit.prefix) {
return unit;
}
if (treatKiloGmAsUnscaled && unit.getIriAbbreviated() === "unit:KiloGM") {
return unit;
}
return unit.scalingOf;
}
/**
* Return a list of {@link FactorUnit}s with the same exponents as the specified `factorUnits` but their base units as units.
*
* @param factorUnits the factor units to unscale
* @return the unscaled factor units
*/
static unscaleFactorUnits(factorUnits, treatKiloGMAsUnscaled = true, treatPrefixlessAsUnscaled = true) {
return factorUnits.map((fu) => new FactorUnit(Qudt.unscale(fu.unit, treatKiloGMAsUnscaled, treatPrefixlessAsUnscaled), fu.exponent));
}
/**
* Instantiates a QuantityValue.
* @param value a Decimal
* @param unit a Unit or unit IRI.
* @return a QuantityValue with the specified data
*/
static quantityValue(value, unit) {
if (typeof unit === "string") {
return new QuantityValue(value, Qudt.unitRequired(unit));
}
return new QuantityValue(value, unit);
}
/**
* Converts the specified value from the unit it is in (`fromUnit`) to the specified target unit (`toUnit`).
* @param value: a Decimal, the value to convert.
* @param fromUnit: a Unit or string. A string is interpreted as a Unit IRI.
* @param toUnit: a Unit or string. A string is interpreted as a Unit IRI.
* @return the resulting value
*/
static convert(value, fromUnit, toUnit, quantityKind) {
if (!fromUnit) {
throw "Parameter 'fromUnit' is required";
}
if (!toUnit) {
throw "Parameter 'toUnit' is required";
}
const from = typeof fromUnit === "string" ? Qudt.unitRequired(fromUnit) : fromUnit;
const to = typeof toUnit === "string" ? Qudt.unitRequired(toUnit) : toUnit;
const qk = isNullish(quantityKind)
? undefined
: typeof quantityKind === "string"
? Qudt.quantityKindRequired(quantityKind)
: quantityKind;
return from.convert(value, to, qk);
}
/**
* Converts the specified QuantityValue from to the specified target unit (`toUnit`).
* @param value: a Decimal, the value to convert.
* @param fromUnit: a Unit or string. A string is interpreted as a Unit IRI.
* @param toUnit: a Unit or string. A string is interpreted as a Unit IRI.
* @return a QuantityValue holding the result
*/
static convertQuantityValue(from, toUnit, quantityKind) {
if (!from) {
throw "Parameter 'from' is required";
}
if (!toUnit) {
throw "Parameter 'toUnit' is required";
}
const to = typeof toUnit === "string" ? Qudt.unitRequired(toUnit) : toUnit;
const qk = isNullish(quantityKind)
? undefined
: typeof quantityKind === "string"
? Qudt.quantityKindRequired(quantityKind)
: quantityKind;
return from.convert(to, qk);
}
/**
* Returns `true` if the two units can be converted into each other.
* @param fromUnit a Unit or unit IRI
* @param toUnit a Unit or unit IRI
* @return a boolean indicating whether the units are convertible.
*/
static isConvertible(fromUnit, toUnit) {
const from = typeof fromUnit === "string" ? Qudt.unitRequired(fromUnit) : fromUnit;
const to = typeof toUnit === "string" ? Qudt.unitRequired(toUnit) : toUnit;
return from.isConvertible(to);
}
/**
* Returns a [Unit, Decimal] tuple containing the base unit of the specified `unit`
* along with the scale factor needed to convert values from the base unit to
* the specified unit.
*
* @param unit the unit to scale to its base
* @return a [Unit, Decimal] tuple with the base unit and the required scale factor
*/
static scaleToBaseUnit(unit) {
if (!unit.scalingOf) {
return { unit: unit, factor: ONE };
}
const baseUnit = unit.scalingOf;
return { unit: baseUnit, factor: unit.getConversionMultiplier(baseUnit) };
}
static allUnits() {
const ret = [];
for (const unit of config.units.values()) {
ret.push(unit);
}
return ret;
}
static allQuantityKinds() {
const ret = [];
for (const quantityKind of config.quantityKinds.values()) {
ret.push(quantityKind);
}
return ret;
}
static allPrefixes() {
const ret = [];
for (const prefix of config.prefixes.values()) {
ret.push(prefix);
}
return ret;
}
static allSystemsOfUnits() {
const ret = [];
for (const prefix of config.systemsOfUnits.values()) {
ret.push(prefix);
}
return ret;
}
static allUnitsOfSystem(system) {
const ret = [];
for (const unit of config.units.values()) {
if (system.allowsUnit(unit)) {
ret.push(unit);
}
}
return ret;
}
/**
* Returns the first unit obtained using {@link #correspondingUnitsInSystem(Unit,
* SystemOfUnits)}.
*
* @return the unit corresponding to the specified unit in the specified systemOfUnits.
*/
static correspondingUnitInSystem(unit, systemOfUnits) {
const correspondingUnits = Qudt.correspondingUnitsInSystem(unit, systemOfUnits);
if (typeof correspondingUnits !== "undefined" &&
correspondingUnits.length > 0) {
return correspondingUnits[0];
}
return undefined;
}
/**
* Gets units that correspond to the specified unit are allowed in the specified systemOfUnits.
* The resulting units have to
*
* <ol>
* <li>have the same dimension vector as the unit
* <li>share at least one quantityKind with unit
* </ol>
*
* and they are ascending sorted by dissimilarity in magnitude to the magnitude of the specified
* unit, i.e. the first unit returned is the closest in magnitude.
*
* <p>If two resulting units have the same magnitude difference from the specified one, the
* following comparisons are made consecutively until a difference is found:
*
* <ol>
* <li>the base unit of the specified system is ranked first
* <li>conversion offset closer to the one of the specified unit is ranked first
* <li>the unscaled unit is ranked first
* <li>the unit that has a symbol is ranked first
* <li>the unit with more quantityKinds is ranked first
* <li>the units are ranked by their IRIs lexicographically
* </ol>
*
* that is a base unit of the system is ranked first. If none or both are base units, the one
* with a conversion offset closer to the specified unit's conversion offset is ranked first.
*
* @param unit
* @param systemOfUnits
* @return
*/
static correspondingUnitsInSystem(unit, systemOfUnits) {
if (systemOfUnits.allowsUnit(unit)) {
return [unit];
}
const elegible = Array.from(config.units.values())
.filter((u) => systemOfUnits.allowsUnit(u))
.filter((u) => u.dimensionVectorIri === unit.dimensionVectorIri);
if (elegible.length === 1) {
return elegible;
}
let candidates = [...elegible];
// get the unit that is closest in magnitude (conversionFactor)
// recursively check for factor units
candidates = candidates.filter((u) => u.quantityKinds.some((q) => arrayContains(unit.quantityKinds, q)));
if (candidates.length === 1) {
return candidates;
}
candidates.sort((l, r) => {
const scaleDiffL = Math.abs(Qudt.scaleDifference(l, unit));
const scaleDiffR = Math.abs(Qudt.scaleDifference(r, unit));
const diff = Math.sign(scaleDiffL - scaleDiffR);
if (diff !== 0) {
return diff;
}
// tie breaker: base unit ranked before non-base unit
let cmp = BooleanComparator(systemOfUnits.hasBaseUnit(r), systemOfUnits.hasBaseUnit(l));
if (cmp !== 0) {
return cmp;
}
// tie breaker: closer offset
const offsetDiffL = Math.abs(Qudt.offsetDifference(l, unit));
const offsetDiffR = Math.abs(Qudt.offsetDifference(r, unit));
cmp = Math.sign(offsetDiffL - offsetDiffR);
if (cmp != 0) {
return cmp;
}
// tie breaker: perfer unit that is not scaled
cmp = BooleanComparator(l.isScaled(), r.isScaled());
if (cmp != 0) {
return cmp;
}
// tie breaker prefer the unit that has a symbol (it's more likely to be
// commonly used):
cmp = BooleanComparator(r.hasSymbol(), l.hasSymbol());
if (cmp != 0) {
return cmp;
}
// tie breaker: prefer unit with more quantity kinds (it's less specific)
cmp = NumberComparator(l.quantityKinds.length, r.quantityKinds.length);
if (cmp != 0) {
return cmp;
}
// tie breaker: lexicographically compare iris.
return StringComparator(l.iri, r.iri);
});
return candidates;
}
static scaleDifference(u1, u2) {
const u1Log10 = u1.conversionMultiplier.log(10);
const u2Log10 = u2.conversionMultiplier.log(10);
return u1Log10.toNumber() - u2Log10.toNumber();
}
static offsetDifference(u1, u2) {
const u1Log10 = Qudt.logOrZeroRequireNonNegative(u1.conversionOffset.abs());
const u2Log10 = Qudt.logOrZeroRequireNonNegative(u2.conversionOffset.abs());
return u1Log10.toNumber() - u2Log10.toNumber();
}
static logOrZeroRequireNonNegative(val) {
if (val.isNegative()) {
throw `Cannot get logarithm of negative value ${val}`;
}
if (val.isZero()) {
return ZERO;
}
return val.log(10);
}
}
Qudt.NAMESPACES = QudtNamespaces;
class CaseInsensitiveUnderscoreIgnoringLabelMatcher {
constructor(searchTerm) {
this.compareForEquality = this.convert(searchTerm);
}
convert(term) {
return term.replaceAll("_", " ").toLocaleUpperCase("en-US");
}
matchesString(searchTerm) {
return this.convert(searchTerm) === this.compareForEquality;
}
matchesLangString(searchTerm) {
return this.convert(searchTerm.text) === this.compareForEquality;
}
matchesLangStrings(searchTerms) {
return searchTerms.some((st) => this.convert(st.text) === this.compareForEquality);
}
}
//# sourceMappingURL=qudt.js.map