Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/moon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,16 +1091,20 @@ export const isBlueMoon = (datetime: Date): boolean => {
// then it is considered a Blue Moon. However, it is impossible to have a
// Blue Moon on any other days except the 29th, 30th, or 31st of a given month,
// as the synodic month is 29.530588853 days long.
const today = datetime.getDate()
//
// N.B. The calendar month is the month of UTC, and not the month of the host system: the day of
// the month of the host is not the day of the month of UTC, and so the same instant would be a
// Blue Moon on a host in one timezone and not on a host in another:
const today = datetime.getUTCDate()

// Calculate the fraction of the current day, in milliseconds, of the synodic month:
const f = (LUNAR_SYNODIC_MONTH - 29) * 24 * 60 * 60 * 1000

const ms =
datetime.getHours() * 60 * 60 * 1000 +
datetime.getMinutes() * 60 * 1000 +
datetime.getSeconds() * 1000 +
datetime.getMilliseconds()
datetime.getUTCHours() * 60 * 60 * 1000 +
datetime.getUTCMinutes() * 60 * 1000 +
datetime.getUTCSeconds() * 1000 +
datetime.getUTCMilliseconds()

// If the time on the current day is less than the fraction of the day of the synodic month,
// then the Moon is not a Blue Moon or if the day is not the 29th, 30th, or 31st:
Expand Down
23 changes: 23 additions & 0 deletions tests/moon.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,29 @@ describe('isBlueMoon', () => {
expect(isBlueMoon(datetime)).toBe(false)
})

it('should not depend on the timezone of the host system', () => {
// The calendar month is the month of UTC, and so the same instant is a Blue Moon wherever the
// host is. The instants below fall on the last day of the month of UTC, but on the first day
// of the next month for a host to the east of it, and, for the one late in the day, on the
// penultimate day of the month for a host to the west of it:
const timezone = process.env.TZ

try {
// N.B. The instant on the 29th is compared against the fraction of a day by which the
// synodic month exceeds 29 days, e.g., ~12.7 hours, and so it exercises the time of day as
// well as the day of the month:
for (const iso of ['1999-03-31T23:49:00Z', '2023-08-31T01:35:00Z', '2004-08-29T18:00:00Z']) {
for (const TZ of ['UTC', 'Pacific/Auckland', 'Asia/Tokyo', 'America/Los_Angeles']) {
process.env.TZ = TZ

expect(isBlueMoon(new Date(iso))).toBe(true)
}
}
} finally {
process.env.TZ = timezone
}
})

it('should return true for August 31st 2023', () => {
// We know there is a second full moon in August 2023:
const datetime = new Date('2023-08-31T01:35:00Z')
Expand Down