human-format
Version:
Converts a number to/from a human readable string: `1337` ↔ `1.34 kB`
462 lines (396 loc) • 12.7 kB
JavaScript
// UMD: https://github.com/umdjs/umd/blob/master/returnExports.js
(function (root, factory) {
/* global define: false */
// AMD and browser globals are not testable in Node
/* node:coverage disable */
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === "object") {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.humanFormat = factory();
}
/* node:coverage enable */
})(this, function () {
"use strict";
// =================================================================
function assign(dst, src) {
var i, n, prop;
for (i = 1, n = arguments.length; i < n; ++i) {
src = arguments[i];
if (src != null) {
for (prop in src) {
if (has(src, prop) && src[prop] !== undefined) {
dst[prop] = src[prop];
}
}
}
}
return dst;
}
// https://www.npmjs.org/package/escape-regexp
function escapeRegexp(str) {
return str.replace(/([.*+?=^!:${}()|[\]/\\])/g, "\\$1");
}
// Reads an option without copying opts, which is costly on hot paths.
function getOption(opts, defaults, name) {
var value = has(opts, name) ? opts[name] : undefined;
return value === undefined ? defaults[name] : value;
}
function has(obj, prop) {
return obj != null && Object.prototype.hasOwnProperty.call(obj, prop);
}
// Number.MAX_SAFE_INTEGER is not ES5
var MAX_SAFE_INTEGER = 9007199254740991;
function resolve(container, entry) {
while (typeof entry === "string") {
entry = container[entry];
}
return entry;
}
// =================================================================
function Scale(prefixes) {
this._prefixes = prefixes;
var escapedPrefixes = [];
var list = [];
var lcPrefixes = (this._lcPrefixes = {});
var prefix, lcPrefix;
for (prefix in prefixes) {
if (has(prefixes, prefix)) {
escapedPrefixes.push(escapeRegexp(prefix));
list.push({
factor: prefixes[prefix],
prefix: prefix,
});
// Adds lower cased prefixes for case insensitive fallback, except
// when ambiguous (e.g. M and m).
lcPrefix = prefix.toLowerCase();
if (lcPrefix === prefix || !has(prefixes, lcPrefix)) {
lcPrefixes[lcPrefix] = prefix;
}
}
}
list.sort(function (a, b) {
return a.factor - b.factor;
});
this._list = list;
// longest first
escapedPrefixes.sort(function (a, b) {
return b.length - a.length;
});
this._regexp = new RegExp(
"^\\s*(-)?\\s*(\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?)\\s*(" +
escapedPrefixes.join("|") +
")\\s*(\\D*?)\\s*$",
"i"
);
}
Scale.create = function Scale$create(prefixesList, base, initExp) {
var prefixes = {};
if (initExp === undefined) {
initExp = 0;
}
for (var i = 0, n = prefixesList.length; i < n; ++i) {
prefixes[prefixesList[i]] = Math.pow(base, i + initExp);
}
return new Scale(prefixes);
};
// Binary search to find the greatest index which has a value <=.
Scale.prototype.findPrefix = function Scale$findPrefix(value) {
var list = this._list;
var low = 0;
var high = list.length - 1;
var mid, current;
while (low !== high) {
mid = (low + high + 1) >> 1;
current = list[mid].factor;
if (current > value) {
high = mid - 1;
} else {
low = mid;
}
}
return list[low];
};
Scale.prototype.parse = function Scale$parse(str, strict) {
var matches = str.match(this._regexp);
if (matches === null) {
return;
}
var prefix = matches[3];
var factor;
if (has(this._prefixes, prefix)) {
factor = this._prefixes[prefix];
} else if (
!strict &&
((prefix = prefix.toLowerCase()), has(this._lcPrefixes, prefix))
) {
prefix = this._lcPrefixes[prefix];
factor = this._prefixes[prefix];
} else {
return;
}
var value = +matches[2];
if (matches[1] !== undefined) {
value = -value;
}
return {
factor: factor,
prefix: prefix,
unit: matches[4],
value: value,
};
};
// =================================================================
var scales = {
// https://en.wikipedia.org/wiki/Binary_prefix
binary: Scale.create(",Ki,Mi,Gi,Ti,Pi,Ei,Zi,Yi".split(","), 1024),
// https://en.wikipedia.org/wiki/Metric_prefix
//
// Not all prefixes are present, only those which are multiple of
// 1000, because humans usually prefer to see close numbers using
// the same unit to ease the comparison.
SI: Scale.create(
"q,r,y,z,a,f,p,n,µ,m,,k,M,G,T,P,E,Z,Y,R,Q".split(","),
1000,
-10
),
};
// Prefixes below 1 make no sense for bytes and m would clash with M.
var bytesPrefixes = assign({}, scales.binary._prefixes);
var siPrefixes = scales.SI._prefixes;
for (var siPrefix in siPrefixes) {
if (siPrefixes[siPrefix] >= 1) {
bytesPrefixes[siPrefix] = siPrefixes[siPrefix];
}
}
var bytesScale = new Scale(bytesPrefixes);
var defaults = {
// Decimal digits for formatting.
maxDecimals: 2,
// separator to use between value and units
separator: " ",
// Unit to use for formatting.
unit: "",
};
var rawDefaults = {
// How to round to maxDecimals: round, floor or ceil.
rounding: "round",
scale: "SI",
// Strict mode prevents parsing of incorrectly cased prefixes.
strict: false,
};
function humanFormat(value, opts) {
var decimals = getOption(opts, defaults, "decimals");
// round at the displayed precision so that the prefix is changed if needed
var info = rawFormat(
value,
opts,
decimals !== undefined
? decimals
: getOption(opts, defaults, "maxDecimals")
);
value =
decimals !== undefined
? info.value.toFixed(decimals)
: String(info.value);
var suffix = info.prefix + getOption(opts, defaults, "unit");
return suffix === ""
? value
: value + getOption(opts, defaults, "separator") + suffix;
}
var humanFormat$bytes$opts = { scale: "binary", unit: "B" };
function humanFormat$bytes(value, opts) {
return humanFormat(
value,
opts === undefined
? humanFormat$bytes$opts
: assign({}, humanFormat$bytes$opts, opts)
);
}
function humanFormat$parse(str, opts) {
return parsedValue(humanFormat$parse$raw(str, opts));
}
var humanFormat$parse$bytes$opts = { scale: bytesScale, unit: "B" };
function humanFormat$parse$bytes(str, opts) {
return parsedValue(
rawParse(
str,
opts === undefined
? humanFormat$parse$bytes$opts
: assign({}, humanFormat$parse$bytes$opts, opts),
// b for B, as in 10mb, since bits are not supported
true
)
);
}
function parsedValue(info) {
// 8.3 * 1e6 is 8300000.000000001 but 83 * 1e6 / 10 is exact: when the
// digits times the factor is an exact integer, only the division rounds
var decimals = String(info.value).split(".")[1];
if (decimals !== undefined && decimals.indexOf("e") === -1) {
var power = Math.pow(10, decimals.length);
var scaled = Math.round(info.value * power) * info.factor;
if (scaled % 1 === 0 && Math.abs(scaled) <= MAX_SAFE_INTEGER) {
return scaled / power;
}
}
return info.value * info.factor;
}
function humanFormat$parse$raw(str, opts) {
return rawParse(str, opts, false);
}
function rawParse(str, opts, ignoreUnitCase) {
if (typeof str !== "string") {
throw new TypeError("str must be a string");
}
// Get current scale.
var scale = resolve(scales, getOption(opts, rawDefaults, "scale"));
if (scale === undefined) {
throw new Error("missing scale");
}
// TODO: if multiple units are specified, at least must match and
// the returned value should be: { value: <value>, unit: matchedUnit }
// The unit, if specified, is optional but nothing else is accepted. It is
// removed first, otherwise "1 m" with unit "m" would be parsed as milli.
var strict = getOption(opts, rawDefaults, "strict");
var unit = getOption(opts, rawDefaults, "unit");
if (unit !== undefined) {
str = str.trim();
var end = str.slice(-unit.length);
if (
unit !== "" &&
(end === unit ||
(ignoreUnitCase &&
!strict &&
end.toLowerCase() === unit.toLowerCase()))
) {
str = str.slice(0, -unit.length);
} else {
unit = "";
}
}
var info = scale.parse(str, strict);
if (info === undefined || (unit !== undefined && info.unit !== "")) {
throw new Error("cannot parse str");
}
if (unit !== undefined) {
info.unit = unit;
}
return info;
}
// toPrecision(15) removes floating point errors (4.35 * 100 is
// 434.99999999999994) which Math.round ignores but floor and ceil do not
var roundings = {
ceil: function (value) {
return Math.ceil(+value.toPrecision(15));
},
floor: function (value) {
return Math.floor(+value.toPrecision(15));
},
round: Math.round,
};
var oppositeRoundings = { ceil: "floor", floor: "ceil" };
// 1 decimal below 10, none above, decided before rounding so that it
// happens only once
function autoPower(value, factor) {
return value < 10 * factor ? 10 : 1;
}
function humanFormat$raw(value, opts) {
return rawFormat(value, opts, getOption(opts, rawDefaults, "maxDecimals"));
}
// maxDecimals is a parameter because humanFormat() has its own default and
// overrides it with decimals.
function rawFormat(value, opts, maxDecimals) {
// value !== value is the ES5 equivalent of Number.isNaN(value)
if (typeof value !== "number" || value !== value) {
throw new TypeError("value must be a number");
}
// Zero is a special case, it never has any prefix.
if (value === 0) {
return {
value: 0,
prefix: "",
};
}
// Neither do infinities, no prefix is large enough.
if (!isFinite(value)) {
return {
value: value,
prefix: "",
};
}
var negative = value < 0;
if (negative) {
value = -value;
}
// Get current scale.
var scale = resolve(scales, getOption(opts, rawDefaults, "scale"));
if (scale === undefined) {
throw new Error("missing scale");
}
var rounding = getOption(opts, rawDefaults, "rounding");
if (!has(roundings, rounding)) {
throw new Error("invalid rounding");
}
// floor and ceil apply to the signed value
if (negative && has(oppositeRoundings, rounding)) {
rounding = oppositeRoundings[rounding];
}
var round = roundings[rounding];
var power;
var autoMaxDecimals = maxDecimals === "auto";
if (maxDecimals !== undefined && !autoMaxDecimals) {
power = Math.pow(10, maxDecimals);
}
var prefix = getOption(opts, rawDefaults, "prefix");
var factor;
if (prefix !== undefined) {
if (!has(scale._prefixes, prefix)) {
throw new Error("invalid prefix");
}
factor = scale._prefixes[prefix];
} else {
var _ref = scale.findPrefix(value);
if (maxDecimals !== undefined) {
do {
factor = _ref.factor;
if (autoMaxDecimals) {
power = autoPower(value, factor);
}
// factor is usually >> power, therefore it's better to
// divide factor by power than the other way to limit
// numerical error
var r = factor / power;
value = round(value / r) * r;
} while ((_ref = scale.findPrefix(value)).factor !== factor);
} else {
factor = _ref.factor;
}
prefix = _ref.prefix;
}
if (autoMaxDecimals) {
power = autoPower(value, factor);
}
value =
power === undefined
? value / factor
: round((value * power) / factor) / power;
return {
prefix: prefix,
value: negative ? -value : value,
};
}
humanFormat.bytes = humanFormat$bytes;
humanFormat.parse = humanFormat$parse;
humanFormat$parse.bytes = humanFormat$parse$bytes;
humanFormat$parse.raw = humanFormat$parse$raw;
humanFormat.raw = humanFormat$raw;
humanFormat.Scale = Scale;
return humanFormat;
});