VulpesOS/gaia/apps/email/js/ext/date.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

240 lines
8.2 KiB
JavaScript
Executable file

define(
[
'module',
'exports'
],
function(
$module,
exports
) {
////////////////////////////////////////////////////////////////////////////////
// Time
//
// == JS Dates
//
// We primarily deal in UTC timestamps. When we need to talk dates with IMAP
// (see next section), we need these timestamps to line up with midnight for
// a given day. We do not need to line up with weeks, months, or years,
// saving us a lot of complexity.
//
// Day algebra is straightforward because JS Date objects have no concept of
// leap seconds. We don't need to worry that a leap second will cause adding
// a day to be less than or more than a day. Hooray!
//
// == IMAP and Time
//
// The stock IMAP SEARCH command's SINCE and BEFORE predicates only operate on
// whole-dates (and ignore the non-date time parts). Additionally, SINCE is
// inclusive and BEFORE is exclusive.
//
// We use JS millisecond timestamp values throughout, and it's important to us
// that our date logic is consistent with IMAP's time logic where relevant.
// All of our IMAP-exposed time-interval related logic operates on day
// granularities. Our timestamp/date values are always normalized to midnight
// which happily works out with intuitive range operations.
//
// Observe the pretty ASCII art where as you move to the right you are moving
// forward in time.
//
// ________________________________________
// BEFORE)| midnight (0 millis) ... 11:59:59:999 |
// ON_OR_BEFORE]
// [SINCE......................................
// (AFTER.....................................
//
// Our date range comparisons (noting that larger timestamps are 'younger') are:
// SINCE analog: (testDate >= comparisonDate)
// testDate is as-recent-as or more-recent-than the comparisonDate.
// BEFORE analog: (testDate < comparisonDate)
// testDate is less-recent-than the comparisonDate
//
// Because "who is the test date and who is the range value under discussion"
// can be unclear and the numerical direction of time is not always intuitive,
// I'm introducing simple BEFORE and SINCE helper functions to try and make
// the comparison logic ridiculously explicit as well as calling out where we
// are being consistent with IMAP.
//
// Not all of our time logic is consistent with IMAP! Specifically, use of
// exclusive time bounds without secondary comparison keys means that ranges
// defined in this way cannot spread messages with the same timestamp over
// multiple ranges. This allows for pathological data structure situations
// where there's too much data in a data block, etc.
// Our date ranges are defined by 'startTS' and 'endTS'. Using math syntax, our
// IMAP-consistent time ranges end up as: [startTS, endTS). It is always true
// that BEFORE(startTS, endTS) and SINCE(endTS, startTS) in these cases.
//
// As such, I've also created an ON_OR_BEFORE helper that allows equivalence and
// STRICTLY_AFTER that does not check equivalence to round out all possibilities
// while still being rather explicit.
/**
* IMAP-consistent date comparison; read this as "Is `testDate` BEFORE
* `comparisonDate`"?
*
* !BEFORE(a, b) === SINCE(a, b)
*/
var BEFORE = exports.BEFORE =
function BEFORE(testDate, comparisonDate) {
// testDate is numerically less than comparisonDate, so it is chronologically
// before it.
return testDate < comparisonDate;
};
var ON_OR_BEFORE = exports.ON_OR_BEFORE =
function ON_OR_BEFORE(testDate, comparisonDate) {
return testDate <= comparisonDate;
};
/**
* IMAP-consistent date comparison; read this as "Is `testDate` SINCE
* `comparisonDate`"?
*
* !SINCE(a, b) === BEFORE(a, b)
*/
var SINCE = exports.SINCE =
function SINCE(testDate, comparisonDate) {
// testDate is numerically greater-than-or-equal-to comparisonDate, so it
// chronologically after/since it.
return testDate >= comparisonDate;
};
var STRICTLY_AFTER = exports.STRICTLY_AFTER =
function STRICTLY_AFTER(testDate, comparisonDate) {
return testDate > comparisonDate;
};
var IN_BS_DATE_RANGE = exports.IN_BS_DATE_RANGE =
function IN_BS_DATE_RANGE(testDate, startTS, endTS) {
return testDate >= startTS && testDate < endTS;
};
var PASTWARDS = 1, FUTUREWARDS = -1;
/**
* Check if `testDate` is "beyond" the comparison date given the `dir`. If
* the direction is pastwards, we will return true if testDate happened
* chronologically before comparisonDate. If the direction is futurewards,
* we will return true if testDate happened chronologically after
* comparisonDate.
*/
var TIME_DIR_AT_OR_BEYOND = exports.TIME_DIR_AT_OR_BEYOND =
function TIME_DIR_AT_OR_BEYOND(dir, testDate, comparisonDate) {
if (dir === PASTWARDS)
return testDate <= comparisonDate;
// we use null as a sentinel value for 'the future'/'now'
else if (comparisonDate === null)
return testDate >= NOW();
else // FUTUREWARDS
return testDate >= comparisonDate;
};
/**
* Compute the delta of the `testDate` relative to the `comparisonDate` where
* a positive value indicates `testDate` is beyond the `comparisonDate` in
* the given direction and a negative value indicates it is before it.
*/
var TIME_DIR_DELTA = exports.TIME_DIR_DELTA =
function TIME_DIR_DELTA(dir, testDate, comparisonDate) {
if (dir === PASTWARDS)
return testDate - comparisonDate;
else // FUTUREWARDS
return comparisonDate - testDate;
};
/**
* Add `time` to the `baseDate` in the given direction. So if the direction
* is `PASTWARDS`, then we add the date, otherwise we subtract it.
*/
var TIME_DIR_ADD = exports.TIME_DIR_ADD =
function TIME_DIR_ADD(dir, baseDate, time) {
if (dir === PASTWARDS)
return baseDate + time;
else // FUTUREWARDS
return baseDate - time;
};
//function DATE_RANGES_OVERLAP(A_startTS, A_endTS, B_startTS, B_endTS) {
//}
var HOUR_MILLIS = exports.HOUR_MILLIS = 60 * 60 * 1000;
var DAY_MILLIS = exports.DAY_MILLIS = 24 * 60 * 60 * 1000;
/**
* Testing override that when present replaces use of Date.now().
*/
var TIME_WARPED_NOW = null;
/**
* Pretend that 'now' is actually a fixed point in time for the benefit of
* unit tests using canned message stores.
*/
exports.TEST_LetsDoTheTimewarpAgain = function(fakeNow) {
if (fakeNow === null) {
TIME_WARPED_NOW = null;
return;
}
if (typeof(fakeNow) !== 'number')
fakeNow = fakeNow.valueOf();
TIME_WARPED_NOW = fakeNow;
};
var NOW = exports.NOW =
function NOW() {
return TIME_WARPED_NOW || Date.now();
};
/**
* Like NOW, but uses performance.now instead. This means it can be reset or
* changed to a value different than the last window or worker lifetime value.
* Allows TIME_WARP overrides and falls back to Date.now. So while all the
* values are in milliseconds, if performance.now() is used, can have a decimal
* value indicating up to one thousandth of a millisecond.
*/
var perfObj = typeof performance !== 'undefined' ? performance : Date;
exports.PERFNOW = function PERFNOW() {
return TIME_WARPED_NOW || perfObj.now();
};
/**
* Make a timestamp some number of days in the past, quantized to midnight of
* that day. This results in rounding up; if it's noon right now and you
* ask for 2 days ago, you really get 2.5 days worth of time.
*/
var makeDaysAgo = exports.makeDaysAgo =
function makeDaysAgo(numDays) {
var past = quantizeDate(TIME_WARPED_NOW || Date.now()) -
numDays * DAY_MILLIS;
return past;
};
var makeDaysBefore = exports.makeDaysBefore =
function makeDaysBefore(date, numDaysBefore) {
if (date === null)
return makeDaysAgo(numDaysBefore - 1);
return quantizeDate(date) - numDaysBefore * DAY_MILLIS;
};
/**
* Quantize a date to midnight on that day.
*/
var quantizeDate = exports.quantizeDate =
function quantizeDate(date) {
if (date === null)
return null;
if (typeof(date) === 'number')
date = new Date(date);
return date.setUTCHours(0, 0, 0, 0).valueOf();
};
/**
* If a date is already lined up with midnight of its day, then return that,
* otherwise round up to the midnight of the next day.
*/
var quantizeDateUp = exports.quantizeDateUp =
function quantizeDateUp(date) {
if (typeof(date) === 'number')
date = new Date(date);
var truncated = date.setUTCHours(0, 0, 0, 0).valueOf();
if (date.valueOf() === truncated)
return truncated;
return truncated + DAY_MILLIS;
};
}); // end define