Skip to content

Commit

Permalink
Merge branch 'davinov-currency-prefixes' into merge-70-
Browse files Browse the repository at this point in the history
(Conflicts: src/formatTypes.js src/locale.js)
  • Loading branch information
Fil committed Jul 7, 2020
2 parents a2b4760 + 76c6681 commit 11ebd71
Show file tree
Hide file tree
Showing 19 changed files with 299 additions and 34 deletions.
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"]
}
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,5 +1,5 @@
import formatBinaryPrefixAuto from "./formatBinaryPrefixAuto.js";
import formatPrefixAuto from "./formatPrefixAuto.js";
import formatPrefixAuto, { createFormatCurrencyPrefixAutoForLocale } from "./formatPrefixAuto.js";
import formatRounded from "./formatRounded.js";

export default {
Expand All @@ -11,6 +11,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
54 changes: 37 additions & 17 deletions src/locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {binaryPrefixExponent} from "./formatBinaryPrefixAuto.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"],
binaryPrefixes = ["", "Ki","Mi","Gi","Ti","Pi","Ei","Zi","Yi"];
SIprefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],
binaryPrefixes = ["", "Ki","Mi","Gi","Ti","Pi","Ei","Zi","Yi"],
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 @@ -54,14 +56,18 @@ export default function(locale) {
// Is this an integer type?
// Can this type generate exponential notation?
var formatType = formatTypes[type],
maybeSuffix = /[Bdefgprs%]/.test(type);
maybeSuffix = /[BdefgKprs%]/.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
: /[Bgprs]/.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)
: /[BgKprs]/.test(type) ? Math.max(1, Math.min(21, precision))
: Math.max(0, Math.min(20, precision));

function format(value) {
Expand Down Expand Up @@ -89,11 +95,19 @@ 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]
: type === "B" ? binaryPrefixes[binaryPrefixExponent / 10]
: ""
) + valueSuffix + (valueNegative && sign === "(" ? ")" : "");

switch (type) {
case "s":
valueSuffix = SIprefixes[8 + prefixExponent / 3] + valueSuffix;
break;
case "K":
valueSuffix = currencyAbbreviations[prefixExponent / 3] + valueSuffix;
break;
case "B":
valueSuffix = binaryPrefixes[binaryPrefixExponent / 10] + valueSuffix;
break;
}
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 @@ -137,18 +151,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

0 comments on commit 11ebd71

Please sign in to comment.