Skip to content

Commit

Permalink
Immutable units (#1344)
Browse files Browse the repository at this point in the history
* Fixed unit base recognition and formatting for user-defined units

* Removed side effects from Unit.format()

* minor fix
  • Loading branch information
ericman314 authored and josdejong committed Dec 5, 2018
1 parent 9634f5e commit 6c03139
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 98 deletions.
101 changes: 49 additions & 52 deletions src/type/unit/Unit.js
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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)
}
Expand All @@ -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 = {}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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')
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6c03139

Please sign in to comment.