UnitMath icon indicating copy to clipboard operation
UnitMath copied to clipboard

Implement an explicit method to prevent a unit from being simplified

Open josdejong opened this issue 3 years ago • 2 comments

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?

josdejong avatar Dec 12 '21 09:12 josdejong

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()).

ericman314 avatar Dec 12 '21 21:12 ericman314

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

josdejong avatar Dec 13 '21 07:12 josdejong

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.

ericman314 avatar Feb 17 '23 03:02 ericman314

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.

josdejong avatar Feb 17 '23 07:02 josdejong

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 }).

ericman314 avatar Feb 17 '23 12:02 ericman314

That makes sense indeed.

josdejong avatar Feb 17 '23 14:02 josdejong

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.

ericman314 avatar Feb 19 '23 07:02 ericman314

o wow, that makes sense indeed! So simple.

josdejong avatar Feb 20 '23 08:02 josdejong