/*! Moment Duration Format v2.2.2 * https://github.com/jsmreese/moment-duration-format * Date: 2018-02-16 * * Duration format plugin function for the Moment.js library * http://momentjs.com/ * * Copyright 2018 John Madhavan-Reese * Released under the MIT license */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['moment'], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but only CommonJS-like // enviroments that support module.exports, like Node. try { module.exports = factory(require('moment')); } catch (e) { // If moment is not available, leave the setup up to the user. // Like when using moment-timezone or similar moment-based package. module.exports = factory; } } if (root) { // Globals. root.momentDurationFormatSetup = root.moment ? factory(root.moment) : factory; } })(this, function (moment) { // `Number#tolocaleString` is tested on plugin initialization. // If the feature test passes, `toLocaleStringWorks` will be set to `true` and the // native function will be used to generate formatted output. If the feature // test fails, the fallback format function internal to this plugin will be // used. var toLocaleStringWorks = false; // `Number#toLocaleString` rounds incorrectly for select numbers in Microsoft // environments (Edge, IE11, Windows Phone) and possibly other environments. // If the rounding test fails and `toLocaleString` will be used for formatting, // the plugin will "pre-round" number values using the fallback number format // function before passing them to `toLocaleString` for final formatting. var toLocaleStringRoundingWorks = false; // `Intl.NumberFormat#format` is tested on plugin initialization. // If the feature test passes, `intlNumberFormatRoundingWorks` will be set to // `true` and the native function will be used to generate formatted output. // If the feature test fails, either `Number#tolocaleString` (if // `toLocaleStringWorks` is `true`), or the fallback format function internal // to this plugin will be used. var intlNumberFormatWorks = false; // `Intl.NumberFormat#format` rounds incorrectly for select numbers in Microsoft // environments (Edge, IE11, Windows Phone) and possibly other environments. // If the rounding test fails and `Intl.NumberFormat#format` will be used for // formatting, the plugin will "pre-round" number values using the fallback number // format function before passing them to `Intl.NumberFormat#format` for final // formatting. var intlNumberFormatRoundingWorks = false; // Token type names in order of descending magnitude. var types = "escape years months weeks days hours minutes seconds milliseconds general".split(" "); var bubbles = [ { type: "seconds", targets: [ { type: "minutes", value: 60 }, { type: "hours", value: 3600 }, { type: "days", value: 86400 }, { type: "weeks", value: 604800 }, { type: "months", value: 2678400 }, { type: "years", value: 31536000 } ] }, { type: "minutes", targets: [ { type: "hours", value: 60 }, { type: "days", value: 1440 }, { type: "weeks", value: 10080 }, { type: "months", value: 44640 }, { type: "years", value: 525600 } ] }, { type: "hours", targets: [ { type: "days", value: 24 }, { type: "weeks", value: 168 }, { type: "months", value: 744 }, { type: "years", value: 8760 } ] }, { type: "days", targets: [ { type: "weeks", value: 7 }, { type: "months", value: 31 }, { type: "years", value: 365 } ] }, { type: "months", targets: [ { type: "years", value: 12 } ] } ]; // stringIncludes function stringIncludes(str, search) { if (search.length > str.length) { return false; } return str.indexOf(search) !== -1; } // repeatZero(qty) // Returns "0" repeated `qty` times. // `qty` must be a integer >= 0. function repeatZero(qty) { var result = ""; while (qty) { result += "0"; qty -= 1; } return result; } function stringRound(digits) { var digitsArray = digits.split("").reverse(); var i = 0; var carry = true; while (carry && i < digitsArray.length) { if (i) { if (digitsArray[i] === "9") { digitsArray[i] = "0"; } else { digitsArray[i] = (parseInt(digitsArray[i], 10) + 1).toString(); carry = false; } } else { if (parseInt(digitsArray[i], 10) < 5) { carry = false; } digitsArray[i] = "0"; } i += 1; } if (carry) { digitsArray.push("1"); } return digitsArray.reverse().join(""); } // cachedNumberFormat // Returns an `Intl.NumberFormat` instance for the given locale and configuration. // On first use of a particular configuration, the instance is cached for fast // repeat access. function cachedNumberFormat(locale, options) { // Create a sorted, stringified version of `options` // for use as part of the cache key var optionsString = map( keys(options).sort(), function(key) { return key + ':' + options[key]; } ).join(','); // Set our cache key var cacheKey = locale + '+' + optionsString; // If we don't have this configuration cached, configure and cache it if (!cachedNumberFormat.cache[cacheKey]) { cachedNumberFormat.cache[cacheKey] = Intl.NumberFormat(locale, options); } // Return the cached version of this configuration return cachedNumberFormat.cache[cacheKey]; } cachedNumberFormat.cache = {}; // formatNumber // Formats any number greater than or equal to zero using these options: // - userLocale // - useToLocaleString // - useGrouping // - grouping // - maximumSignificantDigits // - minimumIntegerDigits // - fractionDigits // - groupingSeparator // - decimalSeparator // // `useToLocaleString` will use `Intl.NumberFormat` or `toLocaleString` for formatting. // `userLocale` option is passed through to the formatting function. // `fractionDigits` is passed through to `maximumFractionDigits` and `minimumFractionDigits` // Using `maximumSignificantDigits` will override `minimumIntegerDigits` and `fractionDigits`. function formatNumber(number, options, userLocale) { var useToLocaleString = options.useToLocaleString; var useGrouping = options.useGrouping; var grouping = useGrouping && options.grouping.slice(); var maximumSignificantDigits = options.maximumSignificantDigits; var minimumIntegerDigits = options.minimumIntegerDigits || 1; var fractionDigits = options.fractionDigits || 0; var groupingSeparator = options.groupingSeparator; var decimalSeparator = options.decimalSeparator; if (useToLocaleString && userLocale) { var localeStringOptions = { minimumIntegerDigits: minimumIntegerDigits, useGrouping: useGrouping }; if (fractionDigits) { localeStringOptions.maximumFractionDigits = fractionDigits; localeStringOptions.minimumFractionDigits = fractionDigits; } // toLocaleString output is "0.0" instead of "0" for HTC browsers // when maximumSignificantDigits is set. See #96. if (maximumSignificantDigits && number > 0) { localeStringOptions.maximumSignificantDigits = maximumSignificantDigits; } if (intlNumberFormatWorks) { if (!intlNumberFormatRoundingWorks) { var roundingOptions = extend({}, options); roundingOptions.useGrouping = false; roundingOptions.decimalSeparator = "."; number = parseFloat(formatNumber(number, roundingOptions), 10); } return cachedNumberFormat(userLocale, localeStringOptions).format(number); } else { if (!toLocaleStringRoundingWorks) { var roundingOptions = extend({}, options); roundingOptions.useGrouping = false; roundingOptions.decimalSeparator = "."; number = parseFloat(formatNumber(number, roundingOptions), 10); } return number.toLocaleString(userLocale, localeStringOptions); } } var numberString; // Add 1 to digit output length for floating point errors workaround. See below. if (maximumSignificantDigits) { numberString = number.toPrecision(maximumSignificantDigits + 1); } else { numberString = number.toFixed(fractionDigits + 1); } var integerString; var fractionString; var exponentString; var temp = numberString.split("e"); exponentString = temp[1] || ""; temp = temp[0].split("."); fractionString = temp[1] || ""; integerString = temp[0] || ""; // Workaround for floating point errors in `toFixed` and `toPrecision`. // (3.55).toFixed(1); --> "3.5" // (123.55 - 120).toPrecision(2); --> "3.5" // (123.55 - 120); --> 3.549999999999997 // (123.55 - 120).toFixed(2); --> "3.55" // Round by examing the string output of the next digit. // *************** Implement String Rounding here *********************** // Check integerString + fractionString length of toPrecision before rounding. // Check length of fractionString from toFixed output before rounding. var integerLength = integerString.length; var fractionLength = fractionString.length; var digitCount = integerLength + fractionLength; var digits = integerString + fractionString; if (maximumSignificantDigits && digitCount === (maximumSignificantDigits + 1) || !maximumSignificantDigits && fractionLength === (fractionDigits + 1)) { // Round digits. digits = stringRound(digits); if (digits.length === digitCount + 1) { integerLength = integerLength + 1; } // Discard final fractionDigit. if (fractionLength) { digits = digits.slice(0, -1); } // Separate integer and fraction. integerString = digits.slice(0, integerLength); fractionString = digits.slice(integerLength); } // Trim trailing zeroes from fractionString because toPrecision outputs // precision, not significant digits. if (maximumSignificantDigits) { fractionString = fractionString.replace(/0*$/, ""); } // Handle exponent. var exponent = parseInt(exponentString, 10); if (exponent > 0) { if (fractionString.length <= exponent) { fractionString = fractionString + repeatZero(exponent - fractionString.length); integerString = integerString + fractionString; fractionString = ""; } else { integerString = integerString + fractionString.slice(0, exponent); fractionString = fractionString.slice(exponent); } } else if (exponent < 0) { fractionString = (repeatZero(Math.abs(exponent) - integerString.length) + integerString + fractionString); integerString = "0"; } if (!maximumSignificantDigits) { // Trim or pad fraction when not using maximumSignificantDigits. fractionString = fractionString.slice(0, fractionDigits); if (fractionString.length < fractionDigits) { fractionString = fractionString + repeatZero(fractionDigits - fractionString.length); } // Pad integer when using minimumIntegerDigits // and not using maximumSignificantDigits. if (integerString.length < minimumIntegerDigits) { integerString = repeatZero(minimumIntegerDigits - integerString.length) + integerString; } } var formattedString = ""; // Handle grouping. if (useGrouping) { temp = integerString; var group; while (temp.length) { if (grouping.length) { group = grouping.shift(); } if (formattedString) { formattedString = groupingSeparator + formattedString; } formattedString = temp.slice(-group) + formattedString; temp = temp.slice(0, -group); } } else { formattedString = integerString; } // Add decimalSeparator and fraction. if (fractionString) { formattedString = formattedString + decimalSeparator + fractionString; } return formattedString; } // durationLabelCompare function durationLabelCompare(a, b) { if (a.label.length > b.label.length) { return -1; } if (a.label.length < b.label.length) { return 1; } // a must be equal to b return 0; } // durationGetLabels function durationGetLabels(token, localeData) { var labels = []; each(keys(localeData), function (localeDataKey) { if (localeDataKey.slice(0, 15) !== "_durationLabels") { return; } var labelType = localeDataKey.slice(15).toLowerCase(); each(keys(localeData[localeDataKey]), function (labelKey) { if (labelKey.slice(0, 1) === token) { labels.push({ type: labelType, key: labelKey, label: localeData[localeDataKey][labelKey] }); } }); }); return labels; } // durationPluralKey function durationPluralKey(token, integerValue, decimalValue) { // Singular for a value of `1`, but not for `1.0`. if (integerValue === 1 && decimalValue === null) { return token; } return token + token; } var engLocale = { durationLabelsStandard: { S: 'millisecond', SS: 'milliseconds', s: 'second', ss: 'seconds', m: 'minute', mm: 'minutes', h: 'hour', hh: 'hours', d: 'day', dd: 'days', w: 'week', ww: 'weeks', M: 'month', MM: 'months', y: 'year', yy: 'years' }, durationLabelsShort: { S: 'msec', SS: 'msecs', s: 'sec', ss: 'secs', m: 'min', mm: 'mins', h: 'hr', hh: 'hrs', d: 'dy', dd: 'dys', w: 'wk', ww: 'wks', M: 'mo', MM: 'mos', y: 'yr', yy: 'yrs' }, durationTimeTemplates: { HMS: 'h:mm:ss', HM: 'h:mm', MS: 'm:ss' }, durationLabelTypes: [ { type: "standard", string: "__" }, { type: "short", string: "_" } ], durationPluralKey: durationPluralKey }; // isArray function isArray(array) { return Object.prototype.toString.call(array) === "[object Array]"; } // isObject function isObject(obj) { return Object.prototype.toString.call(obj) === "[object Object]"; } // findLast function findLast(array, callback) { var index = array.length; while (index -= 1) { if (callback(array[index])) { return array[index]; } } } // find function find(array, callback) { var index = 0; var max = array && array.length || 0; var match; if (typeof callback !== "function") { match = callback; callback = function (item) { return item === match; }; } while (index < max) { if (callback(array[index])) { return array[index]; } index += 1; } } // each function each(array, callback) { var index = 0, max = array.length; if (!array || !max) { return; } while (index < max) { if (callback(array[index], index) === false) { return; } index += 1; } } // map function map(array, callback) { var index = 0, max = array.length, ret = []; if (!array || !max) { return ret; } while (index < max) { ret[index] = callback(array[index], index); index += 1; } return ret; } // pluck function pluck(array, prop) { return map(array, function (item) { return item[prop]; }); } // compact function compact(array) { var ret = []; each(array, function (item) { if (item) { ret.push(item); } }); return ret; } // unique function unique(array) { var ret = []; each(array, function (_a) { if (!find(ret, _a)) { ret.push(_a); } }); return ret; } // intersection function intersection(a, b) { var ret = []; each(a, function (_a) { each(b, function (_b) { if (_a === _b) { ret.push(_a); } }); }); return unique(ret); } // rest function rest(array, callback) { var ret = []; each(array, function (item, index) { if (!callback(item)) { ret = array.slice(index); return false; } }); return ret; } // initial function initial(array, callback) { var reversed = array.slice().reverse(); return rest(reversed, callback).reverse(); } // extend function extend(a, b) { for (var key in b) { if (b.hasOwnProperty(key)) { a[key] = b[key]; } } return a; } // keys function keys(a) { var ret = []; for (var key in a) { if (a.hasOwnProperty(key)) { ret.push(key); } } return ret; } // any function any(array, callback) { var index = 0, max = array.length; if (!array || !max) { return false; } while (index < max) { if (callback(array[index], index) === true) { return true; } index += 1; } return false; } // flatten function flatten(array) { var ret = []; each(array, function(child) { ret = ret.concat(child); }); return ret; } function toLocaleStringSupportsLocales() { var number = 0; try { number.toLocaleString('i'); } catch (e) { return e.name === 'RangeError'; } return false; } function featureTestFormatterRounding(formatter) { return formatter(3.55, "en", { useGrouping: false, minimumIntegerDigits: 1, minimumFractionDigits: 1, maximumFractionDigits: 1 }) === "3.6"; } function featureTestFormatter(formatter) { var passed = true; // Test minimumIntegerDigits. passed = passed && formatter(1, "en", { minimumIntegerDigits: 1 }) === "1"; passed = passed && formatter(1, "en", { minimumIntegerDigits: 2 }) === "01"; passed = passed && formatter(1, "en", { minimumIntegerDigits: 3 }) === "001"; if (!passed) { return false; } // Test maximumFractionDigits and minimumFractionDigits. passed = passed && formatter(99.99, "en", { maximumFractionDigits: 0, minimumFractionDigits: 0 }) === "100"; passed = passed && formatter(99.99, "en", { maximumFractionDigits: 1, minimumFractionDigits: 1 }) === "100.0"; passed = passed && formatter(99.99, "en", { maximumFractionDigits: 2, minimumFractionDigits: 2 }) === "99.99"; passed = passed && formatter(99.99, "en", { maximumFractionDigits: 3, minimumFractionDigits: 3 }) === "99.990"; if (!passed) { return false; } // Test maximumSignificantDigits. passed = passed && formatter(99.99, "en", { maximumSignificantDigits: 1 }) === "100"; passed = passed && formatter(99.99, "en", { maximumSignificantDigits: 2 }) === "100"; passed = passed && formatter(99.99, "en", { maximumSignificantDigits: 3 }) === "100"; passed = passed && formatter(99.99, "en", { maximumSignificantDigits: 4 }) === "99.99"; passed = passed && formatter(99.99, "en", { maximumSignificantDigits: 5 }) === "99.99"; if (!passed) { return false; } // Test grouping. passed = passed && formatter(1000, "en", { useGrouping: true }) === "1,000"; passed = passed && formatter(1000, "en", { useGrouping: false }) === "1000"; if (!passed) { return false; } return true; } // durationsFormat(durations [, template] [, precision] [, settings]) function durationsFormat() { var args = [].slice.call(arguments); var settings = {}; var durations; // Parse arguments. each(args, function (arg, index) { if (!index) { if (!isArray(arg)) { throw "Expected array as the first argument to durationsFormat."; } durations = arg; } if (typeof arg === "string" || typeof arg === "function") { settings.template = arg; return; } if (typeof arg === "number") { settings.precision = arg; return; } if (isObject(arg)) { extend(settings, arg); } }); if (!durations || !durations.length) { return []; } settings.returnMomentTypes = true; var formattedDurations = map(durations, function (dur) { return dur.format(settings); }); // Merge token types from all durations. var outputTypes = intersection(types, unique(pluck(flatten(formattedDurations), "type"))); var largest = settings.largest; if (largest) { outputTypes = outputTypes.slice(0, largest); } settings.returnMomentTypes = false; settings.outputTypes = outputTypes; return map(durations, function (dur) { return dur.format(settings); }); } // durationFormat([template] [, precision] [, settings]) function durationFormat() { var args = [].slice.call(arguments); var settings = extend({}, this.format.defaults); // Keep a shadow copy of this moment for calculating remainders. // Perform all calculations on positive duration value, handle negative // sign at the very end. var asMilliseconds = this.asMilliseconds(); var asMonths = this.asMonths(); // Treat invalid durations as having a value of 0 milliseconds. if (typeof this.isValid === "function" && this.isValid() === false) { asMilliseconds = 0; asMonths = 0; } var isNegative = asMilliseconds < 0; // Two shadow copies are needed because of the way moment.js handles // duration arithmetic for years/months and for weeks/days/hours/minutes/seconds. var remainder = moment.duration(Math.abs(asMilliseconds), "milliseconds"); var remainderMonths = moment.duration(Math.abs(asMonths), "months"); // Parse arguments. each(args, function (arg) { if (typeof arg === "string" || typeof arg === "function") { settings.template = arg; return; } if (typeof arg === "number") { settings.precision = arg; return; } if (isObject(arg)) { extend(settings, arg); } }); var momentTokens = { years: "y", months: "M", weeks: "w", days: "d", hours: "h", minutes: "m", seconds: "s", milliseconds: "S" }; var tokenDefs = { escape: /\[(.+?)\]/, years: /\*?[Yy]+/, months: /\*?M+/, weeks: /\*?[Ww]+/, days: /\*?[Dd]+/, hours: /\*?[Hh]+/, minutes: /\*?m+/, seconds: /\*?s+/, milliseconds: /\*?S+/, general: /.+?/ }; // Types array is available in the template function. settings.types = types; var typeMap = function (token) { return find(types, function (type) { return tokenDefs[type].test(token); }); }; var tokenizer = new RegExp(map(types, function (type) { return tokenDefs[type].source; }).join("|"), "g"); // Current duration object is available in the template function. settings.duration = this; // Eval template function and cache template string. var template = typeof settings.template === "function" ? settings.template.apply(settings) : settings.template; // outputTypes and returnMomentTypes are settings to support durationsFormat(). // outputTypes is an array of moment token types that determines // the tokens returned in formatted output. This option overrides // trim, largest, stopTrim, etc. var outputTypes = settings.outputTypes; // returnMomentTypes is a boolean that sets durationFormat to return // the processed momentTypes instead of formatted output. var returnMomentTypes = settings.returnMomentTypes; var largest = settings.largest; // Setup stopTrim array of token types. var stopTrim = []; if (!outputTypes) { if (isArray(settings.stopTrim)) { settings.stopTrim = settings.stopTrim.join(""); } // Parse stopTrim string to create token types array. if (settings.stopTrim) { each(settings.stopTrim.match(tokenizer), function (token) { var type = typeMap(token); if (type === "escape" || type === "general") { return; } stopTrim.push(type); }); } } // Cache moment's locale data. var localeData = moment.localeData(); if (!localeData) { localeData = {}; } // Fall back to this plugin's `eng` extension. each(keys(engLocale), function (key) { if (typeof engLocale[key] === "function") { if (!localeData[key]) { localeData[key] = engLocale[key]; } return; } if (!localeData["_" + key]) { localeData["_" + key] = engLocale[key]; } }); // Replace Duration Time Template strings. // For locale `eng`: `_HMS_`, `_HM_`, and `_MS_`. each(keys(localeData._durationTimeTemplates), function (item) { template = template.replace("_" + item + "_", localeData._durationTimeTemplates[item]); }); // Determine user's locale. var userLocale = settings.userLocale || moment.locale(); var useLeftUnits = settings.useLeftUnits; var usePlural = settings.usePlural; var precision = settings.precision; var forceLength = settings.forceLength; var useGrouping = settings.useGrouping; var trunc = settings.trunc; // Use significant digits only when precision is greater than 0. var useSignificantDigits = settings.useSignificantDigits && precision > 0; var significantDigits = useSignificantDigits ? settings.precision : 0; var significantDigitsCache = significantDigits; var minValue = settings.minValue; var isMinValue = false; var maxValue = settings.maxValue; var isMaxValue = false; // formatNumber fallback options. var useToLocaleString = settings.useToLocaleString; var groupingSeparator = settings.groupingSeparator; var decimalSeparator = settings.decimalSeparator; var grouping = settings.grouping; useToLocaleString = useToLocaleString && (toLocaleStringWorks || intlNumberFormatWorks); // Trim options. var trim = settings.trim; if (isArray(trim)) { trim = trim.join(" "); } if (trim === null && (largest || maxValue || useSignificantDigits)) { trim = "all"; } if (trim === null || trim === true || trim === "left" || trim === "right") { trim = "large"; } if (trim === false) { trim = ""; } var trimIncludes = function (item) { return item.test(trim); }; var rLarge = /large/; var rSmall = /small/; var rBoth = /both/; var rMid = /mid/; var rAll = /^all|[^sm]all/; var rFinal = /final/; var trimLarge = largest > 0 || any([rLarge, rBoth, rAll], trimIncludes); var trimSmall = any([rSmall, rBoth, rAll], trimIncludes); var trimMid = any([rMid, rAll], trimIncludes); var trimFinal = any([rFinal, rAll], trimIncludes); // Parse format string to create raw tokens array. var rawTokens = map(template.match(tokenizer), function (token, index) { var type = typeMap(token); if (token.slice(0, 1) === "*") { token = token.slice(1); if (type !== "escape" && type !== "general") { stopTrim.push(type); } } return { index: index, length: token.length, text: "", // Replace escaped tokens with the non-escaped token text. token: (type === "escape" ? token.replace(tokenDefs.escape, "$1") : token), // Ignore type on non-moment tokens. type: ((type === "escape" || type === "general") ? null : type) }; }); // Associate text tokens with moment tokens. var currentToken = { index: 0, length: 0, token: "", text: "", type: null }; var tokens = []; if (useLeftUnits) { rawTokens.reverse(); } each(rawTokens, function (token) { if (token.type) { if (currentToken.type || currentToken.text) { tokens.push(currentToken); } currentToken = token; return; } if (useLeftUnits) { currentToken.text = token.token + currentToken.text; } else { currentToken.text += token.token; } }); if (currentToken.type || currentToken.text) { tokens.push(currentToken); } if (useLeftUnits) { tokens.reverse(); } // Find unique moment token types in the template in order of // descending magnitude. var momentTypes = intersection(types, unique(compact(pluck(tokens, "type")))); // Exit early if there are no moment token types. if (!momentTypes.length) { return pluck(tokens, "text").join(""); } // Calculate values for each moment type in the template. // For processing the settings, values are associated with moment types. // Values will be assigned to tokens at the last step in order to // assume nothing about frequency or order of tokens in the template. momentTypes = map(momentTypes, function (momentType, index) { // Is this the least-magnitude moment token found? var isSmallest = ((index + 1) === momentTypes.length); // Is this the greatest-magnitude moment token found? var isLargest = (!index); // Get the raw value in the current units. var rawValue; if (momentType === "years" || momentType === "months") { rawValue = remainderMonths.as(momentType); } else { rawValue = remainder.as(momentType); } var wholeValue = Math.floor(rawValue); var decimalValue = rawValue - wholeValue; var token = find(tokens, function (token) { return momentType === token.type; }); if (isLargest && maxValue && rawValue > maxValue) { isMaxValue = true; } if (isSmallest && minValue && Math.abs(settings.duration.as(momentType)) < minValue) { isMinValue = true; } // Note the length of the largest-magnitude moment token: // if it is greater than one and forceLength is not set, // then default forceLength to `true`. // // Rationale is this: If the template is "h:mm:ss" and the // moment value is 5 minutes, the user-friendly output is // "5:00", not "05:00". We shouldn't pad the `minutes` token // even though it has length of two if the template is "h:mm:ss"; // // If the minutes output should always include the leading zero // even when the hour is trimmed then set `{ forceLength: true }` // to output "05:00". If the template is "hh:mm:ss", the user // clearly wanted everything padded so we should output "05:00"; // // If the user wants the full padded output, they can use // template "hh:mm:ss" and set `{ trim: false }` to output // "00:05:00". if (isLargest && forceLength === null && token.length > 1) { forceLength = true; } // Update remainder. remainder.subtract(wholeValue, momentType); remainderMonths.subtract(wholeValue, momentType); return { rawValue: rawValue, wholeValue: wholeValue, // Decimal value is only retained for the least-magnitude // moment type in the format template. decimalValue: isSmallest ? decimalValue : 0, isSmallest: isSmallest, isLargest: isLargest, type: momentType, // Tokens can appear multiple times in a template string, // but all instances must share the same length. tokenLength: token.length }; }); var truncMethod = trunc ? Math.floor : Math.round; var truncate = function (value, places) { var factor = Math.pow(10, places); return truncMethod(value * factor) / factor; }; var foundFirst = false; var bubbled = false; var formatValue = function (momentType, index) { var formatOptions = { useGrouping: useGrouping, groupingSeparator: groupingSeparator, decimalSeparator: decimalSeparator, grouping: grouping, useToLocaleString: useToLocaleString }; if (useSignificantDigits) { if (significantDigits <= 0) { momentType.rawValue = 0; momentType.wholeValue = 0; momentType.decimalValue = 0; } else { formatOptions.maximumSignificantDigits = significantDigits; momentType.significantDigits = significantDigits; } } if (isMaxValue && !bubbled) { if (momentType.isLargest) { momentType.wholeValue = maxValue; momentType.decimalValue = 0; } else { momentType.wholeValue = 0; momentType.decimalValue = 0; } } if (isMinValue && !bubbled) { if (momentType.isSmallest) { momentType.wholeValue = minValue; momentType.decimalValue = 0; } else { momentType.wholeValue = 0; momentType.decimalValue = 0; } } if (momentType.isSmallest || momentType.significantDigits && momentType.significantDigits - momentType.wholeValue.toString().length <= 0) { // Apply precision to least significant token value. if (precision < 0) { momentType.value = truncate(momentType.wholeValue, precision); } else if (precision === 0) { momentType.value = truncMethod(momentType.wholeValue + momentType.decimalValue); } else { // precision > 0 if (useSignificantDigits) { if (trunc) { momentType.value = truncate(momentType.rawValue, significantDigits - momentType.wholeValue.toString().length); } else { momentType.value = momentType.rawValue; } if (momentType.wholeValue) { significantDigits -= momentType.wholeValue.toString().length; } } else { formatOptions.fractionDigits = precision; if (trunc) { momentType.value = momentType.wholeValue + truncate(momentType.decimalValue, precision); } else { momentType.value = momentType.wholeValue + momentType.decimalValue; } } } } else { if (useSignificantDigits && momentType.wholeValue) { // Outer Math.round required here to handle floating point errors. momentType.value = Math.round(truncate(momentType.wholeValue, momentType.significantDigits - momentType.wholeValue.toString().length)); significantDigits -= momentType.wholeValue.toString().length; } else { momentType.value = momentType.wholeValue; } } if (momentType.tokenLength > 1 && (forceLength || foundFirst)) { formatOptions.minimumIntegerDigits = momentType.tokenLength; if (bubbled && formatOptions.maximumSignificantDigits < momentType.tokenLength) { delete formatOptions.maximumSignificantDigits; } } if (!foundFirst && (momentType.value > 0 || trim === "" /* trim: false */ || find(stopTrim, momentType.type) || find(outputTypes, momentType.type))) { foundFirst = true; } momentType.formattedValue = formatNumber(momentType.value, formatOptions, userLocale); formatOptions.useGrouping = false; formatOptions.decimalSeparator = "."; momentType.formattedValueEn = formatNumber(momentType.value, formatOptions, "en"); if (momentType.tokenLength === 2 && momentType.type === "milliseconds") { momentType.formattedValueMS = formatNumber(momentType.value, { minimumIntegerDigits: 3, useGrouping: false }, "en").slice(0, 2); } return momentType; }; // Calculate formatted values. momentTypes = map(momentTypes, formatValue); momentTypes = compact(momentTypes); // Bubble rounded values. if (momentTypes.length > 1) { var findType = function (type) { return find(momentTypes, function (momentType) { return momentType.type === type; }); }; var bubbleTypes = function (bubble) { var bubbleMomentType = findType(bubble.type); if (!bubbleMomentType) { return; } each(bubble.targets, function (target) { var targetMomentType = findType(target.type); if (!targetMomentType) { return; } if (parseInt(bubbleMomentType.formattedValueEn, 10) === target.value) { bubbleMomentType.rawValue = 0; bubbleMomentType.wholeValue = 0; bubbleMomentType.decimalValue = 0; targetMomentType.rawValue += 1; targetMomentType.wholeValue += 1; targetMomentType.decimalValue = 0; targetMomentType.formattedValueEn = targetMomentType.wholeValue.toString(); bubbled = true; } }); }; each(bubbles, bubbleTypes); } // Recalculate formatted values. if (bubbled) { foundFirst = false; significantDigits = significantDigitsCache; momentTypes = map(momentTypes, formatValue); momentTypes = compact(momentTypes); } if (outputTypes && !(isMaxValue && !settings.trim)) { momentTypes = map(momentTypes, function (momentType) { if (find(outputTypes, function (outputType) { return momentType.type === outputType; })) { return momentType; } return null; }); momentTypes = compact(momentTypes); } else { // Trim Large. if (trimLarge) { momentTypes = rest(momentTypes, function (momentType) { // Stop trimming on: // - the smallest moment type // - a type marked for stopTrim // - a type that has a whole value return !momentType.isSmallest && !momentType.wholeValue && !find(stopTrim, momentType.type); }); } // Largest. if (largest && momentTypes.length) { momentTypes = momentTypes.slice(0, largest); } // Trim Small. if (trimSmall && momentTypes.length > 1) { momentTypes = initial(momentTypes, function (momentType) { // Stop trimming on: // - a type marked for stopTrim // - a type that has a whole value // - the largest momentType return !momentType.wholeValue && !find(stopTrim, momentType.type) && !momentType.isLargest; }); } // Trim Mid. if (trimMid) { momentTypes = map(momentTypes, function (momentType, index) { if (index > 0 && index < momentTypes.length - 1 && !momentType.wholeValue) { return null; } return momentType; }); momentTypes = compact(momentTypes); } // Trim Final. if (trimFinal && momentTypes.length === 1 && !momentTypes[0].wholeValue && !(!trunc && momentTypes[0].isSmallest && momentTypes[0].rawValue < minValue)) { momentTypes = []; } } if (returnMomentTypes) { return momentTypes; } // Localize and pluralize unit labels. each(tokens, function (token) { var key = momentTokens[token.type]; var momentType = find(momentTypes, function (momentType) { return momentType.type === token.type; }); if (!key || !momentType) { return; } var values = momentType.formattedValueEn.split("."); values[0] = parseInt(values[0], 10); if (values[1]) { values[1] = parseFloat("0." + values[1], 10); } else { values[1] = null; } var pluralKey = localeData.durationPluralKey(key, values[0], values[1]); var labels = durationGetLabels(key, localeData); var autoLocalized = false; var pluralizedLabels = {}; // Auto-Localized unit labels. each(localeData._durationLabelTypes, function (labelType) { var label = find(labels, function (label) { return label.type === labelType.type && label.key === pluralKey; }); if (label) { pluralizedLabels[label.type] = label.label; if (stringIncludes(token.text, labelType.string)) { token.text = token.text.replace(labelType.string, label.label); autoLocalized = true; } } }); // Auto-pluralized unit labels. if (usePlural && !autoLocalized) { labels.sort(durationLabelCompare); each(labels, function (label) { if (pluralizedLabels[label.type] === label.label) { if (stringIncludes(token.text, label.label)) { // Stop checking this token if its label is already // correctly pluralized. return false; } // Skip this label if it is correct, but not present in // the token's text. return; } if (stringIncludes(token.text, label.label)) { // Replece this token's label and stop checking. token.text = token.text.replace(label.label, pluralizedLabels[label.type]); return false; } }); } }); // Build ouptut. tokens = map(tokens, function (token) { if (!token.type) { return token.text; } var momentType = find(momentTypes, function (momentType) { return momentType.type === token.type; }); if (!momentType) { return ""; } var out = ""; if (useLeftUnits) { out += token.text; } if (isNegative && isMaxValue || !isNegative && isMinValue) { out += "< "; isMaxValue = false; isMinValue = false; } if (isNegative && isMinValue || !isNegative && isMaxValue) { out += "> "; isMaxValue = false; isMinValue = false; } if (isNegative && (momentType.value > 0 || trim === "" || find(stopTrim, momentType.type) || find(outputTypes, momentType.type))) { out += "-"; isNegative = false; } if (token.type === "milliseconds" && momentType.formattedValueMS) { out += momentType.formattedValueMS; } else { out += momentType.formattedValue; } if (!useLeftUnits) { out += token.text; } return out; }); // Trim leading and trailing comma, space, colon, and dot. return tokens.join("").replace(/(,| |:|\.)*$/, "").replace(/^(,| |:|\.)*/, ""); } // defaultFormatTemplate function defaultFormatTemplate() { var dur = this.duration; var findType = function findType(type) { return dur._data[type]; }; var firstType = find(this.types, findType); var lastType = findLast(this.types, findType); // Default template strings for each duration dimension type. switch (firstType) { case "milliseconds": return "S __"; case "seconds": // Fallthrough. case "minutes": return "*_MS_"; case "hours": return "_HMS_"; case "days": // Possible Fallthrough. if (firstType === lastType) { return "d __"; } case "weeks": if (firstType === lastType) { return "w __"; } if (this.trim === null) { this.trim = "both"; } return "w __, d __, h __"; case "months": // Possible Fallthrough. if (firstType === lastType) { return "M __"; } case "years": if (firstType === lastType) { return "y __"; } if (this.trim === null) { this.trim = "both"; } return "y __, M __, d __"; default: if (this.trim === null) { this.trim = "both"; } return "y __, d __, h __, m __, s __"; } } // init function init(context) { if (!context) { throw "Moment Duration Format init cannot find moment instance."; } context.duration.format = durationsFormat; context.duration.fn.format = durationFormat; context.duration.fn.format.defaults = { // Many options are defaulted to `null` to distinguish between // 'not set' and 'set to `false`' // trim // Can be a string, a delimited list of strings, an array of strings, // or a boolean. // "large" - will trim largest-magnitude zero-value tokens until // finding a token with a value, a token identified as 'stopTrim', or // the final token of the format string. // "small" - will trim smallest-magnitude zero-value tokens until // finding a token with a value, a token identified as 'stopTrim', or // the final token of the format string. // "both" - will execute "large" trim then "small" trim. // "mid" - will trim any zero-value tokens that are not the first or // last tokens. Usually used in conjunction with "large" or "both". // e.g. "large mid" or "both mid". // "final" - will trim the final token if it is zero-value. Use this // option with "large" or "both" to output an empty string when // formatting a zero-value duration. e.g. "large final" or "both final". // "all" - Will trim all zero-value tokens. Shorthand for "both mid final". // "left" - maps to "large" to support plugin's version 1 API. // "right" - maps to "large" to support plugin's version 1 API. // `false` - template tokens are not trimmed. // `true` - treated as "large". // `null` - treated as "large". trim: null, // stopTrim // A moment token string, a delimited set of moment token strings, // or an array of moment token strings. Trimming will stop when a token // listed in this option is reached. A "*" character in the format // template string will also mark a moment token as stopTrim. // e.g. "d [days] *h:mm:ss" will always stop trimming at the 'hours' token. stopTrim: null, // largest // Set to a positive integer to output only the "n" largest-magnitude // moment tokens that have a value. All lesser-magnitude moment tokens // will be ignored. This option takes effect even if `trim` is set // to `false`. largest: null, // maxValue // Use `maxValue` to render generalized output for large duration values, // e.g. `"> 60 days"`. `maxValue` must be a positive integer and is /// applied to the greatest-magnitude moment token in the format template. maxValue: null, // minValue // Use `minValue` to render generalized output for small duration values, // e.g. `"< 5 minutes"`. `minValue` must be a positive integer and is // applied to the least-magnitude moment token in the format template. minValue: null, // precision // If a positive integer, number of decimal fraction digits to render. // If a negative integer, number of integer place digits to truncate to 0. // If `useSignificantDigits` is set to `true` and `precision` is a positive // integer, sets the maximum number of significant digits used in the // formatted output. precision: 0, // trunc // Default behavior rounds final token value. Set to `true` to // truncate final token value, which was the default behavior in // version 1 of this plugin. trunc: false, // forceLength // Force first moment token with a value to render at full length // even when template is trimmed and first moment token has length of 1. forceLength: null, // userLocale // Formatted numerical output is rendered using `toLocaleString` // and the locale of the user's environment. Set this option to render // numerical output using a different locale. Unit names are rendered // and detected using the locale set in moment.js, which can be different // from the locale of user's environment. userLocale: null, // usePlural // Will automatically singularize or pluralize unit names when they // appear in the text associated with each moment token. Standard and // short unit labels are singularized and pluralized, based on locale. // e.g. in english, "1 second" or "1 sec" would be rendered instead // of "1 seconds" or "1 secs". The default pluralization function // renders a plural label for a value with decimal precision. // e.g. "1.0 seconds" is never rendered as "1.0 second". // Label types and pluralization function are configurable in the // localeData extensions. usePlural: true, // useLeftUnits // The text to the right of each moment token in a format string // is treated as that token's units for the purposes of trimming, // singularizing, and auto-localizing. // e.g. "h [hours], m [minutes], s [seconds]". // To properly singularize or localize a format string such as // "[hours] h, [minutes] m, [seconds] s", where the units appear // to the left of each moment token, set useLeftUnits to `true`. // This plugin is not tested in the context of rtl text. useLeftUnits: false, // useGrouping // Enables locale-based digit grouping in the formatted output. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString useGrouping: true, // useSignificantDigits // Treat the `precision` option as the maximum significant digits // to be rendered. Precision must be a positive integer. Significant // digits extend across unit types, // e.g. "6 hours 37.5 minutes" represents 4 significant digits. // Enabling this option causes token length to be ignored. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString useSignificantDigits: false, // template // The template string used to format the duration. May be a function // or a string. Template functions are executed with the `this` binding // of the settings object so that template strings may be dynamically // generated based on the duration object (accessible via `this.duration`) // or any of the other settings. Leading and trailing space, comma, // period, and colon characters are trimmed from the resulting string. template: defaultFormatTemplate, // useToLocaleString // Set this option to `false` to ignore the `toLocaleString` feature // test and force the use of the `formatNumber` fallback function // included in this plugin. useToLocaleString: true, // formatNumber fallback options. // When `toLocaleString` is detected and passes the feature test, the // following options will have no effect: `toLocaleString` will be used // for formatting and the grouping separator, decimal separator, and // integer digit grouping will be determined by the user locale. // groupingSeparator // The integer digit grouping separator used when using the fallback // formatNumber function. groupingSeparator: ",", // decimalSeparator // The decimal separator used when using the fallback formatNumber // function. decimalSeparator: ".", // grouping // The integer digit grouping used when using the fallback formatNumber // function. Must be an array. The default value of `[3]` gives the // standard 3-digit thousand/million/billion digit groupings for the // "en" locale. Setting this option to `[3, 2]` would generate the // thousand/lakh/crore digit groupings used in the "en-IN" locale. grouping: [3] }; context.updateLocale('en', engLocale); } // Run feature tests for `Number#toLocaleString`. var toLocaleStringFormatter = function(number, locale, options) { return number.toLocaleString(locale, options); }; toLocaleStringWorks = toLocaleStringSupportsLocales() && featureTestFormatter(toLocaleStringFormatter); toLocaleStringRoundingWorks = toLocaleStringWorks && featureTestFormatterRounding(toLocaleStringFormatter); // Run feature tests for `Intl.NumberFormat#format`. var intlNumberFormatFormatter = function(number, locale, options) { if (typeof window !== 'undefined' && window && window.Intl && window.Intl.NumberFormat) { return window.Intl.NumberFormat(locale, options).format(number); } }; intlNumberFormatWorks = featureTestFormatter(intlNumberFormatFormatter); intlNumberFormatRoundingWorks = intlNumberFormatWorks && featureTestFormatterRounding(intlNumberFormatFormatter); // Initialize duration format on the global moment instance. init(moment); // Return the init function so that duration format can be // initialized on other moment instances. return init; });