From e249b2eb00d1319f9d3090e7263d365d57a43fe9 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Thu, 28 Nov 2019 20:19:22 +0100 Subject: [PATCH 1/7] Add formatCurrencyPrefix and currencyPrecisionPrefix These functons will be used todetermine which prefixes will be used when formatting currencies. Common prefixes are for thousands (K), millions (M), billions (B) and trillions (T). --- src/currencyPrecisionPrefix.js | 3 ++ src/defaultLocale.js | 2 ++ src/index.js | 3 +- src/locale.js | 23 +++++++----- src/precisionPrefix.js | 8 +++-- test/currencyPrecisionPrefix-test.js | 52 ++++++++++++++++++++++++++++ test/formatCurrencyPrefix-test.js | 48 +++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 src/currencyPrecisionPrefix.js create mode 100644 test/currencyPrecisionPrefix-test.js create mode 100644 test/formatCurrencyPrefix-test.js diff --git a/src/currencyPrecisionPrefix.js b/src/currencyPrecisionPrefix.js new file mode 100644 index 0000000..e4cb9c5 --- /dev/null +++ b/src/currencyPrecisionPrefix.js @@ -0,0 +1,3 @@ +import { createPrecisionPrefix } from "./precisionPrefix.js"; + +export default createPrecisionPrefix(0, 4); diff --git a/src/defaultLocale.js b/src/defaultLocale.js index 9ecf0fa..200725b 100644 --- a/src/defaultLocale.js +++ b/src/defaultLocale.js @@ -2,6 +2,7 @@ import formatLocale from "./locale.js"; var locale; export var format; +export var formatCurrencyPrefix; export var formatPrefix; defaultLocale({ @@ -15,6 +16,7 @@ defaultLocale({ export default function defaultLocale(definition) { locale = formatLocale(definition); format = locale.format; + formatCurrencyPrefix = locale.formatCurrencyPrefix; formatPrefix = locale.formatPrefix; return locale; } diff --git a/src/index.js b/src/index.js index 22ae6b2..5baab41 100644 --- a/src/index.js +++ b/src/index.js @@ -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"; diff --git a/src/locale.js b/src/locale.js index a8ea919..e5fcaf6 100644 --- a/src/locale.js +++ b/src/locale.js @@ -8,7 +8,8 @@ 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"]; + prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"], + currencyPrefixes = ["", "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 + ""), @@ -131,18 +132,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(prefixes, -8, 8); + var formatCurrencyPrefix = createFormatPrefix(currencyPrefixes, 0, 4); + return { format: newFormat, + formatCurrencyPrefix: formatCurrencyPrefix, formatPrefix: formatPrefix }; } diff --git a/src/precisionPrefix.js b/src/precisionPrefix.js index fd6af84..3e2e28e 100644 --- a/src/precisionPrefix.js +++ b/src/precisionPrefix.js @@ -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); diff --git a/test/currencyPrecisionPrefix-test.js b/test/currencyPrecisionPrefix-test.js new file mode 100644 index 0000000..49aed8e --- /dev/null +++ b/test/currencyPrecisionPrefix-test.js @@ -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(); +}); + diff --git a/test/formatCurrencyPrefix-test.js b/test/formatCurrencyPrefix-test.js new file mode 100644 index 0000000..23fa7b4 --- /dev/null +++ b/test/formatCurrencyPrefix-test.js @@ -0,0 +1,48 @@ +var tape = require("tape"), + format = require("../dist/d3-format"); + +tape("formatCurrencyPrefix(\"K\", value)(number) formats with the thousands prefix if appropriate to the specified value", function(test) { + test.equal(format.formatCurrencyPrefix(",.0K", 1e3)(42000), "42K"); + test.equal(format.formatCurrencyPrefix(",.0K", 1e3)(420000), "420K"); + test.equal(format.formatCurrencyPrefix(",.3K", 1e3)(420), "0.420K"); + test.end(); +}); + +tape("formatCurrencyPrefix(\"K\", value)(number) formats with the millions prefix if appropriate to the specified value", function(test) { + test.equal(format.formatCurrencyPrefix(",.0K", 1e6)(42000000), "42M"); + test.equal(format.formatCurrencyPrefix(",.0K", 1e6)(420000000), "420M"); + test.equal(format.formatCurrencyPrefix(",.3K", 1e6)(420000), "0.420M"); + test.end(); +}); + +tape("formatCurrencyPrefix(\"K\", value)(number) formats with the billions prefix if appropriate to the specified value", function(test) { + test.equal(format.formatCurrencyPrefix(",.0K", 1e9)(42 * 1e9), "42B"); + test.equal(format.formatCurrencyPrefix(",.0K", 1e9)(420 * 1e9), "420B"); + test.equal(format.formatCurrencyPrefix(",.3K", 1e9)(420 * 1e6), "0.420B"); + test.end(); +}); + +tape("formatCurrencyPrefix(\"K\", value)(number) formats with the trillions prefix if appropriate to the specified value", function(test) { + test.equal(format.formatCurrencyPrefix(",.0K", 1e12)(42 * 1e12), "42T"); + test.equal(format.formatCurrencyPrefix(",.0K", 1e12)(420 * 1e12), "420T"); + test.equal(format.formatCurrencyPrefix(",.3K", 1e12)(420 * 1e9), "0.420T"); + test.end(); +}); + + +tape("formatCurrencyPrefix(\"K\", value)(number) uses nothing for very small reference values", function(test) { + test.equal(format.formatCurrencyPrefix(",.3K", 1e-3)(0.1), "0.100"); + test.end(); +}); + +tape("formatCurrencyPrefix(\"K\", value)(number) uses trillions for very large reference values", function(test) { + test.equal(format.formatCurrencyPrefix(",.0K", 1e15)(1e15), "1,000T"); + test.end(); +}); + +tape("formatCurrencyPrefix(\"$,K\", value)(number) formats with the associated currency", function(test) { + var f = format.formatCurrencyPrefix(" $12,.1K", 1e9); + test.equal(f(-42e9), " -$42.0B"); + test.equal(f(+4.2e9), " $4.2B"); + test.end(); +}); From d1ed244527f9a7c2fcdae93f4ea6ab0d069ddfca Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 29 Nov 2019 11:53:48 +0100 Subject: [PATCH 2/7] Introduce K type for currency prefix --- src/formatPrefixAuto.js | 14 ++++-- src/formatTypes.js | 3 +- src/locale.js | 19 +++++--- test/format-type-K-test.js | 91 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 test/format-type-K-test.js diff --git a/src/formatPrefixAuto.js b/src/formatPrefixAuto.js index c7ef7be..e3cc6bc 100644 --- a/src/formatPrefixAuto.js +++ b/src/formatPrefixAuto.js @@ -2,15 +2,23 @@ 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 formatCurrencyPrefixAuto(x, p) { + return formatSignificantDigitsForPrefixes(x, p, 0, 4); +} + +export default function(x, p) { + return formatSignificantDigitsForPrefixes(x, p, -8, 8); } diff --git a/src/formatTypes.js b/src/formatTypes.js index cc7421d..5327455 100644 --- a/src/formatTypes.js +++ b/src/formatTypes.js @@ -1,4 +1,4 @@ -import formatPrefixAuto from "./formatPrefixAuto.js"; +import formatPrefixAuto, { formatCurrencyPrefixAuto } from "./formatPrefixAuto.js"; import formatRounded from "./formatRounded.js"; export default { @@ -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": formatCurrencyPrefixAuto, "o": function(x) { return Math.round(x).toString(8); }, "p": function(x, p) { return formatRounded(x * 100, p); }, "r": formatRounded, diff --git a/src/locale.js b/src/locale.js index e5fcaf6..d101fc3 100644 --- a/src/locale.js +++ b/src/locale.js @@ -8,7 +8,7 @@ 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"], currencyPrefixes = ["", "K", "M", "B", "T"]; export default function(locale) { @@ -53,14 +53,15 @@ 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); // 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) { @@ -88,7 +89,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 = currencyPrefixes[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. @@ -144,7 +151,7 @@ export default function(locale) { } } - var formatPrefix = createFormatPrefix(prefixes, -8, 8); + var formatPrefix = createFormatPrefix(SIprefixes, -8, 8); var formatCurrencyPrefix = createFormatPrefix(currencyPrefixes, 0, 4); return { diff --git a/test/format-type-K-test.js b/test/format-type-K-test.js new file mode 100644 index 0000000..ed5c1ab --- /dev/null +++ b/test/format-type-K-test.js @@ -0,0 +1,91 @@ +var tape = require("tape"), + format = require("../dist/d3-format"); + +tape("format(\"K\") outputs currency prefix notation with default 3 significant digits", function(test) { + var f = format.format("K"); + test.equal(f(0), "0.00"); + test.equal(f(0.2), "0.20"); + test.equal(f(1), "1.00"); + test.equal(f(10), "10.0"); + test.equal(f(100), "100"); + test.equal(f(1000), "1.00K"); + test.end(); +}); + +tape("format(\"[.precision]K\") outputs currency-prefix notation with precision significant digits", function(test) { + var f1 = format.format(".2K"); + test.equal(f1(0), "0.0"); + test.equal(f1(1), "1.0"); + test.equal(f1(10), "10"); + test.equal(f1(100), "100"); + test.equal(f1(999.5), "1.0K"); + test.equal(f1(999500), "1.0M"); + test.equal(f1(1000), "1.0K"); + test.equal(f1(1500.5), "1.5K"); + test.equal(f1(145500000), "150M"); + test.equal(f1(145999999.999999347), "150M"); + test.equal(f1(1e17), "100000T"); + test.equal(f1(.000001), "0.000001"); + var f2 = format.format(".4K"); + test.equal(f2(999.5), "999.5"); + test.equal(f2(999500), "999.5K"); + test.end(); +}); + +tape("format(\"K\") formats numbers smaller than 1", function(test) { + var f = format.format(".2K"); + test.equal(f(1.29e-2), "0.0129"); + test.equal(f(1.29e-3), "0.00129"); + test.equal(f(-1.29e-2), "-0.0129"); + test.equal(f(-1.29e-3), "-0.00129"); + test.end(); +}); + +tape("format(\"K\") formats numbers larger than thousands of trillions", function(test) { + var f = format.format(".2K"); + test.equal(f(1.23e+15), "1200T"); + test.equal(f(1.23e+16), "12000T"); + test.equal(f(-1.23e+15), "-1200T"); + test.equal(f(-1.23e+16), "-12000T"); + test.end(); +}); + +tape("format(\"$K\") outputs currency-prefix notation with a currency symbol", function(test) { + var f1 = format.format("$.2K"); + test.equal(f1(0), "$0.0"); + test.equal(f1(2.5e5), "$250K"); + test.equal(f1(-2.5e8), "-$250M"); + test.equal(f1(2.5e11), "$250B"); + var f2 = format.format("$.3K"); + test.equal(f2(0), "$0.00"); + test.equal(f2(1), "$1.00"); + test.equal(f2(10), "$10.0"); + test.equal(f2(100), "$100"); + test.equal(f2(999.5), "$1.00K"); + test.equal(f2(999500), "$1.00M"); + test.equal(f2(1000), "$1.00K"); + test.equal(f2(1500.5), "$1.50K"); + test.equal(f2(145500000), "$146M"); + test.equal(f2(145999999.999999347), "$146M"); + test.equal(f2(1e18), "$1000000T"); + test.equal(f2(.000001), "$0.000001"); + test.equal(f2(.009995), "$0.01"); + var f3 = format.format("$.4K"); + test.equal(f3(999.5), "$999.5"); + test.equal(f3(999500), "$999.5K"); + test.equal(f3(.009995), "$0.001"); + test.end(); +}); + +tape("format(\"0[width],K\") will group thousands due to zero fill", function(test) { + var f = format.format("015,K"); + test.equal(f(42), "0,000,000,042.0"); + test.equal(f(42e12), "0,000,000,042.0T"); + test.end(); +}); + +tape("format(\",K\") will group thousands for very large numbers", function(test) { + var f = format.format(",K"); + test.equal(f(42e30), "42,000,000,000,000,000,000T"); + test.end(); +}); From fad5de71582461116fd668d46e3b4a631323389e Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 29 Nov 2019 12:05:50 +0100 Subject: [PATCH 3/7] Documentation for type K and associated methods --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2259cde..f52affa 100644 --- a/README.md +++ b/README.md @@ -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" @@ -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. @@ -167,7 +168,7 @@ d3.format(".1")(42); // "4e+1" d3.format(".1")(4.2); // "4" ``` -# locale.formatPrefix(specifier, value) [<>](https://github.com/d3/d3-format/blob/master/src/locale.js#L127 "Source") +# locale.formatPrefix(specifier, value) [<>](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: @@ -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. +# locale.formatCurrencyPrefix(specifier, value) [<>](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¹² + # d3.formatSpecifier(specifier) [<>](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: @@ -290,6 +301,10 @@ f(1.2e6); // "1.2M" f(1.3e6); // "1.3M" ``` +# d3.currencyPrecisionPrefix(step, value) [<>](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. + # d3.precisionRound(step, max) [<>](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: From 3fa19c7f26a00c047ec5a2ae341bd562e35f04c1 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 29 Nov 2019 13:59:21 +0100 Subject: [PATCH 4/7] Allow currency abbreviations in locale definition --- README.md | 1 + src/locale.js | 7 ++++--- test/locale-test.js | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f52affa..1f4d8ef 100644 --- a/README.md +++ b/README.md @@ -346,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 vales (array of 5 elements: units, thousands, millions, billions and trillions) (default `["", "K", "M", "B", "T"]`). * `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, `"-"`). diff --git a/src/locale.js b/src/locale.js index d101fc3..a9cb8c8 100644 --- a/src/locale.js +++ b/src/locale.js @@ -9,10 +9,11 @@ import identity from "./identity.js"; var map = Array.prototype.map, SIprefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"], - currencyPrefixes = ["", "K", "M", "B", "T"]; + 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 + "", @@ -93,7 +94,7 @@ export default function(locale) { if (type === "s") valueSuffix = SIprefixes[8 + prefixExponent / 3] + valueSuffix else if (type === "K") - valueSuffix = currencyPrefixes[prefixExponent / 3] + valueSuffix + valueSuffix = currencyAbbreviations[prefixExponent / 3] + valueSuffix valueSuffix = valueSuffix + (valueNegative && sign === "(" ? ")" : ""); @@ -152,7 +153,7 @@ export default function(locale) { } var formatPrefix = createFormatPrefix(SIprefixes, -8, 8); - var formatCurrencyPrefix = createFormatPrefix(currencyPrefixes, 0, 4); + var formatCurrencyPrefix = createFormatPrefix(currencyAbbreviations, 0, 4); return { format: newFormat, diff --git a/test/locale-test.js b/test/locale-test.js index 394811d..494a09c 100644 --- a/test/locale-test.js +++ b/test/locale-test.js @@ -21,6 +21,11 @@ tape("formatLocale({currency: [prefix, suffix]}) places the currency suffix afte test.end(); }); +tape("formatLocale({currencyAbbreviations: [list of abbreviations]}) should abbreviate thousands, millions, billions and trillions", function (test) { + test.equal(d3.formatLocale({ currencyAbbreviations: ["", "k", "m", "bn", "tn"] }).format("$.3K")(1.2e9), "1.20bn"); + test.end(); +}); + tape("formatLocale({grouping: undefined}) does not perform any grouping", function(test) { test.equal(d3.formatLocale({decimal: "."}).format("012,.2f")(2), "000000002.00"); test.end(); From 7ee5a2c9756b5830aeb1666f36a27dc6369747d6 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 29 Nov 2019 15:21:27 +0100 Subject: [PATCH 5/7] dd currency abbreviations for en-GB --- locale/en-GB.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locale/en-GB.json b/locale/en-GB.json index 3d22d7a..dd45db2 100644 --- a/locale/en-GB.json +++ b/locale/en-GB.json @@ -2,5 +2,6 @@ "decimal": ".", "thousands": ",", "grouping": [3], - "currency": ["£", ""] + "currency": ["£", ""], + "currencyAbbreviations": ["", "k", "m", "bn", "t"] } From a0de748f296e454ab5ec4ff3692164d9631e1a48 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 29 Nov 2019 15:25:59 +0100 Subject: [PATCH 6/7] Some type K abbreviations can be omitted in locale --- README.md | 2 +- locale/fr-FR.json | 3 ++- src/formatPrefixAuto.js | 6 ++++-- src/formatTypes.js | 4 ++-- src/locale.js | 5 ++++- test/locale-test.js | 5 +++++ 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1f4d8ef..3f9415d 100644 --- a/README.md +++ b/README.md @@ -346,7 +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 vales (array of 5 elements: units, thousands, millions, billions and trillions) (default `["", "K", "M", "B", "T"]`). +* `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, `"-"`). diff --git a/locale/fr-FR.json b/locale/fr-FR.json index e0cf89d..349868d 100644 --- a/locale/fr-FR.json +++ b/locale/fr-FR.json @@ -3,5 +3,6 @@ "thousands": "\u00a0", "grouping": [3], "currency": ["", "\u00a0€"], - "percent": "\u202f%" + "percent": "\u202f%", + "currencyAbbreviations": ["", "K", "Mio", "Mrd"] } diff --git a/src/formatPrefixAuto.js b/src/formatPrefixAuto.js index e3cc6bc..d11bce8 100644 --- a/src/formatPrefixAuto.js +++ b/src/formatPrefixAuto.js @@ -15,8 +15,10 @@ function formatSignificantDigitsForPrefixes(x, p, minPrefixOrder, maxPrefixOrder : "0." + new Array(1 - i).join("0") + formatDecimal(x, Math.max(0, p + i - 1))[0]; // less than the smallest prefix } -export function formatCurrencyPrefixAuto(x, p) { - return formatSignificantDigitsForPrefixes(x, p, 0, 4); +export function createFormatCurrencyPrefixAutoForLocale(currencyAbbreviations) { + return function formatCurrencyPrefixAuto(x, p) { + return formatSignificantDigitsForPrefixes(x, p, 0, currencyAbbreviations.length - 1); + } } export default function(x, p) { diff --git a/src/formatTypes.js b/src/formatTypes.js index 5327455..fd81632 100644 --- a/src/formatTypes.js +++ b/src/formatTypes.js @@ -1,4 +1,4 @@ -import formatPrefixAuto, { formatCurrencyPrefixAuto } from "./formatPrefixAuto.js"; +import formatPrefixAuto, { createFormatCurrencyPrefixAutoForLocale } from "./formatPrefixAuto.js"; import formatRounded from "./formatRounded.js"; export default { @@ -9,7 +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": formatCurrencyPrefixAuto, + "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, diff --git a/src/locale.js b/src/locale.js index a9cb8c8..41fb186 100644 --- a/src/locale.js +++ b/src/locale.js @@ -56,6 +56,9 @@ export default function(locale) { var formatType = formatTypes[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]. @@ -153,7 +156,7 @@ export default function(locale) { } var formatPrefix = createFormatPrefix(SIprefixes, -8, 8); - var formatCurrencyPrefix = createFormatPrefix(currencyAbbreviations, 0, 4); + var formatCurrencyPrefix = createFormatPrefix(currencyAbbreviations, 0, currencyAbbreviations.length - 1); return { format: newFormat, diff --git a/test/locale-test.js b/test/locale-test.js index 494a09c..48c21dd 100644 --- a/test/locale-test.js +++ b/test/locale-test.js @@ -26,6 +26,11 @@ tape("formatLocale({currencyAbbreviations: [list of abbreviations]}) should abbr test.end(); }); +tape("formatLocale({currencyAbbreviations: [list of abbreviations]}) should abbreviate only specified levels", function (test) { + test.equal(d3.formatLocale({ currencyAbbreviations: ["", "M", "Mio", "Mrd"] }).format("$.3K")(1.2e12), "1200Mrd"); + test.end(); +}); + tape("formatLocale({grouping: undefined}) does not perform any grouping", function(test) { test.equal(d3.formatLocale({decimal: "."}).format("012,.2f")(2), "000000002.00"); test.end(); From 76c6681f58a7696e6b7114fcbf3317e31e660a7f Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Sat, 20 Jun 2020 14:15:19 +0200 Subject: [PATCH 7/7] Use CLDR for currencyAbbreviations Implement en, fr, de, es, it and ln locales See https://github.com/PrestaShop/PrestaShop/blob/develop/localization/CLDR/core/common/main/.xml for reference --- locale/de-DE.json | 3 ++- locale/en-GB.json | 2 +- locale/en-US.json | 3 ++- locale/es-ES.json | 3 ++- locale/fr-FR.json | 2 +- locale/it-IT.json | 3 ++- locale/nl-NL.json | 3 ++- test/locale-test.js | 4 ++-- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/locale/de-DE.json b/locale/de-DE.json index a249762..320d2b0 100644 --- a/locale/de-DE.json +++ b/locale/de-DE.json @@ -2,5 +2,6 @@ "decimal": ",", "thousands": ".", "grouping": [3], - "currency": ["", "\u00a0€"] + "currency": ["", "\u00a0€"], + "currencyAbbreviations": ["", "", "\u00a0Mio.", "\u00a0Mrd.", "\u00a0Bio."] } diff --git a/locale/en-GB.json b/locale/en-GB.json index dd45db2..d370be3 100644 --- a/locale/en-GB.json +++ b/locale/en-GB.json @@ -3,5 +3,5 @@ "thousands": ",", "grouping": [3], "currency": ["£", ""], - "currencyAbbreviations": ["", "k", "m", "bn", "t"] + "currencyAbbreviations": ["", "K", "M", "B", "T"] } diff --git a/locale/en-US.json b/locale/en-US.json index f075b86..d9aa81b 100644 --- a/locale/en-US.json +++ b/locale/en-US.json @@ -2,5 +2,6 @@ "decimal": ".", "thousands": ",", "grouping": [3], - "currency": ["$", ""] + "currency": ["$", ""], + "currencyAbbreviations": ["", "K", "M", "B", "T"] } diff --git a/locale/es-ES.json b/locale/es-ES.json index a249762..2ffed3a 100644 --- a/locale/es-ES.json +++ b/locale/es-ES.json @@ -2,5 +2,6 @@ "decimal": ",", "thousands": ".", "grouping": [3], - "currency": ["", "\u00a0€"] + "currency": ["", "\u00a0€"], + "currencyAbbreviations": ["", "\u00a0mil", "\u00a0M", "\u00a0mil M", "\u00a0B"] } diff --git a/locale/fr-FR.json b/locale/fr-FR.json index 349868d..151c02d 100644 --- a/locale/fr-FR.json +++ b/locale/fr-FR.json @@ -4,5 +4,5 @@ "grouping": [3], "currency": ["", "\u00a0€"], "percent": "\u202f%", - "currencyAbbreviations": ["", "K", "Mio", "Mrd"] + "currencyAbbreviations": ["", "\u00a0k", "\u00a0M", "\u00a0Md", "\u00a0Bn"] } diff --git a/locale/it-IT.json b/locale/it-IT.json index 564ed46..202322a 100644 --- a/locale/it-IT.json +++ b/locale/it-IT.json @@ -2,5 +2,6 @@ "decimal": ",", "thousands": ".", "grouping": [3], - "currency": ["€", ""] + "currency": ["€", ""], + "currencyAbbreviations": ["", "", "\u00a0Mio", "\u00a0Mrd", "\u00a0Bln"] } diff --git a/locale/nl-NL.json b/locale/nl-NL.json index 7176b37..78ab0ab 100644 --- a/locale/nl-NL.json +++ b/locale/nl-NL.json @@ -2,5 +2,6 @@ "decimal": ",", "thousands": ".", "grouping": [3], - "currency": ["€\u00a0", ""] + "currency": ["€\u00a0", ""], + "currencyAbbreviations": ["", "K", "\u00a0mln.", "\u00a0mld.", "\u00a0bln."] } diff --git a/test/locale-test.js b/test/locale-test.js index 48c21dd..dc3716c 100644 --- a/test/locale-test.js +++ b/test/locale-test.js @@ -22,12 +22,12 @@ tape("formatLocale({currency: [prefix, suffix]}) places the currency suffix afte }); tape("formatLocale({currencyAbbreviations: [list of abbreviations]}) should abbreviate thousands, millions, billions and trillions", function (test) { - test.equal(d3.formatLocale({ currencyAbbreviations: ["", "k", "m", "bn", "tn"] }).format("$.3K")(1.2e9), "1.20bn"); + test.equal(d3.formatLocale({ currencyAbbreviations: ["", "\u00a0k", "\u00a0M", "\u00a0Md", "\u00a0Bn"] }).format("$.3K")(1.2e9), "1.20\u00a0Md"); test.end(); }); tape("formatLocale({currencyAbbreviations: [list of abbreviations]}) should abbreviate only specified levels", function (test) { - test.equal(d3.formatLocale({ currencyAbbreviations: ["", "M", "Mio", "Mrd"] }).format("$.3K")(1.2e12), "1200Mrd"); + test.equal(d3.formatLocale({ currencyAbbreviations: ["", "\u00a0k", "\u00a0M", "\u00a0Md", "\u00a0Bn"] }).format("$.3K")(1.2e15), "1200\u00a0Bn"); test.end(); });