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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more