ericman314 / UnitMath

JavaScript library for unit conversion and arithmetic
Apache License 2.0
30 stars 7 forks source link

Implement an explicit method to prevent a unit from being simplified #46

Closed josdejong closed 1 year ago

josdejong commented 2 years ago

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?

ericman314 commented 2 years ago

Yeah that's a good suggestion, I agree that the different use case for .to() without arguments could be confusing. There will still be a side effect of using .to(valuelessUnit), which is the returned unit has a flag set that prevents automatic simplification. You can also prevent simplification by saying u.toString({ simplify: 'never', prefix: 'never' }).

I'll make a new method fix() which implements the parameter-less to(), and then remove the parameter-less implementation from to. In the docs I will highlight how calling u.fix() is equivalent but faster than u.to(u.getUnits()).

josdejong commented 2 years ago

Sounds good. The naming may be a thing: fix can potentially be confused with the mathematical function to round a number towards zero.

https://mathjs.org/docs/reference/functions/fix.html https://www.mathworks.com/help/matlab/ref/fix.html

ericman314 commented 1 year ago

I'm reading back through these issues and just now realizing how right you are. .to() and .fix() feel very side effect-y.

I wonder if it is better to follow the example of a modern library like the date/time library Luxon. Like unitmath, it also uses immutable types. However, unlike unitmath, and much to its credit, its methods don't cause side effects. I feel like this is extremely important in modern JavaScript. So instead of .to and .fix, we should just have .format(), which converts a Unit to a string, and parse(), which converts a string to a Unit. And of course we have all the other Unit operations which return a new Unit. But the two methods format and parse are the only two "interfaces" between the internal representation and human readable forms.

I don't know if this will be possible, especially if we want units to "remember" how they were constructed in order to provide hints as to how they should be formatted. But part of me believes any such "hints" will become a black box that will confuse users when they can't figure out why a unit is being formatted in a certain way.

josdejong commented 1 year ago

Agree, having an immutable API is a great thing, it can save a lot of headache.

So instead of .to and .fix, we should just have .format(), which converts a Unit to a string, and parse(), which converts a string to a Unit.

That sounds good and straightforward.

If the units need to "remember" how they where constructed, I think they can save this information themselves in their internal "state", and copy this into any new unit constructed from it. I suppose that a unit can store config like { simplify: 'never', prefix: 'never' }, and that sounds quite clear to me. I think people will understand if a unit has this config. It maybe even useful to read and alter the config, like:

const a = unit('2 m/s')
const b = unit('2 m/s'), { simplify: 'never', prefix: 'never' })
const c = a.config({ simplify: 'never', prefix: 'never' })

// now, the following three are equivalent:
a.toString({ simplify: 'never', prefix: 'never' })
b.toString()
c.toString()

Just some thoughts.

ericman314 commented 1 year ago

Yes, upon more thinking, a unit will have to remember its internal state. That way you can do expressions like unit('1 km').to('m').value to get the number 1000, which you might want to do your own calculations with.

Having it remember its own formatting config probably goes too far though imho. You could always do opts = { simplify: 'never' }, and then reuse opts when needed: a.toString(opts)

And, I'm also thinking the default behavior should actually be to not simplify the units, but output the internal representation exactly as it is when calling toString() with no arguments. I believe that would help increase transparency into the internal workings of the library, and decrease a lot of the mystery that currently happens when formatting as a string. Then to get a simplified output, you could do a.simplify().toString() or a.toString({ simplify: true }).

josdejong commented 1 year ago

That makes sense indeed.

ericman314 commented 1 year ago

1.0.0-rc2 is published! The breakthrough I had was realizing that instead of having a separate function and state to prevent simplification, that we should make simplify itself explicit. So the user has to call simplify in order to simplify the unit--it doesn't happen automatically any more. That solved so many of the problems I was having, and the API puts the user in control.

josdejong commented 1 year ago

o wow, that makes sense indeed! So simple.