Dates & Times
Important Utilities
dateUtil - A utility that wraps a lot of helpful date related functionality. By default it's not timezone aware, but you can make it timezone aware by using the DateUtilDecorator provided by @sprucelabs/calendar-utils.
export interface DateUtil {
eventDaysOfWeek: {
sun: string;
mon: string;
tue: string;
wed: string;
thur: string;
fri: string;
sat: string;
};
getStartOfDay(timestamp?: number): number;
getStartOfWeek(timestamp?: number): number;
getEndOfDay(timestamp?: number): number;
getEndOfWeek(timestamp: number): number;
getStartOfMonth(timestamp?: number): number;
getEndOfMonth(timestamp?: number): number;
addMinutes(startTimestamp: number, minutes: number): number;
addMilliseconds(startTimestamp: number, ms: number): number;
addDays(startTimestamp: number, days: number): number;
addWeeks(startTimestamp: number, weeks: number): number;
addMonths(timestamp: number, months: number): number;
addYears(timestamp: number, years: number): number;
getDurationMs(timestamp: number, endTimestamp: number): number;
getDurationMinutes(timestamp: number, endTimestamp: number): number;
getDurationDays(timestamp: number, endTimestamp: number): number;
getDayOfWeek(timestamp: number): DayOfWeek;
getDayOfWeekIndex(timestamp: number): number;
splitDate(timestamp: number): {
year: number;
month: number;
day: number;
hour: number;
minute: number;
};
setTimeOfDay(timestamp: number, hours: number, minutes?: number, seconds?: number, milliseconds?: number): number;
getDateNDaysFromStartOfDay(days: number, timestamp?: number): number;
getDateNMonthsFromStartOfDay(count: number, timestamp?: number): number;
getDateNMonthsFromStartOfMonth(count: number, timestamp?: number): number;
date(date?: IDate): number;
/**
* Unit_______________Pattern___Results
*
* Era________________G..GGG____AD,BC
*
* ___________________GGGG______Anno Domini, Before Christ
*
* ___________________GGGGG_____A,B
*
*
*
* Calendar year_______y_________44, 1, 1900, 2017
*
* ___________________yo________44th, 1st, 0th, 17th
*
* ___________________yy________44, 01, 00, 17
*
* ___________________yyy_______044, 001, 1900, 2017
*
* ___________________yyyy______0044, 0001, 1900, 2017
*
*
*
* Local week_________Y_________44, 1, 1900, 2017
*
* ___________________Yo________44th, 1st, 1900th, 2017th
*
* ___________________YY________44, 01, 00, 17
*
* ___________________YYY_______044, 001, 1900, 2017
*
* ___________________YYYY______0044, 0001, 1900, 2017
*
*
*
* ISO week___________R_________-43, 0, 1, 1900, 2017
*
* ___________________________RR________-43, 00, 01, 1900, 2017
*
* __________________________RRR_______-043, 000, 001, 1900, 2017
*
* _________________________RRRR______-0043, 0000, 0001, 1900, 2017
*
*
*
*
* Extended year______u_________-43, 0, 1, 1900, 2017
*
* ___________________uu________-43, 01, 1900, 2017
*
* ___________________uuu_______-043, 001, 1900, 2017
*
* ___________________uuuu______-0043, 0001, 1900, 2017
*
*
*
*
* Quarter (pretty)_____Q_________1, 2, 3, 4
*
* ___________________Qo________1st, 2nd, 3rd, 4th
*
* ___________________QQ________01, 02, 03, 04
*
* ___________________QQQ_______Q1, Q2, Q3, Q4
*
* ___________________QQQQ______1st quarter, 2nd quarter, ...
*
* ___________________QQQQQ_____1, 2, 3, 4
*
*
*
* Quarter____________q_________1, 2, 3, 4
*
* ___________________qo________1st, 2nd, 3rd, 4th
*
* ___________________qq________01, 02, 03, 04
*
* ___________________qqq_______Q1, Q2, Q3, Q4
*
* ___________________qqqq______1st quarter, 2nd quarter,...
*
* ___________________qqqqq_____1, 2, 3, 4
*
*
*
* Month (pretty)______M_________1, 2, ..., 12
*
* ___________________Mo________1st, 2nd, ..., 12th
*
* ___________________MM________01, 02, ..., 12
*
* ___________________MMM_______Jan, Feb, ..., Dec
*
* ___________________MMMM______January, February, ..., December
*
* ___________________MMMMM_____J, F, ..., D
*
*
*
* Month______________L_________1, 2, ..., 12
*
* ___________________Lo________1st, 2nd, ..., 12th
*
* ___________________LL________01, 02, ..., 12
*
* ___________________LLL_______Jan, Feb, ..., Dec
*
* ___________________LLLL______January, February, ..., December
*
* ___________________LLLLL_____J, F, ..., D
*
*
*
* Local Week_________w_________1, 2, ..., 53
*
* ___________________wo________1st, 2nd, ..., 53th
*
* ___________________ww________01, 02, ..., 53
*
*
*
* ISO week of year_____I_________1, 2, ..., 53
*
* ___________________Io________1st, 2nd, ..., 53th
*
* ___________________II________01, 02, ..., 53
*
*
*
* Day of month_______d_________1, 2, ..., 31
*
* ___________________do________1st, 2nd, ..., 31st
*
* ___________________dd________01, 02, ..., 31
*
*
*
* Day of year________D_________1, 2, ..., 365, 366
*
* ___________________Do________1st, 2nd, ..., 365th, 366th
*
* ___________________DD________01, 02, ..., 365, 366
*
* ___________________DDD_______001, 002, ..., 365, 366
*
*
*
* Day of week________E..EEE____Mon, Tue, Wed, ..., Sun
*
* ___________________EEEE______Monday, Tuesday, ..., Sunday
*
* ___________________EEEEE_____M, T, W, T, F, S, S
*
* ___________________EEEEEE____Mo, Tu, We, Th, Fr, Sa, Su
*
*
*
* ISO day of week_____i_________1, 2, 3, ..., 7
*
* ___________________io________1st, 2nd, ..., 7th
*
* ___________________ii________01, 02, ..., 07
*
* ___________________iii_______Mon, Tue, Wed, ..., Sun
*
* ___________________iiii______Monday, Tuesday, ..., Sunday
*
* ___________________iiiii_____M, T, W, T, F, S, S
*
* ___________________iiiiii____Mo, Tu, We, Th, Fr, Sa, Su
*
*
*
* Local day of week_____e_________2, 3, 4, ..., 1
*
* ___________________eo________2nd, 3rd, ..., 1st
*
* ___________________ee________02, 03, ..., 01
*
* ___________________eee_______Mon, Tue, Wed, ..., Sun
*
* ___________________eeee______Monday, Tuesday, ..., Sunday
*
* ___________________eeeee_____M, T, W, T, F, S, S
*
* ___________________eeeeee____Mo, Tu, We, Th, Fr, Sa, Su
*
*
*
*
* Day of week_________c_________2, 3, 4, ..., 1
*
* ___________________co________2nd, 3rd, ..., 1st
*
* ___________________cc________02, 03, ..., 01
*
* ___________________ccc_______Mon, Tue, Wed, ..., Sun
*
* ___________________cccc______Monday, Tuesday, ..., Sunday
*
* ___________________ccccc_____M, T, W, T, F, S, S
*
* ___________________cccccc____Mo, Tu, We, Th, Fr, Sa, Su
*
*
*
* AM,PM_____________a..aa_____AM, PM
*
* ___________________aaa_______am, pm
*
* ___________________aaaa______a.m., p.m.
*
* ___________________aaaaa_____a,p
*
*
* AM, PM, noon, mid_b..bb____AM, PM, noon, midnight
*
* _____________________bbb______am, pm, noon, midnight
*
* _____________________bbbb_____a.m.,_p.m.,_noon,_midnight
*
* _____________________bbbbb____a,_p,_n,_mi
*
*
* Flexible day period_B..BBB___at night, in the morning, ...
*
* _____________________BBBB_____at night, in the morning, ...
*
* _____________________BBBBB____at night, in the morning, ...
*
*
*
* Hour [1-12]_________h________1, 2, ..., 11, 12
*
* ___________________ho_______1st, 2nd, ..., 11th, 12th
*
* ___________________hh_______01,_02,_...,_11,_12
*
*
*
* Hour [0-23]______________H________0,_1,_2,_...,_23
*
* __________________________Ho_______0th,_1st,_2nd,_...,_23rd
*
* __________________________HH_______00,_01,_02,_...,_23
*
*
*
* Hour [0-11]______________K________1,_2,_...,_11,
*
* __________________________Ko_______1st,_2nd,_...,_11th,_0th
*
* __________________________KK_______01,_02,_...,_11,_00
*
*
*
* Hour [1-24]______________k________24,_1,_2,_...,_23
*
* __________________________ko_______24th,_1st,_2nd,_...,_23rd
*
* __________________________kk_______24,_01,_02,_...,_23
*
*
*
* Minute___________________m__________0,_1,_...,_59
*
* ___________________________mo_________0th,_1st,_...,_59th
*
* ___________________________mm_________00,_01,_...,_59
*
*
*
* Second___________________s__________0,_1,_...,_59
*
* ___________________________so_________0th,_1st,_...,_59th
*
* ___________________________ss_________00,_01,_...,_59
*
*
*
* Fraction_of_second_______S__________0,_1,_...,
*
* ___________________________SS_________00,_01,_...,_99
*
* ___________________________SSS________000,_001,_...,_999
*
*
*
* Timezone_(ISO-8601_w/_Z)_X__________-08,_+0530,_Z
*
* ___________________________XX_________-0800,_+0530,_Z
*
* ___________________________XXX________-08:00,_+05:30,_Z
*
* ___________________________XXXX_______-0800,_+0530,_Z,_+123456
*
* ___________________________XXXXX______-08:00,_+05:30,_Z,_+12:34:56
*
*
*
* Timezone_(ISO-8601_w/o_Z_x__________-08,_+0530,_+00
*
* ___________________________xx_________-0800,_+0530,_+0000
*
* ___________________________xxx________-08:00,_+05:30,_+00:00
*
* ___________________________xxxx_______-0800,_+0530,_+0000,_+123456
*
* ___________________________xxxxx______-08:00,_+05:30,_+00:00,_+12:34:56
*
*
* Timezone_(GMT)___________O...OOO____GMT-8,_GMT+5:30,_GMT+0
*
* ___________________________OOOO_______GMT-08:00,_GMT+05:30,_GMT+00:00
*
*
* Timezone_(specific_non-l_z...zzz____GMT-8,_GMT+5:30,_GMT+0
*
* ___________________________zzzz_______GMT-08:00,_GMT+05:30,_GMT+00:00
*
*
* Seconds_timestamp________t__________512969520
*
*
* Milliseconds_timestamp___T__________512969520900
*
*
* Long localized date______P__________04/29/1453
*
* ___________________________PP_________Apr 29, 1453
*
* ___________________________PPP________April 29th, 1453
*
* ___________________________PPPP_______Friday,_April_29th,_1453
*
*
* Long_localized_time______p__________12:00_AM
*
* ___________________________pp_________12:00:00_AM
*
* ___________________________ppp________12:00:00_AM_GMT+2
*
* ___________________________pppp_______12:00:00_AM_GMT+02:00
*
*
*
* Date & Time_______________Pp_________04/29/1453,_12:00_AM
*
* ___________________________PPpp_______Apr_29,_1453,_12:00:00_AM
*
* ___________________________PPPppp_____April_29th,_1453_at_...
*
* ___________________________PPPPpppp___Friday,_April_29th,_1453_at_...
*
*/
format(timestamp: number, format: string): string;
formatTime(timestamp: number): string;
formatDate(timestamp: number): string;
formatDateTime(timestamp: number): string;
add(timestamp: number, count: number, unit: DateUnit): any;
isSameDay(timestamp1: number, timestamp2: number): boolean;
getTotalDaysInMonth(year: number, month: number): number;
};
DateUtilDecorator - A decorator that makes the dateUtil timezone aware. This is done automatically for you in your Skill Views.
Coming soon…
durationUtil - A utility that helps you render durations (timespans, distances, etc.) in various ways.
Coming soon…
Timezones
In your ViewControllers
In your ViewControllers
the dateUtil
is already Locale
aware. All you have to do is access the dateUtil
via this.dates
and you’re good to go. Example:
class RootSkillView extends AbstarctSkillViewController {
public async load() {
const startOfToday = this.dates.getStartOfDay()
console.log('Timestamp for start of day in the current timezone:', startOfToday)
}
}
Note: Timezone will default to the client’s timezone (browser or app). If
Scope
is set, it’ll use currentLocation
’s orOrganization
’s timezone. The presidence isLocation
>Organization
>Client
.
In the backend
It gets a little more complicated in the backend because there is no way to know the timezone of the client or current scope. By default, the timezone will match whatever the server is set to. As you can imagine, that is not going to work in most cases.
Test 1: Assert dateUtil is built with the correct timezone
In this test, we’re going to assume you already have tested your Listener
and are ready to ensure the dateUtil
is built with the correct timezone. This example is very contrived, but lets say you want to show the date a family member scheduled an event in the location’s timezone. We added the usesTheLocationsTimezone()
test, which should fail at this point.
import { AbstractSpruceFixtureTest } from '@sprucelabs/spruce-test-fixtures'
export default class GetFamilyMemberListenerTest extends AbstractSpruceFixtureTest {
@seed('locations', 1)
@seed('familyMembers', 1)
protected static async beforeEach() {
await super.beforeEach()
await this.bootSkill()
}
@test()
protected static async usesTheLocationsTimezone() {
this.fakedLocations[0].timezone = 'America/Denver'
await this.emitGetFamilyMember()
dateAssert.timezoneOfLastBuiltDateUtilEquals('America/Denver')
}
public static async emitGetFamilyMember() {
const [{ familyMembers}] = await this.client.emitAndFlattenResponses(
'eightbitstories.get-family-member::v2023_09_05',
{
target: {
locationId: this.fakedLocations[0].id,
},
})
return familyMembers
}
}
Production 1: Build dateUtil with the hardcoded timezone
Coming soon...
``
</details>
## Rendering time until a date
The `durationUtil` provided by `@sprucelabs/calendar-utils` is useful for rendering time until a date, like "in 2 hours" or "5 days ago" or "today".
### `durationUtil` in the backend
If you need to render a time span from a listener or something invoked from a listener, here is how you would on that.
<details>
<summary><strong>Test 1:</strong> Assert <em>durationUtil.renderDateTimeUntil(...)</em> is called</summary>
You are safe to monkey patch the `durationUtil` on the `DurationUtilBuilder` to spy on the `renderDateTimeUntil(...)` method. Make sure to call `DurationBuilder.reset()` in the `beforeEach()` of your test suite to make sure the `durationUtil` is reset to its original state.
```ts
import { DurationUtilBuilder } from '@sprucelabs/calendar-utils'
protected static async beforeEach() {
await super.beforeEach()
DurationUtilBuilder.reset()
}
@test()
protected static async myOperationCallsRenderDateTimeUntil() {
const dateTimeUntil = generateId()
DurationUtilBuilder.durationUtil.renderDateTimeUntil = () => {
return dateTimeUntil
}
const message = await this.someOperation()
assert.doesInclude(message, dateTimeUntil)
}
Production 1: Call durationUtil.renderDateTimeUntil(...)
In this first attempt, you’re only making sure that the durationUtil.renderDateTimeUntil(...)
method is called. You’re not concerned with the parameters passed to it nor are you concerned with the timezone, just drop in something random to start.
import { DurationUtilBuilder } from '@sprucelabs/calendar-utils'
public async someOperation() {
...
const durationUtil = await DurationUtilBuilder.getFromTimezone('America/Denver')
const timeUntil = durationUtil.renderDateTimeUntil(0, 0)
const message = `Your journey starts in ${timeUntil}!`
...
return message
}
Test 2: Assert durationUtil.renderDateTimeUntil(...) is called with correct params
Now that you know the durationUtil.renderDateTimeUntil(...)
method is called, you can spy on the parameters passed to it. You can use assert.isBetween(...)
to ensure the beginning
and end
parameters are within a reasonable range.
import { DurationUtilBuilder } from '@sprucelabs/calendar-utils'
@test()
protected static async myOperationCallsRenderDateTimeUntil() {
let passedEnd: number | undefined
const dateTimeUntil = generateId()
const expectedEnd = 0 //Some date in the future
DurationUtilBuilder.durationUtil.renderDateTimeUntil = (end) => {
passedEnd = end
return dateTimeUntil
}
const message = await this.someOperation()
assert.doesInclude(message, dateTimeUntil)
assert.isEqual(passendEnd, expectedEnd)
}
Production 2: Call durationUtil.renderDateTimeUntil(...) with correct params
In a lot of cases, you’ll just want to pass Date.now()
as the beginning
parameter. That’s what I’ll show you here.
import { DurationUtilBuilder } from '@sprucelabs/calendar-utils'
public async someOperation() {
...
const someDateInFuture = 0 //Some date in the future
const durationUtil = await DurationUtilBuilder.getFromTimezone('America/Denver')
const timeUntil = durationUtil.renderDateTimeUntil(someDateInFuture)
const message = `Your journey starts in ${timeUntil}!`
...
}
Test 3: Assert durationUtil.renderDateTimeUntil(...) is called with correct timezone
You can start a new test and use the dateAssert
utility from @sprucelabs/calendar-utils
to assert the timezone based on DurationUtilBuilder.lastBuiltDurationUtil
. Note: You can get the timezone
off a Location
or Person
if you don’t want to hardcode it like this example.
import { DurationUtilBuilder, dateAssert } from '@sprucelabs/calendar-utils'
@test()
protected static async myOperationCalledWithTheExpectedTimezone() {
await this.someOperation()
dateAssert.currentTimezoneEquals(
DurationUtilBuilder.lastBuiltDurationUtil,
'Africa/Johannesburg'
)
}
Production 3: Call `durationUtil.renderDateTimeUntil(...)` with correct timezone
Finally! You can bring it home by calling the DurationUtilBuilder.getFromTimezone()
method with the correct timezone! You could obviously do this first, it’s totally up to you!
import { DurationUtilBuilder } from '@sprucelabs/calendar-utils'
public async someOperation() {
...
const someDateInFuture = 0 //Some date in the future
const durationUtil = await DurationUtilBuilder.getFromTimezone('Africa/Johannesburg')
const timeUntil = durationUtil.renderDateTimeUntil(someDateInFuture)
const message = `Your journey starts in ${timeUntil}!`
...
}
durationUtil
in views
If you want to render the time until an event in a View Controller, you go about it slightly differently, but it’s pretty easy!
Test 1: Assert durationUtil is configured correctly
import { vcDurationAssert } from '@sprucelabs/heartwood-view-controllers'
@test()
protected static async myViewHasDurationUtilConfigured() {
const vc = this.views.Controller('eightbitstories.root', {})
vcDurationAssert.durationUtilIsConfiguredForVc(vc)
}
Test 1a: Ensure vcDurationAssert is configured correctly
You should have gotten an error telling you to call vcDurationAssert.beforeEach(this.views)
to get the assertion library to work correctly. Lets do that now.
import { vcDurationAssert } from '@sprucelabs/heartwood-view-controllers'
protected static async beforeEach() {
await super.beforeEach()
vcDurationAssert.beforeEach(this.views)
}
@test()
protected static async myViewHasDurationUtilConfigured() {
const vc = this.views.Controller('eightbitstories.root', {})
vcDurationAssert.durationUtilIsConfiguredForVc(vc)
}
Production 1: Configure durationUtil in the View Controller
Your View Controller will come with a fully timezone aware dateUtil
accessibly via this.dates
. Your job is to set the durationUtil.dates
to this.dates
in the constructor of your View Controller to make sure the durationUtil
is timezone aware.
class RootSkillView extends AbstractSkillViewController {
public constructor(options: SkillViewControllerOptions) {
super(options)
durationUtil.dates = this.dates
}
}
durationUtil
in messages
Sending a message that renders the time until is a bit different than the other two examples. You actually don’t need to use the durationUtil
at all because it’s handled by Mercury for you using Message Context!
Test 1: Assert a message is sent
import { eventFaker } from '@sprucelabs/spruce-test-fixtures'
@test()
protected static async messageIsSent() {
let wasHit = false
await eventFaker.on('send-message::v2020_12_25', () => {
wasHit = true
return {
message: {
body: generateId(),
classification: 'transactional' as const,
id: generateId(),
dateCreated: Date.now(),
source: {},
target: {
personId: generateId(),
},
},
}
})
await this.someOperationThatSendsAMessage()
assert.isTrue(wasHit, `Message was not sent!`)
}
Production 1: Send a message
Follow the process for [sending messages](../messages) to work your way through testing sending a message. We'll only pay attention to the parts relevant to rendering the time until a date.private async someOperationThatSendsAMessage() {
await this.client.emitAndFlattenResponses('send-message::v2020_12_25', {
target: {},
payload: {},
}
}
Test 2: Assert the message is sent with expected placeholder
Now we'll check the body to see if it contains the `` placeholder. Also, we can remove the `didHit` assertion because it's redundant. Lastly, I'm not gonna show the full response because it's not relevant to this example.import { eventFaker } from '@sprucelabs/spruce-test-fixtures'
@test()
protected static async messageIsSent() {
let passedBody: string | undefined
await eventFaker.on('send-message::v2020_12_25', ( { payload }) => {
passedBody = payload.message.body
return {
message: {
...
},
}
})
await this.someOperationThatSendsAMessage()
assert.doesInclude(passedBody, '')
}
Production 2: Send a message with the placeholder
private async someOperationThatSendsAMessage() {
await this.client.emitAndFlattenResponses('send-message::v2020_12_25', {
target: {},
payload: {
message: {
...,
body: `Your journey starts in !`,
}
},
}
}
Test 3: Assert the message has the correct context
The formatDateTimeUntil
placeholder is a plugin that accepts a variable that is named after anything in the Message Context. In this case, we’re using eventDateMs
as the variable name. We need to make sure that the eventDateMs
is in the context of the message. This variable could be called anything as long as it’s a key in the context. Also, the formatDateTimeUntil
plugin will default to the target’s timezone. Meaning, if you target a location, it’ll use that location’s timezone. Or, if you target a person, it’ll use that person’s timezone. In this example, we want to target a timezone manuall, just to show you how to do it.
import { eventFaker } from '@sprucelabs/spruce-test-fixtures'
@test()
protected static async messageIsSent() {
let passedBody: string | undefined
let passedContext: Record<string, any> | undefined
const expectedEventDateMs = 0 //some date in the future or past
await eventFaker.on('send-message::v2020_12_25', ( { payload }) => {
passedBody = payload.message.body
passedContext = payload.message.context
return {
message: {
...
},
}
})
await this.someOperationThatSendsAMessage()
assert.doesInclude(passedBody, '')
assert.isEqualDeep(passedContext, { timezone: 'Africa/Johannesburg', eventDateMs: expectedEventDateMs })
}
Production 3: Send a message with the context
private async someOperationThatSendsAMessage() {
await this.client.emitAndFlattenResponses('send-message::v2020_12_25', {
target: {},
payload: {
message: {
...,
body: `Your journey starts in {{formatDateTimeUntil eventDateMs}}!`,
context: {
eventDateMs: 0 //some date in the future or past,
timezone: 'Africa/Johannesburg'
}
}
},
}
}
Test 4: Parameterize the timezone
Lastly, lets parameterize this test to let us test different timezones.
import { eventFaker } from '@sprucelabs/spruce-test-fixtures'
import { TimezoneName } from '@sprucelabs/calendar-utils'
@test('message is sent with timezone Africa/Johannesburg')
@test('message is sent with timezone America/Denver')
protected static async messageIsSent(timezone: TimezoneName) {
let passedBody: string | undefined
let passedContext: Record<string, any> | undefined
const expectedEventDateMs = 0 //some date in the future or past
this.timezoneLoaderTestDouble.setTimezone(timezone) //use some test double that can be accessed in the production code
await eventFaker.on('send-message::v2020_12_25', ( { payload }) => {
passedBody = payload.message.body
passedContext = payload.message.context
return {
message: {
...
},
}
})
await this.someOperationThatSendsAMessage()
assert.doesInclude(passedBody, '')
assert.isEqualDeep(passedContext, { timezone, eventDateMs: expectedEventDateMs })
}
Production 4: Send a message with the timezone
private async someOperationThatSendsAMessage() {
const timezone = this.someDataSource.getSomeTimezone() //some method that returns a timezone that is test doubled
await this.client.emitAndFlattenResponses('send-message::v2020_12_25', {
target: {},
payload: {
message: {
...,
body: `Your journey starts in {{formatDateTimeUntil eventDateMs}}!`,
context: {
eventDateMs: 0 //some date in the future or past,
timezone,
}
}
},
}
}