UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

230 lines (229 loc) 9.84 kB
import { RequiredError } from "../error/RequiredError.js"; import { DAY, HOUR, MONTH, SECOND, WEEK } from "./constants.js"; import { TIME_UNITS } from "./units.js"; /** * Is a value a valid date? * - Note: `Date` instances can be invalid (e.g. `new Date("blah blah").getTime()` returns `NaN`). These are detected and will always return `false` */ export function isDate(value) { return value instanceof Date && Number.isFinite(value.getTime()); } /** Assert that a value is a `Date` instance. */ export function assertDate(value, caller = assertDate) { if (!isDate(value)) throw new RequiredError("Must be valid date", { received: value, caller }); } /** * Convert an unknown value to a valid `Date` instance, or return `undefined` if it couldn't be converted. * - Note: `Date` instances can be invalid (e.g. `new Date("blah blah").getTime()` returns `NaN`). These are detected and will always return `null` * * Conversion rules: * - `Date` instance returns unchanged (BUT if the date isn't valid, `undefined` is returned). * - `null` or `undefined` or `""` empty string returns `undefined` * - The string `"now"` returns the current date (e.g. `new Date()`). * - The string `"today"` returns the current date at midnight (e.g. `getMidnight()`). * - The string `"tomorrow"` returns tomorrow's date at midnight (e.g. `addDays(getMidnight(), 1)`). * - The string `"yesterday"` returns yesterday's date at midnight (e.g. `addDays(getMidnight(), 1)`). * - Strings (e.g. `"2003-09-12"` or `"2003 feb 20:09"`) return the corresponding date (using `new Date(string)`). * - Numbers are return the corresponding date (using `new Date(number)`, i.e. milliseconds since 01/01/1970). * - Anything else returns `undefined` * * @param value Any value that we want to parse as a valid date (defaults to `undefined`). * @returns `Date` instance if the value could be converted to a valid date, and `null` if not. */ export function getDate(value) { if (value === "now") return getNow(); if (value === "yesterday") return getYesterday(); if (value === "today") return getToday(); if (value === "tomorrow") return getTomorrow(); if (isDate(value)) return value; if (typeof value === "string" || typeof value === "number") { const date = new Date(value); if (Number.isFinite(date.getTime())) return date; } } /** Get a date representing this exact moment. */ export function getNow() { return new Date(); } /** Get a date representing midnight of the previous day. */ export function getYesterday() { const date = new Date(); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() - 1); return date; } /** Get a date representing midnight of the current day. */ export function getToday() { const date = new Date(); date.setHours(0, 0, 0, 0); return date; } /** Get a date representing midnight of the next day. */ export function getTomorrow() { const date = new Date(); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 1); return date; } /** * Convert a possible date to a `Date` instance, or throw `RequiredError` if it couldn't be converted. * @param value Any value that we want to parse as a valid date (defaults to `"now"`). */ export function requireDate(value = "now", caller = requireDate) { const date = getDate(value); assertDate(date, caller); return date; } /** Convert an unknown value to a timestamp (milliseconds past Unix epoch), or `undefined` if it couldn't be converted. */ export function getTimestamp(value) { return getDate(value)?.getTime(); } /** Convert a possible date to a timestamp (milliseconds past Unix epoch), or throw `RequiredError` if it couldn't be converted. */ export function requireTimestamp(value) { return requireDate(value, requireTimestamp).getTime(); } /** Convert an unknown value to a YMD date string like "2015-09-12", or `undefined` if it couldn't be converted. */ export function getYMD(value) { const date = getDate(value); if (date) return _ymd(date); } /** Convert a `Date` instance to a YMD string like "2015-09-12", or throw `RequiredError` if it couldn't be converted. */ export function requireYMD(value, caller = requireYMD) { return _ymd(requireDate(value, caller)); } function _ymd(date) { const y = _pad(date.getUTCFullYear(), 4); const m = _pad(date.getUTCMonth() + 1, 2); const d = _pad(date.getUTCDate(), 2); return `${y}-${m}-${d}`; } function _pad(num, size) { return num.toString(10).padStart(size, "0000"); } /** List of day-of-week strings. */ export const DAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; /** Convert a `Date` instance to a day-of-week string like "monday" */ export function getDay(target) { return DAYS[requireDate(target, getDay).getDay()]; } /** Get a Date representing exactly midnight of the specified date. */ export function getMidnight(target, caller = getMidnight) { const date = new Date(requireDate(target, caller)); // New instance, because we modify it. date.setHours(0, 0, 0, 0); return date; } /** Get a Date representing midnight on Monday of the specified week. */ export function getMonday(target, caller = getMonday) { const date = getMidnight(target, caller); // New instance, because we modify it. const day = date.getDay(); if (day === 0) date.setDate(date.getDate() - 6); else if (day !== 1) date.setDate(date.getDate() - (day - 1)); return date; } /** Return a new date that increase or decreases the number of days based on an input date. */ export function addDays(change, target) { const date = new Date(requireDate(target, addDays)); // New instance, because we modify it. date.setDate(date.getDate() + change); return date; } /** Return a new date that increase or decreases the number of hours based on an input date. */ export function addHours(change, target) { const date = new Date(requireDate(target, addHours)); // New instance, because we modify it. date.setHours(date.getHours() + change); return date; } /** * Get the duration (in milliseconds) between two dates. * * @param target The date when the thing will happen or did happen. * @param current Today's date (or a different date to measure from). */ export function getMillisecondsUntil(target, current, caller = getMillisecondsUntil) { return requireDate(target, caller).getTime() - requireDate(current, caller).getTime(); } /** Count the number of seconds until a date. */ export function getSecondsUntil(target, current, caller = getSecondsUntil) { return getMillisecondsUntil(target, current, caller) / SECOND; } /** Count the number of days ago a date was. */ export function getSecondsAgo(target, current, caller = getSecondsAgo) { return 0 - getSecondsUntil(target, current, caller); } /** Count the number of days until a date. */ export function getDaysUntil(target, current, caller = getDaysUntil) { return Math.round((requireDate(target, caller).getTime() - requireDate(current, caller).getTime()) / DAY); } /** Count the number of days ago a date was. */ export function getDaysAgo(target, current, caller = getDaysAgo) { return 0 - getDaysUntil(target, current, caller); } /** Count the number of weeks until a date. */ export function getWeeksUntil(target, current, caller = getWeeksUntil) { return Math.floor(getDaysUntil(target, current, caller) / 7); } /** Count the number of weeks ago a date was. */ export function getWeeksAgo(target, current, caller = getWeeksAgo) { return 0 - getWeeksUntil(target, current, caller); } /** Is a date in the past? */ export function isPast(target, current, caller = isPast) { return getMillisecondsUntil(target, current, caller) < 0; } /** Is a date in the future? */ export function isFuture(target, current, caller = isFuture) { return getMillisecondsUntil(target, current, caller) > 0; } /** Is a date today (taking into account midnight). */ export function isToday(target, current, caller = isToday) { return getDaysUntil(target, current, caller) === 0; } /** Get an appropriate time unit based on an amount in milliseconds. */ function getBestTimeUnit(ms) { const abs = Math.abs(ms); if (abs > 18 * MONTH) return TIME_UNITS.require("year"); if (abs > 10 * WEEK) return TIME_UNITS.require("month"); if (abs > 2 * WEEK) return TIME_UNITS.require("week"); if (abs > DAY) return TIME_UNITS.require("day"); if (abs > HOUR) return TIME_UNITS.require("hour"); if (abs > 9949) return TIME_UNITS.require("minute"); if (abs > SECOND) return TIME_UNITS.require("second"); return TIME_UNITS.require("millisecond"); } /** Compact when a date happens/happened, e.g. `in 10d` or `2h ago` or `in 1w` or `just now` */ export function formatWhen(target, current, options) { const ms = getMillisecondsUntil(target, current, formatWhen); const abs = Math.abs(ms); if (abs < 30 * SECOND) return "just now"; const unit = getBestTimeUnit(ms); return ms > 0 ? `in ${unit.format(unit.from(abs), options)}` : `${unit.format(unit.from(abs), options)} ago`; } /** Compact when a date happens, e.g. `10d` or `2h` or `-1w` */ export function formatUntil(target, current, options) { const ms = getMillisecondsUntil(target, current, formatUntil); const unit = getBestTimeUnit(ms); return unit.format(unit.from(ms), options); } /** Compact when a date will happen, e.g. `10d` or `2h` or `-1w` */ export function formatAgo(target, current, options) { const ms = 0 - getMillisecondsUntil(target, current, formatAgo); const unit = getBestTimeUnit(ms); return unit.format(unit.from(ms), options); }