Skip to content

Commit c4522be

Browse files
authored
Merge pull request #183 from neaps/utc
Fix timezone handling by using UTC internally
2 parents afb4c3d + ac374a9 commit c4522be

10 files changed

Lines changed: 76 additions & 100 deletions

File tree

benchmarks/noaa.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { findStation } from 'neaps'
66
import { stations as db } from '@neaps/tide-database'
77
import createFetch from 'make-fetch-happen'
88

9-
process.env.TZ = 'UTC'
10-
119
const __dirname = new URL('.', import.meta.url).pathname
1210
const fetch = createFetch.defaults({
1311
cachePath: join(__dirname, '.cache'),
@@ -59,7 +57,7 @@ for (const id of stations) {
5957

6058
const noaaEvents: Extreme[] | undefined = (
6159
await fetchNOAAdata(id, datum)
62-
).predictions?.map((e) => ({
60+
).predictions?.map((e: { t: string; v: string; type: 'H' | 'L' }) => ({
6361
time: new Date(e.t + ' GMT').getTime(),
6462
level: parseFloat(e.v),
6563
type: e.type
@@ -90,8 +88,14 @@ for (const id of stations) {
9088
const dtMinutes: number[] = []
9189
const dhMeters: number[] = []
9290

93-
const noaa = Object.groupBy(noaaEvents, (e) => e.type)
94-
const neaps = Object.groupBy(neapsEvents, (e) => e.type)
91+
const noaa = Object.groupBy(noaaEvents, (e) => e.type) as Record<
92+
'H' | 'L',
93+
Extreme[] | undefined
94+
>
95+
const neaps = Object.groupBy(neapsEvents, (e) => e.type) as Record<
96+
'H' | 'L',
97+
Extreme[] | undefined
98+
>
9599

96100
const matchAndCollect = (noaaList: Extreme[], neapsList: Extreme[]) => {
97101
let j = 0
@@ -152,8 +156,8 @@ for (const id of stations) {
152156
matchAndCollect(noaa.H ?? [], neaps.H ?? [])
153157
matchAndCollect(noaa.L ?? [], neaps.L ?? [])
154158

155-
const events_noaa = noaa.H.length + noaa.L.length
156-
const events_model = neaps.H.length + neaps.L.length
159+
const events_noaa = (noaa.H?.length ?? 0) + (noaa.L?.length ?? 0)
160+
const events_model = (neaps.H?.length ?? 0) + (neaps.L?.length ?? 0)
157161

158162
// Timing metrics (minutes)
159163
const absDt = sort(dtMinutes.map((v) => Math.abs(v)))

packages/neaps/test/index.test.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,49 @@ import {
99
} from '../src/index.js'
1010
import { describe, test, expect } from 'vitest'
1111

12-
// FIXME: this is required for these tests to pass. I can't figure out how to get accurate
13-
// predictions for a station in a non-UTC timezone without this.
14-
process.env.TZ = 'UTC'
12+
describe('timezone independence', () => {
13+
const location = { lat: 26.772, lon: -80.05 }
14+
15+
test('equivalent instants yield identical extremes', () => {
16+
const station = nearestStation(location)
17+
18+
// Same instant range expressed in different offsets
19+
const utc = {
20+
start: new Date('2025-12-18T00:00:00Z'),
21+
end: new Date('2025-12-19T00:00:00Z'),
22+
timeFidelity: 60,
23+
datum: 'MLLW' as const
24+
}
25+
const newYork = {
26+
start: new Date('2025-12-17T19:00:00-05:00'),
27+
end: new Date('2025-12-18T19:00:00-05:00'),
28+
timeFidelity: 60,
29+
datum: 'MLLW' as const
30+
}
31+
const tokyo = {
32+
start: new Date('2025-12-18T09:00:00+09:00'),
33+
end: new Date('2025-12-19T09:00:00+09:00'),
34+
timeFidelity: 60,
35+
datum: 'MLLW' as const
36+
}
37+
38+
const baseline = station.getExtremesPrediction(utc).extremes
39+
const ny = station.getExtremesPrediction(newYork).extremes
40+
const jp = station.getExtremesPrediction(tokyo).extremes
41+
42+
;[ny, jp].forEach((result) => {
43+
expect(result.length).toBe(baseline.length)
44+
result.forEach((extreme, index) => {
45+
const base = baseline[index]
46+
expect(extreme.time.valueOf()).toBe(base.time.valueOf())
47+
expect(extreme.high).toBe(base.high)
48+
expect(extreme.low).toBe(base.low)
49+
expect(extreme.label).toBe(base.label)
50+
expect(extreme.level).toBeCloseTo(base.level, 6)
51+
})
52+
})
53+
})
54+
})
1555

1656
describe('getExtremesPrediction', () => {
1757
const options = {

packages/tide-predictor/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const highLowTides = TidePredictor(constituents, {
4141
})
4242
```
4343

44-
Note that, for now, Neaps **will not** do any timezone corrections. This means you need to pass date objects that align with whatever timezone the constituents are in.
44+
Note that all times internally are evaluated as UTC, so be sure to specify a timezone offset when constructing dates if you want to work in a local time. For example, to get tides for January 1st, 2019 in New York (UTC-5), create a date `new Date('2019-01-01T00:00:00-05:00')`
4545

4646
## Tide prediction object
4747

packages/tide-predictor/src/astronomy/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ const T = (t: Date): number => {
5252

5353
// Meeus formula 7.1
5454
const JD = (t: Date): number => {
55-
let Y = t.getFullYear()
56-
let M = t.getMonth() + 1
55+
let Y = t.getUTCFullYear()
56+
let M = t.getUTCMonth() + 1
5757
const D =
58-
t.getDate() +
59-
t.getHours() / 24.0 +
60-
t.getMinutes() / (24.0 * 60.0) +
61-
t.getSeconds() / (24.0 * 60.0 * 60.0) +
62-
t.getMilliseconds() / (24.0 * 60.0 * 60.0 * 1e6)
58+
t.getUTCDate() +
59+
t.getUTCHours() / 24.0 +
60+
t.getUTCMinutes() / (24.0 * 60.0) +
61+
t.getUTCSeconds() / (24.0 * 60.0 * 60.0) +
62+
t.getUTCMilliseconds() / (24.0 * 60.0 * 60.0 * 1e6)
6363
if (M <= 2) {
6464
Y = Y - 1
6565
M = M + 12

packages/tide-predictor/test/astronomy/index.test.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,7 @@ import astro, {
1111
_nupp
1212
} from '../../src/astronomy/index.js'
1313

14-
const sampleTime = new Date()
15-
sampleTime.setFullYear(2019)
16-
sampleTime.setMonth(9)
17-
sampleTime.setDate(4)
18-
sampleTime.setHours(10)
19-
sampleTime.setMinutes(15)
20-
sampleTime.setSeconds(40)
21-
sampleTime.setMilliseconds(10)
14+
const sampleTime = new Date('2019-10-04T10:15:40.010Z')
2215

2316
describe('astronomy', () => {
2417
it('complete astronomic calculation', () => {
@@ -63,11 +56,12 @@ describe('astronomy', () => {
6356
})
6457

6558
it('evaluates Meeus formula 7.1 (JD) correctly', () => {
66-
sampleTime.setMonth(9)
67-
expect(JD(sampleTime)).toBeCloseTo(2458760.92755, 2)
59+
const time = new Date(sampleTime)
60+
time.setUTCMonth(9)
61+
expect(JD(time)).toBeCloseTo(2458760.92755, 2)
6862
// Months of less than 2 go back a year
69-
sampleTime.setMonth(0)
70-
expect(JD(sampleTime)).toBeCloseTo(2458487.92755, 2)
63+
time.setUTCMonth(0)
64+
expect(JD(time)).toBeCloseTo(2458487.92755, 2)
7165
})
7266

7367
it('evaluates Meeus formula 11.1 (T) correctly', () => {

packages/tide-predictor/test/constituents/compound-constituent.test.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,7 @@ import compoundConstituent from '../../src/constituents/compound-constituent.js'
33
import Constituent from '../../src/constituents/constituent.js'
44
import astro from '../../src/astronomy/index.js'
55

6-
const sampleTime = new Date()
7-
sampleTime.setFullYear(2019)
8-
sampleTime.setMonth(9)
9-
sampleTime.setDate(4)
10-
sampleTime.setHours(10)
11-
sampleTime.setMinutes(15)
12-
sampleTime.setSeconds(40)
13-
sampleTime.setMilliseconds(10)
14-
6+
const sampleTime = new Date('2019-10-04T10:15:40.010Z')
157
const testAstro = astro(sampleTime)
168

179
// This is a made-up doodson number for a test coefficient

packages/tide-predictor/test/constituents/constituent.test.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,7 @@ import constituent, {
66
} from '../../src/constituents/constituent.js'
77
import astro from '../../src/astronomy/index.js'
88

9-
const sampleTime = new Date()
10-
sampleTime.setFullYear(2019)
11-
sampleTime.setMonth(9)
12-
sampleTime.setDate(4)
13-
sampleTime.setHours(10)
14-
sampleTime.setMinutes(15)
15-
sampleTime.setSeconds(40)
16-
sampleTime.setMilliseconds(10)
17-
9+
const sampleTime = new Date('2019-10-04T10:15:40.010Z')
1810
const testAstro = astro(sampleTime)
1911

2012
// This is a made-up doodson number for a test coefficient

packages/tide-predictor/test/constituents/index.test.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@ import { describe, it, expect } from 'vitest'
22
import constituents from '../../src/constituents/index.js'
33
import astro from '../../src/astronomy/index.js'
44

5-
const sampleTime = new Date()
6-
sampleTime.setFullYear(2019)
7-
sampleTime.setMonth(9)
8-
sampleTime.setDate(4)
9-
sampleTime.setHours(10)
10-
sampleTime.setMinutes(15)
11-
sampleTime.setSeconds(40)
12-
sampleTime.setMilliseconds(10)
13-
5+
const sampleTime = new Date('2019-10-04T10:15:40.010Z')
146
const testAstro = astro(sampleTime)
157

168
describe('Base constituent definitions', () => {

packages/tide-predictor/test/harmonics/prediction.test.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,9 @@ import { describe, it, expect } from 'vitest'
22
import harmonics, { ExtremeOffsets } from '../../src/harmonics/index.js'
33
import mockHarmonicConstituents from '../_mocks/constituents.js'
44

5-
const startDate = new Date()
6-
startDate.setFullYear(2019)
7-
startDate.setMonth(8)
8-
startDate.setDate(1)
9-
startDate.setHours(0)
10-
startDate.setMinutes(0)
11-
startDate.setSeconds(0)
12-
startDate.setMilliseconds(0)
13-
14-
const endDate = new Date()
15-
endDate.setFullYear(2019)
16-
endDate.setMonth(8)
17-
endDate.setDate(1)
18-
endDate.setHours(6)
19-
endDate.setMinutes(0)
20-
endDate.setSeconds(0)
21-
endDate.setMilliseconds(0)
22-
23-
const extremesEndDate = new Date()
24-
extremesEndDate.setFullYear(2019)
25-
extremesEndDate.setMonth(8)
26-
extremesEndDate.setDate(3)
27-
extremesEndDate.setHours(0)
28-
extremesEndDate.setMinutes(0)
29-
extremesEndDate.setSeconds(0)
30-
extremesEndDate.setMilliseconds(0)
5+
const startDate = new Date('2019-09-01T00:00:00Z')
6+
const endDate = new Date('2019-09-01T06:00:00Z')
7+
const extremesEndDate = new Date('2019-09-03T00:00:00Z')
318

329
const setUpPrediction = () => {
3310
const harmonic = harmonics({

packages/tide-predictor/test/index.test.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,8 @@ import { describe, it, expect } from 'vitest'
22
import mockConstituents from './_mocks/constituents.js'
33
import tidePrediction from '../src/index.js'
44

5-
const startDate = new Date()
6-
startDate.setFullYear(2019)
7-
startDate.setMonth(8)
8-
startDate.setDate(1)
9-
startDate.setHours(0)
10-
startDate.setMinutes(0)
11-
startDate.setSeconds(0)
12-
startDate.setMilliseconds(0)
13-
14-
const endDate = new Date()
15-
endDate.setFullYear(2019)
16-
endDate.setMonth(8)
17-
endDate.setDate(1)
18-
endDate.setHours(6)
19-
endDate.setMinutes(0)
20-
endDate.setSeconds(0)
21-
endDate.setMilliseconds(0)
5+
const startDate = new Date('2019-09-01T00:00:00Z')
6+
const endDate = new Date('2019-09-01T06:00:00Z')
227

238
describe('Tidal station', () => {
249
it('it is created correctly', () => {

0 commit comments

Comments
 (0)