Skip to content

Commit

Permalink
Implement binary prefixes
Browse files Browse the repository at this point in the history
Closes: #33
  • Loading branch information
Rúnar Berg authored and Rúnar Berg committed Mar 28, 2020
1 parent 79cf3ca commit 1dfc9ad
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 5 deletions.
45 changes: 45 additions & 0 deletions src/formatBinaryPrefixAuto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export var binaryPrefixExponent;

export default function(x, p) {
var binaryExponent = 0;

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

if (p <= 3 && Math.round(x) >= 1000) {
// Unlike SI prefixes, intergers 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;
}
2 changes: 2 additions & 0 deletions src/formatTypes.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import formatBinaryPrefixAuto from "./formatBinaryPrefixAuto.js";
import formatPrefixAuto 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); },
Expand Down
15 changes: 10 additions & 5 deletions src/locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ""),
Expand Down Expand Up @@ -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) {
Expand All @@ -85,8 +87,11 @@ 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.
Expand Down
150 changes: 150 additions & 0 deletions test/format-type-bi-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
var tape = require("tape"),
format = require("../");

tape("format(\"B\") outputs binary-prefix notation with default precision 6", function(test) {
var f = format.format("B");
test.equal(f(0), "0.00000");
test.equal(f(1), "1.00000");
test.equal(f(10), "10.0000");
test.equal(f(100), "100.000");
test.equal(f(999.5), "999.500");
test.equal(f(1000), "1000.00");
test.equal(f(999500), "976.074Ki");
test.equal(f(1000000), "976.563Ki");
test.equal(f(100), "100.000");
test.equal(f(1024), "1.00000Ki");
test.equal(f(1280), "1.25000Ki");
test.equal(f(1536.512), "1.50050Ki");
test.equal(f(.00001), "0.00001");
test.equal(f(.000001), "0.000001");
test.end();
});

tape("format(\"[.precision]B\") outputs binary-prefix notation with precision significant digits", function(test) {
var f1 = format.format(".3B");
test.equal(f1(0), "0.00");
test.equal(f1(1), "1.00");
test.equal(f1(10), "10.0");
test.equal(f1(100), "100");
test.equal(f1(1023.5), "1.00Ki");
test.equal(f1(1048576), "1.00Mi");
test.equal(f1(1048064), "1.00Mi");
test.equal(f1(1040000), "0.99Mi");
test.equal(f1(1024), "1.00Ki");
test.equal(f1(1536), "1.50Ki");
test.equal(f1(152567808), "146Mi"); // 145.5Mi
test.equal(f1(152567807), "145Mi"); // 145.499999Mi
test.equal(f1(100 * Math.pow(2, 80)), "100Yi");
var f2 = format.format(".4B");
test.equal(f2(999.5), "999.5");
test.equal(f2(1000), "1000");
test.equal(f2(999.5 * 1024), "999.5Ki");
test.equal(f2(1000 * 1024), "1000Ki");
test.end();
});

tape("format(\"B\") formats numbers smaller than 1", function(test) {
var f = format.format(".8B");
test.equal(f(1.29e-6), "0.0000013"); // Note: rounded!
test.equal(f(1.29e-5), "0.0000129");
test.equal(f(1.29e-4), "0.0001290");
test.equal(f(1.29e-3), "0.0012900");
test.equal(f(1.29e-2), "0.0129000");
test.equal(f(1.29e-1), "0.1290000");
test.end();
});

tape("format(\"B\") formats numbers larger than 2**80 with yobi", function(test) {
var f = format.format(".8B");
test.equal(f(1.23 * Math.pow(2, 70)), "1.2300000Zi");
test.equal(f(12.3 * Math.pow(2, 70)), "12.300000Zi");
test.equal(f(123 * Math.pow(2, 70)), "123.00000Zi");
test.equal(f(1.23 * Math.pow(2, 80)), "1.2300000Yi");
test.equal(f(12.3 * Math.pow(2, 80)), "12.300000Yi");
test.equal(f(123 * Math.pow(2, 80)), "123.00000Yi");
test.equal(f(1230 * Math.pow(2, 80)), "1230.0000Yi");
test.equal(f(12300 * Math.pow(2, 80)), "12300.000Yi");
test.equal(f(123000 * Math.pow(2, 80)), "123000.00Yi");
test.equal(f(1230000 * Math.pow(2, 80)), "1230000.0Yi");
test.equal(f(1234567.89 * Math.pow(2, 80)), "1234567.9Yi");
test.equal(f(-1.23 * Math.pow(2, 70)), "-1.2300000Zi");
test.equal(f(-12.3 * Math.pow(2, 70)), "-12.300000Zi");
test.equal(f(-123 * Math.pow(2, 70)), "-123.00000Zi");
test.equal(f(-1.23 * Math.pow(2, 80)), "-1.2300000Yi");
test.equal(f(-12.3 * Math.pow(2, 80)), "-12.300000Yi");
test.equal(f(-123 * Math.pow(2, 80)), "-123.00000Yi");
test.equal(f(-1230 * Math.pow(2, 80)), "-1230.0000Yi");
test.equal(f(-12300 * Math.pow(2, 80)), "-12300.000Yi");
test.equal(f(-123000 * Math.pow(2, 80)), "-123000.00Yi");
test.equal(f(-1230000 * Math.pow(2, 80)), "-1230000.0Yi");
test.equal(f(-1234567.89 * Math.pow(2, 80)), "-1234567.9Yi");
test.end();
});

tape("format(\"$B\") outputs binary-prefix notation with a currency symbol", function(test) {
var f1 = format.format("$.2B");
test.equal(f1(0), "$0.0");
test.equal(f1(256000), "$250Ki");
test.equal(f1(-250 * Math.pow(2, 20)), "-$250Mi");
test.equal(f1(250 * Math.pow(2, 30)), "$250Gi");
var f2 = format.format("$.3B");
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.4), "$999");
test.equal(f2(999.5), "$0.98Ki");
test.equal(f2(.9995 * Math.pow(2, 10)), "$1.00Ki");
test.equal(f2(.9995 * Math.pow(2, 20)), "$1.00Mi");
test.equal(f2(1024), "$1.00Ki");
test.equal(f2(1535.5), "$1.50Ki");
test.equal(f2(152567808), "$146Mi");
test.equal(f2(152567807), "$145Mi");
test.equal(f2(100 * Math.pow(2, 80)), "$100Yi");
test.equal(f2(.000001), "$0.000001");
test.equal(f2(.009995), "$0.01");
var f3 = format.format("$.4B");
test.equal(f3(1023), "$1023");
test.equal(f3(1023 * Math.pow(2, 10)), "$1023Ki");
var f4 = format.format("$.5B");
test.equal(f4(1023.5), "$0.9995Ki");
test.equal(f4(1023.5 * Math.pow(2, 10)), "$0.9995Mi");
test.end();
});

