mirror of
https://github.com/josdejong/mathjs.git
synced 2026-02-01 16:07:46 +00:00
Immutable units (#1344)
* Fixed unit base recognition and formatting for user-defined units * Removed side effects from Unit.format() * minor fix
This commit is contained in:
parent
9634f5eba4
commit
6c03139ac8
@ -77,7 +77,7 @@ function factory (type, config, load, typed, math) {
|
||||
|
||||
// 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.isUnitListSimplified = true
|
||||
this.skipAutomaticSimplification = true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -422,7 +422,7 @@ function factory (type, config, load, typed, math) {
|
||||
const unit = new Unit()
|
||||
|
||||
unit.fixPrefix = this.fixPrefix
|
||||
unit.isUnitListSimplified = this.isUnitListSimplified
|
||||
unit.skipAutomaticSimplification = this.skipAutomaticSimplification
|
||||
|
||||
unit.value = clone(this.value)
|
||||
unit.dimensions = this.dimensions.slice(0)
|
||||
@ -652,7 +652,7 @@ function factory (type, config, load, typed, math) {
|
||||
res.dimensions[i] = (this.dimensions[i] || 0) + (other.dimensions[i] || 0)
|
||||
}
|
||||
|
||||
// Append other's units list onto res (simplify later in Unit.prototype.format)
|
||||
// Append other's units list onto res
|
||||
for (let i = 0; i < other.units.length; i++) {
|
||||
// Make a deep copy
|
||||
const inverted = {}
|
||||
@ -671,8 +671,7 @@ function factory (type, config, load, typed, math) {
|
||||
res.value = null
|
||||
}
|
||||
|
||||
// Trigger simplification of the unit list at some future time
|
||||
res.isUnitListSimplified = false
|
||||
res.skipAutomaticSimplification = false
|
||||
|
||||
return getNumericIfUnitless(res)
|
||||
}
|
||||
@ -691,7 +690,7 @@ function factory (type, config, load, typed, math) {
|
||||
res.dimensions[i] = (this.dimensions[i] || 0) - (other.dimensions[i] || 0)
|
||||
}
|
||||
|
||||
// Invert and append other's units list onto res (simplify later in Unit.prototype.format)
|
||||
// Invert and append other's units list onto res
|
||||
for (let i = 0; i < other.units.length; i++) {
|
||||
// Make a deep copy
|
||||
const inverted = {}
|
||||
@ -711,8 +710,7 @@ function factory (type, config, load, typed, math) {
|
||||
res.value = null
|
||||
}
|
||||
|
||||
// Trigger simplification of the unit list at some future time
|
||||
res.isUnitListSimplified = false
|
||||
res.skipAutomaticSimplification = false
|
||||
|
||||
return getNumericIfUnitless(res)
|
||||
}
|
||||
@ -748,8 +746,7 @@ function factory (type, config, load, typed, math) {
|
||||
res.value = null
|
||||
}
|
||||
|
||||
// Trigger lazy evaluation of the unit list
|
||||
res.isUnitListSimplified = false
|
||||
res.skipAutomaticSimplification = false
|
||||
|
||||
return getNumericIfUnitless(res)
|
||||
}
|
||||
@ -809,7 +806,7 @@ function factory (type, config, load, typed, math) {
|
||||
|
||||
other.value = clone(value)
|
||||
other.fixPrefix = true
|
||||
other.isUnitListSimplified = true
|
||||
other.skipAutomaticSimplification = true
|
||||
return other
|
||||
} else if (type.isUnit(valuelessUnit)) {
|
||||
if (!this.equalBase(valuelessUnit)) {
|
||||
@ -821,7 +818,7 @@ function factory (type, config, load, typed, math) {
|
||||
other = valuelessUnit.clone()
|
||||
other.value = clone(value)
|
||||
other.fixPrefix = true
|
||||
other.isUnitListSimplified = true
|
||||
other.skipAutomaticSimplification = true
|
||||
return other
|
||||
} else {
|
||||
throw new Error('String or Unit expected as parameter')
|
||||
@ -846,14 +843,14 @@ function factory (type, config, load, typed, math) {
|
||||
* @return {number | BigNumber | Fraction} Returns the unit value
|
||||
*/
|
||||
Unit.prototype.toNumeric = function (valuelessUnit) {
|
||||
let other = this
|
||||
let other
|
||||
if (valuelessUnit) {
|
||||
// Allow getting the numeric value without converting to a different unit
|
||||
other = this.to(valuelessUnit)
|
||||
} else {
|
||||
other = this.clone()
|
||||
}
|
||||
|
||||
other.simplifyUnitListLazy()
|
||||
|
||||
if (other._isDerived()) {
|
||||
return other._denormalize(other.value)
|
||||
} else {
|
||||
@ -906,27 +903,25 @@ function factory (type, config, load, typed, math) {
|
||||
Unit.prototype.valueOf = Unit.prototype.toString
|
||||
|
||||
/**
|
||||
* Attempt to simplify the list of units for this unit according to the dimensions array and the current unit system. After the call, this Unit will contain a list of the "best" units for formatting.
|
||||
* Intended to be evaluated lazily. You must set isUnitListSimplified = false before the call! After the call, isUnitListSimplified will be set to true.
|
||||
* 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.simplifyUnitListLazy = function () {
|
||||
if (this.isUnitListSimplified || this.value === null) {
|
||||
return
|
||||
}
|
||||
Unit.prototype.simplify = function () {
|
||||
const ret = this.clone()
|
||||
|
||||
const proposedUnitList = []
|
||||
|
||||
// Search for a matching base
|
||||
let matchingBase
|
||||
for (const key in currentUnitSystem) {
|
||||
if (this.hasBase(BASE_UNITS[key])) {
|
||||
if (ret.hasBase(BASE_UNITS[key])) {
|
||||
matchingBase = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingBase === 'NONE') {
|
||||
this.units = []
|
||||
ret.units = []
|
||||
} else {
|
||||
let matchingUnit
|
||||
if (matchingBase) {
|
||||
@ -936,7 +931,7 @@ function factory (type, config, load, typed, math) {
|
||||
}
|
||||
}
|
||||
if (matchingUnit) {
|
||||
this.units = [{
|
||||
ret.units = [{
|
||||
unit: matchingUnit.unit,
|
||||
prefix: matchingUnit.prefix,
|
||||
power: 1.0
|
||||
@ -948,12 +943,12 @@ function factory (type, config, load, typed, math) {
|
||||
let missingBaseDim = false
|
||||
for (let i = 0; i < BASE_DIMENSIONS.length; i++) {
|
||||
const baseDim = BASE_DIMENSIONS[i]
|
||||
if (Math.abs(this.dimensions[i] || 0) > 1e-12) {
|
||||
if (Math.abs(ret.dimensions[i] || 0) > 1e-12) {
|
||||
if (currentUnitSystem.hasOwnProperty(baseDim)) {
|
||||
proposedUnitList.push({
|
||||
unit: currentUnitSystem[baseDim].unit,
|
||||
prefix: currentUnitSystem[baseDim].prefix,
|
||||
power: this.dimensions[i] || 0
|
||||
power: ret.dimensions[i] || 0
|
||||
})
|
||||
} else {
|
||||
missingBaseDim = true
|
||||
@ -962,16 +957,19 @@ function factory (type, config, load, typed, math) {
|
||||
}
|
||||
|
||||
// Is the proposed unit list "simpler" than the existing one?
|
||||
if (proposedUnitList.length < this.units.length && !missingBaseDim) {
|
||||
if (proposedUnitList.length < ret.units.length && !missingBaseDim) {
|
||||
// Replace this unit list with the proposed list
|
||||
this.units = proposedUnitList
|
||||
ret.units = proposedUnitList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isUnitListSimplified = true
|
||||
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()
|
||||
|
||||
@ -999,20 +997,17 @@ function factory (type, config, load, typed, math) {
|
||||
ret.units = proposedUnitList
|
||||
|
||||
ret.fixPrefix = true
|
||||
ret.isUnitListSimplified = true
|
||||
ret.skipAutomaticSimplification = true
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string representation of the units of this Unit, without the value.
|
||||
* 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 () {
|
||||
// Lazy evaluation of the unit list
|
||||
this.simplifyUnitListLazy()
|
||||
|
||||
let strNum = ''
|
||||
let strDen = ''
|
||||
let nNum = 0
|
||||
@ -1076,41 +1071,43 @@ function factory (type, config, load, typed, math) {
|
||||
* @return {string}
|
||||
*/
|
||||
Unit.prototype.format = function (options) {
|
||||
// Simplfy the unit list, if necessary
|
||||
this.simplifyUnitListLazy()
|
||||
// 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 (this.value) !== 'undefined' && this.value !== null && type.isComplex(this.value)) {
|
||||
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(this.value.re) < 1e-14
|
||||
isImaginary = Math.abs(simp.value.re) < 1e-14
|
||||
}
|
||||
|
||||
for (const i in this.units) {
|
||||
if (this.units[i].unit) {
|
||||
if (this.units[i].unit.name === 'VA' && isImaginary) {
|
||||
this.units[i].unit = UNITS['VAR']
|
||||
} else if (this.units[i].unit.name === 'VAR' && !isImaginary) {
|
||||
this.units[i].unit = UNITS['VA']
|
||||
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 (this.units.length === 1 && !this.fixPrefix) {
|
||||
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(this.units[0].power - Math.round(this.units[0].power)) < 1e-14) {
|
||||
if (Math.abs(simp.units[0].power - Math.round(simp.units[0].power)) < 1e-14) {
|
||||
// Apply the best prefix
|
||||
this.units[0].prefix = this._bestPrefix()
|
||||
simp.units[0].prefix = simp._bestPrefix()
|
||||
}
|
||||
}
|
||||
|
||||
const value = this._denormalize(this.value)
|
||||
let str = (this.value !== null) ? format(value, options || {}) : ''
|
||||
const unitStr = this.formatUnits()
|
||||
if (this.value && type.isComplex(this.value)) {
|
||||
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) {
|
||||
@ -1500,7 +1497,7 @@ function factory (type, config, load, typed, math) {
|
||||
|
||||
const BASE_UNIT_NONE = {}
|
||||
|
||||
const UNIT_NONE = { name: '', base: BASE_UNIT_NONE, value: 1, offset: 0, dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 0] }
|
||||
const UNIT_NONE = { name: '', base: BASE_UNIT_NONE, value: 1, offset: 0, dimensions: BASE_DIMENSIONS.map(x => 0) }
|
||||
|
||||
const UNITS = {
|
||||
// length
|
||||
|
||||
@ -80,13 +80,13 @@ describe('Unit', function () {
|
||||
assert.throws(function () { console.log(new Unit(0, 3)) })
|
||||
})
|
||||
|
||||
it('should flag unit as already simplified', function () {
|
||||
it('should skip automatic simplification if created directly in the constructor', function () {
|
||||
const unit1 = new Unit(9.81, 'kg m/s^2')
|
||||
assert.strictEqual(unit1.isUnitListSimplified, true)
|
||||
assert.strictEqual(unit1.skipAutomaticSimplification, true)
|
||||
assert.strictEqual(unit1.toString(), '9.81 (kg m) / s^2')
|
||||
|
||||
const unit2 = new Unit(null, 'kg m/s^2')
|
||||
assert.strictEqual(unit2.isUnitListSimplified, true)
|
||||
assert.strictEqual(unit2.skipAutomaticSimplification, true)
|
||||
assert.strictEqual(unit2.toString(), '(kg m) / s^2')
|
||||
})
|
||||
})
|
||||
@ -236,12 +236,6 @@ describe('Unit', function () {
|
||||
const u = new Unit(math.fraction(1, 3), 'cm')
|
||||
assert.deepStrictEqual(u.toNumeric('mm'), math.fraction(10, 3))
|
||||
})
|
||||
it('should simplify units before returning a numeric value', function () {
|
||||
const cm = new Unit(1, 'gram')
|
||||
const m = new Unit(1, 'kilogram')
|
||||
const product = cm.multiply(m)
|
||||
assert.deepStrictEqual(product.toNumeric(), 0.001)
|
||||
})
|
||||
})
|
||||
|
||||
describe('to', function () {
|
||||
@ -398,11 +392,15 @@ describe('Unit', function () {
|
||||
assert.strictEqual(u2.fixPrefix, true)
|
||||
})
|
||||
|
||||
it('should set isUnitListSimplified to true', function () {
|
||||
const u1 = new Unit(1, 'ft lbf')
|
||||
const u2 = u1.to('in lbf')
|
||||
assert.strictEqual(u2.isUnitListSimplified, true)
|
||||
assert.strictEqual(u2.toString(), '12 in lbf')
|
||||
it('should skip automatic simplification if unit is result of to or toSI', function () {
|
||||
const u1 = new Unit(1, 'ft lbf').multiply(new Unit(2, 'rad'))
|
||||
assert.strictEqual(u1.skipAutomaticSimplification, false)
|
||||
const u2 = u1.to('in lbf rad')
|
||||
assert.strictEqual(u2.skipAutomaticSimplification, true)
|
||||
assert.strictEqual(u2.toString(), '24 in lbf rad')
|
||||
const u3 = u1.toSI()
|
||||
assert.strictEqual(u3.skipAutomaticSimplification, true)
|
||||
assert.strictEqual(u3.format(5), '2.7116 (kg m^2 rad) / s^2')
|
||||
})
|
||||
|
||||
it('should throw an error when converting to an incompatible unit', function () {
|
||||
@ -493,7 +491,7 @@ describe('Unit', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('simplifyUnitListLazy', function () {
|
||||
describe('simplify', function () {
|
||||
it('should not simplify units created with new Unit()', function () {
|
||||
const unit1 = new Unit(10, 'kg m/s^2')
|
||||
assert.strictEqual(unit1.units[0].unit.name, 'g')
|
||||
@ -503,25 +501,12 @@ describe('Unit', function () {
|
||||
})
|
||||
|
||||
it('should only simplify units with values', function () {
|
||||
let unit1 = new Unit(null, 'kg m mol / s^2 / mol')
|
||||
unit1.isUnitListSimplified = false
|
||||
unit1.simplifyUnitListLazy()
|
||||
let unit1 = new Unit(null, 'kg m mol / s^2 / mol').pow(1) // Remove the "skipSimplify" flag
|
||||
assert.strictEqual(unit1.toString(), '(kg m mol) / (s^2 mol)')
|
||||
unit1 = math.multiply(unit1, 1)
|
||||
assert.strictEqual(unit1.toString(), '1 N')
|
||||
})
|
||||
|
||||
it('should simplify units resulting from multiply/divide/power functions only when formatting for output', function () {
|
||||
const unit1 = new Unit(2, 'kg')
|
||||
const unit2 = new Unit(5, 'm/s^2')
|
||||
const unit3 = math.multiply(unit1, unit2)
|
||||
assert.strictEqual(unit3.units[0].unit.name, 'g')
|
||||
assert.strictEqual(unit3.units[1].unit.name, 'm')
|
||||
assert.strictEqual(unit3.units[2].unit.name, 's')
|
||||
assert.strictEqual(unit3.toString(), '10 N') // Triggers simplification
|
||||
assert.strictEqual(unit3.units[0].unit.name, 'N')
|
||||
})
|
||||
|
||||
it('should simplify units when they cancel out with {predictable: true}', function () {
|
||||
const origConfig = math.config()
|
||||
math.config({ predictable: true })
|
||||
@ -529,11 +514,11 @@ describe('Unit', function () {
|
||||
const unit2 = new Unit(2, 's')
|
||||
const unit3 = math.multiply(unit1, unit2)
|
||||
assert.strictEqual(unit3.toString(), '4')
|
||||
assert.strictEqual(unit3.units.length, 0)
|
||||
assert.strictEqual(unit3.simplify().units.length, 0)
|
||||
|
||||
const nounit = math.eval('40m * 40N / (40J)')
|
||||
assert.strictEqual(nounit.toString(), '40')
|
||||
assert.strictEqual(nounit.units.length, 0)
|
||||
assert.strictEqual(nounit.simplify().units.length, 0)
|
||||
|
||||
const a = math.unit('3 s^-1')
|
||||
const b = math.unit('4 s')
|
||||
@ -584,21 +569,18 @@ describe('Unit', function () {
|
||||
it('should simplify units according to chosen unit system', function () {
|
||||
const unit1 = new Unit(10, 'N')
|
||||
Unit.setUnitSystem('us')
|
||||
unit1.isUnitListSimplified = false
|
||||
assert.strictEqual(unit1.toString(), '2.248089430997105 lbf')
|
||||
assert.strictEqual(unit1.units[0].unit.name, 'lbf')
|
||||
assert.strictEqual(unit1.simplify().toString(), '2.248089430997105 lbf')
|
||||
assert.strictEqual(unit1.simplify().units[0].unit.name, 'lbf')
|
||||
|
||||
Unit.setUnitSystem('cgs')
|
||||
unit1.isUnitListSimplified = false
|
||||
assert.strictEqual(unit1.format(2), '1 Mdyn')
|
||||
assert.strictEqual(unit1.units[0].unit.name, 'dyn')
|
||||
assert.strictEqual(unit1.simplify().format(2), '1 Mdyn')
|
||||
assert.strictEqual(unit1.simplify().units[0].unit.name, 'dyn')
|
||||
})
|
||||
|
||||
it('should correctly simplify units when unit system is "auto"', function () {
|
||||
Unit.setUnitSystem('auto')
|
||||
const unit1 = new Unit(5, 'lbf min / s')
|
||||
unit1.isUnitListSimplified = false
|
||||
assert.strictEqual(unit1.toString(), '300 lbf')
|
||||
assert.strictEqual(unit1.simplify().toString(), '300 lbf')
|
||||
})
|
||||
|
||||
it('should simplify user-defined units when unit system is "auto"', function () {
|
||||
@ -961,19 +943,17 @@ describe('Unit', function () {
|
||||
assert.strictEqual(Unit.parse('34 kg m / s^2')._isDerived(), true)
|
||||
const unit1 = Unit.parse('34 kg m / s^2')
|
||||
assert.strictEqual(unit1._isDerived(), true)
|
||||
unit1.isUnitListSimplified = false
|
||||
unit1.simplifyUnitListLazy()
|
||||
assert.strictEqual(unit1._isDerived(), false)
|
||||
assert.strictEqual(unit1.simplify()._isDerived(), false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiply, divide, and pow', function () {
|
||||
it('should flag the unit as requiring simplification', function () {
|
||||
it('should return a Unit that will be automatically simplified', function () {
|
||||
const unit1 = new Unit(10, 'kg')
|
||||
const unit2 = new Unit(9.81, 'm/s^2')
|
||||
assert.strictEqual(unit1.multiply(unit2).isUnitListSimplified, false)
|
||||
assert.strictEqual(unit1.divide(unit2).isUnitListSimplified, false)
|
||||
assert.strictEqual(unit1.pow(2).isUnitListSimplified, false)
|
||||
assert.strictEqual(unit1.multiply(unit2).skipAutomaticSimplification, false)
|
||||
assert.strictEqual(unit1.divide(unit2).skipAutomaticSimplification, false)
|
||||
assert.strictEqual(unit1.pow(2).skipAutomaticSimplification, false)
|
||||
})
|
||||
|
||||
it('should retain the units of their operands without simplifying', function () {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user