Automatic unit conversion for Intl.NumberFormat

Hi! I recently tried to format the size of a file using the NumberFormat and the byte unit. It works great, but obviously, "400.000 Bytes" is not very user-friendly. So I would like to propose something like automatic unit conversion.

Maybe it could look something like this: (problematic with imperial units, see comments below)

{
  style: 'unit',
  unit: ['byte', 'kilobyte', 'megabyte'],
  unitConversionTreshold: 1000,
}

This would cause NumberFormat to convert whenever the treshold is reached, like this:

  • 50 → "50 Bytes"
  • 1.000 → "1 Kilobyte"
  • 1.050 → "1.05 Kilobyte"
  • 1.000.000 → "1 Megabyte"

Another, less flexible way to configure this might be with "measurements":

{
  style: 'measurement',
  measurement: 'length',
  measurementBaseUnit: 'meter'
}

This would automatically convert through a series of units, e.g. 'millimeter' → 'centimeter' → 'meter' → 'kilometer', starting with a configurable base unit.

I think it would make great sense to include such a feature in this API, but I couldn't find any proposal for it. What do you think about this? Do I need to elaborate more on the details? I'm looking forward to your feedback!

Cheers,
Till

2 Likes

Looks interesting. Would it have a way to handle "uneven" units such as US measurements (12 inches/foot, 3 feet/yard)? Would there be a way to configure pluralization (foot/feet)?

Great question! I haven't considered that issue. That would make the first configuration example impossible. It could maybe be extended to support multiple thresholds, specific to each unit. But that would probably be unnecessarily complicated. So the second configuration example would probably be much easier to use and also without issues regarding "uneven" units.

Regarding your second remark – pluralization: Isn't this a more general problem? I'm not sure if this is in the scope of my idea because it could apply also to the current implementation using the style unit.

Good point. You might make your example use abbreviated units, just to avoid the issue.

For reference, I've initially asked for a way to do that on Stack Overflow.

To avoid the issue regarding uneven conversions, my idea is to use percentages instead of fixed numbers.

There are also other things to consider when doing automatic conversions.

It should be distinguished between the input unit and the output units. E.g. One might have a number representing days, which should be output as days, months, or years depending on the size of the number.

With that in mind, the options could look like this:

{
  style: 'unit',
  unit: 'day',
  outputUnits: ['day', 'month', 'year'],
  unitConversionThreshold: 0.9
}

The output would then be something like this:

  • 5 → "5 days"
  • 120 → "4 months"
  • 340 → "0.93 years"
  • 365 → "1 year"

outputUnits would then default to an array with only the unit defined in unit to have backwards compatibility.

With an outputUnits option we could even have implicit conversion between different measuring systems.

For example:

{
  style: 'unit',
  unit: 'foot',
  outputUnits: ['meter', 'kilometer']
}
  • 3.28084 → "1 meter"
  • 33 → "10.06 meters"
  • 10000 → "3.05 kilometers"

Though maybe the conversion could also be more explicit. I.e. by creating a separate Intl.UnitConversion API.

let output = new Intl.UnitConversion({
  inputUnit: 'gram',
  outputUnits: ['ounce', 'pound'],
  unitConversionThreshold: 0.8
}).convert(number);

where output would be an object containing a value and a unit property.

  • 150 → { value: 5.291094, unit: 'ounce' }
  • 200 → { value: 7.054792, unit: 'ounce' }
  • 400 → { value: 0.881849, unit: 'pound' }
  • 3000 → { value: 6.613868, unit: 'pound' }

This could then be used as input for Intl.NumberFormat(), for doing calculations, and other things.
Separating unit conversions from formatting further has the advantage of allowing individual extension in the future. Though it also requires authors to write a bit more if they want to output the numbers via Intl.NumberFormat() after conversion.

Sebastian

Interesting suggestions!

outputUnits
I'm not sure about outputUnits as part of the NumberFormat configuration object, because the unit property already means: "output the number I gave you using that unit". I'm afraid that would be confusing.

Relative unit conversion threshold
So the way this would work is (referring to your example above): when the input is 120 days, it would convert a month to 30 days, a year to 360 days and then determine, that 120 is less than 90 % of 360 and decide to output that as "4 months"? I'm a little worried that this approach might lead to a difficult implementation because of the implicit conversions taking place. But I like the idea as it would fix the issue of my first example ;) But then I would still go with an array for the unit-property instead of outputUnits as that can also be backwards compatible as long as the unit-property accepts both an array of strings as well as a string.

{
  style: 'unit',
  unit: ['byte', 'kilobyte', 'megabyte'],
  unitConversionTreshold: 0.9,
}

UnitConversion API

That might also be a good approach as we could separate concerns properly. I thought about the possibility of having UnitConversion returning an object that could be passed to NumberFormat, but that introduced a variety of problems. I think we have two different approaches now: the integrated one above and UnitConversion as the separated one. I like the ease of use of the integrated approach, but there would be something like UnitConversion in the background anyway, so it might make more sense to just go with that.

Currently, unit covers both the output unit as well as the input unit. You could interpret it as "interpret the number I gave you using that unit and output it using it". My approach restricts its meaning to only being the input unit. In order to be backwards compatible, outputUnits would take the value of unit when not defined.

In your suggested syntax it is not clear what's the input unit. I assume you probably meant the smallest one to be the input unit. Though what happens when you write unit: ['kilobyte', 'megabyte']? Would the given number then still be interpreted as byte or would it be kilobyte?

See this counter example:

const sizeFormat = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'megabyte',
  outputUnits: ['byte', 'kilobyte', 'megabyte']
})
console.log(sizeFormat.format(0.1));

Having unit be interpreted as input unit, it's clear that the output is "100 kB".

I actually calculated with 365 days for a year. :smiley: Though for the rest you're right in how I thought this to work. But that outlines nicely the issue with date conversions. In common calculations you always have rounding errors. Therefore, for correct date unit conversions you'd also have to provide a start date.
Also note that I restricted my calculations to the Gregorian calendar. Other calendars will probably require other conversion thresholds.
So, automatic date unit conversions are very tricky. Other unit conversions should not have these issues, though.

I thought the same. We could also have both. Though rather I tend to introduce the explicit UnitConversion API.

Pros:

  • It can be used beyond output formatting.
  • It can evolve individually.
  • Clear separation between formatting numbers and number conversion.

Cons:

  • It's more verbose for the conversion + formatting use case.

Sebastian