human-format
Version:
Converts a number to/from a human readable string: `1337` ↔ `1.34kB`
361 lines (299 loc) • 9.03 kB
JavaScript
// UMD: https://github.com/umdjs/umd/blob/master/returnExports.js
(function (root, factory) {
/* global define: false */
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();
}
})(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)) {
dst[prop] = src[prop];
}
}
}
}
return dst;
}
function compareLongestFirst(a, b) {
return b.length - a.length;
}
function compareSmallestFactorFirst(a, b) {
return a.factor - b.factor;
}
// https://www.npmjs.org/package/escape-regexp
function escapeRegexp(str) {
return str.replace(/([.*+?=^!:${}()|[\]/\\])/g, "\\$1");
}
function forEach(arr, iterator) {
var i, n;
for (i = 0, n = arr.length; i < n; ++i) {
iterator(arr[i], i);
}
}
function forOwn(obj, iterator) {
var prop;
for (prop in obj) {
if (has(obj, prop)) {
iterator(obj[prop], prop);
}
}
}
var has = (function (hasOwnProperty) {
return function has(obj, prop) {
return obj != null && hasOwnProperty.call(obj, prop);
};
})(Object.prototype.hasOwnProperty);
function resolve(container, entry) {
while (typeof entry === "string") {
entry = container[entry];
}
return entry;
}
// =================================================================
function Scale(prefixes) {
this._prefixes = prefixes;
var escapedPrefixes = [];
var list = [];
forOwn(prefixes, function (factor, prefix) {
escapedPrefixes.push(escapeRegexp(prefix));
list.push({
factor: factor,
prefix: prefix,
});
});
// Adds lower cased prefixes for case insensitive fallback.
var lcPrefixes = (this._lcPrefixes = {});
forOwn(prefixes, function (factor, prefix) {
var lcPrefix = prefix.toLowerCase();
if (!has(prefixes, lcPrefix)) {
lcPrefixes[lcPrefix] = prefix;
}
});
list.sort(compareSmallestFactorFirst);
this._list = list;
escapedPrefixes.sort(compareLongestFirst);
this._regexp = new RegExp(
"^\\s*(-)?\\s*(\\d+(?:\\.\\d+)?)\\s*(" +
escapedPrefixes.join("|") +
")\\s*(.*)\\s*?$",
"i"
);
}
Scale.create = function Scale$create(prefixesList, base, initExp) {
var prefixes = {};
if (initExp === undefined) {
initExp = 0;
}
forEach(prefixesList, function (prefix, i) {
prefixes[prefix] = 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("y,z,a,f,p,n,µ,m,,k,M,G,T,P,E,Z,Y".split(","), 1000, -8),
};
var defaults = {
// Decimal digits for formatting.
maxDecimals: 2,
// separator to use between value and units
separator: " ",
// Unit to use for formatting.
unit: "",
};
var rawDefaults = {
scale: "SI",
// Strict mode prevents parsing of incorrectly cased prefixes.
strict: false,
};
function humanFormat(value, opts) {
opts = assign({}, defaults, opts);
var decimals = opts.decimals;
if (decimals !== undefined) {
// humanFormat$raw should not round when using decimals option
delete opts.maxDecimals;
}
var info = humanFormat$raw(value, opts);
value =
decimals !== undefined
? info.value.toFixed(decimals)
: String(info.value);
var suffix = info.prefix + opts.unit;
return suffix === "" ? value : value + opts.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) {
var info = humanFormat$parse$raw(str, opts);
return info.value * info.factor;
}
function humanFormat$parse$raw(str, opts) {
if (typeof str !== "string") {
throw new TypeError("str must be a string");
}
// Merge default options.
opts = assign({}, rawDefaults, opts);
// Get current scale.
var scale = resolve(scales, opts.scale);
if (scale === undefined) {
throw new Error("missing scale");
}
// TODO: the unit should be checked: it might be absent but it
// should not differ from the one expected.
//
// TODO: if multiple units are specified, at least must match and
// the returned value should be: { value: <value>, unit: matchedUnit }
var info = scale.parse(str, opts.strict);
if (info === undefined) {
throw new Error("cannot parse str");
}
return info;
}
function humanFormat$raw(value, opts) {
// Zero is a special case, it never has any prefix.
if (value === 0) {
return {
value: 0,
prefix: "",
};
} else if (value < 0) {
var result = humanFormat$raw(-value, opts);
result.value = -result.value;
return result;
}
if (typeof value !== "number" || Number.isNaN(value)) {
throw new TypeError("value must be a number");
}
// Merge default options.
opts = assign({}, rawDefaults, opts);
// Get current scale.
var scale = resolve(scales, opts.scale);
if (scale === undefined) {
throw new Error("missing scale");
}
var power;
var maxDecimals = opts.maxDecimals;
var autoMaxDecimals = maxDecimals === "auto";
if (autoMaxDecimals) {
power = 10;
} else if (maxDecimals !== undefined) {
power = Math.pow(10, maxDecimals);
}
var prefix = opts.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 (power !== undefined) {
do {
factor = _ref.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 = Math.round(value / r) * r;
} while ((_ref = scale.findPrefix(value)).factor !== factor);
} else {
factor = _ref.factor;
}
prefix = _ref.prefix;
}
value =
power === undefined
? value / factor
: Math.round((value * power) / factor) / power;
if (autoMaxDecimals && Math.abs(value) >= 10) {
value = Math.round(value);
}
return {
prefix: prefix,
value: value,
};
}
humanFormat.bytes = humanFormat$bytes;
humanFormat.parse = humanFormat$parse;
humanFormat$parse.raw = humanFormat$parse$raw;
humanFormat.raw = humanFormat$raw;
humanFormat.Scale = Scale;
return humanFormat;
});