ericman314 / unitmath Goto Github PK
View Code? Open in Web Editor NEWJavaScript library for unit conversion and arithmetic
License: Apache License 2.0
JavaScript library for unit conversion and arithmetic
License: Apache License 2.0
I have an issue with the simplify
conversion of certain complex units, which seems overly aggressive to me.
The following is an example using two simple units: energy and energy by square meter (very common in many sectors).
// Simple energy
e = unit(123456, 'Wh')
console.log(e.simplify().toString()) // 123.456 kWh
// Energy by square meter
e = unit(123456, 'Wh / m^2')
console.log(e.simplify().toString()) // 444441600 kg / s^2
Clearly the second simplification does not make much sense, even if it is absolutely correct.
How can this be prevented?
A few tests in math.js are still failing; these are related to creating custom units, and expecting those units to be used when simplifying expressions (they are not currently). In UnitMath, a unit has to be part of a system in order for it to appear in a simplified expression. When the chosen unit system is auto
, N m
becomes J
, and ft lbf
becomes BTU
.
Say we do createUnit(mph, 1 mi/hr)
in math.js. Currently, mi / hr
does not simplify to mph
as we would expect. To fix this, we need to assign mph
as the VELOCITY
unit of the us
system. This is possible if you include additional options to UnitMath
:
definitions: {
units: {
mph: '1 mi/hr'
},
unitSystems: {
us: {
VELOCITY: 'mph'
}
}
}
But math.js doesn't have access to the logic used to match quantities or systems, so it wouldn't be able to produce the unitSystems
object above. Things get even more complicated if a new quantity is introduced. I imagine that many users of UnitMath (including math.js) will not want to be encumbered by the verbose definitions required to make custom units members of a unit system:
definitions: {
units: {
snap: '1 m/s^4'
},
unitSystems: {
si: {
JOUNCE: 'snap'
}
},
quantities: {
JOUNCE: 'LENGTH TIME^-4'
}
}
We could, however, create a flag that would cause the unitSystems
and quantities
objects to be created automatically. That way, if we then do createUnit(mph, 1 mi/hr)
followed by 5 mi / hr
, we should get 5 mph
. But 5 km / hr
will still give 5 km/hr
since it's a different unit system.
definitions: {
units: {
snap: {
value: '1 m/s^4',
system: 'si' // or 'auto' to auto-select based on value
}
}
}
So in math.js, we would merely add the system: 'auto'
to each custom unit.
@harrysarson, @josdejong, any thoughts?
P.S. How it might work:
Just before the units are finished being created in UnitStore.js
, we look at each unit and if it has the system
key, we match its value to existing quantities. If an existing quantity matches, we use that, and if there is not a match, we add an additional quantity (snap_QUANT
or something). Then, if system
is an existing system we use that, and if it is 'auto'
then we match value
to a unit system (si
in this case due to the m
). Then we add the additional entries to quantities
and unitSystems
. Since system
is part of the unit definition, the user can retrieve the current definitions using unit.definitions()
, recovering the original options passed.
Seems to not be an issue in vitest but on metro, the bundler error.
getting this error:
Unable to resolve "./src/Unit.js" from "../../node_modules/unitmath/index.js"
tried 1.1.0 and 1.0.0 version.
importing import unit from "unitmath/es/UnitMath"
seems to work for now
The compare
method isn't consistent in its handling of NaNs. It should sort NaN values consistently. At the moment it breaks the sorting as calls to the compare function return surprise values.
There are a number of scenarios where you would like to aggregate units of different types together in a "sum".
A simple example might be ingredients for a recipe. Native support for a unit sum makes it easy to do things like combine the ingredients for any number of recipes together with simple addition.
Beyond addition, there are other operations that make sense in the context of a UnitSum. For example there are two definitions of "inner product" that are valuable.
One is defined as the sum-product of "exact" unit matches, so (2 egg + 2 tomato + 2 egg / day) • (3 egg) == 6 egg
The other is the sum-product of "fuzzy" unit matches,
so (1 egg / day + 1 croissant / mile) • (2 day + 4 mile) = (2 egg + 4 croissant)
All of the above is implemented in https://observablehq.com/@ajbouh/math-js-with-unitsum
I'd love to contribute this upstream!
I should add that there are a few open questions, like what the names for these operations should be and how to avoid needing to override so many internal mathjs operations.
Referencing josdejong/mathjs#1449 (comment)
I haven't found anything that solves the US system, Imperial, Metric differences of cup, Tbs etc well, same issue happens in measuring-cup. The need of
- Transform between systems (US, Imperial, Metric, etc)
- Transform between units (cup of sugar -> g of sugar)
Are very common use cases for cooking, (the 2nd point is a total different issue, but would be amazing to have something to do it).
For the second point maybe a complement like Moment timezone, could store all the transformations density of common ingredient, and some way to add explicit densities would be great.
UnitMath is shaping up nicely :) I have a question:
From the README (v0.3)
This means that if your custom type cannot be represented exactly by a floating-point number, you must use the two-argument
unit(value, unitString)
to construct units:// Correct let d = unit(Decimal('3.1415926535897932384626433832795', 'rad')) let f = unit(Fraction(1, 2), 'kg') // Incorrect let d = unit('3.1415926535897932384626433832795 rad') // Will lose precision let f = unit('1 / 2 kg') // Parse error
This seems like an unnecessary footgun to introduce and I wonder if it is not possible to provide a better alternative.
Querying quantities: I do not think it will be possible to "filter on all related units" using the
quantity
value. Defining this value tells the API to create a new base unit, like length or time. So when defining a unit such asmph
, there will not be aquantity
field. Instead ofquantity: 'SPEED'
, it will have the propertydimension: { LENGTH: 1, TIME: -1 }
, which it determines from the definition:"mph": "1 mi / hr"
.Having a
quantity
property for each unit was very appealing but I realized it was probably unnecessary. This is because there is already anequalsQuantity
method that compares the quantity of one unit with another, by comparing theirdimension
objects. In theory you could create your own reference object of units to compare against:const refQuantities = { MASS: 'kg', SPEED: 'm/s', ... ]; let myUnit = unit('4 mile/hour'); myUnit.equalsQuantity(refQuantities.SPEED); // trueI realize it's not quite as convenient as writing
myUnit.quantity
, but I really want to slim down the library to only the essential parts. I wanted to make it super easy to define your own units without requiring tons of boilerplate definitions. Perhaps eventually we could add a built-in feature to spit out"SPEED"
automatically, but I don't want the inner workings of the library to be dependent on this feature, as it would require users to specify these quantities for each custom unit they add. (We could also add agetMatchingUnits
method which returns an array of all the defined units which match the given unit. This could be used to populate a drop-down selector for a unit conversion tool, for example.)
Originally posted by @ericman314 in #34 (comment)
Exactly as stated in the above reply, a new getMatchingUnits
method would be of great use to developers. It would make developing front end unit manipulation components a lot easier.
With the new method, a personal use case where it would be beneficial involves simplifying the design of a data grid. Developers could populate a column with units of a specific quantity. The system would then allow the end user to select the display units from a custom component located in the column header. This component would include a dropdown menu with valid matching units for the given quantity.
It would also be useful for the library to provide a default reference quantities refQuantities
object, populated with the most common physical quantities, such as the mentioned "MASS"
, "SPEED"
, but also uncompounded powers of base quantities such as "AREA"
and "VOLUME"
. Developers could of course define their own object (or update the default one through the config
function/method of the module) for any custom quantities.
Such an addition would also make it easier to more easily enforce certain quantities when end users input units through making it possible to validate the input against these reference quantities.
Sometimes you want to convert a given unit to either X, Y or Z, and you need to select the best.
It would be very handy in cases where you need for example to design an UI, and the UI (due for example to client requests) can only have units m
and cm
, but not for example dm
, in
or dam
.
It is quite simple to convert the unit to all of them, then simply select the first one with value above (or closest to?) your minimum value (prefixMin
in unitmath
lingo).
This could be a nice little handy feature to be added to this nice little handy library!
It would be terrific if custom and built-in units could be defined based off of other units.
Example:
const UNITS = {
// length
meter: {
base: DIMENSIONS.LENGTH,
prefixes: PREFIXES.LONG,
commonPrefixes: ['nano', 'micro', 'milli', 'centi', '', 'kilo'],
value: 1
},
m: {
prefixes: PREFIXES.SHORT,
commonPrefixes: ['n', 'u', 'm', 'c', '', 'k'],
value: '1 meter'
},
inch: {
value: '0.0254 m'
},
foot: '12 inch',
...
Advantages:
PREFIXES.NONE
)foot
versus inch
above)UnitStore.js
file sizeDIMENSIONS
or determine the unit's value in SI unitsDisadvantages:
kg
would cause dependent units to change as well, which might be considered good or bad depending on the circumstances.What files do you plan to include in the published version? I have thought of the following:
"module"
field in package.json
."main"
field in package.json
.From the docs:
If the prefix or simplify options are set to 'auto' or 'always', the toString and format methods will try to simplify the unit before outputting. This can be prevented by calling .to() on a unit with no parameters, which will return a new unit that will not be simplified automatically.
It feels a bit like a side effect to use .to()
to prevent a unit from being simplified. Maybe that could use an explicit function, like .fixUnits()
or .disableSimplify()
or unit('2 m/s', { simplify: false})
or so. What do you think?
Hello
For explicitness and to prevent ugly errors of units misuse I propose to use long unit names in output:
now:
> 1b
1 b
> 1 m
1 m
after:
> 1 b
1 bit
> 1 m
1 meter
Lodash forces objects into values via valueOf
when sorting. This would be useful for sorting arrays of units, without having to pass custom compare function, etc.
v1.0 is getting very close! Thank you to @m93a for adding typescript support, and everyone else for your helpful comments.
Just a few more things left I can think of:
Currently in Math.js, the parser in Unit.js parses input such that the last two units below are parsed as 8.314 (J / mol) * K
, and are not equal to the first two:
8.314 J / mol / K
8.314 J / (mol K)
8.314 J / mol K
8.314 J / mol * K
I would like to propose a much simpler set of parsing rules that would make all these units equal:
*
's are ignored./
are in the numerator./
are in the denominator./
's are ignored.Being able to parse multiple *
, /
, and parentheses is cool, but it really seems beyond the scope of this library.
If I am not mistaken, in Math.js, math.eval
constructs compound units by parsing each individual node, then uses its own comprehensive parsing rules to build a tree. Only if a user calls math.Unit()
directly would the Unit.js
parser be invoked. So this change probably would not affect typical Math.js usage very much, I think.
Hello, maybe this is a very dumb question, but I can't figure out how to test this package.
If I manually load it in Node using require
, after installing with npm i unitmath
:
> require('unitmath')
/dev/shm/unitmath/node_modules/unitmath/index.js:1
import UnitMath from './src/Unit.js'
^^^^^^
Uncaught:
SyntaxError: Cannot use import statement inside the Node.js REPL, alternatively use dynamic import:
>
Node using import
:
> await import ("unitmath")
(node:145890) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
Uncaught [SyntaxError: Unexpected token (1:13)
] {
pos: 13,
loc: Position { line: 1, column: 13 },
raisedAt: 23
}
Deno using import
:
> await import('unitmath')
Uncaught TypeError: 'import', and 'export' cannot be used outside of module code at file:///dev/shm/unitmath/node_modules/.deno/[email protected]/node_modules/unitmath/index.js:1:1
import UnitMath from './src/Unit.js'
~~~~~~
at async <anonymous>:1:22
The same seem to occur when running a .js
or .mjs
file with the following content:
const unit = require('unitmath')
let a = unit('5 m').div('2 s') // 2.5 m / s
let b = unit('40 km').to('mile') // 24.8548476894934 mile
b.toString({ precision: 4 }) // "24.85 mile"
What am I doing wrong?
The API will be cleaner, bugs harder to introduce and maintenance a lot less of a burden if every type defined in unitmath is immutable*.
The major downside would be a possible performance cost but I would argue against any premature optimisations without benchmarks that show cloning units is a bottleneck.
Related: when configuring the global options, any changes made in one file should be independent from changes made in other files. Currently
unit.config(options)
updates the global unit configuration. A better API might be
newUnit = oldUnit.config(options)
where the config()
function returns a brand new instance.
I think part of the reason development has stalled, is the API is getting too complicated. The "auto add to unit system" feature, which is needed by mathjs, is what pushed me over the edge. As an experiment, I am starting a new branch where I am stripping out base quantities, quantities, and unit systems, and keeping only the units and prefixes--and see how many of the features I can retain with these simplifications. @josdejong, someday I hope it can be integrated into math.js.
In definitions.unitts, change prefixes to allowedPrefixes to avoid confusion with definitions.prefixes
I've added a proposed API to the README. Please share your comments and ideas below!
Hey Eric!
I've been reading through the code of make-things-less-complicated
and there's a part that didn't make sense to me. On line 384 of Unit.ts
there is this code:
// Did not find a matching unit in the system
// 4. Build a representation from the base units of all defined units
for (let dim in result.dimension) {
if (Math.abs(result.dimension[dim] || 0) > 1e-12) {
let found = false
for (let unit in unitStore.defs.units) {
if (unit.quantity === dim) {
// TODO: Try to use a matching unit from the specified system, instead of the base unit that was just found
proposedUnitList.push({
unit,
prefix: unit.basePrefix || '',
power: result.dimension[dim]
})
found = true
break
}
}
if (!found) ok = false
}
}
The for..in
loops define variables dim
and unit
as the keys of an object, right? Then how could unit.quantity
and unit.basePrefix
be anything other than undefined
? Did you mean something like this instead?
for (let unit of Object.values(unitStore.defs.units)) {
The Object.values
function returns the values of enumerable properties of an object.
When I try to compile dependabot update PR for Unitmath v1.0.0 I see the following:
ERROR in ../../node_modules/unitmath/index.js 1:0-36
--
| Module not found: Error: Can't resolve './src/Unit.js' in '/home/app/current/node_modules/unitmath'
| resolve './src/Unit.js' in '/home/app/current/node_modules/unitmath'
| using description file: /home/app/current/node_modules/unitmath/package.json (relative path: .)
| Field 'browser' doesn't contain a valid alias configuration
| using description file: /home/app/current/node_modules/unitmath/package.json (relative path: ./src/Unit.js)
| no extension
| Field 'browser' doesn't contain a valid alias configuration
| /home/app/current/node_modules/unitmath/src/Unit.js doesn't exist
| .ts
| Field 'browser' doesn't contain a valid alias configuration
| /home/app/current/node_modules/unitmath/src/Unit.js.ts doesn't exist
| .tsx
| Field 'browser' doesn't contain a valid alias configuration
| /home/app/current/node_modules/unitmath/src/Unit.js.tsx doesn't exist
| .js
| Field 'browser' doesn't contain a valid alias configuration
| /home/app/current/node_modules/unitmath/src/Unit.js.js doesn't exist
| .jsx
| Field 'browser' doesn't contain a valid alias configuration
| /home/app/current/node_modules/unitmath/src/Unit.js.jsx doesn't exist
| .min.js
| Field 'browser' doesn't contain a valid alias configuration
| /home/app/current/node_modules/unitmath/src/Unit.js.min.js doesn't exist
| as directory
| /home/app/current/node_modules/unitmath/src/Unit.js doesn't exist
I think it's taking issue with import UnitMath from "./src/Unit.js";
in the index.js as inside /src there is only Unit.ts - should this be changed to Unit.ts for it to work?
Or perhaps something not quite right with rollup config?
The library looks really great already 👍
After reading #15, I was thinking: there are about 10 functions that you have to implement to support a custom type like bignumbers. If I'm not mistaken, the library will not warn you when you have forgotten to implement some of them. This may give unexpected behavior that could cause loss of precision for example.
How about either forcing to implement all of the functions, or throwing a warning when using a custom type function that is not overridden whilst others are?
This is a reminder for me to put together a workflow for deploying new versions.
We have proposed allowing unitmath to be extended with custom numeric types by defining add
, multiply
etc in the config.
What happens if a user defines a unit based on a custom numeric type and tries to add a unit based on Number
s to it? What happens when a user tries to add two units based on different custom numeric types?
I'm using this awesome library in a larger TS project and it would be awesome to somehow have types for the objects returned by the library. I'm on an older version right now but the version 1.0.0 seems to not export any type information.
I saw that you started to adopt TS for the development of the library. Are there plans to somehow export types for users of this library? I'm no sure if i understand the code enough, but would you be interested in a Pull Request?
Do you consider providing TypeScript definitions for the API? Or, better, porting the source code to TypeScript entirely? I consider using this package in a repository where type safety is crucial, so this would be a welcome addition for me.
If you do consider any of these two things, I'll be hapy to help with that in any way I can :)
Looking at the docs of the custom formatter, https://github.com/ericman314/UnitMath#custom-formatter , I have the feeling that the formatter
option can be simpler, by removing the possibility to pass additional arguments to it via toString. I think you can achieve that already by changing:
unit('3.14159 rad').toString({ formatter: funnyFormatter }, '$', '_'
to
unit('3.14159 rad').toString({ formatter: value => funnyFormatter(value, '$', '_') }
// or
const funnyFormatter$ = value => funnyFormatter(value, '$', '_')
unit('3.14159 rad').toString({ formatter: funnyFormatter$ }
I would like to somehow make the config a place for custom units. I do not think it is necessary to have the createUnit
function to add units one at a time. Instead I think it best if all the custom units are included in the config and declared all at once. In fact, I would like to expose a frozen object of all the currently defined units somewhere, so that a user may query all the units and better discover how they work and are declared, and then call config with their own units, supplying an object with the same structure, which will extend the built-in units and return a new config.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.