intl-unofficial-duration-unit-format
Version:
Unofficial implementation of duration UnitFormat
222 lines (199 loc) • 7.75 kB
JavaScript
'use strict';
var IntlMessageFormat = require('intl-messageformat');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var IntlMessageFormat__default = /*#__PURE__*/_interopDefaultLegacy(IntlMessageFormat);
const INITIAL_ZERO = /^0/;
function leftTrim(parts) {
let previousEmpty = true;
return parts.filter((token) => {
if (token.type === 'literal' && !token.value.trim()) {
if (previousEmpty) return false;
previousEmpty = true;
return true;
} else {
previousEmpty = false;
}
return true;
});
}
function trim(parts, trimFirstPaddedValue = false) {
const trimmed = leftTrim(leftTrim(parts).reverse()).reverse();
if (trimFirstPaddedValue) {
const first = trimmed.find((token) => token.type !== 'literal');
first.value = first.value.replace(INITIAL_ZERO, '');
}
return trimmed;
}
function DurationUnitFormat(locales, options = defaultOptions) {
this.locales = locales;
// TODO I'm ignoring the unit for now, value is always expressed in seconds
this.unit = 'second';
// .style determines how the placeholders are converted to plain text
this.style = options.style || DurationUnitFormat.styles.LONG;
// .isTimer determines some special behaviour, we want to keep the 0s
this.isTimer = this.style === DurationUnitFormat.styles.TIMER;
// .format used `seconds`, `minutes`, `hours`, ... as placeholders
this._format = options.format || (this.isTimer ? '{minutes}:{seconds}' : '{seconds}');
// How to format unit according to style
this.formatUnits = options.formatUnits || defaultOptions.formatUnits;
// .formatDuration determines whether we use a space or not
this.formatDuration = options.formatDuration || defaultOptions.formatDuration;
this.shouldRound = options.round === true;
}
DurationUnitFormat.units = {
DAY: 'day',
HOUR: 'hour',
MINUTE: 'minute',
SECOND: 'second',
};
DurationUnitFormat.styles = {
CUSTOM: 'custom',
TIMER: 'timer',
// http://www.unicode.org/cldr/charts/27/summary/pl.html#5556
LONG: 'long',
SHORT: 'short',
NARROW: 'narrow',
};
DurationUnitFormat.prototype.format = function (value) {
return this.formatToParts(value).map(({ value }) => value).join('');
};
DurationUnitFormat.prototype.formatToParts = function (value) {
// Extract all the parts that are actually used from the localised format
const parts = new IntlMessageFormat__default['default'](this._format, this.locales).formatToParts({
second: { unit: DurationUnitFormat.units.SECOND },
seconds: { unit: DurationUnitFormat.units.SECOND },
minute: { unit: DurationUnitFormat.units.MINUTE },
minutes: { unit: DurationUnitFormat.units.MINUTE },
hour: { unit: DurationUnitFormat.units.HOUR },
hours: { unit: DurationUnitFormat.units.HOUR },
day: { unit: DurationUnitFormat.units.DAY },
days: { unit: DurationUnitFormat.units.DAY },
});
// Compute the value of each bucket depending on which parts are used
const buckets = splitSecondsInBuckets(value, this.unit, parts, this.shouldRound);
// Each part from the format message could potentially contain multiple parts
const result = parts.reduce((all, token) => all.concat(this._formatToken(token, buckets)), []);
return this._trimOutput(result, parts);
};
DurationUnitFormat.prototype._formatToken = function(token, buckets) {
const {value} = token;
if (value.unit) {
const number = buckets[value.unit];
return (number || this.isTimer) ? this._formatDurationToParts(value.unit, number) : [];
} else if (value) {
// If there is no .unit it's text, but it could be an empty string
return [{ type: 'literal', value }];
}
return [];
};
DurationUnitFormat.prototype._formatDurationToParts = function(unit, number) {
if (this.isTimer) {
// With timer style, we only show the value
return [{ type: unit, value: this._formatValue(number) }];
} else if (isSpecialStyle(this.style)) {
return new Intl.NumberFormat(this.locales, {
style: 'unit',
unit: unit,
unitDisplay: this.style,
}).formatToParts(number).map((_) => ({
// NumberFormat uses 'integer' for types, but I prefer using the unit
// This is more similar to what happens in DateTimeFormat
type: _.type === 'integer' ? unit : _.type,
value: _.value,
}));
}
// This is now only needed for the custom formatting
return this.formatDuration.split(SPLIT_POINTS).map((text) => {
if (text === '{value}') {
return { type: unit, value: this._formatValue(number) };
}
if (text === '{unit}') {
const message = this.formatUnits[unit] || '{value}';
const formattedUnit = new IntlMessageFormat__default['default'](message, this.locales).format({ value: number });
return { type: 'unit', value: formattedUnit };
}
if (text) {
return { type: 'literal', value: text };
}
}).filter(Boolean);
};
DurationUnitFormat.prototype._formatValue = function (number) {
return this.isTimer ? number.toString().padStart(2, '0') : number.toString();
};
DurationUnitFormat.prototype._trimOutput = function (result, parts) {
const trimmed = trim(result, this.isTimer);
if (!trimmed.find((_) => _.type !== 'literal')) {
// if everything cancels out and there are only literals,
// then return 0 on the lowest available unit
const minUnit = [
DurationUnitFormat.units.SECOND,
DurationUnitFormat.units.MINUTE,
DurationUnitFormat.units.HOUR,
DurationUnitFormat.units.DAY,
].find((unit) => has(parts, unit));
return this._formatDurationToParts(minUnit, 0);
}
return trimmed;
};
const defaultOptions = {
// unit: DurationUnitFormat.units.SECOND,
formatDuration: '{value} {unit}',
formatUnits: {
// custom values
[DurationUnitFormat.units.DAY]: '{value, plural, one {day} other {days}}',
[DurationUnitFormat.units.HOUR]: '{value, plural, one {hour} other {hours}}',
[DurationUnitFormat.units.MINUTE]: '{value, plural, one {minute} other {minutes}}',
[DurationUnitFormat.units.SECOND]: '{value, plural, one {second} other {seconds}}',
},
style: DurationUnitFormat.styles.LONG,
};
const SPLIT_POINTS = /(\{value\}|\{unit\})/;
const SECONDS_IN = {
day: 24 * 60 * 60,
hour: 60 * 60,
minute: 60,
second: 1,
};
function has(parts, unit) {
return !!parts.find((_) => _.value.unit === unit);
}
function splitSecondsInBuckets(value, valueUnit, parts, shouldRound) {
let seconds = value * SECONDS_IN[valueUnit];
// Rounding will only affect the lowest unit
// check how many seconds we need to add
if (shouldRound) {
const lowestUnit = [
DurationUnitFormat.units.SECOND,
DurationUnitFormat.units.MINUTE,
DurationUnitFormat.units.HOUR,
DurationUnitFormat.units.DAY,
].find((unit) => has(parts, unit));
// These many seconds will be ignored by the lowest unit
const remainder = seconds % SECONDS_IN[lowestUnit];
if (2 * remainder >= SECONDS_IN[lowestUnit]) {
// The remainder is large, add enough seconds to increse the lowest unit
seconds += SECONDS_IN[lowestUnit] - remainder;
}
}
const buckets = {};
[
DurationUnitFormat.units.DAY,
DurationUnitFormat.units.HOUR,
DurationUnitFormat.units.MINUTE,
DurationUnitFormat.units.SECOND,
].forEach((unit) => {
if (has(parts, unit)) {
buckets[unit] = Math.floor(seconds / SECONDS_IN[unit]);
seconds -= buckets[unit] * SECONDS_IN[unit];
}
});
return buckets;
}
function isSpecialStyle(style) {
return [
DurationUnitFormat.styles.LONG,
DurationUnitFormat.styles.SHORT,
DurationUnitFormat.styles.NARROW,
].includes(style);
}
module.exports = DurationUnitFormat;