Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Currency abbreviations #81

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ The *symbol* can be:

The *zero* (`0`) option enables zero-padding; this implicitly sets *fill* to `0` and *align* to `=`. The *width* defines the minimum field width; if not specified, then the width will be determined by the content. The *comma* (`,`) option enables the use of a group separator, such as a comma for thousands.

Depending on the *type*, the *precision* either indicates the number of digits that follow the decimal point (types `f` and `%`), or the number of significant digits (types `​`, `e`, `g`, `r`, `s` and `p`). If the precision is not specified, it defaults to 6 for all types except `​` (none), which defaults to 12. Precision is ignored for integer formats (types `b`, `o`, `d`, `x`, `X` and `c`). See [precisionFixed](#precisionFixed) and [precisionRound](#precisionRound) for help picking an appropriate precision.
Depending on the *type*, the *precision* either indicates the number of digits that follow the decimal point (types `f` and `%`), or the number of significant digits (types `​`, `e`, `g`, `K`, `r`, `s` and `p`). If the precision is not specified, it defaults to 6 for all types except `​` (none), which defaults to 12. Precision is ignored for integer formats (types `b`, `o`, `d`, `x`, `X` and `c`). See [precisionFixed](#precisionFixed) and [precisionRound](#precisionRound) for help picking an appropriate precision.

The `~` option trims insignificant trailing zeros across all format types. This is most commonly used in conjunction with types `r`, `e`, `s` and `%`. For example:
The `~` option trims insignificant trailing zeros across all format types. This is most commonly used in conjunction with types `r`, `e`, `s`, `K` and `%`. For example:

```js
d3.format("s")(1500); // "1.50000k"
Expand All @@ -149,6 +149,7 @@ The available *type* values are:
* `g` - either decimal or exponent notation, rounded to significant digits.
* `r` - decimal notation, rounded to significant digits.
* `s` - decimal notation with an [SI prefix](#locale_formatPrefix), rounded to significant digits.
* `K` - decimal notation with an [currency prefix](#locale_formatCurrencyPrefix), rounded to significant digits.
* `%` - multiply by 100, and then decimal notation with a percent sign.
* `p` - multiply by 100, round to significant digits, and then decimal notation with a percent sign.
* `b` - binary notation, rounded to integer.
Expand All @@ -167,7 +168,7 @@ d3.format(".1")(42); // "4e+1"
d3.format(".1")(4.2); // "4"
```

<a name="locale_formatPrefix" href="#locale_formatPrefix">#</a> <i>locale</i>.<b>formatPrefix</b>(<i>specifier</i>, <i>value</i>) [<>](https://github.com/d3/d3-format/blob/master/src/locale.js#L127 "Source")
<a name="locale_formatPrefix" href="#locale_formatPrefix">#</a> <i>locale</i>.<b>formatPrefix</b>(<i>specifier</i>, <i>value</i>) [<>](https://github.com/d3/d3-format/blob/master/src/locale.js#L152 "Source")

Equivalent to [*locale*.format](#locale_format), except the returned function will convert values to the units of the appropriate [SI prefix](https://en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes) for the specified numeric reference *value* before formatting in fixed point notation. The following prefixes are supported:

Expand Down Expand Up @@ -199,6 +200,16 @@ f(0.0042); // "4,200µ"

This method is useful when formatting multiple numbers in the same units for easy comparison. See [precisionPrefix](#precisionPrefix) for help picking an appropriate precision, and [bl.ocks.org/9764126](http://bl.ocks.org/mbostock/9764126) for an example.

<a name="locale_formatCurrencyPrefix" href="#locale_formatCurrencyPrefix">#</a> <i>locale</i>.<b>formatCurrencyPrefix</b>(<i>specifier</i>, <i>value</i>) [<>](https://github.com/d3/d3-format/blob/master/src/locale.js#L153 "Source")

Equivalent to [*locale*.locale_formatPrefix](#locale_formatPrefix), except it uses common currency abbreviations:

* `​` (none) - 10⁰
* `K` - thousands, 10³
* `M` - millions, 10⁶
* `B` - billions, 10⁹
* `T` - trillions, 10¹²

<a name="formatSpecifier" href="#formatSpecifier">#</a> d3.<b>formatSpecifier</b>(<i>specifier</i>) [<>](https://github.com/d3/d3-format/blob/master/src/formatSpecifier.js "Source")

Parses the specified *specifier*, returning an object with exposed fields that correspond to the [format specification mini-language](#locale_format) and a toString method that reconstructs the specifier. For example, `formatSpecifier("s")` returns:
Expand Down Expand Up @@ -290,6 +301,10 @@ f(1.2e6); // "1.2M"
f(1.3e6); // "1.3M"
```

<a name="currencyPrecisionPrefix" href="#currencyPrecisionPrefix">#</a> d3.<b>currencyPrecisionPrefix</b>(<i>step</i>, <i>value</i>) [<>](https://github.com/d3/d3-format/blob/master/src/currencyPrecisionPrefix.js "Source")

Returns a suggested decimal precision for use with [*locale*.formatCurrencyPrefix](#locale_formatCurrencyPrefix) given the specified numeric *step* and reference *value*. This is the equivalent of [*locale*.precisionPrefix](#locale_precisionPrefix) using common currency abbreviations instead of SI prefixes.

<a name="precisionRound" href="#precisionRound">#</a> d3.<b>precisionRound</b>(<i>step</i>, <i>max</i>) [<>](https://github.com/d3/d3-format/blob/master/src/precisionRound.js "Source")

Returns a suggested decimal precision for format types that round to significant digits given the specified numeric *step* and *max* values. The *step* represents the minimum absolute difference between values that will be formatted, and the *max* represents the largest absolute value that will be formatted. (This assumes that the values to be formatted are also multiples of *step*.) For example, given the numbers 0.99, 1.0, and 1.01, the *step* should be 0.01, the *max* should be 1.01, and the suggested precision is 3:
Expand Down Expand Up @@ -331,6 +346,7 @@ Returns a *locale* object for the specified *definition* with [*locale*.format](
* `thousands` - the group separator (e.g., `","`).
* `grouping` - the array of group sizes (e.g., `[3]`), cycled as needed.
* `currency` - the currency prefix and suffix (e.g., `["$", ""]`).
* `currencyAbbreviations` - the list of abbreviated suffixes for currency values; an array of elements for each: units, thousands, millions, billions and trillions; defaults to `["", "K", "M", "B", "T"]`. The number of elements can vary.
* `numerals` - optional; an array of ten strings to replace the numerals 0-9.
* `percent` - optional; the percent sign (defaults to `"%"`).
* `minus` - optional; the minus sign (defaults to hyphen-minus, `"-"`).
Expand Down
3 changes: 2 additions & 1 deletion locale/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"decimal": ",",
"thousands": ".",
"grouping": [3],
"currency": ["", "\u00a0€"]
"currency": ["", "\u00a0€"],
"currencyAbbreviations": ["", "", "\u00a0Mio.", "\u00a0Mrd.", "\u00a0Bio."]
}
3 changes: 2 additions & 1 deletion locale/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"decimal": ".",
"thousands": ",",
"grouping": [3],
"currency": ["£", ""]
"currency": ["£", ""],
"currencyAbbreviations": ["", "K", "M", "B", "T"]
Copy link

@curran curran Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Language Matters: Millions, Billions and Other Large Numbers

The most commonly seen short forms for thousand, million, billion and trillion in North America and the United Kingdom, respectively, are outlined in the table below.

image

Might we consider changing locale/en-GB.json to match these?

"currencyAbbreviations": ["", "k", "m", "bn", "tn"]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed in #96

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these. I removed them from this PR originally (7ee5a2c) because they are not indicated in the CDLR, but I think they should be!

}
3 changes: 2 additions & 1 deletion locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"decimal": ".",
"thousands": ",",
"grouping": [3],
"currency": ["$", ""]
"currency": ["$", ""],
"currencyAbbreviations": ["", "K", "M", "B", "T"]
}
3 changes: 2 additions & 1 deletion locale/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"decimal": ",",
"thousands": ".",
"grouping": [3],
"currency": ["", "\u00a0€"]
"currency": ["", "\u00a0€"],
"currencyAbbreviations": ["", "\u00a0mil", "\u00a0M", "\u00a0mil M", "\u00a0B"]
}
3 changes: 2 additions & 1 deletion locale/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"thousands": "\u00a0",
"grouping": [3],
"currency": ["", "\u00a0€"],
"percent": "\u202f%"
"percent": "\u202f%",
"currencyAbbreviations": ["", "\u00a0k", "\u00a0M", "\u00a0Md", "\u00a0Bn"]
}
3 changes: 2 additions & 1 deletion locale/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"decimal": ",",
"thousands": ".",
"grouping": [3],
"currency": ["€", ""]
"currency": ["€", ""],
"currencyAbbreviations": ["", "", "\u00a0Mio", "\u00a0Mrd", "\u00a0Bln"]
}
3 changes: 2 additions & 1 deletion locale/nl-NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"decimal": ",",
"thousands": ".",
"grouping": [3],
"currency": ["€\u00a0", ""]
"currency": ["€\u00a0", ""],
"currencyAbbreviations": ["", "K", "\u00a0mln.", "\u00a0mld.", "\u00a0bln."]
}
3 changes: 3 additions & 0 deletions src/currencyPrecisionPrefix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createPrecisionPrefix } from "./precisionPrefix.js";

export default createPrecisionPrefix(0, 4);
2 changes: 2 additions & 0 deletions src/defaultLocale.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import formatLocale from "./locale.js";

var locale;
export var format;
export var formatCurrencyPrefix;
export var formatPrefix;

defaultLocale({
Expand All @@ -15,6 +16,7 @@ defaultLocale({
export default function defaultLocale(definition) {
locale = formatLocale(definition);
format = locale.format;
formatCurrencyPrefix = locale.formatCurrencyPrefix;
formatPrefix = locale.formatPrefix;
return locale;
}
16 changes: 13 additions & 3 deletions src/formatPrefixAuto.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@ import formatDecimal from "./formatDecimal.js";

export var prefixExponent;

export default function(x, p) {
function formatSignificantDigitsForPrefixes(x, p, minPrefixOrder, maxPrefixOrder) {
var d = formatDecimal(x, p);
if (!d) return x + "";
var coefficient = d[0],
exponent = d[1],
i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1,
i = exponent - (prefixExponent = Math.max(minPrefixOrder, Math.min(maxPrefixOrder, Math.floor(exponent / 3))) * 3) + 1,
n = coefficient.length;
return i === n ? coefficient
: i > n ? coefficient + new Array(i - n + 1).join("0")
: i > 0 ? coefficient.slice(0, i) + "." + coefficient.slice(i)
: "0." + new Array(1 - i).join("0") + formatDecimal(x, Math.max(0, p + i - 1))[0]; // less than 1y!
: "0." + new Array(1 - i).join("0") + formatDecimal(x, Math.max(0, p + i - 1))[0]; // less than the smallest prefix
}

export function createFormatCurrencyPrefixAutoForLocale(currencyAbbreviations) {
return function formatCurrencyPrefixAuto(x, p) {
return formatSignificantDigitsForPrefixes(x, p, 0, currencyAbbreviations.length - 1);
}
}

export default function(x, p) {
return formatSignificantDigitsForPrefixes(x, p, -8, 8);
}
3 changes: 2 additions & 1 deletion src/formatTypes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import formatPrefixAuto from "./formatPrefixAuto.js";
import formatPrefixAuto, { createFormatCurrencyPrefixAutoForLocale } from "./formatPrefixAuto.js";
import formatRounded from "./formatRounded.js";

export default {
Expand All @@ -9,6 +9,7 @@ export default {
"e": function(x, p) { return x.toExponential(p); },
"f": function(x, p) { return x.toFixed(p); },
"g": function(x, p) { return x.toPrecision(p); },
"K": createFormatCurrencyPrefixAutoForLocale, // depends of the current locale
"o": function(x) { return Math.round(x).toString(8); },
"p": function(x, p) { return formatRounded(x * 100, p); },
"r": formatRounded,
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {default as formatDefaultLocale, format, formatPrefix} from "./defaultLocale.js";
export {default as currencyPrecisionPrefix} from "./currencyPrecisionPrefix.js";
export { default as formatDefaultLocale, format, formatCurrencyPrefix, formatPrefix} from "./defaultLocale.js";
export {default as formatLocale} from "./locale.js";
export {default as formatSpecifier, FormatSpecifier} from "./formatSpecifier.js";
export {default as precisionFixed} from "./precisionFixed.js";
Expand Down
42 changes: 30 additions & 12 deletions src/locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {prefixExponent} from "./formatPrefixAuto.js";
import identity from "./identity.js";

var map = Array.prototype.map,
prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];
SIprefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],
defaultCurrencyAbbreviations = ["", "K", "M", "B", "T"];

export default function(locale) {
var group = locale.grouping === undefined || locale.thousands === undefined ? identity : formatGroup(map.call(locale.grouping, Number), locale.thousands + ""),
currencyAbbreviations = locale.currencyAbbreviations === undefined ? defaultCurrencyAbbreviations : locale.currencyAbbreviations,
currencyPrefix = locale.currency === undefined ? "" : locale.currency[0] + "",
currencySuffix = locale.currency === undefined ? "" : locale.currency[1] + "",
decimal = locale.decimal === undefined ? "." : locale.decimal + "",
Expand Down Expand Up @@ -52,14 +54,18 @@ export default function(locale) {
// Is this an integer type?
// Can this type generate exponential notation?
var formatType = formatTypes[type],
maybeSuffix = /[defgprs%]/.test(type);
maybeSuffix = /[defgKprs%]/.test(type);

if (type === 'K')
formatType = formatType(currencyAbbreviations);

// Set the default precision if not specified,
// or clamp the specified precision to the supported range.
// For significant precision, it must be in [1, 21].
// For fixed precision, it must be in [0, 20].
precision = precision === undefined ? 6
: /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision))
// For financial type, default precision is 3 significant digits instead of 6.
precision = precision === undefined ? (type === "K" ? 3 : 6)
: /[gKprs]/.test(type) ? Math.max(1, Math.min(21, precision))
: Math.max(0, Math.min(20, precision));

function format(value) {
Expand Down Expand Up @@ -87,7 +93,13 @@ export default function(locale) {

// Compute the prefix and suffix.
valuePrefix = (valueNegative ? (sign === "(" ? sign : minus) : sign === "-" || sign === "(" ? "" : sign) + valuePrefix;
valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : "");

if (type === "s")
valueSuffix = SIprefixes[8 + prefixExponent / 3] + valueSuffix
else if (type === "K")
valueSuffix = currencyAbbreviations[prefixExponent / 3] + valueSuffix

valueSuffix = valueSuffix + (valueNegative && sign === "(" ? ")" : "");

// Break the formatted value into the integer “value” part that can be
// grouped, and fractional or exponential “suffix” part that is not.
Expand Down Expand Up @@ -131,18 +143,24 @@ export default function(locale) {
return format;
}

function formatPrefix(specifier, value) {
var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3,
function createFormatPrefix(prefixes, minimumPrefixOrder, maximumPrefixOrder) {
return function(specifier, value) {
var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
e = Math.max(minimumPrefixOrder, Math.min(maximumPrefixOrder, Math.floor(exponent(value) / 3))) * 3,
k = Math.pow(10, -e),
prefix = prefixes[8 + e / 3];
return function(value) {
return f(k * value) + prefix;
};
prefix = prefixes[(-1 * minimumPrefixOrder) + e / 3];
return function (value) {
return f(k * value) + prefix;
};
}
}

var formatPrefix = createFormatPrefix(SIprefixes, -8, 8);
var formatCurrencyPrefix = createFormatPrefix(currencyAbbreviations, 0, currencyAbbreviations.length - 1);

return {
format: newFormat,
formatCurrencyPrefix: formatCurrencyPrefix,
formatPrefix: formatPrefix
};
}
8 changes: 6 additions & 2 deletions src/precisionPrefix.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import exponent from "./exponent.js";

export default function(step, value) {
return Math.max(0, Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 - exponent(Math.abs(step)));
export function createPrecisionPrefix(minimumPrefixOrder, maximumPrefixOrder) {
return function (step, value) {
return Math.max(0, Math.max(minimumPrefixOrder, Math.min(maximumPrefixOrder, Math.floor(exponent(value) / 3))) * 3 - exponent(Math.abs(step)));
}
}

export default createPrecisionPrefix(-8, 8);
52 changes: 52 additions & 0 deletions test/currencyPrecisionPrefix-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
var tape = require("tape"),
format = require("../");

// For currencies, only 4 prefixes are commonly used:
// thousands (K), millions (M), billions (B) and trillions (T)

tape("precisionPrefix(step, value) returns zero between 1 and 100 (unit step)", function (test) {
test.equal(format.currencyPrecisionPrefix(1e+0, 1e+0), 0); // 1
test.equal(format.currencyPrecisionPrefix(1e+0, 1e+1), 0); // 10
test.equal(format.currencyPrecisionPrefix(1e+0, 1e+2), 0); // 100
test.end()
});

tape("precisionPrefix(step, value) returns zero between 1 and 100 (thousand step)", function (test) {
test.equal(format.currencyPrecisionPrefix(1e+3, 1e+3), 0); // 1K
test.equal(format.currencyPrecisionPrefix(1e+3, 1e+4), 0); // 10K
test.equal(format.currencyPrecisionPrefix(1e+3, 1e+5), 0); // 100K
test.end()
});

tape("precisionPrefix(step, value) returns zero between 1 and 100 (million step)", function (test) {
test.equal(format.currencyPrecisionPrefix(1e+6, 1e+6), 0); // 1M
test.equal(format.currencyPrecisionPrefix(1e+6, 1e+7), 0); // 10M
test.equal(format.currencyPrecisionPrefix(1e+6, 1e+8), 0); // 100M
test.end()
});

tape("precisionPrefix(step, value) returns zero between 1 and 100 (billion step)", function (test) {
test.equal(format.currencyPrecisionPrefix(1e+9, 1e+9), 0); // 1B
test.equal(format.currencyPrecisionPrefix(1e+9, 1e+10), 0); // 10B
test.equal(format.currencyPrecisionPrefix(1e+9, 1e+11), 0); // 100B
test.end()
});

tape("currencyPrecisionPrefix(step, value) returns the expected precision when value is greater than one trillion", function(test) {
test.equal(format.currencyPrecisionPrefix(1e+12, 1e+12), 0); // 1T
test.equal(format.currencyPrecisionPrefix(1e+12, 1e+13), 0); // 10T
test.equal(format.currencyPrecisionPrefix(1e+12, 1e+14), 0); // 100T
test.equal(format.currencyPrecisionPrefix(1e+12, 1e+15), 0); // 1000T
test.equal(format.currencyPrecisionPrefix(1e+11, 1e+15), 1); // 1000.0T
test.end();
});

tape("currencyPrecisionPrefix(step, value) returns the expected precision when value is less than one unit", function(test) {
test.equal(format.currencyPrecisionPrefix(1e+0, 1e+0), 0); // 1
test.equal(format.currencyPrecisionPrefix(1e-1, 1e-1), 1); // 0.1
test.equal(format.currencyPrecisionPrefix(1e-2, 1e-2), 2); // 0.01
test.equal(format.currencyPrecisionPrefix(1e-3, 1e-3), 3); // 0.001
test.equal(format.currencyPrecisionPrefix(1e-4, 1e-4), 4); // 0.0001
test.end();
});

Loading