@@ -13,23 +13,43 @@ import { describe, test, expect } from 'vitest'
1313// predictions for a station in a non-UTC timezone without this.
1414process . env . TZ = 'UTC'
1515
16+ expect . extend ( {
17+ toBeWithin ( received , expected , delta ) {
18+ const diff = Math . abs ( received - expected )
19+ const pass = diff <= delta
20+ return {
21+ pass,
22+ message : ( ) =>
23+ `expected ${ received } to be within ±${ delta } of ${ expected } , but was ${ diff } `
24+ }
25+ }
26+ } )
27+
28+ interface CustomMatchers < R = unknown > {
29+ toBeWithin : ( expected : number , delta : number ) => R
30+ }
31+
32+ declare module 'vitest' {
33+ interface Matchers < T = any > extends CustomMatchers < T > { }
34+ }
35+
1636describe ( 'getExtremesPrediction' , ( ) => {
1737 test ( 'gets extremes from nearest station' , ( ) => {
1838 const prediction = getExtremesPrediction ( {
19- lat : 26.7 ,
39+ lat : 26.772 ,
2040 lon : - 80.05 ,
2141 start : new Date ( '2025-12-18T00:00:00-05:00' ) ,
2242 end : new Date ( '2025-12-19T00:00:00-05:00' ) ,
23- timeFidelity : 6 ,
43+ timeFidelity : 60 ,
2444 datum : 'MLLW'
2545 } )
2646
27- expect ( prediction . station . id ) . toEqual ( 'us-fl-port-of-west- palm-beach' )
47+ expect ( prediction . station . id ) . toEqual ( 'us-fl-port-of-palm-beach' )
2848 expect ( prediction . datum ) . toBe ( 'MLLW' )
2949
3050 const { extremes } = prediction
3151 expect ( extremes . length ) . toBe ( 4 )
32- expect ( extremes [ 0 ] . time ) . toEqual ( new Date ( '2025-12-18T05:29:48 .000Z' ) )
52+ expect ( extremes [ 0 ] . time ) . toEqual ( new Date ( '2025-12-18T05:30:00 .000Z' ) )
3353 expect ( extremes [ 0 ] . level ) . toBeCloseTo ( 0.02 , 2 )
3454 expect ( extremes [ 0 ] . high ) . toBe ( false )
3555 expect ( extremes [ 0 ] . low ) . toBe ( true )
@@ -40,13 +60,13 @@ describe('getExtremesPrediction', () => {
4060describe ( 'getTimelinePrediction' , ( ) => {
4161 test ( 'gets timeline from nearest station' , ( ) => {
4262 const timeline = getTimelinePrediction ( {
43- lat : 26.7 ,
63+ lat : 26.772 ,
4464 lon : - 80.05 ,
4565 start : new Date ( '2025-12-19T00:00:00-05:00' ) ,
4666 end : new Date ( '2025-12-19T01:00:00-05:00' )
4767 } )
4868
49- expect ( timeline . station . id ) . toEqual ( 'us-fl-port-of-west- palm-beach' )
69+ expect ( timeline . station . id ) . toEqual ( 'us-fl-port-of-palm-beach' )
5070 expect ( timeline . datum ) . toBe ( 'MLLW' )
5171 expect ( timeline . timeline . length ) . toBe ( 7 ) // Every 10 minutes for 1 hour = 7 points
5272 } )
@@ -55,13 +75,13 @@ describe('getTimelinePrediction', () => {
5575describe ( 'getWaterLevelAtTime' , ( ) => {
5676 test ( 'gets water level at specific time from nearest station' , ( ) => {
5777 const prediction = getWaterLevelAtTime ( {
58- lat : 26.7 ,
78+ lat : 26.772 ,
5979 lon : - 80.05 ,
6080 time : new Date ( '2025-12-19T00:30:00-05:00' ) ,
6181 datum : 'MSL'
6282 } )
6383
64- expect ( prediction . station . id ) . toEqual ( 'us-fl-port-of-west- palm-beach' )
84+ expect ( prediction . station . id ) . toEqual ( 'us-fl-port-of-palm-beach' )
6585 expect ( prediction . datum ) . toBe ( 'MSL' )
6686 expect ( prediction . time ) . toEqual ( new Date ( '2025-12-19T05:30:00.000Z' ) )
6787 expect ( typeof prediction . level ) . toBe ( 'number' )
@@ -73,27 +93,88 @@ describe('for a specific station', () => {
7393
7494 describe ( 'getExtremesPrediction' , ( ) => {
7595 test ( 'can return extremes from station' , ( ) => {
76- const station = nearestStation ( { lat : 26.7 , lon : - 80.05 } )
96+ const station = nearestStation ( { lat : 26.772 , lon : - 80.05 } )
7797
7898 const start = new Date ( '2025-12-17T00:00:00-05:00' )
7999 const end = new Date ( '2025-12-18T05:00:00-05:00' )
80100
81101 const { extremes : predictions } = station . getExtremesPrediction ( {
82102 start,
83103 end,
84- timeFidelity : 6 ,
104+ timeFidelity : 60 ,
85105 datum : 'MLLW'
86106 } )
87107
88108 expect ( predictions . length ) . toBe ( 4 )
89- expect ( predictions [ 0 ] . time ) . toEqual ( new Date ( '2025-12-17T11:23:06 .000Z' ) )
109+ expect ( predictions [ 0 ] . time ) . toEqual ( new Date ( '2025-12-17T11:23:00 .000Z' ) )
90110 expect ( predictions [ 0 ] . level ) . toBeCloseTo ( 0.9 , 1 )
91111 expect ( predictions [ 0 ] . high ) . toBe ( true )
92112 expect ( predictions [ 0 ] . low ) . toBe ( false )
93113 expect ( predictions [ 0 ] . label ) . toBe ( 'High' )
94114 } )
95115 } )
96116
117+ describe ( 'for a subordinate station' , ( ) => {
118+ const station = findStation ( '9411189' )
119+
120+ test ( 'gets datums and harmonic_constituents from reference station' , ( ) => {
121+ expect ( station . type ) . toBe ( 'subordinate' )
122+ expect ( station . datums ) . toBeDefined ( )
123+ expect ( station . datums ) . toEqual ( findStation ( '9410660' ) . datums )
124+ expect ( station . harmonic_constituents ) . toBeDefined ( )
125+ expect ( station . harmonic_constituents ) . toEqual (
126+ findStation ( '9410660' ) . harmonic_constituents
127+ )
128+ expect ( station . defaultDatum ) . toBe ( 'MLLW' )
129+ } )
130+
131+ describe ( 'getExtremesPrediction' , ( ) => {
132+ test ( 'returns adjusted predictions' , async ( ) => {
133+ const bigtime = stations
134+ . filter ( ( s ) => s . type === 'subordinate' )
135+ . sort (
136+ ( a , b ) =>
137+ Math . abs ( a . offsets ?. height . high ) -
138+ Math . abs ( b . offsets ?. height . high )
139+ )
140+ . reverse ( )
141+
142+ console . log ( bigtime [ 0 ] )
143+
144+ const station = useStation ( bigtime [ 0 ] )
145+
146+ const start = new Date ( '2025-12-17T00:00:00Z' )
147+ const end = new Date ( '2025-12-19T00:00:00Z' )
148+
149+ const prediction = station . getExtremesPrediction ( {
150+ start,
151+ end,
152+ timeFidelity : 60 ,
153+ datum : 'MLLW'
154+ } )
155+
156+ console . log ( prediction . extremes )
157+
158+ // https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?format=json&product=predictions&units=metric&time_zone=gmt&station=TEC4769&begin_date=2025-12-17&end_date=2025-12-18&interval=hilo&datum=MLLW
159+ const noaa = [
160+ { t : '2025-12-17T01:44:00Z' , v : - 0.091 , type : 'L' } ,
161+ { t : '2025-12-17T08:50:00Z' , v : 0.75 , type : 'H' } ,
162+ { t : '2025-12-18T02:15:00Z' , v : - 0.13 , type : 'L' } ,
163+ { t : '2025-12-18T09:24:00Z' , v : 0.754 , type : 'H' }
164+ ]
165+
166+ noaa . forEach ( ( expected , index ) => {
167+ const actual = prediction . extremes [ index ]
168+ expect ( actual . time ) . toBeWithin (
169+ new Date ( expected . t ) ,
170+ 25 * 60 * 1000 /* min */
171+ )
172+ expect ( actual . level ) . toBeWithin ( expected . v , 0.04 /* m */ )
173+ } )
174+ } )
175+ } )
176+ } )
177+
97178 describe ( 'getTimelinePrediction' , ( ) => {
98179 test ( 'gets timeline' , ( ) => {
99180 const prediction = station . getTimelinePrediction ( {
@@ -119,20 +200,14 @@ describe('for a specific station', () => {
119200} )
120201
121202describe ( 'nearestStation' , ( ) => {
122- test ( 'finds the nearest station' , ( ) => {
123- const station = nearestStation ( { lat : 26.7 , lon : - 80.05 } )
124- expect ( station . source . id ) . toBe ( '8722588' )
125- expect ( station . latitude ) . toBeCloseTo ( 26.77 )
126- expect ( station . longitude ) . toBeCloseTo ( - 80.0517 )
127- } )
128203 ; [
129- { lat : 26.7 , lon : - 80.05 } ,
130- { lat : 26.7 , lng : - 80.05 } ,
131- { latitude : 26.7 , longitude : - 80.05 }
204+ { lat : 26.772 , lon : - 80.052 } ,
205+ { lat : 26.772 , lng : - 80.052 } ,
206+ { latitude : 26.772 , longitude : - 80.052 }
132207 ] . forEach ( ( position ) => {
133208 test ( `finds station with ${ Object . keys ( position ) . join ( '/' ) } ` , ( ) => {
134209 const station = nearestStation ( position )
135- expect ( station . id ) . toEqual ( 'us-fl-port-of-west-palm-beach ')
210+ expect ( station . source . id ) . toBe ( '8722588 ')
136211 } )
137212 } )
138213} )
@@ -143,9 +218,9 @@ describe('findStation', () => {
143218 } )
144219
145220 test ( 'finds station by id' , ( ) => {
146- const station = findStation ( 'us-fl-ankona-indian-river ' )
221+ const station = findStation ( 'us-ma-boston ' )
147222 expect ( station ) . toBeDefined ( )
148- expect ( station . id ) . toBe ( 'us-fl-ankona-indian-river ' )
223+ expect ( station . id ) . toBe ( 'us-ma-boston ' )
149224 expect ( station . getExtremesPrediction ) . toBeDefined ( )
150225 } )
151226
@@ -159,7 +234,7 @@ describe('findStation', () => {
159234
160235describe ( 'datum' , ( ) => {
161236 test ( 'defaults to MLLW datum' , ( ) => {
162- const station = findStation ( 'us-fl-ankona-indian-river ' )
237+ const station = findStation ( '8722274 ' )
163238 const extremes = station . getExtremesPrediction ( {
164239 start : new Date ( '2025-12-17T00:00:00Z' ) ,
165240 end : new Date ( '2025-12-18T00:00:00Z' )
@@ -168,7 +243,7 @@ describe('datum', () => {
168243 } )
169244
170245 test ( 'accepts datum option' , ( ) => {
171- const station = findStation ( 'us-fl-ankona-indian-river ' )
246+ const station = findStation ( '8722274 ' )
172247 const extremes = station . getExtremesPrediction ( {
173248 start : new Date ( '2025-12-17T00:00:00Z' ) ,
174249 end : new Date ( '2025-12-18T00:00:00Z' ) ,
@@ -191,21 +266,26 @@ describe('datum', () => {
191266 test ( 'throws error when missing MSL datum' , ( ) => {
192267 // Find station without MSL but with other datums
193268 const station = stations . find (
194- ( s ) => ! ( 'MSL' in s . datums ) && Object . keys ( s . datums ) . length > 0
269+ ( s ) =>
270+ s . type === 'reference' &&
271+ ! ( 'MSL' in s . datums ) &&
272+ Object . keys ( s . datums ) . length > 0
195273 )
196274 if ( ! station ) expect . fail ( 'No station without MSL datum found' )
197275 expect ( ( ) => {
198276 useStation ( station ) . getExtremesPrediction ( {
199277 start : new Date ( '2025-12-17T00:00:00Z' ) ,
200278 end : new Date ( '2025-12-18T00:00:00Z' ) ,
201- datum : 'NAVD88'
279+ datum : Object . keys ( station . datums ) [ 0 ]
202280 } )
203281 } ) . toThrow ( / m i s s i n g M S L / )
204282 } )
205283
206284 test ( 'does not apply datums when non available' , ( ) => {
207285 // Find a station with no datums
208- const station = stations . find ( ( s ) => Object . entries ( s . datums ) . length === 0 )
286+ const station = stations . find (
287+ ( s ) => s . type === 'reference' && Object . entries ( s . datums ) . length === 0
288+ )
209289 if ( ! station ) expect . fail ( 'No station without datums found' )
210290 const extremes = useStation ( station ) . getExtremesPrediction ( {
211291 start : new Date ( '2025-12-17T00:00:00Z' ) ,
0 commit comments