Skip to content

Commit 0c41ba3

Browse files
ctcpipblakeembrey
andcommitted
✨ add timezone support
Co-authored-by: Blake Embrey <[email protected]>
1 parent 607ebe3 commit 0c41ba3

File tree

4 files changed

+90
-5
lines changed

4 files changed

+90
-5
lines changed

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ inputs:
1111
schedules:
1212
description: 'An array of strings representing the meeting schedules in the format YYYY-MM-DD (ISO-8601 intervals).'
1313
required: true
14+
timezone:
15+
description: 'Timezone to use for schedule times (e.g. "America/Chicago", "Europe/Madrid"). Useful for following daylight saving time (DST) / summer time. Default: "UTC"'
16+
required: false
1417
createWithin:
1518
description: 'Defines when the meeting issues are created using ISO-8601 durations. Defaults to one week before the meeting (Using the ISO-8601 durations format, this is P7D).'
1619
default: 'P7D'

lib/meetings.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module.exports.setMeetingIssueBody = async function (client, opts) {
2323

2424
function getNextIssue (opts) {
2525
const now = opts.now || Temporal.Now.instant()
26-
const date = getNextScheduledMeeting(opts.schedules, now)
26+
const date = getNextScheduledMeeting(opts.schedules, now, opts.timezone)
2727
const title = typeof opts.issueTitle === 'function' ? opts.issueTitle({ date }) : opts.issueTitle
2828

2929
const issue = {
@@ -76,18 +76,33 @@ const shouldCreateNextMeetingIssue = module.exports.shouldCreateNextMeetingIssue
7676
return issue
7777
}
7878

79-
const getNextScheduledMeeting = module.exports.getNextScheduledMeeting = function (schedules = [], now = Temporal.Now.instant()) {
79+
const getNextScheduledMeeting = module.exports.getNextScheduledMeeting = function (schedules = [], now = Temporal.Now.instant(), timezone) {
8080
return schedules
8181
.map((s = `${now}/P7D`) => {
8282
// Parse interval
8383
const [startStr, durationStr] = s.split('/')
84-
const start = Temporal.Instant.from(startStr)
8584
const duration = Temporal.Duration.from(durationStr)
85+
let next
86+
87+
if (timezone) {
88+
// parse as local in specified tz
89+
try {
90+
const zonedStart = Temporal.ZonedDateTime.from(`${startStr}[${timezone}]`)
91+
next = zonedStart.toInstant()
92+
} catch (e) {
93+
console.warn(`invalid timezone '${timezone}'; using UTC`)
94+
console.error(e) // s/b caused by invalid timezone but log error just in case for troubleshooting
95+
}
96+
}
97+
98+
if (!next) {
99+
// parse as UTC
100+
next = Temporal.Instant.from(startStr)
101+
}
86102

87103
// Get datetime for next event after now
88-
let next = start
89104
while (next.epochMilliseconds <= now.epochMilliseconds) {
90-
const zonedNext = next.toZonedDateTimeISO('UTC')
105+
const zonedNext = next.toZonedDateTimeISO(timezone || 'UTC')
91106
next = zonedNext.add(duration).toInstant()
92107
}
93108

run.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const pkg = require('./package.json')
2020
// variables we use for timing
2121
const schedules = list(core.getInput('schedules'))
2222
const createWithin = core.getInput('createWithin')
23+
const timezone = core.getInput('timezone')
2324

2425
// variables we use for labels
2526
const agendaLabel = core.getInput('agendaLabel')
@@ -115,6 +116,7 @@ const pkg = require('./package.json')
115116
const opts = {
116117
...repo,
117118
schedules,
119+
timezone,
118120
meetingLabels,
119121
createWithin,
120122
agendaLabel,

test/index.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,71 @@ suite(`${pkg.name} unit`, () => {
131131
assert(formattedDate.includes('Mar'))
132132
assert(formattedDate.includes('08:00 AM'))
133133
})
134+
135+
test('schedule parsing with timezone - UTC default', () => {
136+
const schedules = ['2024-03-27T21:00:00.0Z/P2W']
137+
const now = Temporal.Instant.from('2024-03-20T13:00:00.0Z')
138+
const next = meetings.getNextScheduledMeeting(schedules, now)
139+
const expected = Temporal.Instant.from('2024-03-27T21:00:00.0Z')
140+
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
141+
})
142+
143+
test('schedule parsing with timezone - Chicago', () => {
144+
const schedules = ['2024-03-27T21:00:00.0/P2W']
145+
const now = Temporal.Instant.from('2024-03-20T13:00:00.0Z')
146+
const next = meetings.getNextScheduledMeeting(schedules, now, 'America/Chicago')
147+
// 21:00 CDT == 02:00 UTC next day
148+
const expected = Temporal.Instant.from('2024-03-28T02:00:00.0Z')
149+
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
150+
})
151+
152+
test('schedule parsing with timezone - DST spring forward', () => {
153+
const schedules = ['2025-03-09T01:00:00.0/P2W']
154+
155+
const beforeSpring = Temporal.Instant.from('2025-03-08T13:00:00.0Z')
156+
const nextBefore = meetings.getNextScheduledMeeting(schedules, beforeSpring, 'America/Chicago')
157+
// 01:00 CST == 07:00 UTC (before spring forward)
158+
const expectedBefore = Temporal.Instant.from('2025-03-09T07:00:00.0Z')
159+
assert.strictEqual(nextBefore.epochMilliseconds, expectedBefore.epochMilliseconds)
160+
161+
const afterSpring = Temporal.Instant.from('2025-03-09T13:00:00.0Z')
162+
const nextAfter = meetings.getNextScheduledMeeting(schedules, afterSpring, 'America/Chicago')
163+
// 01:00 CDT == 06:00 UTC (after spring forward)
164+
const expectedAfter = Temporal.Instant.from('2025-03-23T06:00:00.0Z')
165+
assert.strictEqual(nextAfter.epochMilliseconds, expectedAfter.epochMilliseconds)
166+
})
167+
168+
test('schedule parsing with timezone - DST fall back', () => {
169+
const schedules = ['2025-11-02T01:00:00.0/P2W']
170+
171+
const beforeFallBack = Temporal.Instant.from('2025-11-01T13:00:00.0Z')
172+
const nextBefore = meetings.getNextScheduledMeeting(schedules, beforeFallBack, 'America/Chicago')
173+
// 01:00 CDT == 06:00 UTC (before fall back)
174+
const expectedBefore = Temporal.Instant.from('2025-11-02T06:00:00.0Z')
175+
assert.strictEqual(nextBefore.epochMilliseconds, expectedBefore.epochMilliseconds)
176+
177+
const afterFallBack = Temporal.Instant.from('2025-11-02T13:00:00.0Z')
178+
const nextAfter = meetings.getNextScheduledMeeting(schedules, afterFallBack, 'America/Chicago')
179+
// 01:00 CST == 07:00 UTC (after fall back)
180+
const expectedAfter = Temporal.Instant.from('2025-11-16T07:00:00.0Z')
181+
assert.strictEqual(nextAfter.epochMilliseconds, expectedAfter.epochMilliseconds)
182+
})
183+
184+
test('schedule parsing with timezone - Los Angeles PST', () => {
185+
const schedules = ['2020-04-02T17:00:00/P7D']
186+
const now = Temporal.Instant.from('2021-01-01T00:00:00.0Z')
187+
const next = meetings.getNextScheduledMeeting(schedules, now, 'America/Los_Angeles')
188+
const expected = Temporal.Instant.from('2020-12-31T17:00:00.0-08:00')
189+
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
190+
})
191+
192+
test('schedule parsing with timezone - Los Angeles PDT', () => {
193+
const schedules = ['2020-04-02T17:00:00/P7D']
194+
const now = Temporal.Instant.from('2021-04-01T00:00:00.0Z')
195+
const next = meetings.getNextScheduledMeeting(schedules, now, 'America/Los_Angeles')
196+
const expected = Temporal.Instant.from('2021-04-01T17:00:00.0-07:00')
197+
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
198+
})
134199
})
135200

136201
test('shorthands transform', async () => {

0 commit comments

Comments
 (0)