From 3f2383dfd40560e5bde14e61fe0b148ee5c679d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Berg?= Date: Mon, 6 May 2019 11:47:43 -0700 Subject: [PATCH] Implement binary prefixes Closes: #33 --- src/formatBinaryPrefixAuto.js | 47 ++++++++++ src/formatTypes.js | 2 + src/locale.js | 10 ++- test/format-type-bi-test.js | 160 ++++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 src/formatBinaryPrefixAuto.js create mode 100644 test/format-type-bi-test.js diff --git a/src/formatBinaryPrefixAuto.js b/src/formatBinaryPrefixAuto.js new file mode 100644 index 0000000..3351e18 --- /dev/null +++ b/src/formatBinaryPrefixAuto.js @@ -0,0 +1,47 @@ +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; +} diff --git a/src/formatTypes.js b/src/formatTypes.js index 007db36..aad025f 100644 --- a/src/formatTypes.js +++ b/src/formatTypes.js @@ -1,9 +1,11 @@ +import formatBinaryPrefixAuto from "./formatBinaryPrefixAuto.js"; import formatDecimal from "./formatDecimal.js"; import formatPrefixAuto from "./formatPrefixAuto.js"; import formatRounded from "./formatRounded.js"; export default { "%": (x, p) => (x * 100).toFixed(p), + "B": formatBinaryPrefixAuto, "b": (x) => Math.round(x).toString(2), "c": (x) => x + "", "d": formatDecimal, diff --git a/src/locale.js b/src/locale.js index 404f941..b5729a2 100644 --- a/src/locale.js +++ b/src/locale.js @@ -5,10 +5,12 @@ 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"]; + 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"]; export default function(locale) { var group = locale.grouping === undefined || locale.thousands === undefined ? identity : formatGroup(map.call(locale.grouping, Number), locale.thousands + ""), @@ -52,14 +54,14 @@ export default function(locale) { // Is this an integer type? // Can this type generate exponential notation? var formatType = formatTypes[type], - maybeSuffix = /[defgprs%]/.test(type); + maybeSuffix = /[Bdefgprs%]/.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)) + : /[Bgprs]/.test(type) ? Math.max(1, Math.min(21, precision)) : Math.max(0, Math.min(20, precision)); function format(value) { @@ -87,7 +89,7 @@ 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 === "(" ? ")" : ""); + valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : type === "B" ? binaryPrefixes[binaryPrefixExponent / 10] : "") + 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. diff --git a/test/format-type-bi-test.js b/test/format-type-bi-test.js new file mode 100644 index 0000000..ba9bb08 --- /dev/null +++ b/test/format-type-bi-test.js @@ -0,0 +1,160 @@ +import assert from "assert"; +import { format } from "../src/index.js"; + +it('format("B") outputs binary-prefix notation with default precision 6', () => { + const f = format("B"); + + assert.strictEqual(f(0), "0.00000"); + assert.strictEqual(f(1), "1.00000"); + assert.strictEqual(f(10), "10.0000"); + assert.strictEqual(f(100), "100.000"); + assert.strictEqual(f(999.5), "999.500"); + assert.strictEqual(f(1000), "1000.00"); + assert.strictEqual(f(999500), "976.074Ki"); + assert.strictEqual(f(1000000), "976.563Ki"); + assert.strictEqual(f(100), "100.000"); + assert.strictEqual(f(1024), "1.00000Ki"); + assert.strictEqual(f(1280), "1.25000Ki"); + assert.strictEqual(f(1536.512), "1.50050Ki"); + assert.strictEqual(f(0.00001), "0.00001"); + assert.strictEqual(f(0.000001), "0.000001"); +}); + +it('format("[.precision]B") outputs binary-prefix notation with precision significant digits', () => { + const f1 = format(".3B"); + + assert.strictEqual(f1(0), "0.00"); + assert.strictEqual(f1(1), "1.00"); + assert.strictEqual(f1(10), "10.0"); + assert.strictEqual(f1(100), "100"); + assert.strictEqual(f1(1023.5), "1.00Ki"); + assert.strictEqual(f1(1048576), "1.00Mi"); + assert.strictEqual(f1(1048064), "1.00Mi"); + assert.strictEqual(f1(1040000), "0.99Mi"); + assert.strictEqual(f1(1024), "1.00Ki"); + assert.strictEqual(f1(1536), "1.50Ki"); + assert.strictEqual(f1(152567808), "146Mi"); // 145.5Mi + assert.strictEqual(f1(152567807), "145Mi"); // 145.499999Mi + assert.strictEqual(f1(100 * Math.pow(2, 80)), "100Yi"); + + const f2 = format(".4B"); + + assert.strictEqual(f2(999.5), "999.5"); + assert.strictEqual(f2(1000), "1000"); + assert.strictEqual(f2(999.5 * 1024), "999.5Ki"); + assert.strictEqual(f2(1000 * 1024), "1000Ki"); +}); + +it('format("B") formats numbers smaller than 1', () => { + const f = format(".8B"); + + assert.strictEqual(f(1.29e-6), "0.0000013"); // Note: rounded! + assert.strictEqual(f(1.29e-5), "0.0000129"); + assert.strictEqual(f(1.29e-4), "0.0001290"); + assert.strictEqual(f(1.29e-3), "0.0012900"); + assert.strictEqual(f(1.29e-2), "0.0129000"); + assert.strictEqual(f(1.29e-1), "0.1290000"); +}); + +it('format("B") formats numbers larger than 2**80 with yobi', () => { + const f = format(".8B"); + + assert.strictEqual(f(1.23 * Math.pow(2, 70)), "1.2300000Zi"); + assert.strictEqual(f(12.3 * Math.pow(2, 70)), "12.300000Zi"); + assert.strictEqual(f(123 * Math.pow(2, 70)), "123.00000Zi"); + assert.strictEqual(f(1.23 * Math.pow(2, 80)), "1.2300000Yi"); + assert.strictEqual(f(12.3 * Math.pow(2, 80)), "12.300000Yi"); + assert.strictEqual(f(123 * Math.pow(2, 80)), "123.00000Yi"); + assert.strictEqual(f(1230 * Math.pow(2, 80)), "1230.0000Yi"); + assert.strictEqual(f(12300 * Math.pow(2, 80)), "12300.000Yi"); + assert.strictEqual(f(123000 * Math.pow(2, 80)), "123000.00Yi"); + assert.strictEqual(f(1230000 * Math.pow(2, 80)), "1230000.0Yi"); + assert.strictEqual(f(1234567.89 * Math.pow(2, 80)), "1234567.9Yi"); + assert.strictEqual(f(-1.23 * Math.pow(2, 70)), "−1.2300000Zi"); + assert.strictEqual(f(-12.3 * Math.pow(2, 70)), "−12.300000Zi"); + assert.strictEqual(f(-123 * Math.pow(2, 70)), "−123.00000Zi"); + assert.strictEqual(f(-1.23 * Math.pow(2, 80)), "−1.2300000Yi"); + assert.strictEqual(f(-12.3 * Math.pow(2, 80)), "−12.300000Yi"); + assert.strictEqual(f(-123 * Math.pow(2, 80)), "−123.00000Yi"); + assert.strictEqual(f(-1230 * Math.pow(2, 80)), "−1230.0000Yi"); + assert.strictEqual(f(-12300 * Math.pow(2, 80)), "−12300.000Yi"); + assert.strictEqual(f(-123000 * Math.pow(2, 80)), "−123000.00Yi"); + assert.strictEqual(f(-1230000 * Math.pow(2, 80)), "−1230000.0Yi"); + assert.strictEqual(f(-1234567.89 * Math.pow(2, 80)), "−1234567.9Yi"); +}); + +it('format("$B") outputs binary-prefix notation with a currency symbol', () => { + const f1 = format("$.2B"); + + assert.strictEqual(f1(0), "$0.0"); + assert.strictEqual(f1(256000), "$250Ki"); + assert.strictEqual(f1(-250 * Math.pow(2, 20)), "−$250Mi"); + assert.strictEqual(f1(250 * Math.pow(2, 30)), "$250Gi"); + + const f2 = format("$.3B"); + + assert.strictEqual(f2(0), "$0.00"); + assert.strictEqual(f2(1), "$1.00"); + assert.strictEqual(f2(10), "$10.0"); + assert.strictEqual(f2(100), "$100"); + assert.strictEqual(f2(999.4), "$999"); + assert.strictEqual(f2(999.5), "$0.98Ki"); + assert.strictEqual(f2(0.9995 * Math.pow(2, 10)), "$1.00Ki"); + assert.strictEqual(f2(0.9995 * Math.pow(2, 20)), "$1.00Mi"); + assert.strictEqual(f2(1024), "$1.00Ki"); + assert.strictEqual(f2(1535.5), "$1.50Ki"); + assert.strictEqual(f2(152567808), "$146Mi"); + assert.strictEqual(f2(152567807), "$145Mi"); + assert.strictEqual(f2(100 * Math.pow(2, 80)), "$100Yi"); + assert.strictEqual(f2(0.000001), "$0.000001"); + assert.strictEqual(f2(0.009995), "$0.01"); + + const f3 = format("$.4B"); + + assert.strictEqual(f3(1023), "$1023"); + assert.strictEqual(f3(1023 * Math.pow(2, 10)), "$1023Ki"); + + const f4 = format("$.5B"); + + assert.strictEqual(f4(1023.5), "$0.9995Ki"); + assert.strictEqual(f4(1023.5 * Math.pow(2, 10)), "$0.9995Mi"); +}); + +it('format("B") binary-prefix notation precision is consistent for small and large numbers', () => { + const f1 = format(".0B"); + + assert.strictEqual(f1(1 * Math.pow(2, 0)), "1"); + assert.strictEqual(f1(1e1 * Math.pow(2, 0)), "10"); + assert.strictEqual(f1(1e2 * Math.pow(2, 0)), "100"); + assert.strictEqual(f1(1 * Math.pow(2, 10)), "1Ki"); + assert.strictEqual(f1(1e1 * Math.pow(2, 10)), "10Ki"); + assert.strictEqual(f1(1e2 * Math.pow(2, 10)), "100Ki"); + + const f2 = format(".4B"); + + assert.strictEqual(f2(1 * Math.pow(2, 0)), "1.000"); + assert.strictEqual(f2(1e1 * Math.pow(2, 0)), "10.00"); + assert.strictEqual(f2(1e2 * Math.pow(2, 0)), "100.0"); + assert.strictEqual(f2(1 * Math.pow(2, 10)), "1.000Ki"); + assert.strictEqual(f2(1e1 * Math.pow(2, 10)), "10.00Ki"); + assert.strictEqual(f2(1e2 * Math.pow(2, 10)), "100.0Ki"); +}); + +it('format("0[width],B") will group thousands due to zero fill', () => { + const f = format("020,B"); + + assert.strictEqual(f(42), "000,000,000,042.0000"); + assert.strictEqual(f(42 * Math.pow(2, 40)), "0,000,000,042.0000Ti"); +}); + +it('format(",B") will group thousands for very large numbers', () => { + const f = format(",B"); + + assert.strictEqual(f(42e6 * Math.pow(2, 80)), "42,000,000Yi"); +}); + +it('format("B") will not hang on Infinity', () => { + const f = format("B"); + + assert.strictEqual(f(Infinity), "Infinity"); +});