daet
Version:
Minimal immutable date class that supports relative time, calendar time, and plus/minus of different units.
259 lines (258 loc) • 9.61 kB
JavaScript
import memo from '@bevry/memo';
import getSW from 'start-of-week';
const startOfWeek = getSW();
import * as intl from './intl.js';
export const Millisecond = 1;
export const Second = Millisecond * 1000;
export const Minute = Second * 60;
export const Hour = Minute * 60;
export const Day = Hour * 24;
export const Week = Day * 7;
/** A minimal immutable date class that supports relative time, calendar time, and plus/minus of different units. */
export default class Daet {
/** The raw date object behind this daet instance. */
raw;
/** The tiers used for human relative date display. */
static get tiers() {
return [
{
// right now
limit: Second,
refresh: Second,
message: intl.rightNow,
},
{
// x seconds
limit: Minute,
refresh: Second,
message: intl.relativeDelta,
},
{
// x minutes
limit: Hour,
refresh: Minute,
message: intl.relativeDelta,
},
{
// x hours x minutes
limit: 12 * Hour,
refresh: Minute,
message: intl.relativeDelta,
},
{
// later/earlier today
when: ({ past }) => past
? new Daet().reset('hour').getTime()
: new Daet().plus(1, 'day').reset('hour').getTime(),
message: intl.earlierOrLaterToday,
},
{
// yesterday or tomorrow
when: ({ past }) => past
? new Daet().minus(1, 'day').reset('hour').getTime()
: new Daet().plus(2, 'day').reset('hour').getTime(),
message: intl.yesterdayOrTommorow,
},
{
// this week
when: ({ past }) => past
? new Daet().endOfLastWeek().getTime()
: new Daet().startOfNextWeek().getTime(),
message: intl.relativeThisWeek,
},
{
// next or last week
when: ({ past }) => past
? new Daet().endOfLastWeek().endOfLastWeek().getTime()
: new Daet().startOfNextWeek().startOfNextWeek().getTime(),
message: intl.relativeSecondWeek,
},
{
// earlier or later
limit: Infinity,
message: intl.earlierOrLater,
},
];
}
/** Create a new daet instance based on the input */
static create(input) {
return new this(input);
}
/** Create a new daet instance based on the input */
constructor(input) {
this.raw =
input instanceof Daet
? new Date(input.raw)
: input
? new Date(input)
: new Date();
this.raw.setMilliseconds(0);
}
/** Return a new daet instance that is in the past by the value amount. */
minus(value, unit) {
return this.plus(value * -1, unit);
}
/** Return a new daet instance that is in the future by the value amount. */
plus(value, unit) {
switch (unit) {
case 'millisecond': {
const next = new Date(this.raw.getTime() + value);
return new Daet(next);
}
case 'second': {
return this.plus(value * Second, 'millisecond');
}
case 'minute': {
return this.plus(value * Minute, 'millisecond');
}
case 'hour': {
return this.plus(value * Hour, 'millisecond');
}
case 'day': {
return this.plus(value * Day, 'millisecond');
}
case 'week': {
return this.plus(value * Week, 'millisecond');
}
default:
// https://basarat.gitbooks.io/typescript/docs/types/discriminated-unions.html
const neverCheck = unit;
throw new Error('unknown unit');
}
}
/** Clone this daet instance based on its raw value. */
rawClone() {
return new Date(this.raw);
}
/** Clone this daet instance. */
clone() {
return new Daet(this);
}
/** Return a new daet instance that has the unit set to the desired value. */
set(value, unit) {
switch (unit) {
case 'millisecond': {
const next = this.rawClone().setMilliseconds(value);
return new Daet(next);
}
case 'second': {
const next = this.rawClone().setSeconds(value);
return new Daet(next);
}
case 'minute': {
const next = this.rawClone().setMinutes(value);
return new Daet(next);
}
case 'hour': {
const next = this.rawClone().setHours(value);
return new Daet(next);
}
default:
// https://basarat.gitbooks.io/typescript/docs/types/discriminated-unions.html
const neverCheck = unit;
throw new Error('unknown unit');
}
}
/** Return a new daet instance that has the desired unit reset to 0. */
reset(unit) {
switch (unit) {
case 'millisecond': {
const next = this.rawClone().setMilliseconds(0);
return new Daet(next);
}
case 'second': {
const next = this.rawClone().setSeconds(0, 0);
return new Daet(next);
}
case 'minute': {
const next = this.rawClone().setMinutes(0, 0, 0);
return new Daet(next);
}
case 'hour': {
const next = this.rawClone().setHours(0, 0, 0, 0);
return new Daet(next);
}
default:
// https://basarat.gitbooks.io/typescript/docs/types/discriminated-unions.html
const neverCheck = unit;
throw new Error('unknown unit');
}
}
/** Get the epoch time of this daet instance. */
getTime = memo(() => this.raw.getTime());
/** Get the milliseconds from the passed daet instance to this daet instance. */
getMillisecondsFrom(from) {
const now = from.getTime();
const time = this.getTime();
return time - now;
}
/** Get the milliseconds from now to this daet instance */
getMillisecondsFromNow() {
return this.getMillisecondsFrom(new Daet());
}
/** Use https://devdocs.io/javascript/global_objects/datetimeformat to format our daet instance. */
format(locale, options) {
return new Intl.DateTimeFormat(locale, options).format(this.raw);
}
/** Return a new daet instance that is the start of the week proceeding that of this daet instance. */
startOfNextWeek = memo(() => {
// continue until we become the start of next week
let latest = this.plus(1, 'day');
while (latest.raw.getDay() !== startOfWeek) {
latest = latest.plus(1, 'day');
}
// reset the time, so we become the start time of that week
return latest.reset('hour');
});
/** Return a new daet instance that is the end of the week preceeding that of this daet instance. */
endOfLastWeek = memo(() => {
// continue until we become the start of this week
let latest = this;
while (latest.raw.getDay() !== startOfWeek) {
latest = latest.minus(1, 'day');
}
// reset the time, then minus a second, so we become the end of the prior week
return latest.reset('hour').minus(1, 'second');
});
/** Get the human absolute date display for this daet instance. */
calendar() {
return this.format('en', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
}
/** Return the human relative display from now, as well as the millisecond delta before a refresh is needed. */
fromNowDetails() {
const now = new Daet();
const nowTime = now.getTime();
const eventTime = this.getTime();
const past = nowTime > eventTime;
const delta = Math.abs(eventTime - nowTime);
const tiers = Daet.tiers;
let lastTierDelta = 0;
for (const tier of tiers) {
const limit = tier.limit;
const when = tier.when ? tier.when({ past }) : 0;
const tierDelta = limit || Math.abs(when - nowTime);
if (delta < tierDelta) {
const message = tier.message({ past, delta, when: this });
const refresh = tier.refresh || delta - lastTierDelta;
return { message, refresh };
}
lastTierDelta = tierDelta;
}
throw new Error('no tier matched the input delta');
}
/** Return the human relative display from now. */
fromNow() {
return this.fromNowDetails().message;
}
/** Return the ISO string for this daet instance. */
toISOString = memo(() => this.raw.toISOString());
/** Return the JSON string for this daet instance. */
toJSON = memo(() => this.raw.toJSON());
}