Published
Edited
Jun 12, 2019
Importers
1 star
Insert cell
md`# math.js with UnitSum`
Insert cell
_math = require('https://unpkg.com/mathjs@5.9.0/dist/math.min.js')
Insert cell
/*

// From https://mathjs.org/docs/expressions/security.html
const limitedEval = math.eval

math.import({
'import': function () { throw new Error('Function import is disabled') },
'createUnit': function () { throw new Error('Function createUnit is disabled') },
'eval': function () { throw new Error('Function eval is disabled') },
'parse': function () { throw new Error('Function parse is disabled') },
'simplify': function () { throw new Error('Function simplify is disabled') },
'derivative': function () { throw new Error('Function derivative is disabled') }
}, {override: true})

*/

Insert cell
UnitSum = {
let math = _math
const addendUnitKey = (unit) => {
return JSON.stringify(unit.dimensions)
}

const addendUnitReduction = (addends, reduction, defaultValue, keysAndUnits) => {
// For each [key, unit] of keysAndUnits, replace key slot in addents with the reduction of the current value and with unit
keysAndUnits.forEach(([key, unit]) => {
let existing = addends[key]
let r = reduction(existing === undefined ? defaultValue : addends[key], unit)
if (r === undefined) {
delete addends[key]
} else {
addends[key] = r
}
})
}

const addendUnitCartesianProduct = (addends, keysAndUnits, otherKeysAndUnits, map, reduce) => {
keysAndUnits.forEach(([key, unit]) => {
otherKeysAndUnits.forEach(([otherKey, otherUnit]) => {
let r = map(unit, otherUnit)
if (r === undefined) {
return
}
let rKey = addendUnitKey(r)
let existing = addends[rKey]
if (existing) {
addends[rKey] = reduce(existing, r)
} else {
addends[rKey] = r
}
})
})
}

const addendMap = (addends, fn) => {
Object.keys(addends).forEach(key => {
addends[key] = fn(addends[key])
})
}
class UnitSum {
static fromUnits(units) {
const addends = {}
addendUnitReduction(addends, math.add, 0, units.map(unit => [addendUnitKey(unit), unit]))
return new UnitSum(addends).simplify()
}

constructor(addends) {
this.addends = addends
}

simplify() {
return Object.keys(this.addends).length === 0 ? 0 : this
}
clone() {
return new UnitSum(Object.assign({}, this.addends))
}
add(other) {
let res = this.clone()
addendUnitReduction(res.addends, math.add, 0, Object.entries(other.addends))
return res.simplify()
}

addUnit(unit) {
let res = this.clone()
addendUnitReduction(res.addends, math.add, 0, [[addendUnitKey(unit), unit]])
return res.simplify()
}
dot2(other) {
let addends = {}
console.log("dot2", this.toString(), other.toString())
addendUnitCartesianProduct(
addends,
Object.entries(this.addends),
Object.entries(other.addends),
(a, b) => {
if (!a.units.some(au => b.units.some(bu => au.unit.base.key === bu.unit.base.key))) {
return undefined
}

console.log("dot mult", math.multiply(a, b).toString())
return math.multiply(a, b)
},
(a, b) => math.add(a, b),
)
return new UnitSum(addends).simplify()
}
dot3(other) {
let res = this.clone()
let keys = new Set(Object.keys(res.addends))
Object.keys(other.addends).forEach(key => keys.add(key))
addendUnitReduction(
res.addends,
(a, b) => {
if (a === undefined || b === undefined) {
console.log("dot", a, b, "=>", undefined)
return a || b
}
let bunit = b.clone()
bunit.value = null
let bunitname = bunit.toString()
const r = math.multiply(a.toNumeric(bunitname), b)
console.log("dot", a, b, "=>", r)
return r
},
undefined,
Array.from(keys).map(key => [key, other.addends[key]]),
)
return res
}
dot(other) {
let res = this.clone()
let keys = new Set(Object.keys(res.addends))
Object.keys(other.addends).forEach(key => keys.add(key))
addendUnitReduction(
res.addends,
(a, b) => {
if (a === undefined || b === undefined) {
console.log("dot", a, b, "=>", undefined)
return undefined
}
let bunit = b.clone()
bunit.value = null
let bunitname = bunit.toString()
const r = math.multiply(a.toNumeric(bunitname), b)
console.log("dot", a, b, "=>", r)
return r
},
undefined,
Array.from(keys).map(key => [key, other.addends[key]]),
)
return res.simplify()
}

multiplyScalar(scalar) {
console.log("multiplyScalar", scalar, this)
let res = this.clone()
addendMap(res.addends, a => {
console.log("multiplyScalar multiply", scalar, a)
return math.multiply(scalar, a)
})
return res.simplify()
}

divideScalar(scalar) {
let res = this.clone()
addendMap(res.addends, a => math.divide(a, scalar))
return res.simplify()
}
multiplyUnit(y) {
return UnitSum.fromUnits(
Object.values(this.addends).map(
x => math.multiply(x, y)
)
)
}

divideUnit(y) {
return UnitSum.fromUnits(
Object.values(this.addends).map(
x => math.divide(x, y)
)
)
}

get [Symbol.toStringTag]() {
return 'UnitSum ' + this.toString()
}

format(...args) {
return Array.from(Object.values(this.addends), v => math.format(v, ...args)).join(" + ")
}

toString() {
return Object.values(this.addends).slice().join(" + ")
}

isUnitSum() { return true }
}
return UnitSum
}