tape("format(\"B\") binary-prefix notation precision is consistent for small and large numbers", function(test) {
var f1 = format.format(".0B");
test.equal(f1(1e0 * Math.pow(2, 0)), "1");
test.equal(f1(1e1 * Math.pow(2, 0)), "10");
test.equal(f1(1e2 * Math.pow(2, 0)), "100");
test.equal(f1(1e0 * Math.pow(2, 10)), "1Ki");
test.equal(f1(1e1 * Math.pow(2, 10)), "10Ki");
test.equal(f1(1e2 * Math.pow(2, 10)), "100Ki");
var f2 = format.format(".4B");
test.equal(f2(1e+0 * Math.pow(2, 0)), "1.000");
test.equal(f2(1e+1 * Math.pow(2, 0)), "10.00");
test.equal(f2(1e+2 * Math.pow(2, 0)), "100.0");
test.equal(f2(1e+0 * Math.pow(2, 10)), "1.000Ki");
test.equal(f2(1e+1 * Math.pow(2, 10)), "10.00Ki");
test.equal(f2(1e+2 * Math.pow(2, 10)), "100.0Ki");
test.end();
});

tape("format(\"0[width],B\") will group thousands due to zero fill", function(test) {
var f = format.format("020,B");
test.equal(f(42), "000,000,000,042.0000");
test.equal(f(42 * Math.pow(2, 40)), "0,000,000,042.0000Ti");
test.end();
});

tape("format(\",B\") will group thousands for very large numbers", function(test) {
var f = format.format(",B");
test.equal(f(42e6 * Math.pow(2, 80)), "42,000,000Yi");
test.end();
});

tape("format(\"B\") will not hang on Infinity", function(test) {
var f = format.format("B");
test.equal(f(Infinity), "InfinityYi");
test.end();
});

0 comments on commit 1dfc9ad

Please sign in to comment.