From 057abbb6c1f2e8c9eacb15dde89e9fee6671fa6f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sat, 27 Jan 2024 16:16:32 +0100 Subject: [PATCH 1/2] [mining] update halving widget to be more human readable --- .../components/difficulty-mining/countdown.js | 1358 +++++++++++++++++ .../difficulty-mining.component.html | 30 +- .../difficulty-mining.component.ts | 27 +- 3 files changed, 1403 insertions(+), 12 deletions(-) create mode 100644 frontend/src/app/components/difficulty-mining/countdown.js diff --git a/frontend/src/app/components/difficulty-mining/countdown.js b/frontend/src/app/components/difficulty-mining/countdown.js new file mode 100644 index 000000000..60909abb9 --- /dev/null +++ b/frontend/src/app/components/difficulty-mining/countdown.js @@ -0,0 +1,1358 @@ +/*global window module */ +/** + * @license countdown.js v2.6.1 http://countdownjs.org + * Copyright (c)2006-2014 Stephen M. McKamey. + * Licensed under The MIT License. + */ +/*jshint bitwise:false */ + +/** + * API entry + * @public + * @param {function(Object)|Date|number} start the starting date + * @param {function(Object)|Date|number} end the ending date + * @param {number} units the units to populate + * @return {Object|number} + */ +var countdown = ( + + function() { + /*jshint smarttabs:true */ + + 'use strict'; + + /** + * @private + * @const + * @type {number} + */ + var MILLISECONDS = 0x001; + + /** + * @private + * @const + * @type {number} + */ + var SECONDS = 0x002; + + /** + * @private + * @const + * @type {number} + */ + var MINUTES = 0x004; + + /** + * @private + * @const + * @type {number} + */ + var HOURS = 0x008; + + /** + * @private + * @const + * @type {number} + */ + var DAYS = 0x010; + + /** + * @private + * @const + * @type {number} + */ + var WEEKS = 0x020; + + /** + * @private + * @const + * @type {number} + */ + var MONTHS = 0x040; + + /** + * @private + * @const + * @type {number} + */ + var YEARS = 0x080; + + /** + * @private + * @const + * @type {number} + */ + var DECADES = 0x100; + + /** + * @private + * @const + * @type {number} + */ + var CENTURIES = 0x200; + + /** + * @private + * @const + * @type {number} + */ + var MILLENNIA = 0x400; + + /** + * @private + * @const + * @type {number} + */ + var DEFAULTS = YEARS|MONTHS|DAYS|HOURS|MINUTES|SECONDS; + + /** + * @private + * @const + * @type {number} + */ + var MILLISECONDS_PER_SECOND = 1000; + + /** + * @private + * @const + * @type {number} + */ + var SECONDS_PER_MINUTE = 60; + + /** + * @private + * @const + * @type {number} + */ + var MINUTES_PER_HOUR = 60; + + /** + * @private + * @const + * @type {number} + */ + var HOURS_PER_DAY = 24; + + /** + * @private + * @const + * @type {number} + */ + var MILLISECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND; + + /** + * @private + * @const + * @type {number} + */ + var DAYS_PER_WEEK = 7; + + /** + * @private + * @const + * @type {number} + */ + var MONTHS_PER_YEAR = 12; + + /** + * @private + * @const + * @type {number} + */ + var YEARS_PER_DECADE = 10; + + /** + * @private + * @const + * @type {number} + */ + var DECADES_PER_CENTURY = 10; + + /** + * @private + * @const + * @type {number} + */ + var CENTURIES_PER_MILLENNIUM = 10; + + /** + * @private + * @param {number} x number + * @return {number} + */ + var ceil = Math.ceil; + + /** + * @private + * @param {number} x number + * @return {number} + */ + var floor = Math.floor; + + /** + * @private + * @param {Date} ref reference date + * @param {number} shift number of months to shift + * @return {number} number of days shifted + */ + function borrowMonths(ref, shift) { + var prevTime = ref.getTime(); + + // increment month by shift + ref.setMonth( ref.getMonth() + shift ); + + // this is the trickiest since months vary in length + return Math.round( (ref.getTime() - prevTime) / MILLISECONDS_PER_DAY ); + } + + /** + * @private + * @param {Date} ref reference date + * @return {number} number of days + */ + function daysPerMonth(ref) { + var a = ref.getTime(); + + // increment month by 1 + var b = new Date(a); + b.setMonth( ref.getMonth() + 1 ); + + // this is the trickiest since months vary in length + return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY ); + } + + /** + * @private + * @param {Date} ref reference date + * @return {number} number of days + */ + function daysPerYear(ref) { + var a = ref.getTime(); + + // increment year by 1 + var b = new Date(a); + b.setFullYear( ref.getFullYear() + 1 ); + + // this is the trickiest since years (periodically) vary in length + return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY ); + } + + /** + * Applies the Timespan to the given date. + * + * @private + * @param {Timespan} ts + * @param {Date=} date + * @return {Date} + */ + function addToDate(ts, date) { + date = (date instanceof Date) || ((date !== null) && isFinite(date)) ? new Date(+date) : new Date(); + if (!ts) { + return date; + } + + // if there is a value field, use it directly + var value = +ts.value || 0; + if (value) { + date.setTime(date.getTime() + value); + return date; + } + + value = +ts.milliseconds || 0; + if (value) { + date.setMilliseconds(date.getMilliseconds() + value); + } + + value = +ts.seconds || 0; + if (value) { + date.setSeconds(date.getSeconds() + value); + } + + value = +ts.minutes || 0; + if (value) { + date.setMinutes(date.getMinutes() + value); + } + + value = +ts.hours || 0; + if (value) { + date.setHours(date.getHours() + value); + } + + value = +ts.weeks || 0; + if (value) { + value *= DAYS_PER_WEEK; + } + + value += +ts.days || 0; + if (value) { + date.setDate(date.getDate() + value); + } + + value = +ts.months || 0; + if (value) { + date.setMonth(date.getMonth() + value); + } + + value = +ts.millennia || 0; + if (value) { + value *= CENTURIES_PER_MILLENNIUM; + } + + value += +ts.centuries || 0; + if (value) { + value *= DECADES_PER_CENTURY; + } + + value += +ts.decades || 0; + if (value) { + value *= YEARS_PER_DECADE; + } + + value += +ts.years || 0; + if (value) { + date.setFullYear(date.getFullYear() + value); + } + + return date; + } + + /** + * @private + * @const + * @type {number} + */ + var LABEL_MILLISECONDS = 0; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_SECONDS = 1; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_MINUTES = 2; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_HOURS = 3; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_DAYS = 4; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_WEEKS = 5; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_MONTHS = 6; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_YEARS = 7; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_DECADES = 8; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_CENTURIES = 9; + + /** + * @private + * @const + * @type {number} + */ + var LABEL_MILLENNIA = 10; + + /** + * @private + * @type {Array} + */ + var LABELS_SINGLUAR; + + /** + * @private + * @type {Array} + */ + var LABELS_PLURAL; + + /** + * @private + * @type {string} + */ + var LABEL_LAST; + + /** + * @private + * @type {string} + */ + var LABEL_DELIM; + + /** + * @private + * @type {string} + */ + var LABEL_NOW; + + /** + * Formats a number & unit as a string + * + * @param {number} value + * @param {number} unit + * @return {string} + */ + var formatter; + + /** + * Formats a number as a string + * + * @private + * @param {number} value + * @return {string} + */ + var formatNumber; + + /** + * @private + * @param {number} value + * @param {number} unit unit index into label list + * @return {string} + */ + function plurality(value, unit) { + return formatNumber(value)+((value === 1) ? LABELS_SINGLUAR[unit] : LABELS_PLURAL[unit]); + } + + /** + * Formats the entries with singular or plural labels + * + * @private + * @param {Timespan} ts + * @return {Array} + */ + var formatList; + + /** + * Timespan representation of a duration of time + * + * @private + * @this {Timespan} + * @constructor + */ + function Timespan() {} + + /** + * Formats the Timespan as a sentence + * + * @param {string=} emptyLabel the string to use when no values returned + * @return {string} + */ + Timespan.prototype.toString = function(emptyLabel) { + var label = formatList(this); + + var count = label.length; + if (!count) { + return emptyLabel ? ''+emptyLabel : LABEL_NOW; + } + if (count === 1) { + return label[0]; + } + + var last = LABEL_LAST+label.pop(); + return label.join(LABEL_DELIM)+last; + }; + + /** + * Formats the Timespan as a sentence in HTML + * + * @param {string=} tag HTML tag name to wrap each value + * @param {string=} emptyLabel the string to use when no values returned + * @return {string} + */ + Timespan.prototype.toHTML = function(tag, emptyLabel) { + tag = tag || 'span'; + var label = formatList(this); + + var count = label.length; + if (!count) { + emptyLabel = emptyLabel || LABEL_NOW; + return emptyLabel ? '<'+tag+'>'+emptyLabel+'' : emptyLabel; + } + for (var i=0; i'+label[i]+''; + } + if (count === 1) { + return label[0]; + } + + var last = LABEL_LAST+label.pop(); + return label.join(LABEL_DELIM)+last; + }; + + /** + * Applies the Timespan to the given date + * + * @param {Date=} date the date to which the timespan is added. + * @return {Date} + */ + Timespan.prototype.addTo = function(date) { + return addToDate(this, date); + }; + + /** + * Formats the entries as English labels + * + * @private + * @param {Timespan} ts + * @return {Array} + */ + formatList = function(ts) { + var list = []; + + var value = ts.millennia; + if (value) { + list.push(formatter(value, LABEL_MILLENNIA)); + } + + value = ts.centuries; + if (value) { + list.push(formatter(value, LABEL_CENTURIES)); + } + + value = ts.decades; + if (value) { + list.push(formatter(value, LABEL_DECADES)); + } + + value = ts.years; + if (value) { + list.push(formatter(value, LABEL_YEARS)); + } + + value = ts.months; + if (value) { + list.push(formatter(value, LABEL_MONTHS)); + } + + value = ts.weeks; + if (value) { + list.push(formatter(value, LABEL_WEEKS)); + } + + value = ts.days; + if (value) { + list.push(formatter(value, LABEL_DAYS)); + } + + value = ts.hours; + if (value) { + list.push(formatter(value, LABEL_HOURS)); + } + + value = ts.minutes; + if (value) { + list.push(formatter(value, LABEL_MINUTES)); + } + + value = ts.seconds; + if (value) { + list.push(formatter(value, LABEL_SECONDS)); + } + + value = ts.milliseconds; + if (value) { + list.push(formatter(value, LABEL_MILLISECONDS)); + } + + return list; + }; + + /** + * Borrow any underflow units, carry any overflow units + * + * @private + * @param {Timespan} ts + * @param {string} toUnit + */ + function rippleRounded(ts, toUnit) { + switch (toUnit) { + case 'seconds': + if (ts.seconds !== SECONDS_PER_MINUTE || isNaN(ts.minutes)) { + return; + } + // ripple seconds up to minutes + ts.minutes++; + ts.seconds = 0; + + /* falls through */ + case 'minutes': + if (ts.minutes !== MINUTES_PER_HOUR || isNaN(ts.hours)) { + return; + } + // ripple minutes up to hours + ts.hours++; + ts.minutes = 0; + + /* falls through */ + case 'hours': + if (ts.hours !== HOURS_PER_DAY || isNaN(ts.days)) { + return; + } + // ripple hours up to days + ts.days++; + ts.hours = 0; + + /* falls through */ + case 'days': + if (ts.days !== DAYS_PER_WEEK || isNaN(ts.weeks)) { + return; + } + // ripple days up to weeks + ts.weeks++; + ts.days = 0; + + /* falls through */ + case 'weeks': + if (ts.weeks !== daysPerMonth(ts.refMonth)/DAYS_PER_WEEK || isNaN(ts.months)) { + return; + } + // ripple weeks up to months + ts.months++; + ts.weeks = 0; + + /* falls through */ + case 'months': + if (ts.months !== MONTHS_PER_YEAR || isNaN(ts.years)) { + return; + } + // ripple months up to years + ts.years++; + ts.months = 0; + + /* falls through */ + case 'years': + if (ts.years !== YEARS_PER_DECADE || isNaN(ts.decades)) { + return; + } + // ripple years up to decades + ts.decades++; + ts.years = 0; + + /* falls through */ + case 'decades': + if (ts.decades !== DECADES_PER_CENTURY || isNaN(ts.centuries)) { + return; + } + // ripple decades up to centuries + ts.centuries++; + ts.decades = 0; + + /* falls through */ + case 'centuries': + if (ts.centuries !== CENTURIES_PER_MILLENNIUM || isNaN(ts.millennia)) { + return; + } + // ripple centuries up to millennia + ts.millennia++; + ts.centuries = 0; + /* falls through */ + } + } + + /** + * Ripple up partial units one place + * + * @private + * @param {Timespan} ts timespan + * @param {number} frac accumulated fractional value + * @param {string} fromUnit source unit name + * @param {string} toUnit target unit name + * @param {number} conversion multiplier between units + * @param {number} digits max number of decimal digits to output + * @return {number} new fractional value + */ + function fraction(ts, frac, fromUnit, toUnit, conversion, digits) { + if (ts[fromUnit] >= 0) { + frac += ts[fromUnit]; + delete ts[fromUnit]; + } + + frac /= conversion; + if (frac + 1 <= 1) { + // drop if below machine epsilon + return 0; + } + + if (ts[toUnit] >= 0) { + // ensure does not have more than specified number of digits + ts[toUnit] = +(ts[toUnit] + frac).toFixed(digits); + rippleRounded(ts, toUnit); + return 0; + } + + return frac; + } + + /** + * Ripple up partial units to next existing + * + * @private + * @param {Timespan} ts + * @param {number} digits max number of decimal digits to output + */ + function fractional(ts, digits) { + var frac = fraction(ts, 0, 'milliseconds', 'seconds', MILLISECONDS_PER_SECOND, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'seconds', 'minutes', SECONDS_PER_MINUTE, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'minutes', 'hours', MINUTES_PER_HOUR, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'hours', 'days', HOURS_PER_DAY, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'days', 'weeks', DAYS_PER_WEEK, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'weeks', 'months', daysPerMonth(ts.refMonth)/DAYS_PER_WEEK, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'months', 'years', daysPerYear(ts.refMonth)/daysPerMonth(ts.refMonth), digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'years', 'decades', YEARS_PER_DECADE, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'decades', 'centuries', DECADES_PER_CENTURY, digits); + if (!frac) { return; } + + frac = fraction(ts, frac, 'centuries', 'millennia', CENTURIES_PER_MILLENNIUM, digits); + + // should never reach this with remaining fractional value + if (frac) { throw new Error('Fractional unit overflow'); } + } + + /** + * Borrow any underflow units, carry any overflow units + * + * @private + * @param {Timespan} ts + */ + function ripple(ts) { + var x; + + if (ts.milliseconds < 0) { + // ripple seconds down to milliseconds + x = ceil(-ts.milliseconds / MILLISECONDS_PER_SECOND); + ts.seconds -= x; + ts.milliseconds += x * MILLISECONDS_PER_SECOND; + + } else if (ts.milliseconds >= MILLISECONDS_PER_SECOND) { + // ripple milliseconds up to seconds + ts.seconds += floor(ts.milliseconds / MILLISECONDS_PER_SECOND); + ts.milliseconds %= MILLISECONDS_PER_SECOND; + } + + if (ts.seconds < 0) { + // ripple minutes down to seconds + x = ceil(-ts.seconds / SECONDS_PER_MINUTE); + ts.minutes -= x; + ts.seconds += x * SECONDS_PER_MINUTE; + + } else if (ts.seconds >= SECONDS_PER_MINUTE) { + // ripple seconds up to minutes + ts.minutes += floor(ts.seconds / SECONDS_PER_MINUTE); + ts.seconds %= SECONDS_PER_MINUTE; + } + + if (ts.minutes < 0) { + // ripple hours down to minutes + x = ceil(-ts.minutes / MINUTES_PER_HOUR); + ts.hours -= x; + ts.minutes += x * MINUTES_PER_HOUR; + + } else if (ts.minutes >= MINUTES_PER_HOUR) { + // ripple minutes up to hours + ts.hours += floor(ts.minutes / MINUTES_PER_HOUR); + ts.minutes %= MINUTES_PER_HOUR; + } + + if (ts.hours < 0) { + // ripple days down to hours + x = ceil(-ts.hours / HOURS_PER_DAY); + ts.days -= x; + ts.hours += x * HOURS_PER_DAY; + + } else if (ts.hours >= HOURS_PER_DAY) { + // ripple hours up to days + ts.days += floor(ts.hours / HOURS_PER_DAY); + ts.hours %= HOURS_PER_DAY; + } + + while (ts.days < 0) { + // NOTE: never actually seen this loop more than once + + // ripple months down to days + ts.months--; + ts.days += borrowMonths(ts.refMonth, 1); + } + + // weeks is always zero here + + if (ts.days >= DAYS_PER_WEEK) { + // ripple days up to weeks + ts.weeks += floor(ts.days / DAYS_PER_WEEK); + ts.days %= DAYS_PER_WEEK; + } + + if (ts.months < 0) { + // ripple years down to months + x = ceil(-ts.months / MONTHS_PER_YEAR); + ts.years -= x; + ts.months += x * MONTHS_PER_YEAR; + + } else if (ts.months >= MONTHS_PER_YEAR) { + // ripple months up to years + ts.years += floor(ts.months / MONTHS_PER_YEAR); + ts.months %= MONTHS_PER_YEAR; + } + + // years is always non-negative here + // decades, centuries and millennia are always zero here + + if (ts.years >= YEARS_PER_DECADE) { + // ripple years up to decades + ts.decades += floor(ts.years / YEARS_PER_DECADE); + ts.years %= YEARS_PER_DECADE; + + if (ts.decades >= DECADES_PER_CENTURY) { + // ripple decades up to centuries + ts.centuries += floor(ts.decades / DECADES_PER_CENTURY); + ts.decades %= DECADES_PER_CENTURY; + + if (ts.centuries >= CENTURIES_PER_MILLENNIUM) { + // ripple centuries up to millennia + ts.millennia += floor(ts.centuries / CENTURIES_PER_MILLENNIUM); + ts.centuries %= CENTURIES_PER_MILLENNIUM; + } + } + } + } + + /** + * Remove any units not requested + * + * @private + * @param {Timespan} ts + * @param {number} units the units to populate + * @param {number} max number of labels to output + * @param {number} digits max number of decimal digits to output + */ + function pruneUnits(ts, units, max, digits) { + var count = 0; + + // Calc from largest unit to smallest to prevent underflow + if (!(units & MILLENNIA) || (count >= max)) { + // ripple millennia down to centuries + ts.centuries += ts.millennia * CENTURIES_PER_MILLENNIUM; + delete ts.millennia; + + } else if (ts.millennia) { + count++; + } + + if (!(units & CENTURIES) || (count >= max)) { + // ripple centuries down to decades + ts.decades += ts.centuries * DECADES_PER_CENTURY; + delete ts.centuries; + + } else if (ts.centuries) { + count++; + } + + if (!(units & DECADES) || (count >= max)) { + // ripple decades down to years + ts.years += ts.decades * YEARS_PER_DECADE; + delete ts.decades; + + } else if (ts.decades) { + count++; + } + + if (!(units & YEARS) || (count >= max)) { + // ripple years down to months + ts.months += ts.years * MONTHS_PER_YEAR; + delete ts.years; + + } else if (ts.years) { + count++; + } + + if (!(units & MONTHS) || (count >= max)) { + // ripple months down to days + if (ts.months) { + ts.days += borrowMonths(ts.refMonth, ts.months); + } + delete ts.months; + + if (ts.days >= DAYS_PER_WEEK) { + // ripple day overflow back up to weeks + ts.weeks += floor(ts.days / DAYS_PER_WEEK); + ts.days %= DAYS_PER_WEEK; + } + + } else if (ts.months) { + count++; + } + + if (!(units & WEEKS) || (count >= max)) { + // ripple weeks down to days + ts.days += ts.weeks * DAYS_PER_WEEK; + delete ts.weeks; + + } else if (ts.weeks) { + count++; + } + + if (!(units & DAYS) || (count >= max)) { + //ripple days down to hours + ts.hours += ts.days * HOURS_PER_DAY; + delete ts.days; + + } else if (ts.days) { + count++; + } + + if (!(units & HOURS) || (count >= max)) { + // ripple hours down to minutes + ts.minutes += ts.hours * MINUTES_PER_HOUR; + delete ts.hours; + + } else if (ts.hours) { + count++; + } + + if (!(units & MINUTES) || (count >= max)) { + // ripple minutes down to seconds + ts.seconds += ts.minutes * SECONDS_PER_MINUTE; + delete ts.minutes; + + } else if (ts.minutes) { + count++; + } + + if (!(units & SECONDS) || (count >= max)) { + // ripple seconds down to milliseconds + ts.milliseconds += ts.seconds * MILLISECONDS_PER_SECOND; + delete ts.seconds; + + } else if (ts.seconds) { + count++; + } + + // nothing to ripple milliseconds down to + // so ripple back up to smallest existing unit as a fractional value + if (!(units & MILLISECONDS) || (count >= max)) { + fractional(ts, digits); + } + } + + /** + * Populates the Timespan object + * + * @private + * @param {Timespan} ts + * @param {?Date} start the starting date + * @param {?Date} end the ending date + * @param {number} units the units to populate + * @param {number} max number of labels to output + * @param {number} digits max number of decimal digits to output + */ + function populate(ts, start, end, units, max, digits) { + var now = new Date(); + + ts.start = start = start || now; + ts.end = end = end || now; + ts.units = units; + + ts.value = end.getTime() - start.getTime(); + if (ts.value < 0) { + // swap if reversed + var tmp = end; + end = start; + start = tmp; + } + + // reference month for determining days in month + ts.refMonth = new Date(start.getFullYear(), start.getMonth(), 15, 12, 0, 0); + try { + // reset to initial deltas + ts.millennia = 0; + ts.centuries = 0; + ts.decades = 0; + ts.years = end.getFullYear() - start.getFullYear(); + ts.months = end.getMonth() - start.getMonth(); + ts.weeks = 0; + ts.days = end.getDate() - start.getDate(); + ts.hours = end.getHours() - start.getHours(); + ts.minutes = end.getMinutes() - start.getMinutes(); + ts.seconds = end.getSeconds() - start.getSeconds(); + ts.milliseconds = end.getMilliseconds() - start.getMilliseconds(); + + ripple(ts); + pruneUnits(ts, units, max, digits); + + } finally { + delete ts.refMonth; + } + + return ts; + } + + /** + * Determine an appropriate refresh rate based upon units + * + * @private + * @param {number} units the units to populate + * @return {number} milliseconds to delay + */ + function getDelay(units) { + if (units & MILLISECONDS) { + // refresh very quickly + return MILLISECONDS_PER_SECOND / 30; //30Hz + } + + if (units & SECONDS) { + // refresh every second + return MILLISECONDS_PER_SECOND; //1Hz + } + + if (units & MINUTES) { + // refresh every minute + return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE; + } + + if (units & HOURS) { + // refresh hourly + return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + } + + if (units & DAYS) { + // refresh daily + return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY; + } + + // refresh the rest weekly + return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * DAYS_PER_WEEK; + } + + /** + * API entry point + * + * @public + * @param {Date|number|Timespan|null|function(Timespan,number)} start the starting date + * @param {Date|number|Timespan|null|function(Timespan,number)} end the ending date + * @param {number=} units the units to populate + * @param {number=} max number of labels to output + * @param {number=} digits max number of decimal digits to output + * @return {Timespan|number} + */ + function countdown(start, end, units, max, digits) { + var callback; + + // ensure some units or use defaults + units = +units || DEFAULTS; + // max must be positive + max = (max > 0) ? max : NaN; + // clamp digits to an integer between [0, 20] + digits = (digits > 0) ? (digits < 20) ? Math.round(digits) : 20 : 0; + + // ensure start date + var startTS = null; + if ('function' === typeof start) { + callback = start; + start = null; + + } else if (!(start instanceof Date)) { + if ((start !== null) && isFinite(start)) { + start = new Date(+start); + } else { + if ('object' === typeof startTS) { + startTS = /** @type{Timespan} */(start); + } + start = null; + } + } + + // ensure end date + var endTS = null; + if ('function' === typeof end) { + callback = end; + end = null; + + } else if (!(end instanceof Date)) { + if ((end !== null) && isFinite(end)) { + end = new Date(+end); + } else { + if ('object' === typeof end) { + endTS = /** @type{Timespan} */(end); + } + end = null; + } + } + + // must wait to interpret timespans until after resolving dates + if (startTS) { + start = addToDate(startTS, end); + } + if (endTS) { + end = addToDate(endTS, start); + } + + if (!start && !end) { + // used for unit testing + return new Timespan(); + } + + if (!callback) { + return populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits)); + } + + // base delay off units + var delay = getDelay(units), + timerId, + fn = function() { + callback( + populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits)), + timerId + ); + }; + + fn(); + return (timerId = setInterval(fn, delay)); + } + + /** + * @public + * @const + * @type {number} + */ + countdown.MILLISECONDS = MILLISECONDS; + + /** + * @public + * @const + * @type {number} + */ + countdown.SECONDS = SECONDS; + + /** + * @public + * @const + * @type {number} + */ + countdown.MINUTES = MINUTES; + + /** + * @public + * @const + * @type {number} + */ + countdown.HOURS = HOURS; + + /** + * @public + * @const + * @type {number} + */ + countdown.DAYS = DAYS; + + /** + * @public + * @const + * @type {number} + */ + countdown.WEEKS = WEEKS; + + /** + * @public + * @const + * @type {number} + */ + countdown.MONTHS = MONTHS; + + /** + * @public + * @const + * @type {number} + */ + countdown.YEARS = YEARS; + + /** + * @public + * @const + * @type {number} + */ + countdown.DECADES = DECADES; + + /** + * @public + * @const + * @type {number} + */ + countdown.CENTURIES = CENTURIES; + + /** + * @public + * @const + * @type {number} + */ + countdown.MILLENNIA = MILLENNIA; + + /** + * @public + * @const + * @type {number} + */ + countdown.DEFAULTS = DEFAULTS; + + /** + * @public + * @const + * @type {number} + */ + countdown.ALL = MILLENNIA|CENTURIES|DECADES|YEARS|MONTHS|WEEKS|DAYS|HOURS|MINUTES|SECONDS|MILLISECONDS; + + /** + * Customize the format settings. + * @public + * @param {Object} format settings object + */ + var setFormat = countdown.setFormat = function(format) { + if (!format) { return; } + + if ('singular' in format || 'plural' in format) { + var singular = format.singular || []; + if (singular.split) { + singular = singular.split('|'); + } + var plural = format.plural || []; + if (plural.split) { + plural = plural.split('|'); + } + + for (var i=LABEL_MILLISECONDS; i<=LABEL_MILLENNIA; i++) { + // override any specified units + LABELS_SINGLUAR[i] = singular[i] || LABELS_SINGLUAR[i]; + LABELS_PLURAL[i] = plural[i] || LABELS_PLURAL[i]; + } + } + + if ('string' === typeof format.last) { + LABEL_LAST = format.last; + } + if ('string' === typeof format.delim) { + LABEL_DELIM = format.delim; + } + if ('string' === typeof format.empty) { + LABEL_NOW = format.empty; + } + if ('function' === typeof format.formatNumber) { + formatNumber = format.formatNumber; + } + if ('function' === typeof format.formatter) { + formatter = format.formatter; + } + }; + + /** + * Revert to the default formatting. + * @public + */ + var resetFormat = countdown.resetFormat = function() { + LABELS_SINGLUAR = ' millisecond| second| minute| hour| day| week| month| year| decade| century| millennium'.split('|'); + LABELS_PLURAL = ' milliseconds| seconds| minutes| hours| days| weeks| months| years| decades| centuries| millennia'.split('|'); + LABEL_LAST = ' and '; + LABEL_DELIM = ', '; + LABEL_NOW = ''; + formatNumber = function(value) { return value; }; + formatter = plurality; + }; + + /** + * Override the unit labels. + * @public + * @param {string|Array=} singular a pipe ('|') delimited list of singular unit name overrides + * @param {string|Array=} plural a pipe ('|') delimited list of plural unit name overrides + * @param {string=} last a delimiter before the last unit (default: ' and ') + * @param {string=} delim a delimiter to use between all other units (default: ', ') + * @param {string=} empty a label to use when all units are zero (default: '') + * @param {function(number):string=} formatNumber a function which formats numbers as a string + * @param {function(number,number):string=} formatter a function which formats a number/unit pair as a string + * @deprecated since version 2.6.0 + */ + countdown.setLabels = function(singular, plural, last, delim, empty, formatNumber, formatter) { + setFormat({ + singular: singular, + plural: plural, + last: last, + delim: delim, + empty: empty, + formatNumber: formatNumber, + formatter: formatter + }); + }; + + /** + * Revert to the default unit labels. + * @public + * @deprecated since version 2.6.0 + */ + countdown.resetLabels = resetFormat; + + resetFormat(); + + if (typeof module !== 'undefined' && module.exports) { + module.exports = countdown; + + } else if (typeof window !== 'undefined' && typeof window.define === 'function' && typeof window.define.amd !== 'undefined') { + window.define('countdown', [], function() { + return countdown; + }); + } + + return countdown; + + })(); + \ No newline at end of file diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html index 18e4830c7..03286dc12 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html @@ -47,20 +47,34 @@
-
Next Halving
-
- - {{ i }} blocks - {{ i }} block +
Next Halving
+
+ {{ timeUntilHalving | date }} +
+ +
+ +
+ In + {{ countdownObject.years }} years + {{ countdownObject.months }} months + {{ countdownObject.days }} days + {{ countdownObject.hours }} hours +
+
+
-
-
+ + + {{ i }} blocks + {{ i }} block + +
diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts index c23d7d4b9..543266531 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { combineLatest, Observable, timer } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { combineLatest, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { StateService } from '../../services/state.service'; +const countdown = require('./countdown'); interface EpochProgress { base: string; @@ -15,6 +16,7 @@ interface EpochProgress { previousRetarget: number; blocksUntilHalving: number; timeUntilHalving: number; + timeAvg: number; } @Component({ @@ -26,6 +28,9 @@ interface EpochProgress { export class DifficultyMiningComponent implements OnInit { isLoadingWebSocket$: Observable; difficultyEpoch$: Observable; + countdownObject = null; + timeUntilHalving = 0; + now = new Date().getTime(); @Input() showProgress = true; @Input() showHalving = false; @@ -65,7 +70,20 @@ export class DifficultyMiningComponent implements OnInit { } const blocksUntilHalving = 210000 - (maxHeight % 210000); - const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000); + this.timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000); + this.now = new Date().getTime(); + + if (blocksUntilHalving - 1 === 0) { + this.countdownObject = null; + } else { + this.countdownObject = countdown(this.timeUntilHalving, new Date().getTime(), countdown.YEARS | countdown.MONTHS); + if (this.countdownObject.years === 0) { + this.countdownObject = countdown(this.timeUntilHalving, new Date().getTime(), countdown.DAYS | countdown.HOURS); + } + if (this.countdownObject.hours === 0) { + this.countdownObject = countdown(this.timeUntilHalving, new Date().getTime(), countdown.MINUTES); + } + } const data = { base: `${da.progressPercent.toFixed(2)}%`, @@ -78,7 +96,8 @@ export class DifficultyMiningComponent implements OnInit { estimatedRetargetDate: da.estimatedRetargetDate, previousRetarget: da.previousRetarget, blocksUntilHalving, - timeUntilHalving, + timeUntilHalving: this.timeUntilHalving, + timeAvg: da.timeAvg, }; return data; }) From 87910a6eb78e028d36a6a5989a51bfdac866747e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 27 Jan 2024 19:11:43 +0000 Subject: [PATCH 2/2] Display halving countdown with double units --- .../components/difficulty-mining/countdown.js | 1358 ----------------- .../difficulty-mining.component.html | 10 +- .../difficulty-mining.component.ts | 21 +- .../src/app/components/time/time.component.ts | 216 +-- 4 files changed, 126 insertions(+), 1479 deletions(-) delete mode 100644 frontend/src/app/components/difficulty-mining/countdown.js diff --git a/frontend/src/app/components/difficulty-mining/countdown.js b/frontend/src/app/components/difficulty-mining/countdown.js deleted file mode 100644 index 60909abb9..000000000 --- a/frontend/src/app/components/difficulty-mining/countdown.js +++ /dev/null @@ -1,1358 +0,0 @@ -/*global window module */ -/** - * @license countdown.js v2.6.1 http://countdownjs.org - * Copyright (c)2006-2014 Stephen M. McKamey. - * Licensed under The MIT License. - */ -/*jshint bitwise:false */ - -/** - * API entry - * @public - * @param {function(Object)|Date|number} start the starting date - * @param {function(Object)|Date|number} end the ending date - * @param {number} units the units to populate - * @return {Object|number} - */ -var countdown = ( - - function() { - /*jshint smarttabs:true */ - - 'use strict'; - - /** - * @private - * @const - * @type {number} - */ - var MILLISECONDS = 0x001; - - /** - * @private - * @const - * @type {number} - */ - var SECONDS = 0x002; - - /** - * @private - * @const - * @type {number} - */ - var MINUTES = 0x004; - - /** - * @private - * @const - * @type {number} - */ - var HOURS = 0x008; - - /** - * @private - * @const - * @type {number} - */ - var DAYS = 0x010; - - /** - * @private - * @const - * @type {number} - */ - var WEEKS = 0x020; - - /** - * @private - * @const - * @type {number} - */ - var MONTHS = 0x040; - - /** - * @private - * @const - * @type {number} - */ - var YEARS = 0x080; - - /** - * @private - * @const - * @type {number} - */ - var DECADES = 0x100; - - /** - * @private - * @const - * @type {number} - */ - var CENTURIES = 0x200; - - /** - * @private - * @const - * @type {number} - */ - var MILLENNIA = 0x400; - - /** - * @private - * @const - * @type {number} - */ - var DEFAULTS = YEARS|MONTHS|DAYS|HOURS|MINUTES|SECONDS; - - /** - * @private - * @const - * @type {number} - */ - var MILLISECONDS_PER_SECOND = 1000; - - /** - * @private - * @const - * @type {number} - */ - var SECONDS_PER_MINUTE = 60; - - /** - * @private - * @const - * @type {number} - */ - var MINUTES_PER_HOUR = 60; - - /** - * @private - * @const - * @type {number} - */ - var HOURS_PER_DAY = 24; - - /** - * @private - * @const - * @type {number} - */ - var MILLISECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND; - - /** - * @private - * @const - * @type {number} - */ - var DAYS_PER_WEEK = 7; - - /** - * @private - * @const - * @type {number} - */ - var MONTHS_PER_YEAR = 12; - - /** - * @private - * @const - * @type {number} - */ - var YEARS_PER_DECADE = 10; - - /** - * @private - * @const - * @type {number} - */ - var DECADES_PER_CENTURY = 10; - - /** - * @private - * @const - * @type {number} - */ - var CENTURIES_PER_MILLENNIUM = 10; - - /** - * @private - * @param {number} x number - * @return {number} - */ - var ceil = Math.ceil; - - /** - * @private - * @param {number} x number - * @return {number} - */ - var floor = Math.floor; - - /** - * @private - * @param {Date} ref reference date - * @param {number} shift number of months to shift - * @return {number} number of days shifted - */ - function borrowMonths(ref, shift) { - var prevTime = ref.getTime(); - - // increment month by shift - ref.setMonth( ref.getMonth() + shift ); - - // this is the trickiest since months vary in length - return Math.round( (ref.getTime() - prevTime) / MILLISECONDS_PER_DAY ); - } - - /** - * @private - * @param {Date} ref reference date - * @return {number} number of days - */ - function daysPerMonth(ref) { - var a = ref.getTime(); - - // increment month by 1 - var b = new Date(a); - b.setMonth( ref.getMonth() + 1 ); - - // this is the trickiest since months vary in length - return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY ); - } - - /** - * @private - * @param {Date} ref reference date - * @return {number} number of days - */ - function daysPerYear(ref) { - var a = ref.getTime(); - - // increment year by 1 - var b = new Date(a); - b.setFullYear( ref.getFullYear() + 1 ); - - // this is the trickiest since years (periodically) vary in length - return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY ); - } - - /** - * Applies the Timespan to the given date. - * - * @private - * @param {Timespan} ts - * @param {Date=} date - * @return {Date} - */ - function addToDate(ts, date) { - date = (date instanceof Date) || ((date !== null) && isFinite(date)) ? new Date(+date) : new Date(); - if (!ts) { - return date; - } - - // if there is a value field, use it directly - var value = +ts.value || 0; - if (value) { - date.setTime(date.getTime() + value); - return date; - } - - value = +ts.milliseconds || 0; - if (value) { - date.setMilliseconds(date.getMilliseconds() + value); - } - - value = +ts.seconds || 0; - if (value) { - date.setSeconds(date.getSeconds() + value); - } - - value = +ts.minutes || 0; - if (value) { - date.setMinutes(date.getMinutes() + value); - } - - value = +ts.hours || 0; - if (value) { - date.setHours(date.getHours() + value); - } - - value = +ts.weeks || 0; - if (value) { - value *= DAYS_PER_WEEK; - } - - value += +ts.days || 0; - if (value) { - date.setDate(date.getDate() + value); - } - - value = +ts.months || 0; - if (value) { - date.setMonth(date.getMonth() + value); - } - - value = +ts.millennia || 0; - if (value) { - value *= CENTURIES_PER_MILLENNIUM; - } - - value += +ts.centuries || 0; - if (value) { - value *= DECADES_PER_CENTURY; - } - - value += +ts.decades || 0; - if (value) { - value *= YEARS_PER_DECADE; - } - - value += +ts.years || 0; - if (value) { - date.setFullYear(date.getFullYear() + value); - } - - return date; - } - - /** - * @private - * @const - * @type {number} - */ - var LABEL_MILLISECONDS = 0; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_SECONDS = 1; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_MINUTES = 2; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_HOURS = 3; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_DAYS = 4; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_WEEKS = 5; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_MONTHS = 6; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_YEARS = 7; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_DECADES = 8; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_CENTURIES = 9; - - /** - * @private - * @const - * @type {number} - */ - var LABEL_MILLENNIA = 10; - - /** - * @private - * @type {Array} - */ - var LABELS_SINGLUAR; - - /** - * @private - * @type {Array} - */ - var LABELS_PLURAL; - - /** - * @private - * @type {string} - */ - var LABEL_LAST; - - /** - * @private - * @type {string} - */ - var LABEL_DELIM; - - /** - * @private - * @type {string} - */ - var LABEL_NOW; - - /** - * Formats a number & unit as a string - * - * @param {number} value - * @param {number} unit - * @return {string} - */ - var formatter; - - /** - * Formats a number as a string - * - * @private - * @param {number} value - * @return {string} - */ - var formatNumber; - - /** - * @private - * @param {number} value - * @param {number} unit unit index into label list - * @return {string} - */ - function plurality(value, unit) { - return formatNumber(value)+((value === 1) ? LABELS_SINGLUAR[unit] : LABELS_PLURAL[unit]); - } - - /** - * Formats the entries with singular or plural labels - * - * @private - * @param {Timespan} ts - * @return {Array} - */ - var formatList; - - /** - * Timespan representation of a duration of time - * - * @private - * @this {Timespan} - * @constructor - */ - function Timespan() {} - - /** - * Formats the Timespan as a sentence - * - * @param {string=} emptyLabel the string to use when no values returned - * @return {string} - */ - Timespan.prototype.toString = function(emptyLabel) { - var label = formatList(this); - - var count = label.length; - if (!count) { - return emptyLabel ? ''+emptyLabel : LABEL_NOW; - } - if (count === 1) { - return label[0]; - } - - var last = LABEL_LAST+label.pop(); - return label.join(LABEL_DELIM)+last; - }; - - /** - * Formats the Timespan as a sentence in HTML - * - * @param {string=} tag HTML tag name to wrap each value - * @param {string=} emptyLabel the string to use when no values returned - * @return {string} - */ - Timespan.prototype.toHTML = function(tag, emptyLabel) { - tag = tag || 'span'; - var label = formatList(this); - - var count = label.length; - if (!count) { - emptyLabel = emptyLabel || LABEL_NOW; - return emptyLabel ? '<'+tag+'>'+emptyLabel+'' : emptyLabel; - } - for (var i=0; i'+label[i]+''; - } - if (count === 1) { - return label[0]; - } - - var last = LABEL_LAST+label.pop(); - return label.join(LABEL_DELIM)+last; - }; - - /** - * Applies the Timespan to the given date - * - * @param {Date=} date the date to which the timespan is added. - * @return {Date} - */ - Timespan.prototype.addTo = function(date) { - return addToDate(this, date); - }; - - /** - * Formats the entries as English labels - * - * @private - * @param {Timespan} ts - * @return {Array} - */ - formatList = function(ts) { - var list = []; - - var value = ts.millennia; - if (value) { - list.push(formatter(value, LABEL_MILLENNIA)); - } - - value = ts.centuries; - if (value) { - list.push(formatter(value, LABEL_CENTURIES)); - } - - value = ts.decades; - if (value) { - list.push(formatter(value, LABEL_DECADES)); - } - - value = ts.years; - if (value) { - list.push(formatter(value, LABEL_YEARS)); - } - - value = ts.months; - if (value) { - list.push(formatter(value, LABEL_MONTHS)); - } - - value = ts.weeks; - if (value) { - list.push(formatter(value, LABEL_WEEKS)); - } - - value = ts.days; - if (value) { - list.push(formatter(value, LABEL_DAYS)); - } - - value = ts.hours; - if (value) { - list.push(formatter(value, LABEL_HOURS)); - } - - value = ts.minutes; - if (value) { - list.push(formatter(value, LABEL_MINUTES)); - } - - value = ts.seconds; - if (value) { - list.push(formatter(value, LABEL_SECONDS)); - } - - value = ts.milliseconds; - if (value) { - list.push(formatter(value, LABEL_MILLISECONDS)); - } - - return list; - }; - - /** - * Borrow any underflow units, carry any overflow units - * - * @private - * @param {Timespan} ts - * @param {string} toUnit - */ - function rippleRounded(ts, toUnit) { - switch (toUnit) { - case 'seconds': - if (ts.seconds !== SECONDS_PER_MINUTE || isNaN(ts.minutes)) { - return; - } - // ripple seconds up to minutes - ts.minutes++; - ts.seconds = 0; - - /* falls through */ - case 'minutes': - if (ts.minutes !== MINUTES_PER_HOUR || isNaN(ts.hours)) { - return; - } - // ripple minutes up to hours - ts.hours++; - ts.minutes = 0; - - /* falls through */ - case 'hours': - if (ts.hours !== HOURS_PER_DAY || isNaN(ts.days)) { - return; - } - // ripple hours up to days - ts.days++; - ts.hours = 0; - - /* falls through */ - case 'days': - if (ts.days !== DAYS_PER_WEEK || isNaN(ts.weeks)) { - return; - } - // ripple days up to weeks - ts.weeks++; - ts.days = 0; - - /* falls through */ - case 'weeks': - if (ts.weeks !== daysPerMonth(ts.refMonth)/DAYS_PER_WEEK || isNaN(ts.months)) { - return; - } - // ripple weeks up to months - ts.months++; - ts.weeks = 0; - - /* falls through */ - case 'months': - if (ts.months !== MONTHS_PER_YEAR || isNaN(ts.years)) { - return; - } - // ripple months up to years - ts.years++; - ts.months = 0; - - /* falls through */ - case 'years': - if (ts.years !== YEARS_PER_DECADE || isNaN(ts.decades)) { - return; - } - // ripple years up to decades - ts.decades++; - ts.years = 0; - - /* falls through */ - case 'decades': - if (ts.decades !== DECADES_PER_CENTURY || isNaN(ts.centuries)) { - return; - } - // ripple decades up to centuries - ts.centuries++; - ts.decades = 0; - - /* falls through */ - case 'centuries': - if (ts.centuries !== CENTURIES_PER_MILLENNIUM || isNaN(ts.millennia)) { - return; - } - // ripple centuries up to millennia - ts.millennia++; - ts.centuries = 0; - /* falls through */ - } - } - - /** - * Ripple up partial units one place - * - * @private - * @param {Timespan} ts timespan - * @param {number} frac accumulated fractional value - * @param {string} fromUnit source unit name - * @param {string} toUnit target unit name - * @param {number} conversion multiplier between units - * @param {number} digits max number of decimal digits to output - * @return {number} new fractional value - */ - function fraction(ts, frac, fromUnit, toUnit, conversion, digits) { - if (ts[fromUnit] >= 0) { - frac += ts[fromUnit]; - delete ts[fromUnit]; - } - - frac /= conversion; - if (frac + 1 <= 1) { - // drop if below machine epsilon - return 0; - } - - if (ts[toUnit] >= 0) { - // ensure does not have more than specified number of digits - ts[toUnit] = +(ts[toUnit] + frac).toFixed(digits); - rippleRounded(ts, toUnit); - return 0; - } - - return frac; - } - - /** - * Ripple up partial units to next existing - * - * @private - * @param {Timespan} ts - * @param {number} digits max number of decimal digits to output - */ - function fractional(ts, digits) { - var frac = fraction(ts, 0, 'milliseconds', 'seconds', MILLISECONDS_PER_SECOND, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'seconds', 'minutes', SECONDS_PER_MINUTE, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'minutes', 'hours', MINUTES_PER_HOUR, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'hours', 'days', HOURS_PER_DAY, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'days', 'weeks', DAYS_PER_WEEK, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'weeks', 'months', daysPerMonth(ts.refMonth)/DAYS_PER_WEEK, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'months', 'years', daysPerYear(ts.refMonth)/daysPerMonth(ts.refMonth), digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'years', 'decades', YEARS_PER_DECADE, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'decades', 'centuries', DECADES_PER_CENTURY, digits); - if (!frac) { return; } - - frac = fraction(ts, frac, 'centuries', 'millennia', CENTURIES_PER_MILLENNIUM, digits); - - // should never reach this with remaining fractional value - if (frac) { throw new Error('Fractional unit overflow'); } - } - - /** - * Borrow any underflow units, carry any overflow units - * - * @private - * @param {Timespan} ts - */ - function ripple(ts) { - var x; - - if (ts.milliseconds < 0) { - // ripple seconds down to milliseconds - x = ceil(-ts.milliseconds / MILLISECONDS_PER_SECOND); - ts.seconds -= x; - ts.milliseconds += x * MILLISECONDS_PER_SECOND; - - } else if (ts.milliseconds >= MILLISECONDS_PER_SECOND) { - // ripple milliseconds up to seconds - ts.seconds += floor(ts.milliseconds / MILLISECONDS_PER_SECOND); - ts.milliseconds %= MILLISECONDS_PER_SECOND; - } - - if (ts.seconds < 0) { - // ripple minutes down to seconds - x = ceil(-ts.seconds / SECONDS_PER_MINUTE); - ts.minutes -= x; - ts.seconds += x * SECONDS_PER_MINUTE; - - } else if (ts.seconds >= SECONDS_PER_MINUTE) { - // ripple seconds up to minutes - ts.minutes += floor(ts.seconds / SECONDS_PER_MINUTE); - ts.seconds %= SECONDS_PER_MINUTE; - } - - if (ts.minutes < 0) { - // ripple hours down to minutes - x = ceil(-ts.minutes / MINUTES_PER_HOUR); - ts.hours -= x; - ts.minutes += x * MINUTES_PER_HOUR; - - } else if (ts.minutes >= MINUTES_PER_HOUR) { - // ripple minutes up to hours - ts.hours += floor(ts.minutes / MINUTES_PER_HOUR); - ts.minutes %= MINUTES_PER_HOUR; - } - - if (ts.hours < 0) { - // ripple days down to hours - x = ceil(-ts.hours / HOURS_PER_DAY); - ts.days -= x; - ts.hours += x * HOURS_PER_DAY; - - } else if (ts.hours >= HOURS_PER_DAY) { - // ripple hours up to days - ts.days += floor(ts.hours / HOURS_PER_DAY); - ts.hours %= HOURS_PER_DAY; - } - - while (ts.days < 0) { - // NOTE: never actually seen this loop more than once - - // ripple months down to days - ts.months--; - ts.days += borrowMonths(ts.refMonth, 1); - } - - // weeks is always zero here - - if (ts.days >= DAYS_PER_WEEK) { - // ripple days up to weeks - ts.weeks += floor(ts.days / DAYS_PER_WEEK); - ts.days %= DAYS_PER_WEEK; - } - - if (ts.months < 0) { - // ripple years down to months - x = ceil(-ts.months / MONTHS_PER_YEAR); - ts.years -= x; - ts.months += x * MONTHS_PER_YEAR; - - } else if (ts.months >= MONTHS_PER_YEAR) { - // ripple months up to years - ts.years += floor(ts.months / MONTHS_PER_YEAR); - ts.months %= MONTHS_PER_YEAR; - } - - // years is always non-negative here - // decades, centuries and millennia are always zero here - - if (ts.years >= YEARS_PER_DECADE) { - // ripple years up to decades - ts.decades += floor(ts.years / YEARS_PER_DECADE); - ts.years %= YEARS_PER_DECADE; - - if (ts.decades >= DECADES_PER_CENTURY) { - // ripple decades up to centuries - ts.centuries += floor(ts.decades / DECADES_PER_CENTURY); - ts.decades %= DECADES_PER_CENTURY; - - if (ts.centuries >= CENTURIES_PER_MILLENNIUM) { - // ripple centuries up to millennia - ts.millennia += floor(ts.centuries / CENTURIES_PER_MILLENNIUM); - ts.centuries %= CENTURIES_PER_MILLENNIUM; - } - } - } - } - - /** - * Remove any units not requested - * - * @private - * @param {Timespan} ts - * @param {number} units the units to populate - * @param {number} max number of labels to output - * @param {number} digits max number of decimal digits to output - */ - function pruneUnits(ts, units, max, digits) { - var count = 0; - - // Calc from largest unit to smallest to prevent underflow - if (!(units & MILLENNIA) || (count >= max)) { - // ripple millennia down to centuries - ts.centuries += ts.millennia * CENTURIES_PER_MILLENNIUM; - delete ts.millennia; - - } else if (ts.millennia) { - count++; - } - - if (!(units & CENTURIES) || (count >= max)) { - // ripple centuries down to decades - ts.decades += ts.centuries * DECADES_PER_CENTURY; - delete ts.centuries; - - } else if (ts.centuries) { - count++; - } - - if (!(units & DECADES) || (count >= max)) { - // ripple decades down to years - ts.years += ts.decades * YEARS_PER_DECADE; - delete ts.decades; - - } else if (ts.decades) { - count++; - } - - if (!(units & YEARS) || (count >= max)) { - // ripple years down to months - ts.months += ts.years * MONTHS_PER_YEAR; - delete ts.years; - - } else if (ts.years) { - count++; - } - - if (!(units & MONTHS) || (count >= max)) { - // ripple months down to days - if (ts.months) { - ts.days += borrowMonths(ts.refMonth, ts.months); - } - delete ts.months; - - if (ts.days >= DAYS_PER_WEEK) { - // ripple day overflow back up to weeks - ts.weeks += floor(ts.days / DAYS_PER_WEEK); - ts.days %= DAYS_PER_WEEK; - } - - } else if (ts.months) { - count++; - } - - if (!(units & WEEKS) || (count >= max)) { - // ripple weeks down to days - ts.days += ts.weeks * DAYS_PER_WEEK; - delete ts.weeks; - - } else if (ts.weeks) { - count++; - } - - if (!(units & DAYS) || (count >= max)) { - //ripple days down to hours - ts.hours += ts.days * HOURS_PER_DAY; - delete ts.days; - - } else if (ts.days) { - count++; - } - - if (!(units & HOURS) || (count >= max)) { - // ripple hours down to minutes - ts.minutes += ts.hours * MINUTES_PER_HOUR; - delete ts.hours; - - } else if (ts.hours) { - count++; - } - - if (!(units & MINUTES) || (count >= max)) { - // ripple minutes down to seconds - ts.seconds += ts.minutes * SECONDS_PER_MINUTE; - delete ts.minutes; - - } else if (ts.minutes) { - count++; - } - - if (!(units & SECONDS) || (count >= max)) { - // ripple seconds down to milliseconds - ts.milliseconds += ts.seconds * MILLISECONDS_PER_SECOND; - delete ts.seconds; - - } else if (ts.seconds) { - count++; - } - - // nothing to ripple milliseconds down to - // so ripple back up to smallest existing unit as a fractional value - if (!(units & MILLISECONDS) || (count >= max)) { - fractional(ts, digits); - } - } - - /** - * Populates the Timespan object - * - * @private - * @param {Timespan} ts - * @param {?Date} start the starting date - * @param {?Date} end the ending date - * @param {number} units the units to populate - * @param {number} max number of labels to output - * @param {number} digits max number of decimal digits to output - */ - function populate(ts, start, end, units, max, digits) { - var now = new Date(); - - ts.start = start = start || now; - ts.end = end = end || now; - ts.units = units; - - ts.value = end.getTime() - start.getTime(); - if (ts.value < 0) { - // swap if reversed - var tmp = end; - end = start; - start = tmp; - } - - // reference month for determining days in month - ts.refMonth = new Date(start.getFullYear(), start.getMonth(), 15, 12, 0, 0); - try { - // reset to initial deltas - ts.millennia = 0; - ts.centuries = 0; - ts.decades = 0; - ts.years = end.getFullYear() - start.getFullYear(); - ts.months = end.getMonth() - start.getMonth(); - ts.weeks = 0; - ts.days = end.getDate() - start.getDate(); - ts.hours = end.getHours() - start.getHours(); - ts.minutes = end.getMinutes() - start.getMinutes(); - ts.seconds = end.getSeconds() - start.getSeconds(); - ts.milliseconds = end.getMilliseconds() - start.getMilliseconds(); - - ripple(ts); - pruneUnits(ts, units, max, digits); - - } finally { - delete ts.refMonth; - } - - return ts; - } - - /** - * Determine an appropriate refresh rate based upon units - * - * @private - * @param {number} units the units to populate - * @return {number} milliseconds to delay - */ - function getDelay(units) { - if (units & MILLISECONDS) { - // refresh very quickly - return MILLISECONDS_PER_SECOND / 30; //30Hz - } - - if (units & SECONDS) { - // refresh every second - return MILLISECONDS_PER_SECOND; //1Hz - } - - if (units & MINUTES) { - // refresh every minute - return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE; - } - - if (units & HOURS) { - // refresh hourly - return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; - } - - if (units & DAYS) { - // refresh daily - return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY; - } - - // refresh the rest weekly - return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * DAYS_PER_WEEK; - } - - /** - * API entry point - * - * @public - * @param {Date|number|Timespan|null|function(Timespan,number)} start the starting date - * @param {Date|number|Timespan|null|function(Timespan,number)} end the ending date - * @param {number=} units the units to populate - * @param {number=} max number of labels to output - * @param {number=} digits max number of decimal digits to output - * @return {Timespan|number} - */ - function countdown(start, end, units, max, digits) { - var callback; - - // ensure some units or use defaults - units = +units || DEFAULTS; - // max must be positive - max = (max > 0) ? max : NaN; - // clamp digits to an integer between [0, 20] - digits = (digits > 0) ? (digits < 20) ? Math.round(digits) : 20 : 0; - - // ensure start date - var startTS = null; - if ('function' === typeof start) { - callback = start; - start = null; - - } else if (!(start instanceof Date)) { - if ((start !== null) && isFinite(start)) { - start = new Date(+start); - } else { - if ('object' === typeof startTS) { - startTS = /** @type{Timespan} */(start); - } - start = null; - } - } - - // ensure end date - var endTS = null; - if ('function' === typeof end) { - callback = end; - end = null; - - } else if (!(end instanceof Date)) { - if ((end !== null) && isFinite(end)) { - end = new Date(+end); - } else { - if ('object' === typeof end) { - endTS = /** @type{Timespan} */(end); - } - end = null; - } - } - - // must wait to interpret timespans until after resolving dates - if (startTS) { - start = addToDate(startTS, end); - } - if (endTS) { - end = addToDate(endTS, start); - } - - if (!start && !end) { - // used for unit testing - return new Timespan(); - } - - if (!callback) { - return populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits)); - } - - // base delay off units - var delay = getDelay(units), - timerId, - fn = function() { - callback( - populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits)), - timerId - ); - }; - - fn(); - return (timerId = setInterval(fn, delay)); - } - - /** - * @public - * @const - * @type {number} - */ - countdown.MILLISECONDS = MILLISECONDS; - - /** - * @public - * @const - * @type {number} - */ - countdown.SECONDS = SECONDS; - - /** - * @public - * @const - * @type {number} - */ - countdown.MINUTES = MINUTES; - - /** - * @public - * @const - * @type {number} - */ - countdown.HOURS = HOURS; - - /** - * @public - * @const - * @type {number} - */ - countdown.DAYS = DAYS; - - /** - * @public - * @const - * @type {number} - */ - countdown.WEEKS = WEEKS; - - /** - * @public - * @const - * @type {number} - */ - countdown.MONTHS = MONTHS; - - /** - * @public - * @const - * @type {number} - */ - countdown.YEARS = YEARS; - - /** - * @public - * @const - * @type {number} - */ - countdown.DECADES = DECADES; - - /** - * @public - * @const - * @type {number} - */ - countdown.CENTURIES = CENTURIES; - - /** - * @public - * @const - * @type {number} - */ - countdown.MILLENNIA = MILLENNIA; - - /** - * @public - * @const - * @type {number} - */ - countdown.DEFAULTS = DEFAULTS; - - /** - * @public - * @const - * @type {number} - */ - countdown.ALL = MILLENNIA|CENTURIES|DECADES|YEARS|MONTHS|WEEKS|DAYS|HOURS|MINUTES|SECONDS|MILLISECONDS; - - /** - * Customize the format settings. - * @public - * @param {Object} format settings object - */ - var setFormat = countdown.setFormat = function(format) { - if (!format) { return; } - - if ('singular' in format || 'plural' in format) { - var singular = format.singular || []; - if (singular.split) { - singular = singular.split('|'); - } - var plural = format.plural || []; - if (plural.split) { - plural = plural.split('|'); - } - - for (var i=LABEL_MILLISECONDS; i<=LABEL_MILLENNIA; i++) { - // override any specified units - LABELS_SINGLUAR[i] = singular[i] || LABELS_SINGLUAR[i]; - LABELS_PLURAL[i] = plural[i] || LABELS_PLURAL[i]; - } - } - - if ('string' === typeof format.last) { - LABEL_LAST = format.last; - } - if ('string' === typeof format.delim) { - LABEL_DELIM = format.delim; - } - if ('string' === typeof format.empty) { - LABEL_NOW = format.empty; - } - if ('function' === typeof format.formatNumber) { - formatNumber = format.formatNumber; - } - if ('function' === typeof format.formatter) { - formatter = format.formatter; - } - }; - - /** - * Revert to the default formatting. - * @public - */ - var resetFormat = countdown.resetFormat = function() { - LABELS_SINGLUAR = ' millisecond| second| minute| hour| day| week| month| year| decade| century| millennium'.split('|'); - LABELS_PLURAL = ' milliseconds| seconds| minutes| hours| days| weeks| months| years| decades| centuries| millennia'.split('|'); - LABEL_LAST = ' and '; - LABEL_DELIM = ', '; - LABEL_NOW = ''; - formatNumber = function(value) { return value; }; - formatter = plurality; - }; - - /** - * Override the unit labels. - * @public - * @param {string|Array=} singular a pipe ('|') delimited list of singular unit name overrides - * @param {string|Array=} plural a pipe ('|') delimited list of plural unit name overrides - * @param {string=} last a delimiter before the last unit (default: ' and ') - * @param {string=} delim a delimiter to use between all other units (default: ', ') - * @param {string=} empty a label to use when all units are zero (default: '') - * @param {function(number):string=} formatNumber a function which formats numbers as a string - * @param {function(number,number):string=} formatter a function which formats a number/unit pair as a string - * @deprecated since version 2.6.0 - */ - countdown.setLabels = function(singular, plural, last, delim, empty, formatNumber, formatter) { - setFormat({ - singular: singular, - plural: plural, - last: last, - delim: delim, - empty: empty, - formatNumber: formatNumber, - formatter: formatter - }); - }; - - /** - * Revert to the default unit labels. - * @public - * @deprecated since version 2.6.0 - */ - countdown.resetLabels = resetFormat; - - resetFormat(); - - if (typeof module !== 'undefined' && module.exports) { - module.exports = countdown; - - } else if (typeof window !== 'undefined' && typeof window.define === 'function' && typeof window.define.amd !== 'undefined') { - window.define('countdown', [], function() { - return countdown; - }); - } - - return countdown; - - })(); - \ No newline at end of file diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html index 03286dc12..9f3706916 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html @@ -50,16 +50,12 @@
Next Halving
{{ timeUntilHalving | date }} -
+
- +
- In - {{ countdownObject.years }} years - {{ countdownObject.months }} months - {{ countdownObject.days }} days - {{ countdownObject.hours }} hours +
diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts index 543266531..c1283b8b1 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { StateService } from '../../services/state.service'; -const countdown = require('./countdown'); interface EpochProgress { base: string; @@ -28,7 +27,7 @@ interface EpochProgress { export class DifficultyMiningComponent implements OnInit { isLoadingWebSocket$: Observable; difficultyEpoch$: Observable; - countdownObject = null; + blocksUntilHalving: number | null = null; timeUntilHalving = 0; now = new Date().getTime(); @@ -69,21 +68,9 @@ export class DifficultyMiningComponent implements OnInit { colorPreviousAdjustments = '#ffffff66'; } - const blocksUntilHalving = 210000 - (maxHeight % 210000); - this.timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000); + this.blocksUntilHalving = 210000 - (maxHeight % 210000); + this.timeUntilHalving = new Date().getTime() + (this.blocksUntilHalving * 600000); this.now = new Date().getTime(); - - if (blocksUntilHalving - 1 === 0) { - this.countdownObject = null; - } else { - this.countdownObject = countdown(this.timeUntilHalving, new Date().getTime(), countdown.YEARS | countdown.MONTHS); - if (this.countdownObject.years === 0) { - this.countdownObject = countdown(this.timeUntilHalving, new Date().getTime(), countdown.DAYS | countdown.HOURS); - } - if (this.countdownObject.hours === 0) { - this.countdownObject = countdown(this.timeUntilHalving, new Date().getTime(), countdown.MINUTES); - } - } const data = { base: `${da.progressPercent.toFixed(2)}%`, @@ -95,7 +82,7 @@ export class DifficultyMiningComponent implements OnInit { newDifficultyHeight: da.nextRetargetHeight, estimatedRetargetDate: da.estimatedRetargetDate, previousRetarget: da.previousRetarget, - blocksUntilHalving, + blocksUntilHalving: this.blocksUntilHalving, timeUntilHalving: this.timeUntilHalving, timeAvg: da.timeAvg, }; diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index 2febf6238..679604ff5 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -10,7 +10,6 @@ import { dates } from '../../shared/i18n/dates'; export class TimeComponent implements OnInit, OnChanges, OnDestroy { interval: number; text: string; - units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']; precisionThresholds = { year: 100, month: 18, @@ -29,6 +28,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { @Input() fixedRender = false; @Input() relative = false; @Input() precision: number = 0; + @Input() numUnits: number = 1; + @Input() units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']; @Input() minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second'; @Input() fractionDigits: number = 0; @@ -94,6 +95,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { } let counter: number; + const result = []; + let usedUnits = 0; for (const [index, unit] of this.units.entries()) { let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)]; counter = Math.floor(seconds / this.intervals[unit]); @@ -105,107 +108,126 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { counter = Math.max(1, counter); } if (counter > 0) { - let rounded = Math.round(seconds / this.intervals[precisionUnit]); - if (this.fractionDigits) { - const roundFactor = Math.pow(10,this.fractionDigits); + let rounded; + const roundFactor = Math.pow(10,this.fractionDigits || 0); + if (this.kind === 'until' && usedUnits < this.numUnits) { + rounded = Math.floor((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; + } else { rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; } - const dateStrings = dates(rounded); - switch (this.kind) { - case 'since': - if (rounded === 1) { - switch (precisionUnit) { // singular (1 day) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break; - } - } else { - switch (precisionUnit) { // plural (2 days) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break; - } - } - break; - case 'until': - if (rounded === 1) { - switch (precisionUnit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (precisionUnit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'span': - if (rounded === 1) { - switch (precisionUnit) { // singular (1 day) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break; - } - } else { - switch (precisionUnit) { // plural (2 days) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - default: - if (rounded === 1) { - switch (precisionUnit) { // singular (1 day) - case 'year': return dateStrings.i18nYear; break; - case 'month': return dateStrings.i18nMonth; break; - case 'week': return dateStrings.i18nWeek; break; - case 'day': return dateStrings.i18nDay; break; - case 'hour': return dateStrings.i18nHour; break; - case 'minute': return dateStrings.i18nMinute; break; - case 'second': return dateStrings.i18nSecond; break; - } - } else { - switch (precisionUnit) { // plural (2 days) - case 'year': return dateStrings.i18nYears; break; - case 'month': return dateStrings.i18nMonths; break; - case 'week': return dateStrings.i18nWeeks; break; - case 'day': return dateStrings.i18nDays; break; - case 'hour': return dateStrings.i18nHours; break; - case 'minute': return dateStrings.i18nMinutes; break; - case 'second': return dateStrings.i18nSeconds; break; - } - } + if (this.kind !== 'until' || this.numUnits === 1) { + return this.formatTime(this.kind, precisionUnit, rounded); + } else { + if (!usedUnits) { + result.push(this.formatTime(this.kind, precisionUnit, rounded)); + } else { + result.push(this.formatTime('', precisionUnit, rounded)); + } + seconds -= (rounded * this.intervals[precisionUnit]); + usedUnits++; + if (usedUnits >= this.numUnits) { + return result.join(', '); + } } } } + return result.join(', '); } + private formatTime(kind, unit, number): string { + const dateStrings = dates(number); + switch (kind) { + case 'since': + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break; + case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break; + case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break; + case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break; + case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break; + case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break; + case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break; + case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break; + case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break; + case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break; + case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break; + case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break; + case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break; + } + } + break; + case 'until': + if (number === 1) { + switch (unit) { // singular (In ~1 day) + case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break; + case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break; + case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break; + case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break; + case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break; + case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`; + case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`; + } + } else { + switch (unit) { // plural (In ~2 days) + case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break; + case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break; + case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break; + case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break; + case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break; + case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break; + case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break; + } + } + break; + case 'span': + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break; + case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break; + case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break; + case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break; + case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break; + case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break; + case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break; + case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break; + case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break; + case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break; + case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break; + case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break; + case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break; + } + } + break; + default: + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return dateStrings.i18nYear; break; + case 'month': return dateStrings.i18nMonth; break; + case 'week': return dateStrings.i18nWeek; break; + case 'day': return dateStrings.i18nDay; break; + case 'hour': return dateStrings.i18nHour; break; + case 'minute': return dateStrings.i18nMinute; break; + case 'second': return dateStrings.i18nSecond; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return dateStrings.i18nYears; break; + case 'month': return dateStrings.i18nMonths; break; + case 'week': return dateStrings.i18nWeeks; break; + case 'day': return dateStrings.i18nDays; break; + case 'hour': return dateStrings.i18nHours; break; + case 'minute': return dateStrings.i18nMinutes; break; + case 'second': return dateStrings.i18nSeconds; break; + } + } + } + } }