Skip to content

Instantly share code, notes, and snippets.

@steveluscher
Last active April 30, 2021 21:28
Show Gist options
  • Save steveluscher/4b54e08aa1e0536b7cb9 to your computer and use it in GitHub Desktop.
Save steveluscher/4b54e08aa1e0536b7cb9 to your computer and use it in GitHub Desktop.
Proposed abbreviation API for Intl.NumberFormat

Intl.NumberFormat

Syntax

new Intl.NumberFormat([locales[, options]])
Intl.NumberFormat.call(this[, locales[, options]])

Parameters

options

Optional. An object with some or all of the following properties:

abbreviationThreshold
Numbers whose absolute value is greater than this number will be abbreviated. Possible values are from 0 to Infinity; the default is Infinity, which effectively disables abbreviation.
  <dt>abbreviationRoundingMethod</dt>
  <dd>The rounding behavior to exhibit when abbreviating a number. Possible values are <code>Intl.NumberFormat.ROUND_CEILING</code>, <code>Intl.NumberFormat.ROUND_FLOOR</code>, <code>Intl.NumberFormat.ROUND_UP</code>, <code>Intl.NumberFormat.ROUND_DOWN</code>, <code>Intl.NumberFormat.ROUND_HALF_DOWN</code>, <code>Intl.NumberFormat.ROUND_HALF_EVEN</code>, and <code>Intl.NumberFormat.ROUND_HALF_UP</code>; the default is <code>Intl.NumberFormat.ROUND_HALF_CEILING</code>. For more information about rounding modes, see <a href="#roundingmodes">rounding modes</a>.</dd>
  …
</dl>

Examples

Example: Using options

The results can be customized using the options argument:

var number = 1234567.89;


// abbreviate a number
console.log(new Intl.NumberFormat('en-US', { abbreviationThreshold: 1e6, maximumSignificantDigits: 3 }).format(number));
// → 1.23M
console.log(new Intl.NumberFormat('en-US', { abbreviationThreshold: 1e6, style: 'currency', maximumSignificantDigits: 3 }).format(number));
// → $1.23M
console.log(new Intl.NumberFormat('ja-JP', { abbreviationThreshold: 1e6, maximumSignificantDigits: 3 }).format(number));
// → 123万

Addendum

Rounding modes

Intl.NumberFormat.ROUND_CEILING
Rounding mode to round towards positive infinity.
Intl.NumberFormat.ROUND_FLOOR
Rounding mode to round towards negative infinity.
Intl.NumberFormat.ROUND_DOWN
Rounding mode to round towards zero.
Intl.NumberFormat.ROUND_UP
Rounding mode to round away from zero.
Intl.NumberFormat.ROUND_HALF_CEILING
Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
Intl.NumberFormat.ROUND_HALF_UP
Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
Intl.NumberFormat.ROUND_HALF_EVEN
Rounding mode to round towards the "nearest neighbor" unless both neighbors are equidistant, in which case, round towards the even neighbor.
Intl.NumberFormat.ROUND_HALF_FLOOR
Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
Intl.NumberFormat.ROUND_HALF_DOWN
Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.

