UNPKG

mathjs

Version:

Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with dif

1,479 lines (1,312 loc) 93.3 kB
'use strict' const endsWith = require('../../utils/string').endsWith const clone = require('../../utils/object').clone const constants = require('../../utils/bignumber/constants') function factory (type, config, load, typed, math) { const add = load(require('../../function/arithmetic/addScalar')) const subtract = load(require('../../function/arithmetic/subtract')) const multiply = load(require('../../function/arithmetic/multiplyScalar')) const divide = load(require('../../function/arithmetic/divideScalar')) const pow = load(require('../../function/arithmetic/pow')) const abs = load(require('../../function/arithmetic/abs')) const fix = load(require('../../function/arithmetic/fix')) const round = load(require('../../function/arithmetic/round')) const equal = load(require('../../function/relational/equal')) const isNumeric = load(require('../../function/utils/isNumeric')) const format = load(require('../../function/string/format')) const getTypeOf = load(require('../../function/utils/typeof')) const toNumber = load(require('../../type/number')) const Complex = load(require('../../type/complex/Complex')) /** * A unit can be constructed in the following ways: * * const a = new Unit(value, name) * const b = new Unit(null, name) * const c = Unit.parse(str) * * Example usage: * * const a = new Unit(5, 'cm') // 50 mm * const b = Unit.parse('23 kg') // 23 kg * const c = math.in(a, new Unit(null, 'm') // 0.05 m * const d = new Unit(9.81, "m/s^2") // 9.81 m/s^2 * * @class Unit * @constructor Unit * @param {number | BigNumber | Fraction | Complex | boolean} [value] A value like 5.2 * @param {string} [name] A unit name like "cm" or "inch", or a derived unit of the form: "u1[^ex1] [u2[^ex2] ...] [/ u3[^ex3] [u4[^ex4]]]", such as "kg m^2/s^2", where each unit appearing after the forward slash is taken to be in the denominator. "kg m^2 s^-2" is a synonym and is also acceptable. Any of the units can include a prefix. */ function Unit (value, name) { if (!(this instanceof Unit)) { throw new Error('Constructor must be called with the new operator') } if (!(value === null || value === undefined || isNumeric(value) || type.isComplex(value))) { throw new TypeError('First parameter in Unit constructor must be number, BigNumber, Fraction, Complex, or undefined') } if (name !== undefined && (typeof name !== 'string' || name === '')) { throw new TypeError('Second parameter in Unit constructor must be a string') } if (name !== undefined) { const u = Unit.parse(name) this.units = u.units this.dimensions = u.dimensions } else { this.units = [ { unit: UNIT_NONE, prefix: PREFIXES.NONE, // link to a list with supported prefixes power: 0 } ] this.dimensions = [] for (let i = 0; i < BASE_DIMENSIONS.length; i++) { this.dimensions[i] = 0 } } this.value = (value !== undefined && value !== null) ? this._normalize(value) : null this.fixPrefix = false // if true, function format will not search for the // best prefix but leave it as initially provided. // fixPrefix is set true by the method Unit.to // The justification behind this is that if the constructor is explicitly called, // the caller wishes the units to be returned exactly as he supplied. this.skipAutomaticSimplification = true } /** * Attach type information */ Unit.prototype.type = 'Unit' Unit.prototype.isUnit = true // private variables and functions for the Unit parser let text, index, c function skipWhitespace () { while (c === ' ' || c === '\t') { next() } } function isDigitDot (c) { return ((c >= '0' && c <= '9') || c === '.') } function isDigit (c) { return ((c >= '0' && c <= '9')) } function next () { index++ c = text.charAt(index) } function revert (oldIndex) { index = oldIndex c = text.charAt(index) } function parseNumber () { let number = '' let oldIndex oldIndex = index if (c === '+') { next() } else if (c === '-') { number += c next() } if (!isDigitDot(c)) { // a + or - must be followed by a digit revert(oldIndex) return null } // get number, can have a single dot if (c === '.') { number += c next() if (!isDigit(c)) { // this is no legal number, it is just a dot revert(oldIndex) return null } } else { while (isDigit(c)) { number += c next() } if (c === '.') { number += c next() } } while (isDigit(c)) { number += c next() } // check for exponential notation like "2.3e-4" or "1.23e50" if (c === 'E' || c === 'e') { // The grammar branches here. This could either be part of an exponent or the start of a unit that begins with the letter e, such as "4exabytes" let tentativeNumber = '' const tentativeIndex = index tentativeNumber += c next() if (c === '+' || c === '-') { tentativeNumber += c next() } // Scientific notation MUST be followed by an exponent (otherwise we assume it is not scientific notation) if (!isDigit(c)) { // The e or E must belong to something else, so return the number without the e or E. revert(tentativeIndex) return number } // We can now safely say that this is scientific notation. number = number + tentativeNumber while (isDigit(c)) { number += c next() } } return number } function parseUnit () { let unitName = '' // Alphanumeric characters only; matches [a-zA-Z0-9] let code = text.charCodeAt(index) while ((code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122)) { unitName += c next() code = text.charCodeAt(index) } // Must begin with [a-zA-Z] code = unitName.charCodeAt(0) if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) { return unitName || null } else { return null } } function parseCharacter (toFind) { if (c === toFind) { next() return toFind } else { return null } } /** * Parse a string into a unit. The value of the unit is parsed as number, * BigNumber, or Fraction depending on the math.js config setting `number`. * * Throws an exception if the provided string does not contain a valid unit or * cannot be parsed. * @memberof Unit * @param {string} str A string like "5.2 inch", "4e2 cm/s^2" * @return {Unit} unit */ Unit.parse = function (str, options) { options = options || {} text = str index = -1 c = '' if (typeof text !== 'string') { throw new TypeError('Invalid argument in Unit.parse, string expected') } const unit = new Unit() unit.units = [] let powerMultiplierCurrent = 1 let expectingUnit = false // A unit should follow this pattern: // [number] ...[ [*/] unit[^number] ] // unit[^number] ... [ [*/] unit[^number] ] // Rules: // number is any floating point number. // unit is any alphanumeric string beginning with an alpha. Units with names like e3 should be avoided because they look like the exponent of a floating point number! // The string may optionally begin with a number. // Each unit may optionally be followed by ^number. // Whitespace or a forward slash is recommended between consecutive units, although the following technically is parseable: // 2m^2kg/s^2 // it is not good form. If a unit starts with e, then it could be confused as a floating point number: // 4erg next() skipWhitespace() // Optional number at the start of the string const valueStr = parseNumber() let value = null if (valueStr) { if (config.number === 'BigNumber') { value = new type.BigNumber(valueStr) } else if (config.number === 'Fraction') { value = new type.Fraction(valueStr) } else { // number value = parseFloat(valueStr) } skipWhitespace() // Whitespace is not required here // handle multiplication or division right after the value, like '1/s' if (parseCharacter('*')) { powerMultiplierCurrent = 1 expectingUnit = true } else if (parseCharacter('/')) { powerMultiplierCurrent = -1 expectingUnit = true } } // Stack to keep track of powerMultipliers applied to each parentheses group const powerMultiplierStack = [] // Running product of all elements in powerMultiplierStack let powerMultiplierStackProduct = 1 while (true) { skipWhitespace() // Check for and consume opening parentheses, pushing powerMultiplierCurrent to the stack // A '(' will always appear directly before a unit. while (c === '(') { powerMultiplierStack.push(powerMultiplierCurrent) powerMultiplierStackProduct *= powerMultiplierCurrent powerMultiplierCurrent = 1 next() skipWhitespace() } // Is there something here? let uStr if (c) { const oldC = c uStr = parseUnit() if (uStr === null) { throw new SyntaxError('Unexpected "' + oldC + '" in "' + text + '" at index ' + index.toString()) } } else { // End of input. break } // Verify the unit exists and get the prefix (if any) const res = _findUnit(uStr) if (res === null) { // Unit not found. throw new SyntaxError('Unit "' + uStr + '" not found.') } let power = powerMultiplierCurrent * powerMultiplierStackProduct // Is there a "^ number"? skipWhitespace() if (parseCharacter('^')) { skipWhitespace() const p = parseNumber() if (p === null) { // No valid number found for the power! throw new SyntaxError('In "' + str + '", "^" must be followed by a floating-point number') } power *= p } // Add the unit to the list unit.units.push({ unit: res.unit, prefix: res.prefix, power: power }) for (let i = 0; i < BASE_DIMENSIONS.length; i++) { unit.dimensions[i] += (res.unit.dimensions[i] || 0) * power } // Check for and consume closing parentheses, popping from the stack. // A ')' will always follow a unit. skipWhitespace() while (c === ')') { if (powerMultiplierStack.length === 0) { throw new SyntaxError('Unmatched ")" in "' + text + '" at index ' + index.toString()) } powerMultiplierStackProduct /= powerMultiplierStack.pop() next() skipWhitespace() } // "*" and "/" should mean we are expecting something to come next. // Is there a forward slash? If so, negate powerMultiplierCurrent. The next unit or paren group is in the denominator. expectingUnit = false if (parseCharacter('*')) { // explicit multiplication powerMultiplierCurrent = 1 expectingUnit = true } else if (parseCharacter('/')) { // division powerMultiplierCurrent = -1 expectingUnit = true } else { // implicit multiplication powerMultiplierCurrent = 1 } // Replace the unit into the auto unit system if (res.unit.base) { const baseDim = res.unit.base.key UNIT_SYSTEMS.auto[baseDim] = { unit: res.unit, prefix: res.prefix } } } // Has the string been entirely consumed? skipWhitespace() if (c) { throw new SyntaxError('Could not parse: "' + str + '"') } // Is there a trailing slash? if (expectingUnit) { throw new SyntaxError('Trailing characters: "' + str + '"') } // Is the parentheses stack empty? if (powerMultiplierStack.length !== 0) { throw new SyntaxError('Unmatched "(" in "' + text + '"') } // Are there any units at all? if (unit.units.length === 0 && !options.allowNoUnits) { throw new SyntaxError('"' + str + '" contains no units') } unit.value = (value !== undefined) ? unit._normalize(value) : null return unit } /** * create a copy of this unit * @memberof Unit * @return {Unit} Returns a cloned version of the unit */ Unit.prototype.clone = function () { const unit = new Unit() unit.fixPrefix = this.fixPrefix unit.skipAutomaticSimplification = this.skipAutomaticSimplification unit.value = clone(this.value) unit.dimensions = this.dimensions.slice(0) unit.units = [] for (let i = 0; i < this.units.length; i++) { unit.units[i] = { } for (const p in this.units[i]) { if (this.units[i].hasOwnProperty(p)) { unit.units[i][p] = this.units[i][p] } } } return unit } /** * Return whether the unit is derived (such as m/s, or cm^2, but not N) * @memberof Unit * @return {boolean} True if the unit is derived */ Unit.prototype._isDerived = function () { if (this.units.length === 0) { return false } return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15 } /** * Normalize a value, based on its currently set unit(s) * @memberof Unit * @param {number | BigNumber | Fraction | boolean} value * @return {number | BigNumber | Fraction | boolean} normalized value * @private */ Unit.prototype._normalize = function (value) { let unitValue, unitOffset, unitPower, unitPrefixValue let convert if (value === null || value === undefined || this.units.length === 0) { return value } else if (this._isDerived()) { // This is a derived unit, so do not apply offsets. // For example, with J kg^-1 degC^-1 you would NOT want to apply the offset. let res = value convert = Unit._getNumberConverter(getTypeOf(value)) // convert to Fraction or BigNumber if needed for (let i = 0; i < this.units.length; i++) { unitValue = convert(this.units[i].unit.value) unitPrefixValue = convert(this.units[i].prefix.value) unitPower = convert(this.units[i].power) res = multiply(res, pow(multiply(unitValue, unitPrefixValue), unitPower)) } return res } else { // This is a single unit of power 1, like kg or degC convert = Unit._getNumberConverter(getTypeOf(value)) // convert to Fraction or BigNumber if needed unitValue = convert(this.units[0].unit.value) unitOffset = convert(this.units[0].unit.offset) unitPrefixValue = convert(this.units[0].prefix.value) return multiply(add(value, unitOffset), multiply(unitValue, unitPrefixValue)) } } /** * Denormalize a value, based on its currently set unit(s) * @memberof Unit * @param {number} value * @param {number} [prefixValue] Optional prefix value to be used (ignored if this is a derived unit) * @return {number} denormalized value * @private */ Unit.prototype._denormalize = function (value, prefixValue) { let unitValue, unitOffset, unitPower, unitPrefixValue let convert if (value === null || value === undefined || this.units.length === 0) { return value } else if (this._isDerived()) { // This is a derived unit, so do not apply offsets. // For example, with J kg^-1 degC^-1 you would NOT want to apply the offset. // Also, prefixValue is ignored--but we will still use the prefix value stored in each unit, since kg is usually preferable to g unless the user decides otherwise. let res = value convert = Unit._getNumberConverter(getTypeOf(value)) // convert to Fraction or BigNumber if needed for (let i = 0; i < this.units.length; i++) { unitValue = convert(this.units[i].unit.value) unitPrefixValue = convert(this.units[i].prefix.value) unitPower = convert(this.units[i].power) res = divide(res, pow(multiply(unitValue, unitPrefixValue), unitPower)) } return res } else { // This is a single unit of power 1, like kg or degC convert = Unit._getNumberConverter(getTypeOf(value)) // convert to Fraction or BigNumber if needed unitValue = convert(this.units[0].unit.value) unitPrefixValue = convert(this.units[0].prefix.value) unitOffset = convert(this.units[0].unit.offset) if (prefixValue === undefined || prefixValue === null) { return subtract(divide(divide(value, unitValue), unitPrefixValue), unitOffset) } else { return subtract(divide(divide(value, unitValue), prefixValue), unitOffset) } } } /** * Find a unit from a string * @memberof Unit * @param {string} str A string like 'cm' or 'inch' * @returns {Object | null} result When found, an object with fields unit and * prefix is returned. Else, null is returned. * @private */ function _findUnit (str) { // First, match units names exactly. For example, a user could define 'mm' as 10^-4 m, which is silly, but then we would want 'mm' to match the user-defined unit. if (UNITS.hasOwnProperty(str)) { const unit = UNITS[str] const prefix = unit.prefixes[''] return { unit, prefix } } for (const name in UNITS) { if (UNITS.hasOwnProperty(name)) { if (endsWith(str, name)) { const unit = UNITS[name] const prefixLen = (str.length - name.length) const prefixName = str.substring(0, prefixLen) const prefix = unit.prefixes.hasOwnProperty(prefixName) ? unit.prefixes[prefixName] : undefined if (prefix !== undefined) { // store unit, prefix, and value return { unit, prefix } } } } } return null } /** * Test if the given expression is a unit. * The unit can have a prefix but cannot have a value. * @memberof Unit * @param {string} name A string to be tested whether it is a value less unit. * The unit can have prefix, like "cm" * @return {boolean} true if the given string is a unit */ Unit.isValuelessUnit = function (name) { return (_findUnit(name) !== null) } /** * check if this unit has given base unit * If this unit is a derived unit, this will ALWAYS return false, since by definition base units are not derived. * @memberof Unit * @param {BASE_UNITS | string | undefined} base */ Unit.prototype.hasBase = function (base) { if (typeof (base) === 'string') { base = BASE_UNITS[base] } if (!base) { return false } // All dimensions must be the same for (let i = 0; i < BASE_DIMENSIONS.length; i++) { if (Math.abs((this.dimensions[i] || 0) - (base.dimensions[i] || 0)) > 1e-12) { return false } } return true } /** * Check if this unit has a base or bases equal to another base or bases * For derived units, the exponent on each base also must match * @memberof Unit * @param {Unit} other * @return {boolean} true if equal base */ Unit.prototype.equalBase = function (other) { // All dimensions must be the same for (let i = 0; i < BASE_DIMENSIONS.length; i++) { if (Math.abs((this.dimensions[i] || 0) - (other.dimensions[i] || 0)) > 1e-12) { return false } } return true } /** * Check if this unit equals another unit * @memberof Unit * @param {Unit} other * @return {boolean} true if both units are equal */ Unit.prototype.equals = function (other) { return (this.equalBase(other) && equal(this.value, other.value)) } /** * Multiply this unit with another one * @memberof Unit * @param {Unit} other * @return {Unit} product of this unit and the other unit */ Unit.prototype.multiply = function (other) { const res = this.clone() for (let i = 0; i < BASE_DIMENSIONS.length; i++) { // Dimensions arrays may be of different lengths. Default to 0. res.dimensions[i] = (this.dimensions[i] || 0) + (other.dimensions[i] || 0) } // Append other's units list onto res for (let i = 0; i < other.units.length; i++) { // Make a deep copy const inverted = {} for (const key in other.units[i]) { inverted[key] = other.units[i][key] } res.units.push(inverted) } // If at least one operand has a value, then the result should also have a value if (this.value !== null || other.value !== null) { const valThis = this.value === null ? this._normalize(1) : this.value const valOther = other.value === null ? other._normalize(1) : other.value res.value = multiply(valThis, valOther) } else { res.value = null } res.skipAutomaticSimplification = false return getNumericIfUnitless(res) } /** * Divide this unit by another one * @memberof Unit * @param {Unit} other * @return {Unit} result of dividing this unit by the other unit */ Unit.prototype.divide = function (other) { const res = this.clone() for (let i = 0; i < BASE_DIMENSIONS.length; i++) { // Dimensions arrays may be of different lengths. Default to 0. res.dimensions[i] = (this.dimensions[i] || 0) - (other.dimensions[i] || 0) } // Invert and append other's units list onto res for (let i = 0; i < other.units.length; i++) { // Make a deep copy const inverted = {} for (const key in other.units[i]) { inverted[key] = other.units[i][key] } inverted.power = -inverted.power res.units.push(inverted) } // If at least one operand has a value, the result should have a value if (this.value !== null || other.value !== null) { const valThis = this.value === null ? this._normalize(1) : this.value const valOther = other.value === null ? other._normalize(1) : other.value res.value = divide(valThis, valOther) } else { res.value = null } res.skipAutomaticSimplification = false return getNumericIfUnitless(res) } /** * Calculate the power of a unit * @memberof Unit * @param {number | Fraction | BigNumber} p * @returns {Unit} The result: this^p */ Unit.prototype.pow = function (p) { const res = this.clone() for (let i = 0; i < BASE_DIMENSIONS.length; i++) { // Dimensions arrays may be of different lengths. Default to 0. res.dimensions[i] = (this.dimensions[i] || 0) * p } // Adjust the power of each unit in the list for (let i = 0; i < res.units.length; i++) { res.units[i].power *= p } if (res.value !== null) { res.value = pow(res.value, p) // only allow numeric output, we don't want to return a Complex number // if (!isNumeric(res.value)) { // res.value = NaN // } // Update: Complex supported now } else { res.value = null } res.skipAutomaticSimplification = false return getNumericIfUnitless(res) } /** * Return the numeric value of this unit if it is dimensionless, has a value, and config.predictable == false; or the original unit otherwise * @param {Unit} unit * @returns {number | Fraction | BigNumber | Unit} The numeric value of the unit if conditions are met, or the original unit otherwise */ function getNumericIfUnitless (unit) { if (unit.equalBase(BASE_UNITS.NONE) && unit.value !== null && !config.predictable) { return unit.value } else { return unit } } /** * Calculate the absolute value of a unit * @memberof Unit * @param {number | Fraction | BigNumber} x * @returns {Unit} The result: |x|, absolute value of x */ Unit.prototype.abs = function () { // This gives correct, but unexpected, results for units with an offset. // For example, abs(-283.15 degC) = -263.15 degC !!! const ret = this.clone() ret.value = ret.value !== null ? abs(ret.value) : null for (const i in ret.units) { if (ret.units[i].unit.name === 'VA' || ret.units[i].unit.name === 'VAR') { ret.units[i].unit = UNITS['W'] } } return ret } /** * Convert the unit to a specific unit name. * @memberof Unit * @param {string | Unit} valuelessUnit A unit without value. Can have prefix, like "cm" * @returns {Unit} Returns a clone of the unit with a fixed prefix and unit. */ Unit.prototype.to = function (valuelessUnit) { let other const value = this.value === null ? this._normalize(1) : this.value if (typeof valuelessUnit === 'string') { // other = new Unit(null, valuelessUnit) other = Unit.parse(valuelessUnit) if (!this.equalBase(other)) { throw new Error(`Units do not match ('${other.toString()}' != '${this.toString()}')`) } if (other.value !== null) { throw new Error('Cannot convert to a unit with a value') } other.value = clone(value) other.fixPrefix = true other.skipAutomaticSimplification = true return other } else if (type.isUnit(valuelessUnit)) { if (!this.equalBase(valuelessUnit)) { throw new Error(`Units do not match ('${valuelessUnit.toString()}' != '${this.toString()}')`) } if (valuelessUnit.value !== null) { throw new Error('Cannot convert to a unit with a value') } other = valuelessUnit.clone() other.value = clone(value) other.fixPrefix = true other.skipAutomaticSimplification = true return other } else { throw new Error('String or Unit expected as parameter') } } /** * Return the value of the unit when represented with given valueless unit * @memberof Unit * @param {string | Unit} valuelessUnit For example 'cm' or 'inch' * @return {number} Returns the unit value as number. */ // TODO: deprecate Unit.toNumber? It's always better to use toNumeric Unit.prototype.toNumber = function (valuelessUnit) { return toNumber(this.toNumeric(valuelessUnit)) } /** * Return the value of the unit in the original numeric type * @memberof Unit * @param {string | Unit} valuelessUnit For example 'cm' or 'inch' * @return {number | BigNumber | Fraction} Returns the unit value */ Unit.prototype.toNumeric = function (valuelessUnit) { let other if (valuelessUnit) { // Allow getting the numeric value without converting to a different unit other = this.to(valuelessUnit) } else { other = this.clone() } if (other._isDerived()) { return other._denormalize(other.value) } else { return other._denormalize(other.value, other.units[0].prefix.value) } } /** * Get a string representation of the unit. * @memberof Unit * @return {string} */ Unit.prototype.toString = function () { return this.format() } /** * Get a JSON representation of the unit * @memberof Unit * @returns {Object} Returns a JSON object structured as: * `{"mathjs": "Unit", "value": 2, "unit": "cm", "fixPrefix": false}` */ Unit.prototype.toJSON = function () { return { mathjs: 'Unit', value: this._denormalize(this.value), unit: this.formatUnits(), fixPrefix: this.fixPrefix } } /** * Instantiate a Unit from a JSON object * @memberof Unit * @param {Object} json A JSON object structured as: * `{"mathjs": "Unit", "value": 2, "unit": "cm", "fixPrefix": false}` * @return {Unit} */ Unit.fromJSON = function (json) { const unit = new Unit(json.value, json.unit) unit.fixPrefix = json.fixPrefix || false return unit } /** * Returns the string representation of the unit. * @memberof Unit * @return {string} */ Unit.prototype.valueOf = Unit.prototype.toString /** * Simplify this Unit's unit list and return a new Unit with the simplified list. * The returned Unit will contain a list of the "best" units for formatting. */ Unit.prototype.simplify = function () { const ret = this.clone() const proposedUnitList = [] // Search for a matching base let matchingBase for (const key in currentUnitSystem) { if (ret.hasBase(BASE_UNITS[key])) { matchingBase = key break } } if (matchingBase === 'NONE') { ret.units = [] } else { let matchingUnit if (matchingBase) { // Does the unit system have a matching unit? if (currentUnitSystem.hasOwnProperty(matchingBase)) { matchingUnit = currentUnitSystem[matchingBase] } } if (matchingUnit) { ret.units = [{ unit: matchingUnit.unit, prefix: matchingUnit.prefix, power: 1.0 }] } else { // Multiple units or units with powers are formatted like this: // 5 (kg m^2) / (s^3 mol) // Build an representation from the base units of the current unit system let missingBaseDim = false for (let i = 0; i < BASE_DIMENSIONS.length; i++) { const baseDim = BASE_DIMENSIONS[i] if (Math.abs(ret.dimensions[i] || 0) > 1e-12) { if (currentUnitSystem.hasOwnProperty(baseDim)) { proposedUnitList.push({ unit: currentUnitSystem[baseDim].unit, prefix: currentUnitSystem[baseDim].prefix, power: ret.dimensions[i] || 0 }) } else { missingBaseDim = true } } } // Is the proposed unit list "simpler" than the existing one? if (proposedUnitList.length < ret.units.length && !missingBaseDim) { // Replace this unit list with the proposed list ret.units = proposedUnitList } } } return ret } /** * Returns a new Unit in the SI system with the same value as this one */ Unit.prototype.toSI = function () { const ret = this.clone() const proposedUnitList = [] // Multiple units or units with powers are formatted like this: // 5 (kg m^2) / (s^3 mol) // Build an representation from the base units of the SI unit system for (let i = 0; i < BASE_DIMENSIONS.length; i++) { const baseDim = BASE_DIMENSIONS[i] if (Math.abs(ret.dimensions[i] || 0) > 1e-12) { if (UNIT_SYSTEMS['si'].hasOwnProperty(baseDim)) { proposedUnitList.push({ unit: UNIT_SYSTEMS['si'][baseDim].unit, prefix: UNIT_SYSTEMS['si'][baseDim].prefix, power: ret.dimensions[i] || 0 }) } else { throw new Error('Cannot express custom unit ' + baseDim + ' in SI units') } } } // Replace this unit list with the proposed list ret.units = proposedUnitList ret.fixPrefix = true ret.skipAutomaticSimplification = true return ret } /** * Get a string representation of the units of this Unit, without the value. The unit list is formatted as-is without first being simplified. * @memberof Unit * @return {string} */ Unit.prototype.formatUnits = function () { let strNum = '' let strDen = '' let nNum = 0 let nDen = 0 for (let i = 0; i < this.units.length; i++) { if (this.units[i].power > 0) { nNum++ strNum += ' ' + this.units[i].prefix.name + this.units[i].unit.name if (Math.abs(this.units[i].power - 1.0) > 1e-15) { strNum += '^' + this.units[i].power } } else if (this.units[i].power < 0) { nDen++ } } if (nDen > 0) { for (let i = 0; i < this.units.length; i++) { if (this.units[i].power < 0) { if (nNum > 0) { strDen += ' ' + this.units[i].prefix.name + this.units[i].unit.name if (Math.abs(this.units[i].power + 1.0) > 1e-15) { strDen += '^' + (-this.units[i].power) } } else { strDen += ' ' + this.units[i].prefix.name + this.units[i].unit.name strDen += '^' + (this.units[i].power) } } } } // Remove leading " " strNum = strNum.substr(1) strDen = strDen.substr(1) // Add parans for better copy/paste back into the eval, for example, or for better pretty print formatting if (nNum > 1 && nDen > 0) { strNum = '(' + strNum + ')' } if (nDen > 1 && nNum > 0) { strDen = '(' + strDen + ')' } let str = strNum if (nNum > 0 && nDen > 0) { str += ' / ' } str += strDen return str } /** * Get a string representation of the Unit, with optional formatting options. * @memberof Unit * @param {Object | number | Function} [options] Formatting options. See * lib/utils/number:format for a * description of the available * options. * @return {string} */ Unit.prototype.format = function (options) { // Simplfy the unit list, unless it is valueless or was created directly in the // constructor or as the result of to or toSI const simp = this.skipAutomaticSimplification || this.value === null ? this.clone() : this.simplify() // Apply some custom logic for handling VA and VAR. The goal is to express the value of the unit as a real value, if possible. Otherwise, use a real-valued unit instead of a complex-valued one. let isImaginary = false if (typeof (simp.value) !== 'undefined' && simp.value !== null && type.isComplex(simp.value)) { // TODO: Make this better, for example, use relative magnitude of re and im rather than absolute isImaginary = Math.abs(simp.value.re) < 1e-14 } for (const i in simp.units) { if (simp.units[i].unit) { if (simp.units[i].unit.name === 'VA' && isImaginary) { simp.units[i].unit = UNITS['VAR'] } else if (simp.units[i].unit.name === 'VAR' && !isImaginary) { simp.units[i].unit = UNITS['VA'] } } } // Now apply the best prefix // Units must have only one unit and not have the fixPrefix flag set if (simp.units.length === 1 && !simp.fixPrefix) { // Units must have integer powers, otherwise the prefix will change the // outputted value by not-an-integer-power-of-ten if (Math.abs(simp.units[0].power - Math.round(simp.units[0].power)) < 1e-14) { // Apply the best prefix simp.units[0].prefix = simp._bestPrefix() } } const value = simp._denormalize(simp.value) let str = (simp.value !== null) ? format(value, options || {}) : '' const unitStr = simp.formatUnits() if (simp.value && type.isComplex(simp.value)) { str = '(' + str + ')' // Surround complex values with ( ) to enable better parsing } if (unitStr.length > 0 && str.length > 0) { str += ' ' } str += unitStr return str } /** * Calculate the best prefix using current value. * @memberof Unit * @returns {Object} prefix * @private */ Unit.prototype._bestPrefix = function () { if (this.units.length !== 1) { throw new Error('Can only compute the best prefix for single units with integer powers, like kg, s^2, N^-1, and so forth!') } if (Math.abs(this.units[0].power - Math.round(this.units[0].power)) >= 1e-14) { throw new Error('Can only compute the best prefix for single units with integer powers, like kg, s^2, N^-1, and so forth!') } // find the best prefix value (resulting in the value of which // the absolute value of the log10 is closest to zero, // though with a little offset of 1.2 for nicer values: you get a // sequence 1mm 100mm 500mm 0.6m 1m 10m 100m 500m 0.6km 1km ... // Note: the units value can be any numeric type, but to find the best // prefix it's enough to work with limited precision of a regular number // Update: using mathjs abs since we also allow complex numbers const absValue = this.value !== null ? abs(this.value) : 0 const absUnitValue = abs(this.units[0].unit.value) let bestPrefix = this.units[0].prefix if (absValue === 0) { return bestPrefix } const power = this.units[0].power let bestDiff = Math.log(absValue / Math.pow(bestPrefix.value * absUnitValue, power)) / Math.LN10 - 1.2 if (bestDiff > -2.200001 && bestDiff < 1.800001) return bestPrefix // Allow the original prefix bestDiff = Math.abs(bestDiff) const prefixes = this.units[0].unit.prefixes for (const p in prefixes) { if (prefixes.hasOwnProperty(p)) { const prefix = prefixes[p] if (prefix.scientific) { const diff = Math.abs( Math.log(absValue / Math.pow(prefix.value * absUnitValue, power)) / Math.LN10 - 1.2) if (diff < bestDiff || (diff === bestDiff && prefix.name.length < bestPrefix.name.length)) { // choose the prefix with the smallest diff, or if equal, choose the one // with the shortest name (can happen with SHORTLONG for example) bestPrefix = prefix bestDiff = diff } } } } return bestPrefix } /** * Returns an array of units whose sum is equal to this unit * @memberof Unit * @param {Array} [parts] An array of strings or valueless units. * * Example: * * const u = new Unit(1, 'm') * u.splitUnit(['feet', 'inch']) * [ 3 feet, 3.3700787401575 inch ] * * @return {Array} An array of units. */ Unit.prototype.splitUnit = function (parts) { let x = this.clone() const ret = [] for (let i = 0; i < parts.length; i++) { // Convert x to the requested unit x = x.to(parts[i]) if (i === parts.length - 1) break // Get the numeric value of this unit const xNumeric = x.toNumeric() // Check to see if xNumeric is nearly equal to an integer, // since fix can incorrectly round down if there is round-off error const xRounded = round(xNumeric) let xFixed const isNearlyEqual = equal(xRounded, xNumeric) if (isNearlyEqual) { xFixed = xRounded } else { xFixed = fix(x.toNumeric()) } const y = new Unit(xFixed, parts[i].toString()) ret.push(y) x = subtract(x, y) } // This little bit fixes a bug where the remainder should be 0 but is a little bit off. // But instead of comparing x, the remainder, with zero--we will compare the sum of // all the parts so far with the original value. If they are nearly equal, // we set the remainder to 0. let testSum = 0 for (let i = 0; i < ret.length; i++) { testSum = add(testSum, ret[i].value) } if (equal(testSum, this.value)) { x.value = 0 } ret.push(x) return ret } const PREFIXES = { NONE: { '': { name: '', value: 1, scientific: true } }, SHORT: { '': { name: '', value: 1, scientific: true }, 'da': { name: 'da', value: 1e1, scientific: false }, 'h': { name: 'h', value: 1e2, scientific: false }, 'k': { name: 'k', value: 1e3, scientific: true }, 'M': { name: 'M', value: 1e6, scientific: true }, 'G': { name: 'G', value: 1e9, scientific: true }, 'T': { name: 'T', value: 1e12, scientific: true }, 'P': { name: 'P', value: 1e15, scientific: true }, 'E': { name: 'E', value: 1e18, scientific: true }, 'Z': { name: 'Z', value: 1e21, scientific: true }, 'Y': { name: 'Y', value: 1e24, scientific: true }, 'd': { name: 'd', value: 1e-1, scientific: false }, 'c': { name: 'c', value: 1e-2, scientific: false }, 'm': { name: 'm', value: 1e-3, scientific: true }, 'u': { name: 'u', value: 1e-6, scientific: true }, 'n': { name: 'n', value: 1e-9, scientific: true }, 'p': { name: 'p', value: 1e-12, scientific: true }, 'f': { name: 'f', value: 1e-15, scientific: true }, 'a': { name: 'a', value: 1e-18, scientific: true }, 'z': { name: 'z', value: 1e-21, scientific: true }, 'y': { name: 'y', value: 1e-24, scientific: true } }, LONG: { '': { name: '', value: 1, scientific: true }, 'deca': { name: 'deca', value: 1e1, scientific: false }, 'hecto': { name: 'hecto', value: 1e2, scientific: false }, 'kilo': { name: 'kilo', value: 1e3, scientific: true }, 'mega': { name: 'mega', value: 1e6, scientific: true }, 'giga': { name: 'giga', value: 1e9, scientific: true }, 'tera': { name: 'tera', value: 1e12, scientific: true }, 'peta': { name: 'peta', value: 1e15, scientific: true }, 'exa': { name: 'exa', value: 1e18, scientific: true }, 'zetta': { name: 'zetta', value: 1e21, scientific: true }, 'yotta': { name: 'yotta', value: 1e24, scientific: true }, 'deci': { name: 'deci', value: 1e-1, scientific: false }, 'centi': { name: 'centi', value: 1e-2, scientific: false }, 'milli': { name: 'milli', value: 1e-3, scientific: true }, 'micro': { name: 'micro', value: 1e-6, scientific: true }, 'nano': { name: 'nano', value: 1e-9, scientific: true }, 'pico': { name: 'pico', value: 1e-12, scientific: true }, 'femto': { name: 'femto', value: 1e-15, scientific: true }, 'atto': { name: 'atto', value: 1e-18, scientific: true }, 'zepto': { name: 'zepto', value: 1e-21, scientific: true }, 'yocto': { name: 'yocto', value: 1e-24, scientific: true } }, SQUARED: { '': { name: '', value: 1, scientific: true }, 'da': { name: 'da', value: 1e2, scientific: false }, 'h': { name: 'h', value: 1e4, scientific: false }, 'k': { name: 'k', value: 1e6, scientific: true }, 'M': { name: 'M', value: 1e12, scientific: true }, 'G': { name: 'G', value: 1e18, scientific: true }, 'T': { name: 'T', value: 1e24, scientific: true }, 'P': { name: 'P', value: 1e30, scientific: true }, 'E': { name: 'E', value: 1e36, scientific: true }, 'Z': { name: 'Z', value: 1e42, scientific: true }, 'Y': { name: 'Y', value: 1e48, scientific: true }, 'd': { name: 'd', value: 1e-2, scientific: false }, 'c': { name: 'c', value: 1e-4, scientific: false }, 'm': { name: 'm', value: 1e-6, scientific: true }, 'u': { name: 'u', value: 1e-12, scientific: true }, 'n': { name: 'n', value: 1e-18, scientific: true }, 'p': { name: 'p', value: 1e-24, scientific: true }, 'f': { name: 'f', value: 1e-30, scientific: true }, 'a': { name: 'a', value: 1e-36, scientific: true }, 'z': { name: 'z', value: 1e-42, scientific: true }, 'y': { name: 'y', value: 1e-48, scientific: true } }, CUBIC: { '': { name: '', value: 1, scientific: true }, 'da': { name: 'da', value: 1e3, scientific: false }, 'h': { name: 'h', value: 1e6, scientific: false }, 'k': { name: 'k', value: 1e9, scientific: true }, 'M': { name: 'M', value: 1e18, scientific: true }, 'G': { name: 'G', value: 1e27, scientific: true }, 'T': { name: 'T', value: 1e36, scientific: true }, 'P': { name: 'P', value: 1e45, scientific: true }, 'E': { name: 'E', value: 1e54, scientific: true }, 'Z': { name: 'Z', value: 1e63, scientific: true }, 'Y': { name: 'Y', value: 1e72, scientific: true }, 'd': { name: 'd', value: 1e-3, scientific: false }, 'c': { name: 'c', value: 1e-6, scientific: false }, 'm': { name: 'm', value: 1e-9, scientific: true }, 'u': { name: 'u', value: 1e-18, scientific: true }, 'n': { name: 'n', value: 1e-27, scientific: true }, 'p': { name: 'p', value: 1e-36, scientific: true }, 'f': { name: 'f', value: 1e-45, scientific: true }, 'a': { name: 'a', value: 1e-54, scientific: true }, 'z': { name: 'z', value: 1e-63, scientific: true }, 'y': { name: 'y', value: 1e-72, scientific: true } }, BINARY_SHORT_SI: { '': { name: '', value: 1, scientific: true }, 'k': { name: 'k', value: 1e3, scientific: true }, 'M': { name: 'M', value: 1e6, scientific: true }, 'G': { name: 'G', value: 1e9, scientific: true }, 'T': { name: 'T', value: 1e12, scientific: true }, 'P': { name: 'P', value: 1e15, scientific: true }, 'E': { name: 'E', value: 1e18, scientific: true }, 'Z': { name: 'Z', value: 1e21, scientific: true }, 'Y': { name: 'Y', value: 1e24, scientific: true } }, BINARY_SHORT_IEC: { '': { name: '', value: 1, scientific: true }, 'Ki': { name: 'Ki', value: 1024, scientific: true }, 'Mi': { name: 'Mi', value: Math.pow(1024, 2), scientific: true }, 'Gi': { name: 'Gi', value: Math.pow(1024, 3), scientific: true }, 'Ti': { name: 'Ti', value: Math.pow(1024, 4), scientific: true }, 'Pi': { name: 'Pi', value: Math.pow(1024, 5), scientific: true }, 'Ei': { name: 'Ei', value: Math.pow(1024, 6), scientific: true }, 'Zi': { name: 'Zi', value: Math.pow(1024, 7), scientific: true }, 'Yi': { name: 'Yi', value: Math.pow(1024, 8), scientific: true } }, BINARY_LONG_SI: { '': { name: '', value: 1, scientific: true }, 'kilo': { name: 'kilo', value: 1e3, scientific: true }, 'mega': { name: 'mega', value: 1e6, scientific: true }, 'giga': { name: 'giga', value: 1e9, scientific: true }, 'tera': { name: 'tera', value: 1e12, scientific: true }, 'peta': { name: 'peta', value: 1e15, scientific: true }, 'exa': { name: 'exa', value: 1e18, scientific: true }, 'zetta': { name: 'zetta', value: 1e21, scientific: true }, 'yotta': { name: 'yotta', value: 1e24, scientific: true } }, BINARY_LONG_IEC: { '': { name: '', value: 1, scientific: true }, 'kibi': { name: 'kibi', value: 1024, scientific: true }, 'mebi': { name: 'mebi', value: Math.pow(1024, 2), scientific: true }, 'gibi': { name: 'gibi', value: Math.pow(1024, 3), scientific: true }, 'tebi': { name: 'tebi', value: Math.pow(1024, 4), scientific: true }, 'pebi': { name: 'pebi', value: Math.pow(1024, 5), scientific: true }, 'exi': { name: 'exi', value: Math.pow(1024, 6), scientific: true }, 'zebi': { name: 'zebi', value: Math.pow(1024, 7), scientific: true }, 'yobi': { name: 'yobi', value: Math.pow(1024, 8), scientific: true } }, BTU: { '': { name: '', value: 1, scientific: true }, 'MM': { name: 'MM', value: 1e6, scientific: true } } } PREFIXES.SHORTLONG = Object.assign(PREFIXES.SHORT, PREFIXES.LONG) PREFIXES.BINARY_SHORT = Object.assign(PREFIXES.BINARY_SHORT_SI, PREFIXES.BINARY_SHORT_IEC) PREFIXES.BINARY_LONG = Object.assign(PREFIXES.BINARY_LONG_SI, PREFIXES.BINARY_LONG_IEC) /* Internally, each unit is represented by a value and a dimension array. The elements of the dimensions array have the following meaning: * Index Dimension * ----- --------- * 0 Length * 1 Mass * 2 Time * 3 Current * 4 Temperature * 5 Luminous intensity * 6 Amount of substance * 7 Angle * 8 Bit (digital) * For example, the unit "298.15 K" is a pure temperature and would have a value of 298.15 and a dimension array of [0, 0, 0, 0, 1, 0, 0, 0, 0]. The unit "1 cal / (gm °C)" can be written in terms of the 9 fundamental dimensions as [length^2] / ([time^2] * [temperature]), and would a value of (after conversion to SI) 4184.0 and a dimensions array of [2, 0, -2, 0, -1, 0, 0, 0, 0]. * */ const BASE_DIMENSIONS = ['MASS', 'LENGTH', 'TIME', 'CURRENT', 'TEMPERATURE', 'LUMINOUS_INTENSITY', 'AMOUNT_OF_SUBSTANCE', 'ANGLE', 'BIT'] const BASE_UNITS = { NONE: { dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, MASS: { dimensions: [1, 0, 0, 0, 0, 0, 0, 0, 0] }, LENGTH: { dimensions: [0, 1, 0, 0, 0, 0, 0, 0, 0] }, TIME: { dimensions: [0, 0, 1, 0, 0, 0, 0, 0, 0] }, CURRENT: { dimensions: [0, 0, 0, 1, 0, 0, 0, 0, 0] }, TEMPERATURE: { dimensions: [0, 0, 0, 0, 1, 0, 0, 0, 0] }, LUMINOUS_INTENSITY: { dimensions: [0, 0, 0, 0, 0, 1, 0, 0, 0] }, AMOUNT_OF_SUBSTANCE: { dimensions: [0, 0, 0, 0, 0, 0, 1, 0, 0] }, FORCE: { dimensions: [1, 1, -2, 0, 0, 0, 0, 0, 0] }, SURFACE: { dimensions: [0, 2, 0, 0, 0, 0, 0, 0, 0] }, VOLUME: { dimensions: [0, 3, 0, 0, 0, 0, 0, 0, 0] }, ENERGY: { dimensions: [1, 2, -2, 0, 0, 0, 0, 0, 0] }, POWER: { dimensions: [1, 2, -3, 0, 0, 0, 0, 0, 0] }, PRESSURE: { dimensions: [1, -1, -2, 0, 0, 0, 0, 0, 0] }, ELECTRIC_CHARGE: { dimensions: [0, 0, 1, 1, 0, 0, 0, 0, 0] }, ELECTRIC_CAPACITANCE: { dimensions: [-1, -2, 4, 2, 0, 0, 0, 0, 0] }, ELECTRIC_POTENTIAL: { dimensions: [1, 2, -3, -1, 0, 0, 0, 0, 0] }, ELECTRIC_RESISTANCE: { dimensions: [1, 2, -3, -2, 0, 0, 0, 0, 0] }, ELECTRIC_INDUCTANCE: { dimensions: [1, 2, -2, -2, 0, 0, 0, 0, 0] }, ELECTRIC_CONDUCTANCE: { dimensions: [-1, -2, 3, 2, 0, 0, 0, 0, 0] }, MAGNETIC_FLUX: { dimensions: [1, 2, -2, -1, 0, 0, 0, 0, 0] }, MAGNETIC_FLUX_DENSITY: { dimensions: [1, 0, -2, -1, 0, 0, 0, 0, 0] }, FREQUENCY: { dimensions: