Skip to content

Commit baebe56

Browse files
committed
Add a units option to get predictions in feet
1 parent 68da259 commit baebe56

4 files changed

Lines changed: 126 additions & 43 deletions

File tree

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ const prediction = getExtremesPrediction({
2727
longitude: -80.05, // or `lng` or `lon`
2828
start: new Date('2025-12-17'),
2929
end: new Date('2025-12-18'),
30-
datum: 'MLLW' // optional, defaults to MLLW if available
30+
datum: 'MLLW', // optional, defaults to MLLW if available
31+
units: 'meters' // optional, defaults to 'meters', can also be 'feet'
3132
})
3233

33-
console.log(extremes)
34+
console.log(prediction)
3435
// {
3536
// datum: 'MLLW',
37+
// units: 'meters',
3638
// station: {
3739
// id: '8723214',
3840
// name: 'Fort Lauderdale, FL',
@@ -56,12 +58,14 @@ const timeline = getTimelinePrediction({
5658
lon: -80.05,
5759
start: new Date('2025-12-19T00:00:00-05:00'),
5860
end: new Date('2025-12-19T01:00:00-05:00'),
59-
timeFidelity: 5 * 60 // seconds, defaults to `10 * 60`
61+
timeFidelity: 5 * 60, // seconds, defaults to `10 * 60`
62+
units: 'meters' // optional, defaults to 'meters', can also be 'feet'
6063
})
6164

6265
console.log(timeline)
6366
// {
6467
// datum: 'MLLW',
68+
// units: 'meters',
6569
// station: {
6670
// id: 'us-fl-port-of-west-palm-beach',
6771
// name: 'Port of West Palm Beach',
@@ -86,12 +90,14 @@ const prediction = getWaterLevelAtTime({
8690
lat: 26.7,
8791
lon: -80.05,
8892
time: new Date('2025-12-19T00:30:00-05:00'),
89-
datum: 'MSL'
93+
datum: 'MSL',
94+
units: 'meters' // optional, defaults to 'meters', can also be 'feet'
9095
})
9196

9297
console.log(prediction)
9398
// {
9499
// datum: 'MSL',
100+
// units: 'meters',
95101
// station: {
96102
// id: 'us-fl-port-of-west-palm-beach',
97103
// name: 'Port of West Palm Beach',

examples/units.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getExtremesPrediction } from 'neaps'
2+
3+
const options = {
4+
latitude: 22.24,
5+
longitude: -75.75,
6+
start: new Date('2026-01-06T00:00:00Z'),
7+
end: new Date('2026-01-07T00:00:00Z')
8+
}
9+
10+
const inMeters = getExtremesPrediction(options)
11+
const inFeet = getExtremesPrediction({ ...options, units: 'feet' })
12+
13+
console.table(
14+
inMeters.extremes.map((extreme, i) => ({
15+
time: extreme.time,
16+
type: extreme.label,
17+
meters: Number(extreme.level.toFixed(2)),
18+
feet: Number(inFeet.extremes[i].level.toFixed(2))
19+
}))
20+
)

packages/neaps/src/index.ts

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ import tidePredictor, {
66
} from '@neaps/tide-predictor'
77
import type { GeolibInputCoordinates } from 'geolib/es/types'
88

