/**
* Copyright 2013-2016 HubSpotDev
* MIT Licensed
*
* @module humanize.js
*/
((root, factory) => {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([], () => (root.Humanize = factory()));
} else {
root.Humanize = factory();
}
})(this, () => {
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const TIME_FORMATS = [
{
name: 'second',
value: 1e3
},
{
name: 'minute',
value: 6e4
},
{
name: 'hour',
value: 36e5
},
{
name: 'day',
value: 864e5
},
{
name: 'week',
value: 6048e5
}
];
const LABELS_FOR_POWERS_OF_KILO = {
P: Math.pow(2, 50),
T: Math.pow(2, 40),
G: Math.pow(2, 30),
M: Math.pow(2, 20)
};
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const exists = maybe => typeof maybe !== 'undefined' && maybe !== null;
const isNaN = value => value !== value; // eslint-disable-line
const isFiniteNumber = value => {
return isFinite(value) && !isNaN(parseFloat(value));
};
const isArray = value => {
const type = Object.prototype.toString.call(value);
return type === '[object Array]';
};
//------------------------------------------------------------------------------
// Humanize
//------------------------------------------------------------------------------
const Humanize = {
// Converts a large integer to a friendly text representation.
intword(number, charWidth, decimals = 2) {
/*
* This method is deprecated. Please use compactInteger instead.
* intword will be going away in the next major version.
*/
return Humanize.compactInteger(number, decimals);
},
// Converts an integer into its most compact representation
compactInteger(input, decimals = 0) {
decimals = Math.max(decimals, 0);
const number = parseInt(input, 10);
const signString = number < 0 ? '-' : '';
const unsignedNumber = Math.abs(number);
const unsignedNumberString = String(unsignedNumber);
const numberLength = unsignedNumberString.length;
const numberLengths = [13, 10, 7, 4];
const bigNumPrefixes = ['T', 'B', 'M', 'k'];
// small numbers
if (unsignedNumber < 1000) {
return `${ signString }${ unsignedNumberString }`;
}
// really big numbers
if (numberLength > numberLengths[0] + 3) {
return number.toExponential(decimals).replace('e+', 'x10^');
}
// 999 < unsignedNumber < 999,999,999,999,999
let length;
for (let i = 0; i < numberLengths.length; i++) {
const _length = numberLengths[i];
if (numberLength >= _length) {
length = _length;
break;
}
}
const decimalIndex = numberLength - length + 1;
const unsignedNumberCharacterArray = unsignedNumberString.split('');
const wholePartArray = unsignedNumberCharacterArray.slice(0, decimalIndex);
const decimalPartArray = unsignedNumberCharacterArray.slice(decimalIndex, decimalIndex + decimals + 1);
const wholePart = wholePartArray.join('');
// pad decimalPart if necessary
let decimalPart = decimalPartArray.join('');
if (decimalPart.length < decimals) {
decimalPart += `${ Array(decimals - decimalPart.length + 1).join('0') }`;
}
let output;
if (decimals === 0) {
output = `${ signString }${ wholePart }${ bigNumPrefixes[numberLengths.indexOf(length)] }`;
} else {
const outputNumber = Number(`${ wholePart }.${ decimalPart }`).toFixed(decimals);
output = `${ signString }${ outputNumber }${ bigNumPrefixes[numberLengths.indexOf(length)] }`;
}
return output;
},
// Converts an integer to a string containing commas every three digits.
intComma(number, decimals = 0) {
return Humanize.formatNumber(number, decimals);
},
intcomma(...args) {
return Humanize.intComma(...args);
},
// Formats the value like a 'human-readable' file size (i.e. '13 KB', '4.1 MB', '102 bytes', etc).
fileSize(filesize, precision = 2) {
for (const label in LABELS_FOR_POWERS_OF_KILO) {
if (LABELS_FOR_POWERS_OF_KILO.hasOwnProperty(label)) {
const minnum = LABELS_FOR_POWERS_OF_KILO[label];
if (filesize >= minnum) {
return `${Humanize.formatNumber(filesize / minnum, precision, '')} ${label}B`;
}
}
}
if (filesize >= 1024) {
return `${Humanize.formatNumber(filesize / 1024, 0)} KB`;
}
return Humanize.formatNumber(filesize, 0) + Humanize.pluralize(filesize, ' byte');
},
filesize(...args) {
return Humanize.fileSize(...args);
},
// Formats a number to a human-readable string.
// Localize by overriding the precision, thousand and decimal arguments.
formatNumber(number, precision = 0, thousand = ',', decimal = '.') {
// Create some private utility functions to make the computational
// code that follows much easier to read.
const firstComma = (_number, _thousand, _position) => {
return _position ? _number.substr(0, _position) + _thousand : '';
};
const commas = (_number, _thousand, _position) => {
return _number.substr(_position).replace(/(\d{3})(?=\d)/g, `$1${_thousand}`);
};
const decimals = (_number, _decimal, usePrecision) => {
return usePrecision
? _decimal + Humanize.toFixed(Math.abs(_number), usePrecision).split('.')[1]
: '';
};
const usePrecision = Humanize.normalizePrecision(precision);
// Do some calc
const negative = number < 0 && '-' || '';
const base = String(parseInt(Humanize.toFixed(Math.abs(number || 0), usePrecision), 10));
const mod = base.length > 3 ? base.length % 3 : 0;
// Format the number
return (
negative +
firstComma(base, thousand, mod) +
commas(base, thousand, mod) +
decimals(number, decimal, usePrecision)
);
},
// Fixes binary rounding issues (eg. (0.615).toFixed(2) === '0.61')
toFixed(value, precision) {
precision = exists(precision) ? precision : Humanize.normalizePrecision(precision, 0);
const power = Math.pow(10, precision);
// Multiply up by precision, round accurately, then divide and use native toFixed()
return (Math.round(value * power) / power).toFixed(precision);
},
// Ensures precision value is a positive integer
normalizePrecision(value, base) {
value = Math.round(Math.abs(value));
return isNaN(value) ? base : value;
},
// Converts an integer to its ordinal as a string.
ordinal(value) {
const number = parseInt(value, 10);
if (number === 0) {
return value;
}
const specialCase = number % 100;
if ([11, 12, 13].indexOf(specialCase) >= 0) {
return `${ number }th`;
}
const leastSignificant = number % 10;
let end;
switch (leastSignificant) {
case 1:
end = 'st';
break;
case 2:
end = 'nd';
break;
case 3:
end = 'rd';
break;
default:
end = 'th';
}
return `${ number }${ end }`;
},
// Interprets numbers as occurences. Also accepts an optional array/map of overrides.
times(value, overrides = {}) {
if (isFiniteNumber(value) && value >= 0) {
const number = parseFloat(value);
const smallTimes = ['never', 'once', 'twice'];
if (exists(overrides[number])) {
return String(overrides[number]);
}
const numberString = exists(smallTimes[number]) && smallTimes[number].toString();
return numberString || `${number.toString()} times`;
}
return null;
},
// Returns the plural version of a given word if the value is not 1. The default suffix is 's'.
pluralize(number, singular, plural) {
if (!(exists(number) && exists(singular))) {
return null;
}
plural = exists(plural) ? plural : `${singular}s`;
return parseInt(number, 10) === 1 ? singular : plural;
},
// Truncates a string if it is longer than the specified number of characters (inclusive).
// Truncated strings will end with a translatable ellipsis sequence ("…").
truncate(str, length = 100, ending = '...') {
if (str.length > length) {
return str.substring(0, length - ending.length) + ending;
}
return str;
},
// Truncates a string after a certain number of words.
truncateWords(string, length) {
const array = string.split(' ');
let result = '';
let i = 0;
while (i < length) {
if (exists(array[i])) {
result += `${array[i]} `;
}
i++;
}
if (array.length > length) {
return `${result}...`;
}
return null;
},
truncatewords(...args) {
return Humanize.truncateWords(...args);
},
// Truncates a number to an upper bound.
boundedNumber(num, bound = 100, ending = '+') {
let result;
if (isFiniteNumber(num) && isFiniteNumber(bound)) {
if (num > bound) {
result = bound + ending;
}
}
return (result || num).toString();
},
truncatenumber(...args) {
return Humanize.boundedNumber(...args);
},
// Converts a list of items to a human readable string with an optional limit.
oxford(items, limit, limitStr) {
const numItems = items.length;
let limitIndex;
if (numItems < 2) {
return String(items);
} else if (numItems === 2) {
return items.join(' and ');
} else if (exists(limit) && numItems > limit) {
const extra = numItems - limit;
limitIndex = limit;
limitStr = exists(limitStr) ? limitStr : `, and ${extra} ${Humanize.pluralize(extra, 'other')}`;
} else {
limitIndex = -1;
limitStr = `, and ${items[numItems - 1]}`;
}
return items.slice(0, limitIndex).join(', ') + limitStr;
},
// Converts an object to a definition-like string
dictionary(object, joiner = ' is ', separator = ', ') {
const result = '';
if (exists(object) && typeof object === 'object' && !isArray(object)) {
const defs = [];
for (const key in object) {
if (object.hasOwnProperty(key)) {
const val = object[key];
defs.push(`${ key }${ joiner }${ val }`);
}
}
return defs.join(separator);
}
return result;
},
// Describes how many times an item appears in a list
frequency(list, verb) {
if (!isArray(list)) {
return null;
}
const len = list.length;
const times = Humanize.times(len);
if (len === 0) {
return `${times} ${verb}`;
}
return `${verb} ${times}`;
},
pace(value, intervalMs, unit = 'time') {
if (value === 0 || intervalMs === 0) {
// Needs a better string than this...
return `No ${Humanize.pluralize(0, unit)}`;
}
// Expose these as overridables?
let prefix = 'Approximately';
let timeUnit;
let relativePace;
const rate = value / intervalMs;
for (let i = 0; i < TIME_FORMATS.length; ++i) { // assumes sorted list
const f = TIME_FORMATS[i];
relativePace = rate * f.value;
if (relativePace > 1) {
timeUnit = f.name;
break;
}
}
// Use the last time unit if there is nothing smaller
if (!timeUnit) {
prefix = 'Less than';
relativePace = 1;
timeUnit = TIME_FORMATS[TIME_FORMATS.length - 1].name;
}
const roundedPace = Math.round(relativePace);
unit = Humanize.pluralize(roundedPace, unit);
return `${prefix} ${roundedPace} ${unit} per ${timeUnit}`;
},
// Converts newlines to
tags
nl2br(string, replacement = '
') {
return string.replace(/\n/g, replacement);
},
// Converts
tags to newlines
br2nl(string, replacement = '\r\n') {
return string.replace(/\
/g, replacement);
},
// Capitalizes first letter in a string
capitalize(string, downCaseTail = false) {
return `${ string.charAt(0).toUpperCase() }${ downCaseTail ? string.slice(1).toLowerCase() : string.slice(1) }`;
},
// Capitalizes the first letter of each word in a string
capitalizeAll(string) {
return string.replace(/(?:^|\s)\S/g, (a) => a.toUpperCase());
},
// Titlecase words in a string.
titleCase(string) {
const smallWords = /\b(a|an|and|at|but|by|de|en|for|if|in|of|on|or|the|to|via|vs?\.?)\b/i;
const internalCaps = /\S+[A-Z]+\S*/;
const splitOnWhiteSpaceRegex = /\s+/;
const splitOnHyphensRegex = /-/;
let doTitleCase;
doTitleCase = (_string, hyphenated = false, firstOrLast = true) => {
const titleCasedArray = [];
const stringArray = _string.split(hyphenated ? splitOnHyphensRegex : splitOnWhiteSpaceRegex);
for (let index = 0; index < stringArray.length; ++index) {
const word = stringArray[index];
if (word.indexOf('-') !== -1) {
titleCasedArray.push(doTitleCase(word, true, (index === 0 || index === stringArray.length - 1)));
continue;
}
if (firstOrLast && (index === 0 || index === stringArray.length - 1)) {
titleCasedArray.push(internalCaps.test(word) ? word : Humanize.capitalize(word));
continue;
}
if (internalCaps.test(word)) {
titleCasedArray.push(word);
} else if (smallWords.test(word)) {
titleCasedArray.push(word.toLowerCase());
} else {
titleCasedArray.push(Humanize.capitalize(word));
}
}
return titleCasedArray.join(hyphenated ? '-' : ' ');
};
return doTitleCase(string);
},
titlecase(...args) {
return Humanize.titleCase(...args);
}
};
return Humanize;
});