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

Implement Binary prefix, Currency prefix (Merge #70 #81) #96

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 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.
Fil marked this conversation as resolved.
Show resolved Hide resolved
* `%` - 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:

Copy link

Choose a reason for hiding this comment

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

We might want to mention here that these are for US English locale specifically.

* `​` (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")
Copy link

@curran curran Jul 8, 2020

Choose a reason for hiding this comment

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

Are these necessarily related to currency? IMO this formatter is useful for formatting large numbers in general, not specific to dealing with currency. Although, the currency use case gives some concrete grounding for decisions, which is great. Probably it's fine to call it currencyPrecisionPrefix, but it's actually a more general formatter.


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", "bn", "tn"]
}
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;
}
46 changes: 46 additions & 0 deletions src/formatBinaryPrefixAuto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export var binaryPrefixExponent;

export default function(x, p) {
var binaryExponent = 0;
if (x === Infinity) return binaryPrefixExponent = 0, x;

while (Math.round(x) >= 1024 && binaryExponent < 80) {
binaryExponent += 10;
x /= 1024;
}

if (p <= 3 && Math.round(x) >= 1000) {
// Unlike SI prefixes, integers can take three digits.
binaryExponent += 10;
x /= 1024;
}

binaryPrefixExponent = Math.max(0, Math.min(8, Math.floor(binaryExponent / 10))) * 10;
var i = binaryExponent - binaryPrefixExponent + 1,
coefficient = x * i,
split = ('' + coefficient).split('.'),
integer = split[0],
fraction = split[1] || '',
n = (integer + fraction).length;

if (n === p) return coefficient;

if (n > p) {
var fractionLength = Math.max(0, p - integer.length);

while (+coefficient.toFixed(fractionLength) === 0) {
fractionLength += 1;
}

coefficient = coefficient.toFixed(fractionLength);
} else {
coefficient = integer + '.' + fraction;

while (n < p) {
coefficient += '0';
n += 1;
}
}

return coefficient;
}
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);
}
5 changes: 4 additions & 1 deletion src/formatTypes.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import formatPrefixAuto from "./formatPrefixAuto.js";
import formatBinaryPrefixAuto from "./formatBinaryPrefixAuto.js";
import formatPrefixAuto, { createFormatCurrencyPrefixAutoForLocale } from "./formatPrefixAuto.js";
import formatRounded from "./formatRounded.js";

export default {
"%": function(x, p) { return (x * 100).toFixed(p); },
"B": formatBinaryPrefixAuto,
"b": function(x) { return Math.round(x).toString(2); },
"c": function(x) { return x + ""; },
"d": function(x) { return Math.round(x).toString(10); },
"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";
Fil marked this conversation as resolved.
Show resolved Hide resolved
export {default as formatLocale} from "./locale.js";
export {default as formatSpecifier, FormatSpecifier} from "./formatSpecifier.js";
export {default as precisionFixed} from "./precisionFixed.js";
Expand Down
43 changes: 31 additions & 12 deletions src/locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import formatSpecifier from "./formatSpecifier.js";
import formatTrim from "./formatTrim.js";
import formatTypes from "./formatTypes.js";
import {prefixExponent} from "./formatPrefixAuto.js";
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"];
SIprefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],
defaultCurrencyAbbreviations = ["", "K", "M", "B", "T"],
binaryPrefixes = ["", "Ki","Mi","Gi","Ti","Pi","Ei","Zi","Yi"];

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 +56,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 = /[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
: /[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)
: /[BgKprs]/.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 +95,12 @@ 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 === "(" ? ")" : "");
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 @@ -131,18 +144,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