9-
type DatumOption = {
9+
type Units = 'meters' | 'feet'
10+
type PredictionOptions = {
1011
/** Datum to return predictions in. Defaults to 'MLLW' if available for the nearest station. */
1112
datum?: string
13+
14+
/** Units for returned water levels. Defaults to 'meters'. */
15+
units?: Units
1216
}
1317

14-
export type ExtremesOptions = ExtremesInput & DatumOption
15-
export type TimelineOptions = TimeSpan & DatumOption
16-
export type WaterLevelOptions = { time: Date } & DatumOption
18+
export type ExtremesOptions = ExtremesInput & PredictionOptions
19+
export type TimelineOptions = TimeSpan & PredictionOptions
20+
export type WaterLevelOptions = { time: Date } & PredictionOptions
21+
22+
const feetPerMeter = 3.2808399
23+
const defaultUnits: Units = 'meters'
1724

1825
/**
1926
* Get extremes prediction using the nearest station to the given position.
@@ -105,7 +112,7 @@ export function useStation(station: Station, distance?: number) {
105112
// Use MLLW as the default datum if available
106113
const defaultDatum = 'MLLW' in datums ? 'MLLW' : undefined
107114

108-
function getPredictor({ datum = defaultDatum }: DatumOption = {}) {
115+
function getPredictor({ datum = defaultDatum }: PredictionOptions = {}) {
109116
let offset = 0
110117

111118
if (datum) {
@@ -139,47 +146,62 @@ export function useStation(station: Station, distance?: number) {
139146
datums,
140147
harmonic_constituents,
141148
defaultDatum,
142-
getExtremesPrediction({ datum = defaultDatum, ...input }: ExtremesOptions) {
143-
return {
144-
datum,
145-
distance,
146-
station,
147-
extremes: getPredictor({ datum }).getExtremesPrediction({
148-
...input,
149-
offsets: station.offsets
150-
})
151-
}
149+
getExtremesPrediction({
150+
datum = defaultDatum,
151+
units = defaultUnits,
152+
...options
153+
}: ExtremesOptions) {
154+
const extremes = getPredictor({ datum })
155+
.getExtremesPrediction({ ...options, offsets: station.offsets })
156+
.map((e) => toPreferredUnits(e, units))
157+
158+
return { datum, units, station, distance, extremes }
152159
},
153160

154161
getTimelinePrediction({
155162
datum = defaultDatum,
156-
...params
163+
units = defaultUnits,
164+
...options
157165
}: TimelineOptions) {
158166
if (station.type === 'subordinate') {
159167
throw new Error(
160168
`Timeline predictions are not supported for subordinate stations.`
161169
)
162170
}
171+
const timeline = getPredictor({ datum })
172+
.getTimelinePrediction(options)
173+
.map((e) => toPreferredUnits(e, units))
163174

164-
return {
165-
datum,
166-
station,
167-
timeline: getPredictor({ datum }).getTimelinePrediction(params)
168-
}
175+
return { datum, units, station, distance, timeline }
169176
},
170177

171-
getWaterLevelAtTime({ time, datum = defaultDatum }: WaterLevelOptions) {
178+
getWaterLevelAtTime({
179+
time,
180+
datum = defaultDatum,
181+
units = defaultUnits
182+
}: WaterLevelOptions) {
172183
if (station.type === 'subordinate') {
173184
throw new Error(
174185
`Water level predictions are not supported for subordinate stations.`
175186
)
176187
}
177188

178-
return {
179-
datum,
180-
station,
181-
...getPredictor({ datum }).getWaterLevelAtTime({ time })
182-
}
189+
const prediction = toPreferredUnits(
190+
getPredictor({ datum }).getWaterLevelAtTime({ time }),
191+
units
192+
)
193+
194+
return { datum, units, station, distance, ...prediction }
183195
}
184196
}
185197
}
198+
199+
function toPreferredUnits<T extends { level: number }>(
200+
prediction: T,
201+
units: Units
202+
): T {
203+
let { level } = prediction
204+
if (units === 'feet') level *= feetPerMeter
205+
else if (units !== 'meters') throw new Error(`Unsupported units: ${units}`)
206+
return { ...prediction, level }
207+
}

packages/neaps/test/index.test.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ import { describe, test, expect } from 'vitest'
1414
process.env.TZ = 'UTC'
1515

1616
describe('getExtremesPrediction', () => {
17+
const options = {
18+
lat: 26.772,
19+
lon: -80.05,
20+
start: new Date('2025-12-18T00:00:00-05:00'),
21+
end: new Date('2025-12-19T00:00:00-05:00'),
22+
timeFidelity: 60,
23+
datum: 'MLLW'
24+
}
25+
1726
test('gets extremes from nearest station', () => {
18-
const prediction = getExtremesPrediction({
19-
lat: 26.772,
20-
lon: -80.05,
21-
start: new Date('2025-12-18T00:00:00-05:00'),
22-
end: new Date('2025-12-19T00:00:00-05:00'),
23-
timeFidelity: 60,
24-
datum: 'MLLW'
25-
})
27+
const prediction = getExtremesPrediction(options)
2628

2729
expect(prediction.station.id).toEqual('us/fl/port-of-palm-beach')
2830
expect(prediction.datum).toBe('MLLW')
@@ -34,21 +36,42 @@ describe('getExtremesPrediction', () => {
3436
expect(extremes[0].high).toBe(false)
3537
expect(extremes[0].low).toBe(true)
3638
expect(extremes[0].label).toBe('Low')
39+
expect(prediction.units).toBe('meters')
40+
})
41+
42+
test('with units=feet', () => {
43+
const prediction = getExtremesPrediction({ ...options, units: 'feet' })
44+
expect(prediction.units).toBe('feet')
45+
expect(prediction.extremes[0].level).toBeCloseTo(0.06, 2)
46+
expect(prediction.extremes[1].level).toBeCloseTo(3.01, 2)
3747
})
3848
})
3949

4050
describe('getTimelinePrediction', () => {
4151
test('gets timeline from nearest station', () => {
42-
const timeline = getTimelinePrediction({
52+
const prediction = getTimelinePrediction({
4353
lat: 26.772,
4454
lon: -80.05,
4555
start: new Date('2025-12-19T00:00:00-05:00'),
4656
end: new Date('2025-12-19T01:00:00-05:00')
4757
})
4858

49-
expect(timeline.station.id).toEqual('us/fl/port-of-palm-beach')
50-
expect(timeline.datum).toBe('MLLW')
51-
expect(timeline.timeline.length).toBe(7) // Every 10 minutes for 1 hour = 7 points
59+
expect(prediction.station.id).toEqual('us/fl/port-of-palm-beach')
60+
expect(prediction.datum).toBe('MLLW')
61+
expect(prediction.units).toBe('meters')
62+
expect(prediction.timeline.length).toBe(7) // Every 10 minutes for 1 hour = 7 points
63+
})
64+
65+
test('with units=feet', () => {
66+
const prediction = getTimelinePrediction({
67+
lat: 26.772,
68+
lon: -80.05,
69+
start: new Date('2025-12-19T00:00:00-05:00'),
70+
end: new Date('2025-12-19T01:00:00-05:00'),
71+
units: 'feet'
72+
})
73+
expect(prediction.units).toBe('feet')
74+
expect(prediction.timeline[0].level).toBeCloseTo(0.24, 2)
5275
})
5376
})
5477

@@ -66,6 +89,18 @@ describe('getWaterLevelAtTime', () => {
6689
expect(prediction.time).toEqual(new Date('2025-12-19T05:30:00.000Z'))
6790
expect(typeof prediction.level).toBe('number')
6891
})
92+
93+
test('with units=feet', () => {
94+
const prediction = getWaterLevelAtTime({
95+
lat: 26.772,
96+
lon: -80.05,
97+
time: new Date('2025-12-19T00:30:00-05:00'),
98+
datum: 'MSL',
99+
units: 'feet'
100+
})
101+
expect(prediction.units).toBe('feet')
102+
expect(prediction.level).toBeCloseTo(-1.44, 2)
103+
})
69104
})
70105

71106
describe('for a specific station', () => {

0 commit comments

Comments
 (0)