(Prior art: http://docs.oracle.com/javase/7/docs/api/java/math/RoundingMode.html)

@steveluscher
Copy link
Author

I work on a product used by people who can follow other people. Follower counts can climb high enough that we must abbreviate them. When abbreviating them, we have two requirements:

  1. Show no more than 3 digits, and no more than one decimal place
  2. Never overstate the follower count

This is a proposal for an abbreviation API for Intl.NumberFormat. Adding an abbreviationThreshold option and combining it with the maximumSignificantDigits and maximumFractionDigits options would constitute a slam dunk for requirement 1. To satisfy the second, I need to be able to specify a rounding method.

var number = 7654321;
console.log(new Intl.NumberFormat('en-US', {
  abbreviationThreshold: 1e6,
  abbreviationRoundingMethod: Intl.NumberFormat.ROUND_DOWN,
  maximumFractionDigits: 1,
  maximumSignificantDigits: 3,
}).format(number));
// → 7.6M
console.log(new Intl.NumberFormat('ja-JP', {
  abbreviationThreshold: 1e6,
  abbreviationRoundingMethod: Intl.NumberFormat.ROUND_DOWN,
  maximumFractionDigits: 1,
  maximumSignificantDigits: 3
}).format(number));
// → 765万

According to the usual rules of Math.round, the English number would have been rounded to 7.7 – an overstatement of our follower count. By being able to specify a rounding mode, we can prevent this.

Also, note that in some locales, such as ja-JP, it's not typical to use the ‘millions’ abbreviation, but instead to talk about ‘765 ten thousands.’ Rolling abbreviation options and abbreviation rounding control into Intl.NumberFormat would make these feats of formatting possible with a really elegant API.

@rwaldron
Copy link

Notes...

Where do these come from? (Neither are defined in http://docs.oracle.com/javase/7/docs/api/java/math/RoundingMode.html)

  • Intl.NumberFormat.ROUND_HALF_CEILING
  • Intl.NumberFormat.ROUND_HALF_FLOOR

Presumably the const integer value of these rounding modes are the same as java.math.roundingModes? If so, then UNNECESSARY should exist with a value of 7 before any additional modes are defined.

Intl.NumberFormat.ROUND_UP = 0
Intl.NumberFormat.ROUND_DOWN = 1
Intl.NumberFormat.ROUND_CEILING = 2
Intl.NumberFormat.ROUND_FLOOR = 3
Intl.NumberFormat.ROUND_HALF_UP = 4
Intl.NumberFormat.ROUND_HALF_DOWN = 5
Intl.NumberFormat.ROUND_HALF_EVEN = 6
Intl.NumberFormat.ROUND_UNNECESSARY = 7

Furthermore, I would specify as a frozen object with a null prototype (that is either defined as the value of a static property on Intl or defined as part of a standard library module).

let modes = Object.create(null);

modes.UP = 0;
modes.DOWN = 1;
modes.CEILING = 2;
modes.FLOOR = 3;
modes.HALF_UP = 4;
modes.HALF_DOWN = 5;
modes.HALF_EVEN = 6;
modes.UNNECESSARY = 7;

Object.freeze(modes);

export default modes;
export const roundingModes = modes;

Defining them like this affords nicer usage:

import roundingModes from "@roundingModes";
let number = 7654321;

console.log(new Intl.NumberFormat('en-US', {
  abbreviationThreshold: 1e6,
  roundingMode: roundingModes.DOWN,
  maximumFractionDigits: 1,
  maximumSignificantDigits: 3,
}).format(number));
// → 7.6M

or...

import {DOWN} from "@roundingModes";
let number = 7654321;

console.log(new Intl.NumberFormat('en-US', {
  abbreviationThreshold: 1e6,
  roundingMode: DOWN,
  maximumFractionDigits: 1,
  maximumSignificantDigits: 3,
}).format(number));
// → 7.6M

* abbreviationRoundingMethod is just roundingMode to avoid unnecessary confusion.

@steveluscher
Copy link
Author

Sorry, yes. That was sloppy; ROUND_HALF_CEILING and ROUND_HALF_FLOOR are not in Java 7.

They're based on this article on residue class rounding, and on implementations like joda and jscience.

@srl295
Copy link

srl295 commented Sep 22, 2015

I made this chart to keep the modes straight in my head. ICU docs.

@marnusw
Copy link

marnusw commented Apr 30, 2021

These days this is supported by Intl.NumberFormat with the option { notation: 'compact' }.

@steveluscher
Copy link
Author

Oh yay!

Intl.NumberFormat('en-CA', {notation: 'compact'}).format(7654321)
"7.7M"
Intl.NumberFormat('ja-JP', {notation: 'compact'}).format(7654321)
"765万"

I'm a little concerned that the en-CA implementation rounded up instead of down. Sometimes products don't want to overstate values (eg. follower counts). Is the ability to configure the rounding behavior still on the table?

@marnusw
Copy link

marnusw commented Apr 30, 2021

I have no idea and I'm the wrong person to ask. I just found this gist when I searched for the funcationality before I found the answer on MDN and figured I'd leave a note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment