Sebastian Zartner
Sebastian Zartner

Reputation: 20125

Is there an option in Intl.NumberFormat() to automatically convert to bigger units?

I'd like Intl.NumberFormat() to automatically convert between units from smaller to bigger ones based on common rules. I.e. a given number should be converted to between centimeters, meters, and kilometers in the output depending on how big the number is.

Code examples:

const bytes = 1000000;
const transferSpeed = new Intl.NumberFormat('en-US',
  {style: 'unit', unit: 'byte-per-second', unitDisplay: 'narrow'}).format(bytes);
console.log(transferSpeed);

const days = 365;
const timespan = new Intl.NumberFormat('en-US',
  {style: 'unit', unit: 'day', unitDisplay: 'long'}).format(days);
console.log(timespan);

The output of these two calls is:

1,000,000B/s
365 days

In that case I'd expect this, though:

1MB/s
1 year

And one might want to define the threshold for when to convert to the next bigger unit. So it could be that the conversion should happen once the exact value is reached but also earlier, let's say at 90% of the next bigger unit. Given the examples above, the output would then be this:

0.9MB/s
0.9 years

Are there configuration options for the API to do that?

Upvotes: 13

Views: 8484

Answers (4)

André Jesus
André Jesus

Reputation: 61

Building on DmitryScaletta’s answer, I’ve implemented a method that dynamically adjusts both the unit and value based on the provided input. This approach is flexible and easily extendable to support additional units and, it supports concatenated units using '-per-' to handle compound units:

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf#supported_unit_identifiers
const UNIT_SETS = [
  {
    units: [
      "nanosecond",
      "microsecond",
      "millisecond",
      "second",
      "minute",
      "hour",
      "day",
      "week",
      "month",
      "year",
    ],
    factors: [1000, 1000, 1000, 60, 60, 24, 7, 4.345, 12], // Approximating months (4.345 weeks) and years (12 months)
  },
  {
    units: ["bit", "kilobit", "megabit", "gigabit", "terabit"],
    factors: [1000, 1000, 1000, 1000],
  },
  {
    units: ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte", "petabyte"],
    factors: [1024, 1024, 1024, 1024, 1024],
  },
// ...
];

function formatUnit(
  value,
  options, // Intl.NumberFormatOptions
  locale = "en-US",
) {
  const [numeratorUnit, denominatorUnit] = options.unit.split("-per-");

  const unitSet = UNIT_SETS.find(({ units }) => units.includes(numeratorUnit));
  if (unitSet) {
    const { units, factors } = unitSet;
    let index = units.indexOf(numeratorUnit);
    let adjustedValue = value;

    // Scale up if the value is too large
    while (index < units.length - 1 && adjustedValue >= factors[index]) {
      adjustedValue /= factors[index];
      index++;
    }

    // Scale down if the value is too small
    while (index > 0 && adjustedValue < 1) {
      index--;
      adjustedValue *= factors[index];
    }

    value = adjustedValue;
    options.unit = denominatorUnit
      ? `${units[index]}-per-${denominatorUnit}`
      : units[index];
  }

  const formatter = new Intl.NumberFormat(locale, {
    style: "unit",
    ...options,
  });

  return formatter.format(value);
}

console.log(formatUnit(3600, { unit: "second" })); // "1 hour"
console.log(formatUnit(86400, { unit: "second" })); // "1 day"
console.log(formatUnit(0.000001, { unit: "second" })); // "1 microsecond"
console.log(formatUnit(5000000000, { unit: "bit" })); // "5 gigabits"
console.log(formatUnit(1048576, { unit: "byte" })); // "1 megabyte"
console.log(formatUnit(1, { unit: "gigabyte-per-second" })); // "1 gigabyte per second"
console.log(formatUnit(8192, { unit: "bit" })); // "8 kilobits"
console.log(formatUnit(0.5, { unit: "megabyte" })); // "512 kilobytes"
console.log(formatUnit(1000, { unit: "kilobit" })); // "1 megabit"
console.log(formatUnit(120, { unit: "minute" })); // "2 hours"
console.log(formatUnit(0.001, { unit: "kilobyte" })); // "1 byte"
console.log(formatUnit(7000000000, { unit: "bit" })); // "7 gigabits"
console.log(formatUnit(2, { unit: "terabyte" })); // "2 terabytes"
console.log(formatUnit(1000000000000, { unit: "bit" })); // "1 terabit"

Upvotes: 1

DmitryScaletta
DmitryScaletta

Reputation: 89

By default Intl.NumberFormat displays gigabytes as 1BB (billion bytes) and petabytes as 1000TB.
Here is my workaround.

const UNITS = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte", 'petabyte'];

const getValueAndUnit = (n) => {
  const i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1024));
  const value = n / Math.pow(1024, i);
  return { value, unit: UNITS[i] };
};

const bytePerSecondFormatter = (n) => {
  const { unit, value } = getValueAndUnit(n);
  return new Intl.NumberFormat("en", {
    notation: "compact",
    style: "unit",
    unit: `${unit}-per-second`,
    unitDisplay: "narrow",
  }).format(value);
};

console.log(bytePerSecondFormatter(10));
console.log(bytePerSecondFormatter(200000));
console.log(bytePerSecondFormatter(50000000));
console.log(bytePerSecondFormatter(30000000000));
console.log(bytePerSecondFormatter(70000000000000));
console.log(bytePerSecondFormatter(9000000000000000));

Upvotes: 4

andrec93
andrec93

Reputation: 352

Not exactly a full answer to the question, but I thought I dropped this here in case it can help anybody.

Something close to this is actually possible with Intl.NumberFormat, but it has some limitations. If you wanted to format byte values, for instance, you could:

  1. Use the compact notation.
  2. Use unit as your style, and provide byte as the unit.
  3. Set narrow as unitDisplay.

With these options, the formatter will correctly convert from one unit to the other and, thanks to the unitDisplay value, it will display the unit of measurement as you would expect.

This is of course only usable with the few supported units for which this makes sense, and it limits you to have the unit right next to value, even though that's usually what you'd want anyway. Browser support may also be an issue if you need to target older platforms.

Here's a sample.

const byteValueNumberFormatter = Intl.NumberFormat("en", {
  notation: "compact",
  style: "unit",
  unit: "byte",
  unitDisplay: "narrow",
});

console.log(byteValueNumberFormatter.format(10));

console.log(byteValueNumberFormatter.format(200000));

console.log(byteValueNumberFormatter.format(50000000));

Upvotes: 24

tillsanders
tillsanders

Reputation: 1945

Unfortunately, there is no such feature. You can see all possible options and methods in the MDN documentation. Also, the list of supported units for ECMAScript contains e.g. byte, kilobyte and megabyte as separate units.

I propose you implement this yourself or find a module that suits your needs. In the meantime, I searched the ECMAScript proposals but didn't find anything like it, so I filed an idea in the discussion board. Maybe it will catch on: https://es.discourse.group/t/automatic-unit-conversion-for-intl-numberformat/763

Upvotes: 3

Related Questions