Insert cell
math = {
let math = _math
math.config({
// We want mixed-input arrays like [null, 0.5] to be JavaScript Arrays
matrix: 'Array',
number: 'Fraction',
})
math.typed.addType({
name: 'UnitSum',
test: x => x && x.isUnitSum,
})

// math.typed.addType({
// name: 'ArrayProxy',
// test: x => x && x[NewArrayProxySymbol],
// })
if (!math.type.Unit.UNITS.usd) {
math.createUnit('usd')
math.createUnit('dollar', 'usd')
math.createUnit('dollars', 'dollar')
}

math.import({
'sum': math.typed(
Object.assign({}, math.sum.signatures, {
'Array': (a) => a.reduce((x, y) => math.add(x, y), 0),
})),
'dot': math.typed(
Object.assign({}, math.dot.signatures, {
'UnitSum,UnitSum': (a, b) => a.dot(b),
'Unit,UnitSum': (a, b) => UnitSum.fromUnits([a]).dot(b),
'UnitSum,Unit': (a, b) => a.dot(UnitSum.fromUnits([b])),
'Unit,Unit': (a, b) => UnitSum.fromUnits([a]).dot(UnitSum.fromUnits([b])),
'UnitSum,Fraction': (a, b) => a.multiplyScalar(b),
'Fraction,UnitSum': (a, b) => b.multiplyScalar(a),
'Unit,Fraction': (a, b) => math.multiply(a, b),
'Fraction,Unit': (a, b) => math.multiply(a, b),
'Fraction,Fraction': (a, b) => math.multiply(a, b),
'UnitSum,number': (a, b) => a.multiplyScalar(b),
'number,UnitSum': (a, b) => b.multiplyScalar(a),
'Unit,number': (a, b) => math.multiply(a, b),
'number,Unit': (a, b) => math.multiply(a, b),
'number,number': (a, b) => math.multiply(a, b),
})),
'dot2': math.typed(
Object.assign({}, math.dot.signatures, {
'UnitSum,UnitSum': (a, b) => a.dot2(b),
'Unit,UnitSum': (a, b) => UnitSum.fromUnits([a]).dot2(b),
'UnitSum,Unit': (a, b) => a.dot2(UnitSum.fromUnits([b])),
'Unit,Unit': (a, b) => UnitSum.fromUnits([a]).dot2(UnitSum.fromUnits([b])),
'UnitSum,Fraction': (a, b) => a.multiplyScalar(b),
'Fraction,UnitSum': (a, b) => b.multiplyScalar(a),
'Unit,Fraction': (a, b) => math.multiply(a, b),
'Fraction,Unit': (a, b) => math.multiply(a, b),
'Fraction,Fraction': (a, b) => math.multiply(a, b),
'UnitSum,number': (a, b) => a.multiplyScalar(b),
'number,UnitSum': (a, b) => b.multiplyScalar(a),
'Unit,number': (a, b) => math.multiply(a, b),
'number,Unit': (a, b) => math.multiply(a, b),
'number,number': (a, b) => math.multiply(a, b),
})),
'dot3': math.typed(
Object.assign({}, math.dot.signatures, {
'UnitSum,UnitSum': (a, b) => a.dot3(b),
'Unit,UnitSum': (a, b) => UnitSum.fromUnits([a]).dot3(b),
'UnitSum,Unit': (a, b) => a.dot3(UnitSum.fromUnits([b])),
'Unit,Unit': (a, b) => UnitSum.fromUnits([a]).dot3(UnitSum.fromUnits([b])),
'UnitSum,Fraction': (a, b) => a.multiplyScalar(b),
'Fraction,UnitSum': (a, b) => b.multiplyScalar(a),
'Unit,Fraction': (a, b) => math.multiply(a, b),
'Fraction,Unit': (a, b) => math.multiply(a, b),
'Fraction,Fraction': (a, b) => math.multiply(a, b),
'UnitSum,number': (a, b) => a.multiplyScalar(b),
'number,UnitSum': (a, b) => b.multiplyScalar(a),
'Unit,number': (a, b) => math.multiply(a, b),
'number,Unit': (a, b) => math.multiply(a, b),
'number,number': (a, b) => math.multiply(a, b),
})),
'multiply': math.typed(
Object.assign({}, math.multiply.signatures, {
'UnitSum,Unit': (a, b) => a.multiplyUnit(b),
'Unit,UnitSum': (a, b) => b.multiplyUnit(a),
'UnitSum,Fraction': (a, b) => a.multiplyScalar(b),
'Fraction,UnitSum': (a, b) => b.multiplyScalar(a),
'UnitSum,number': (a, b) => a.multiplyScalar(b),
'number,UnitSum': (a, b) => b.multiplyScalar(a),
})),
'divide': math.typed(
Object.assign({}, math.divide.signatures, {
'UnitSum,Fraction': (a, b) => a.divideScalar(b),
'UnitSum,number': (a, b) => a.divideScalar(b),
'UnitSum,Unit': (a, b) => a.divideUnit(b),
})),
'add': math.typed(
Object.assign({}, math.add.signatures, {
'Unit,number': (x, y) => y == 0 ? x : UnitSum.fromUnits([x, math.unit(y)]),
'number,Unit': (x, y) => x == 0 ? y : UnitSum.fromUnits([y, math.unit(x)]),
'number,UnitSum': (x, y) => x == 0 ? y : y.addUnit(math.unit(x)),
'UnitSum,number': (x, y) => y == 0 ? x : x.addUnit(math.unit(y)),
'Fraction,Unit': (x, y) => x == 0 ? y : UnitSum.fromUnits([y, math.unit(x)]),
'Unit,Fraction': (x, y) => y == 0 ? x : UnitSum.fromUnits([x, math.unit(y)]),
'Fraction,UnitSum': (x, y) => x == 0 ? y : y.addUnit(math.unit(x)),
'UnitSum,Fraction': (x, y) => y == 0 ? x : x.addUnit(math.unit(y)),
'Unit,Unit': function (x, y) {
if (x.value === null || x.value === undefined) {
throw new Error('Parameter x contains a unit with undefined value')
}
if (y.value === null || y.value === undefined) {
throw new Error('Parameter y contains a unit with undefined value')
}

if (!x.equalBase(y)) {
return UnitSum.fromUnits([x, y])
}

const res = x.clone()
res.value = math.add(res.value, y.value)
res.fixPrefix = false
return res
},
'Unit,UnitSum': (a, b) => b.addUnit(a),
'UnitSum,Unit': (a, b) => a.addUnit(b),
'UnitSum,UnitSum': (a, b) => a.add(b),
})
),
}, {override: true})

return math
}
Insert cell
md`## Tests`
Insert cell
math.eval('0 L + 0 W').toString()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more