buoydata 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Graham Eger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # buoydata
2
+
3
+ Modern TypeScript SDK for NOAA NDBC realtime buoy data. The library provides a fetch layer, parsing helpers, and typed models for standard meteorological measurements while still supporting arbitrary realtime2 data types.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add buoydata
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```ts
14
+ import {
15
+ fetchRealtimeData,
16
+ parseRealtimeData,
17
+ parseRealtimeTable,
18
+ } from 'buoydata';
19
+
20
+ const raw = await fetchRealtimeData({ buoyId: '46026', type: 'txt' });
21
+ const data = parseRealtimeData('46026', raw);
22
+
23
+ console.log(data.measurements[0].wind.averageSpeed);
24
+
25
+ const table = parseRealtimeTable(raw);
26
+ console.log(table.headers);
27
+ ```
28
+
29
+ ## Browser usage
30
+
31
+ ```ts
32
+ import { fetchRealtimeData, parseRealtimeTable } from 'buoydata';
33
+
34
+ const raw = await fetchRealtimeData({ buoyId: '46026', type: 'spec' });
35
+ const table = parseRealtimeTable(raw);
36
+ ```
37
+
38
+ ## Node usage
39
+
40
+ Node 18+ includes `fetch` globally. If you need a custom client, pass it explicitly:
41
+
42
+ ```ts
43
+ import { fetchRealtimeData } from 'buoydata';
44
+ import fetch from 'node-fetch';
45
+
46
+ const raw = await fetchRealtimeData({ buoyId: '46026', fetch });
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### fetchRealtimeData
52
+
53
+ Fetches realtime2 files from NDBC.
54
+
55
+ ```ts
56
+ fetchRealtimeData({
57
+ buoyId: string,
58
+ type?: string,
59
+ fetch?: typeof fetch,
60
+ requestInit?: RequestInit,
61
+ baseUrl?: string,
62
+ }): Promise<string>
63
+ ```
64
+
65
+ ### buildRealtimeUrl
66
+
67
+ Builds the realtime2 URL for a buoy and file type.
68
+
69
+ ```ts
70
+ buildRealtimeUrl(buoyId: string, type?: string, baseUrl?: string): string
71
+ ```
72
+
73
+ ### parseRealtimeData
74
+
75
+ Parses a realtime2 text file into typed `Measurement` objects. Standard fields are mapped into structured measurement fields. Unknown columns are ignored unless `includeUnknownFields` is enabled.
76
+
77
+ ```ts
78
+ parseRealtimeData(
79
+ buoyId: string,
80
+ rawText: string,
81
+ options?: {
82
+ coerceNumbers?: boolean;
83
+ missingValue?: number | null;
84
+ missingTokens?: string[];
85
+ commentPrefix?: string;
86
+ includeUnknownFields?: boolean;
87
+ },
88
+ ): BuoyData
89
+ ```
90
+
91
+ ### parseRealtimeTable
92
+
93
+ Parses a realtime2 text file into a generic table representation with headers, units, and raw rows.
94
+
95
+ ```ts
96
+ parseRealtimeTable(
97
+ rawText: string,
98
+ options?: {
99
+ coerceNumbers?: boolean;
100
+ missingValue?: number | null;
101
+ missingTokens?: string[];
102
+ commentPrefix?: string;
103
+ },
104
+ ): RealtimeTable
105
+ ```
106
+
107
+ ### parseRow
108
+
109
+ Parses a single row into values using whitespace splitting and missing-data handling.
110
+
111
+ ```ts
112
+ parseRow(
113
+ rawRow: string,
114
+ options?: {
115
+ coerceNumbers?: boolean;
116
+ missingValue?: number | null;
117
+ missingTokens?: string[];
118
+ },
119
+ ): ParsedValue[]
120
+ ```
121
+
122
+ ### objectifyTable
123
+
124
+ Converts a `RealtimeTable` into an array of records keyed by header values.
125
+
126
+ ```ts
127
+ objectifyTable(table: RealtimeTable): RealtimeRecord[]
128
+ ```
129
+
130
+ ### getMeasurementDate
131
+
132
+ Creates a UTC `Date` instance from a `Measurement` (using year, month, day, hour, minute).
133
+
134
+ ```ts
135
+ getMeasurementDate(measurement: Measurement): Date
136
+ ```
137
+
138
+ ### URL utilities
139
+
140
+ ```ts
141
+ formatQueryParams(params: QueryParams): string
142
+ buildURL(base: string, path?: string, params?: QueryParams): string
143
+ ```
144
+
145
+ ## Data models
146
+
147
+ ### Measurement
148
+
149
+ Structured representation of standard meteorological data:
150
+
151
+ - `year`, `month`, `day`, `hour`, `minute`
152
+ - `airTemperature`, `dewpointTemperature`
153
+ - `pressureTendancy`, `seaLevelPressure`, `stationVisibility`
154
+ - `wind` (`direction`, `averageSpeed`, `peakGustSpeed`)
155
+ - `water` (`averagePeriod`, `dominantDirection`, `dominantPeriod`, `significantHeight`, `surfaceTemperature`, `tide`)
156
+
157
+ ### BuoyData
158
+
159
+ ```ts
160
+ {
161
+ id: string;
162
+ measurements: Measurement[];
163
+ }
164
+ ```
165
+
166
+ ### RealtimeTable
167
+
168
+ ```ts
169
+ {
170
+ headers: string[];
171
+ units: string[];
172
+ rows: (string | number | null)[][];
173
+ rawRows: string[];
174
+ }
175
+ ```
176
+
177
+ ### RealtimeRecord
178
+
179
+ ```ts
180
+ Record<string, string | number | null>
181
+ ```
182
+
183
+ ## Parsing behavior
184
+
185
+ - Comment lines start with `# ` and are ignored for table parsing.
186
+ - The units row (typically `#yr mo dy ...`) is parsed into `units`.
187
+ - Missing data tokens default to `MM` and numeric 9s (e.g. `99`, `999`, `9999`, `99.0`).
188
+ - `parseRealtimeTable` uses `null` as the default missing value; `parseRealtimeData` uses `NaN` by default to align with numeric measurement fields.
189
+ - Numbers are coerced automatically unless `coerceNumbers` is set to `false`.
190
+
191
+ ## Code layout
192
+
193
+ ```
194
+ src/
195
+ index.ts Public exports
196
+ models/
197
+ measurement.ts Typed data models for standard met data
198
+ table.ts Generic table and record types
199
+ realtime/
200
+ fetch.ts Fetch layer and realtime URL builder
201
+ parser.ts Table parsing, objectification, and measurement mapping
202
+ utils/
203
+ date.ts Measurement date helper
204
+ url.ts URL and query param utilities
205
+
206
+ tests/
207
+ fixtures/ Downloaded realtime2 sample files
208
+ parser.test.ts Parsing tests across formats
209
+ fetch.test.ts Fetch layer tests (mocked)
210
+ url.test.ts URL/query param tests
211
+ date.test.ts Measurement date utility test
212
+ ```
213
+
214
+ ## Architecture diagrams
215
+
216
+ ### High-level flow
217
+
218
+ ```
219
+ +----------------------+
220
+ | NDBC realtime2 |
221
+ | (https endpoint) |
222
+ +----------+-----------+
223
+ |
224
+ | fetchRealtimeData
225
+ v
226
+ +------+------+
227
+ | rawText |
228
+ +------+------+
229
+ |
230
+ +---------------+----------------+
231
+ | |
232
+ v v
233
+ parseRealtimeTable parseRealtimeData
234
+ | |
235
+ v v
236
+ RealtimeTable BuoyData (typed)
237
+ |
238
+ v
239
+ objectifyTable
240
+ |
241
+ v
242
+ RealtimeRecord[]
243
+ ```
244
+
245
+ ### Parsing pipeline (parseRealtimeTable)
246
+
247
+ ```
248
+ rawText
249
+ |
250
+ v
251
+ normalizeLines (trim, drop blanks)
252
+ |
253
+ v
254
+ filter comment lines ("# ")
255
+ |
256
+ v
257
+ parse header row --> headers[]
258
+ |
259
+ v
260
+ parse units row --> units[]
261
+ |
262
+ v
263
+ parse data rows --> rows[][]
264
+ ```
265
+
266
+ ### Measurement mapping (parseRealtimeData)
267
+
268
+ ```
269
+ RealtimeTable
270
+ |
271
+ v
272
+ objectifyTable -> RealtimeRecord[]
273
+ |
274
+ v
275
+ toMeasurement
276
+ |
277
+ v
278
+ Measurement[] (BuoyData.measurements)
279
+ ```
280
+
281
+ ## Testing
282
+
283
+ ```bash
284
+ pnpm test
285
+ ```
286
+
287
+ ## License
288
+
289
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function d(r={}){const e=new URLSearchParams;for(const[n,a]of Object.entries(r))if(a!=null){if(Array.isArray(a)){a.forEach(i=>{e.append(n,String(i))});continue}e.append(n,String(a))}const t=e.toString();return t?`?${t}`:""}function f(r,e="",t={}){const n=r.endsWith("/")?r:`${r}/`,a=new URL(e,n),i=d(t);return a.search=i,a.toString()}const h="https://www.ndbc.noaa.gov/data/realtime2/";function g(r,e="txt",t=h){const n=`${r}.${e}`;return f(t,n)}async function P(r){const{buoyId:e,type:t="txt",fetch:n=fetch,requestInit:a,baseUrl:i=h}=r;if(!n)throw new Error("No fetch implementation available.");const s=g(e,t,i),u=await n(s,a);if(!u.ok)throw new Error(`Failed to fetch ${s}: ${u.status}`);return u.text()}const S=["MM"];function R(r,e){return!!(e.includes(r)||/^9{2,}(\.0+|\.9+)?$/.test(r))}function M(r,e){if(R(r,e.missingTokens))return e.missingValue;if(e.coerceNumbers){const t=Number(r);if(!Number.isNaN(t))return t}return r}function N(r,e={}){const t={coerceNumbers:e.coerceNumbers??!0,missingValue:e.missingValue??null,missingTokens:e.missingTokens??S};return r.trim().split(/\s+/).filter(Boolean).map(n=>M(n,t))}function L(r,e){return r.startsWith(`${e} `)}function k(r){return r.split(/\r?\n/).map(e=>e.trim()).filter(e=>e.length>0)}function p(r,e={}){const t=e.commentPrefix??"#",n=k(r).filter(o=>!L(o,t));if(n.length===0)return{headers:[],units:[],rows:[],rawRows:[]};const a=n[0]??"",i={coerceNumbers:!1,missingValue:null,missingTokens:[]},s=N(a,i).map(String);let u=[],c=1;const m=n[1];m&&m.startsWith(t)&&(u=N(m,i).map(o=>{const l=String(o);return l.startsWith(t)?l.slice(t.length):l}),c=2);const y={coerceNumbers:e.coerceNumbers,missingValue:e.missingValue,missingTokens:e.missingTokens},b=n.slice(c),D=b.map(o=>N(o,y));return{headers:s,units:u,rows:D,rawRows:b}}function w(r){const{headers:e,rows:t}=r;return t.map(n=>{const a={};return e.forEach((i,s)=>{a[i]=n[s]??null}),a})}function T(){return{airTemperature:Number.NaN,day:Number.NaN,dewpointTemperature:Number.NaN,hour:Number.NaN,minute:Number.NaN,month:Number.NaN,pressureTendancy:Number.NaN,seaLevelPressure:Number.NaN,stationVisibility:Number.NaN,water:{averagePeriod:Number.NaN,dominantDirection:Number.NaN,dominantPeriod:Number.NaN,significantHeight:Number.NaN,surfaceTemperature:Number.NaN,tide:Number.NaN},wind:{averageSpeed:Number.NaN,direction:Number.NaN,peakGustSpeed:Number.NaN},year:Number.NaN}}const E={"#YY":(r,e)=>{r.year=Number(e)},YY:(r,e)=>{r.year=Number(e)},MM:(r,e)=>{r.month=Number(e)},DD:(r,e)=>{r.day=Number(e)},hh:(r,e)=>{r.hour=Number(e)},mm:(r,e)=>{r.minute=Number(e)},APD:(r,e)=>{r.water.averagePeriod=Number(e)},ATMP:(r,e)=>{r.airTemperature=Number(e)},DEWP:(r,e)=>{r.dewpointTemperature=Number(e)},DPD:(r,e)=>{r.water.dominantPeriod=Number(e)},GST:(r,e)=>{r.wind.peakGustSpeed=Number(e)},MWD:(r,e)=>{r.water.dominantDirection=Number(e)},PRES:(r,e)=>{r.seaLevelPressure=Number(e)},PTDY:(r,e)=>{r.pressureTendancy=Number(e)},TIDE:(r,e)=>{r.water.tide=Number(e)},VIS:(r,e)=>{r.stationVisibility=Number(e)},WDIR:(r,e)=>{r.wind.direction=Number(e)},WSPD:(r,e)=>{r.wind.averageSpeed=Number(e)},WTMP:(r,e)=>{r.water.surfaceTemperature=Number(e)},WVHT:(r,e)=>{r.water.significantHeight=Number(e)}};function U(r){const e=T();return Object.entries(r).forEach(([t,n])=>{const a=E[t];a&&a(e,n)}),e}function V(r,e,t={}){const n=p(e,{...t,missingValue:t.missingValue??Number.NaN}),a=w(n),i=a.map(s=>U(s));if(t.includeUnknownFields){const s=i.map((u,c)=>{const m=a[c];return{...u,...m}});return{id:r,measurements:s}}return{id:r,measurements:i}}function v(r){const{year:e,month:t,day:n,hour:a,minute:i}=r;return new Date(Date.UTC(e,t-1,n,a,i))}exports.buildRealtimeUrl=g;exports.buildURL=f;exports.createMeasurement=T;exports.fetchRealtimeData=P;exports.formatQueryParams=d;exports.getMeasurementDate=v;exports.objectifyTable=w;exports.parseRealtimeData=V;exports.parseRealtimeTable=p;exports.parseRow=N;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/utils/url.ts","../src/realtime/fetch.ts","../src/realtime/parser.ts","../src/utils/date.ts"],"sourcesContent":["export type QueryParamValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean>;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function formatQueryParams(params: QueryParams = {}): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n value.forEach(item => {\n searchParams.append(key, String(item));\n });\n continue;\n }\n\n searchParams.append(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : '';\n}\n\nexport function buildURL(\n base: string,\n path = '',\n params: QueryParams = {},\n): string {\n const normalizedBase = base.endsWith('/') ? base : `${base}/`;\n const url = new URL(path, normalizedBase);\n const query = formatQueryParams(params);\n url.search = query;\n return url.toString();\n}\n","import { buildURL } from '../utils/url';\n\nexport interface FetchRealtimeOptions {\n buoyId: string;\n type?: string;\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://www.ndbc.noaa.gov/data/realtime2/';\n\nexport function buildRealtimeUrl(\n buoyId: string,\n type = 'txt',\n baseUrl = DEFAULT_BASE_URL,\n): string {\n const filename = `${buoyId}.${type}`;\n return buildURL(baseUrl, filename);\n}\n\nexport async function fetchRealtimeData(\n options: FetchRealtimeOptions,\n): Promise<string> {\n const {\n buoyId,\n type = 'txt',\n fetch: fetchImpl = fetch,\n requestInit,\n baseUrl = DEFAULT_BASE_URL,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const url = buildRealtimeUrl(buoyId, type, baseUrl);\n const response = await fetchImpl(url, requestInit);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n return response.text();\n}\n","import { BuoyData, Measurement } from '../models/measurement';\nimport { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';\n\nexport interface ParseRowOptions {\n coerceNumbers?: boolean;\n missingValue?: number | null;\n missingTokens?: string[];\n}\n\nexport interface ParseRealtimeTableOptions extends ParseRowOptions {\n commentPrefix?: string;\n}\n\nexport interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {\n includeUnknownFields?: boolean;\n}\n\nconst DEFAULT_MISSING_TOKENS = ['MM'];\n\nfunction isMissingToken(value: string, missingTokens: string[]): boolean {\n if (missingTokens.includes(value)) {\n return true;\n }\n\n // NDBC missing values are often 9s (e.g. 99, 999, 9999, 99.0).\n if (/^9{2,}(\\.0+|\\.9+)?$/.test(value)) {\n return true;\n }\n\n return false;\n}\n\nfunction coerceValue(\n raw: string,\n options: Required<ParseRowOptions>,\n): ParsedValue {\n if (isMissingToken(raw, options.missingTokens)) {\n return options.missingValue;\n }\n\n if (options.coerceNumbers) {\n const numeric = Number(raw);\n if (!Number.isNaN(numeric)) {\n return numeric;\n }\n }\n\n return raw;\n}\n\nexport function parseRow(\n rawRow: string,\n options: ParseRowOptions = {},\n): ParsedValue[] {\n const resolved: Required<ParseRowOptions> = {\n coerceNumbers: options.coerceNumbers ?? true,\n missingValue: options.missingValue ?? null,\n missingTokens: options.missingTokens ?? DEFAULT_MISSING_TOKENS,\n };\n\n return rawRow\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map(value => coerceValue(value, resolved));\n}\n\nfunction isCommentLine(line: string, commentPrefix: string): boolean {\n return line.startsWith(`${commentPrefix} `);\n}\n\nfunction normalizeLines(rawText: string): string[] {\n return rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n}\n\nexport function parseRealtimeTable(\n rawText: string,\n options: ParseRealtimeTableOptions = {},\n): RealtimeTable {\n const commentPrefix = options.commentPrefix ?? '#';\n const lines = normalizeLines(rawText).filter(\n line => !isCommentLine(line, commentPrefix),\n );\n\n if (lines.length === 0) {\n return { headers: [], units: [], rows: [], rawRows: [] };\n }\n\n const headerLine = lines[0] ?? '';\n const headerOptions: ParseRowOptions = {\n coerceNumbers: false,\n missingValue: null,\n missingTokens: [],\n };\n const headers = parseRow(headerLine, headerOptions).map(String);\n\n let units: string[] = [];\n let dataStartIndex = 1;\n\n const unitLine = lines[1];\n if (unitLine && unitLine.startsWith(commentPrefix)) {\n units = parseRow(unitLine, headerOptions).map(token => {\n const text = String(token);\n return text.startsWith(commentPrefix)\n ? text.slice(commentPrefix.length)\n : text;\n });\n dataStartIndex = 2;\n }\n\n const rowOptions: ParseRowOptions = {\n coerceNumbers: options.coerceNumbers,\n missingValue: options.missingValue,\n missingTokens: options.missingTokens,\n };\n\n const dataRows = lines.slice(dataStartIndex);\n const rows = dataRows.map(row => parseRow(row, rowOptions));\n\n return {\n headers,\n units,\n rows,\n rawRows: dataRows,\n };\n}\n\nexport function objectifyTable(table: RealtimeTable): RealtimeRecord[] {\n const { headers, rows } = table;\n return rows.map(row => {\n const record: RealtimeRecord = {};\n headers.forEach((header, index) => {\n record[header] = row[index] ?? null;\n });\n return record;\n });\n}\n\nexport function createMeasurement(): Measurement {\n return {\n airTemperature: Number.NaN,\n day: Number.NaN,\n dewpointTemperature: Number.NaN,\n hour: Number.NaN,\n minute: Number.NaN,\n month: Number.NaN,\n pressureTendancy: Number.NaN,\n seaLevelPressure: Number.NaN,\n stationVisibility: Number.NaN,\n water: {\n averagePeriod: Number.NaN,\n dominantDirection: Number.NaN,\n dominantPeriod: Number.NaN,\n significantHeight: Number.NaN,\n surfaceTemperature: Number.NaN,\n tide: Number.NaN,\n },\n wind: {\n averageSpeed: Number.NaN,\n direction: Number.NaN,\n peakGustSpeed: Number.NaN,\n },\n year: Number.NaN,\n };\n}\n\nconst FIELD_MAPPINGS: Record<string, (m: Measurement, value: ParsedValue) => void> = {\n '#YY': (m, value) => {\n m.year = Number(value);\n },\n YY: (m, value) => {\n m.year = Number(value);\n },\n MM: (m, value) => {\n m.month = Number(value);\n },\n DD: (m, value) => {\n m.day = Number(value);\n },\n hh: (m, value) => {\n m.hour = Number(value);\n },\n mm: (m, value) => {\n m.minute = Number(value);\n },\n APD: (m, value) => {\n m.water.averagePeriod = Number(value);\n },\n ATMP: (m, value) => {\n m.airTemperature = Number(value);\n },\n DEWP: (m, value) => {\n m.dewpointTemperature = Number(value);\n },\n DPD: (m, value) => {\n m.water.dominantPeriod = Number(value);\n },\n GST: (m, value) => {\n m.wind.peakGustSpeed = Number(value);\n },\n MWD: (m, value) => {\n m.water.dominantDirection = Number(value);\n },\n PRES: (m, value) => {\n m.seaLevelPressure = Number(value);\n },\n PTDY: (m, value) => {\n m.pressureTendancy = Number(value);\n },\n TIDE: (m, value) => {\n m.water.tide = Number(value);\n },\n VIS: (m, value) => {\n m.stationVisibility = Number(value);\n },\n WDIR: (m, value) => {\n m.wind.direction = Number(value);\n },\n WSPD: (m, value) => {\n m.wind.averageSpeed = Number(value);\n },\n WTMP: (m, value) => {\n m.water.surfaceTemperature = Number(value);\n },\n WVHT: (m, value) => {\n m.water.significantHeight = Number(value);\n },\n};\n\nfunction toMeasurement(record: RealtimeRecord): Measurement {\n const measurement = createMeasurement();\n Object.entries(record).forEach(([field, value]) => {\n const mapper = FIELD_MAPPINGS[field];\n if (mapper) {\n mapper(measurement, value);\n }\n });\n return measurement;\n}\n\nexport function parseRealtimeData(\n buoyId: string,\n rawText: string,\n options: ParseRealtimeDataOptions = {},\n): BuoyData {\n const table = parseRealtimeTable(rawText, {\n ...options,\n missingValue: options.missingValue ?? Number.NaN,\n });\n const records = objectifyTable(table);\n\n const measurements = records.map(record => toMeasurement(record));\n\n if (options.includeUnknownFields) {\n const measurementsWithUnknowns = measurements.map((measurement, index) => {\n const record = records[index];\n return { ...measurement, ...record } as Measurement;\n });\n\n return {\n id: buoyId,\n measurements: measurementsWithUnknowns,\n };\n }\n\n return {\n id: buoyId,\n measurements,\n };\n}\n","import { Measurement } from '../models/measurement';\n\n/**\n * Returns a UTC Date instance for a buoy measurement.\n */\nexport function getMeasurementDate(measurement: Measurement): Date {\n const { year, month, day, hour, minute } = measurement;\n return new Date(Date.UTC(year, month - 1, day, hour, minute));\n}\n"],"names":["formatQueryParams","params","searchParams","key","value","item","query","buildURL","base","path","normalizedBase","url","DEFAULT_BASE_URL","buildRealtimeUrl","buoyId","type","baseUrl","filename","fetchRealtimeData","options","fetchImpl","requestInit","response","DEFAULT_MISSING_TOKENS","isMissingToken","missingTokens","coerceValue","raw","numeric","parseRow","rawRow","resolved","isCommentLine","line","commentPrefix","normalizeLines","rawText","parseRealtimeTable","lines","headerLine","headerOptions","headers","units","dataStartIndex","unitLine","token","text","rowOptions","dataRows","rows","row","objectifyTable","table","record","header","index","createMeasurement","FIELD_MAPPINGS","m","toMeasurement","measurement","field","mapper","parseRealtimeData","records","measurements","measurementsWithUnknowns","getMeasurementDate","year","month","day","hour","minute"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type { BuoyData, Measurement, WaterMeasurement, WindMeasurement } from './models/measurement';
2
+ export type { ParsedValue, RealtimeRecord, RealtimeTable } from './models/table';
3
+ export type { FetchRealtimeOptions, } from './realtime/fetch';
4
+ export type { ParseRowOptions, ParseRealtimeTableOptions, ParseRealtimeDataOptions, } from './realtime/parser';
5
+ export { fetchRealtimeData, buildRealtimeUrl } from './realtime/fetch';
6
+ export { parseRealtimeData, parseRealtimeTable, parseRow, objectifyTable, createMeasurement, } from './realtime/parser';
7
+ export { getMeasurementDate } from './utils/date';
8
+ export { buildURL, formatQueryParams } from './utils/url';
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ function g(r = {}) {
2
+ const e = new URLSearchParams();
3
+ for (const [n, a] of Object.entries(r))
4
+ if (a != null) {
5
+ if (Array.isArray(a)) {
6
+ a.forEach((i) => {
7
+ e.append(n, String(i));
8
+ });
9
+ continue;
10
+ }
11
+ e.append(n, String(a));
12
+ }
13
+ const t = e.toString();
14
+ return t ? `?${t}` : "";
15
+ }
16
+ function p(r, e = "", t = {}) {
17
+ const n = r.endsWith("/") ? r : `${r}/`, a = new URL(e, n), i = g(t);
18
+ return a.search = i, a.toString();
19
+ }
20
+ const d = "https://www.ndbc.noaa.gov/data/realtime2/";
21
+ function w(r, e = "txt", t = d) {
22
+ const n = `${r}.${e}`;
23
+ return p(t, n);
24
+ }
25
+ async function V(r) {
26
+ const {
27
+ buoyId: e,
28
+ type: t = "txt",
29
+ fetch: n = fetch,
30
+ requestInit: a,
31
+ baseUrl: i = d
32
+ } = r;
33
+ if (!n)
34
+ throw new Error("No fetch implementation available.");
35
+ const s = w(e, t, i), u = await n(s, a);
36
+ if (!u.ok)
37
+ throw new Error(`Failed to fetch ${s}: ${u.status}`);
38
+ return u.text();
39
+ }
40
+ const T = ["MM"];
41
+ function D(r, e) {
42
+ return !!(e.includes(r) || /^9{2,}(\.0+|\.9+)?$/.test(r));
43
+ }
44
+ function P(r, e) {
45
+ if (D(r, e.missingTokens))
46
+ return e.missingValue;
47
+ if (e.coerceNumbers) {
48
+ const t = Number(r);
49
+ if (!Number.isNaN(t))
50
+ return t;
51
+ }
52
+ return r;
53
+ }
54
+ function l(r, e = {}) {
55
+ const t = {
56
+ coerceNumbers: e.coerceNumbers ?? !0,
57
+ missingValue: e.missingValue ?? null,
58
+ missingTokens: e.missingTokens ?? T
59
+ };
60
+ return r.trim().split(/\s+/).filter(Boolean).map((n) => P(n, t));
61
+ }
62
+ function S(r, e) {
63
+ return r.startsWith(`${e} `);
64
+ }
65
+ function y(r) {
66
+ return r.split(/\r?\n/).map((e) => e.trim()).filter((e) => e.length > 0);
67
+ }
68
+ function R(r, e = {}) {
69
+ const t = e.commentPrefix ?? "#", n = y(r).filter(
70
+ (o) => !S(o, t)
71
+ );
72
+ if (n.length === 0)
73
+ return { headers: [], units: [], rows: [], rawRows: [] };
74
+ const a = n[0] ?? "", i = {
75
+ coerceNumbers: !1,
76
+ missingValue: null,
77
+ missingTokens: []
78
+ }, s = l(a, i).map(String);
79
+ let u = [], c = 1;
80
+ const m = n[1];
81
+ m && m.startsWith(t) && (u = l(m, i).map((o) => {
82
+ const N = String(o);
83
+ return N.startsWith(t) ? N.slice(t.length) : N;
84
+ }), c = 2);
85
+ const f = {
86
+ coerceNumbers: e.coerceNumbers,
87
+ missingValue: e.missingValue,
88
+ missingTokens: e.missingTokens
89
+ }, b = n.slice(c), h = b.map((o) => l(o, f));
90
+ return {
91
+ headers: s,
92
+ units: u,
93
+ rows: h,
94
+ rawRows: b
95
+ };
96
+ }
97
+ function k(r) {
98
+ const { headers: e, rows: t } = r;
99
+ return t.map((n) => {
100
+ const a = {};
101
+ return e.forEach((i, s) => {
102
+ a[i] = n[s] ?? null;
103
+ }), a;
104
+ });
105
+ }
106
+ function E() {
107
+ return {
108
+ airTemperature: Number.NaN,
109
+ day: Number.NaN,
110
+ dewpointTemperature: Number.NaN,
111
+ hour: Number.NaN,
112
+ minute: Number.NaN,
113
+ month: Number.NaN,
114
+ pressureTendancy: Number.NaN,
115
+ seaLevelPressure: Number.NaN,
116
+ stationVisibility: Number.NaN,
117
+ water: {
118
+ averagePeriod: Number.NaN,
119
+ dominantDirection: Number.NaN,
120
+ dominantPeriod: Number.NaN,
121
+ significantHeight: Number.NaN,
122
+ surfaceTemperature: Number.NaN,
123
+ tide: Number.NaN
124
+ },
125
+ wind: {
126
+ averageSpeed: Number.NaN,
127
+ direction: Number.NaN,
128
+ peakGustSpeed: Number.NaN
129
+ },
130
+ year: Number.NaN
131
+ };
132
+ }
133
+ const L = {
134
+ "#YY": (r, e) => {
135
+ r.year = Number(e);
136
+ },
137
+ YY: (r, e) => {
138
+ r.year = Number(e);
139
+ },
140
+ MM: (r, e) => {
141
+ r.month = Number(e);
142
+ },
143
+ DD: (r, e) => {
144
+ r.day = Number(e);
145
+ },
146
+ hh: (r, e) => {
147
+ r.hour = Number(e);
148
+ },
149
+ mm: (r, e) => {
150
+ r.minute = Number(e);
151
+ },
152
+ APD: (r, e) => {
153
+ r.water.averagePeriod = Number(e);
154
+ },
155
+ ATMP: (r, e) => {
156
+ r.airTemperature = Number(e);
157
+ },
158
+ DEWP: (r, e) => {
159
+ r.dewpointTemperature = Number(e);
160
+ },
161
+ DPD: (r, e) => {
162
+ r.water.dominantPeriod = Number(e);
163
+ },
164
+ GST: (r, e) => {
165
+ r.wind.peakGustSpeed = Number(e);
166
+ },
167
+ MWD: (r, e) => {
168
+ r.water.dominantDirection = Number(e);
169
+ },
170
+ PRES: (r, e) => {
171
+ r.seaLevelPressure = Number(e);
172
+ },
173
+ PTDY: (r, e) => {
174
+ r.pressureTendancy = Number(e);
175
+ },
176
+ TIDE: (r, e) => {
177
+ r.water.tide = Number(e);
178
+ },
179
+ VIS: (r, e) => {
180
+ r.stationVisibility = Number(e);
181
+ },
182
+ WDIR: (r, e) => {
183
+ r.wind.direction = Number(e);
184
+ },
185
+ WSPD: (r, e) => {
186
+ r.wind.averageSpeed = Number(e);
187
+ },
188
+ WTMP: (r, e) => {
189
+ r.water.surfaceTemperature = Number(e);
190
+ },
191
+ WVHT: (r, e) => {
192
+ r.water.significantHeight = Number(e);
193
+ }
194
+ };
195
+ function M(r) {
196
+ const e = E();
197
+ return Object.entries(r).forEach(([t, n]) => {
198
+ const a = L[t];
199
+ a && a(e, n);
200
+ }), e;
201
+ }
202
+ function U(r, e, t = {}) {
203
+ const n = R(e, {
204
+ ...t,
205
+ missingValue: t.missingValue ?? Number.NaN
206
+ }), a = k(n), i = a.map((s) => M(s));
207
+ if (t.includeUnknownFields) {
208
+ const s = i.map((u, c) => {
209
+ const m = a[c];
210
+ return { ...u, ...m };
211
+ });
212
+ return {
213
+ id: r,
214
+ measurements: s
215
+ };
216
+ }
217
+ return {
218
+ id: r,
219
+ measurements: i
220
+ };
221
+ }
222
+ function W(r) {
223
+ const { year: e, month: t, day: n, hour: a, minute: i } = r;
224
+ return new Date(Date.UTC(e, t - 1, n, a, i));
225
+ }
226
+ export {
227
+ w as buildRealtimeUrl,
228
+ p as buildURL,
229
+ E as createMeasurement,
230
+ V as fetchRealtimeData,
231
+ g as formatQueryParams,
232
+ W as getMeasurementDate,
233
+ k as objectifyTable,
234
+ U as parseRealtimeData,
235
+ R as parseRealtimeTable,
236
+ l as parseRow
237
+ };
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/utils/url.ts","../src/realtime/fetch.ts","../src/realtime/parser.ts","../src/utils/date.ts"],"sourcesContent":["export type QueryParamValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean>;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function formatQueryParams(params: QueryParams = {}): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n value.forEach(item => {\n searchParams.append(key, String(item));\n });\n continue;\n }\n\n searchParams.append(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : '';\n}\n\nexport function buildURL(\n base: string,\n path = '',\n params: QueryParams = {},\n): string {\n const normalizedBase = base.endsWith('/') ? base : `${base}/`;\n const url = new URL(path, normalizedBase);\n const query = formatQueryParams(params);\n url.search = query;\n return url.toString();\n}\n","import { buildURL } from '../utils/url';\n\nexport interface FetchRealtimeOptions {\n buoyId: string;\n type?: string;\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://www.ndbc.noaa.gov/data/realtime2/';\n\nexport function buildRealtimeUrl(\n buoyId: string,\n type = 'txt',\n baseUrl = DEFAULT_BASE_URL,\n): string {\n const filename = `${buoyId}.${type}`;\n return buildURL(baseUrl, filename);\n}\n\nexport async function fetchRealtimeData(\n options: FetchRealtimeOptions,\n): Promise<string> {\n const {\n buoyId,\n type = 'txt',\n fetch: fetchImpl = fetch,\n requestInit,\n baseUrl = DEFAULT_BASE_URL,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const url = buildRealtimeUrl(buoyId, type, baseUrl);\n const response = await fetchImpl(url, requestInit);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n return response.text();\n}\n","import { BuoyData, Measurement } from '../models/measurement';\nimport { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';\n\nexport interface ParseRowOptions {\n coerceNumbers?: boolean;\n missingValue?: number | null;\n missingTokens?: string[];\n}\n\nexport interface ParseRealtimeTableOptions extends ParseRowOptions {\n commentPrefix?: string;\n}\n\nexport interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {\n includeUnknownFields?: boolean;\n}\n\nconst DEFAULT_MISSING_TOKENS = ['MM'];\n\nfunction isMissingToken(value: string, missingTokens: string[]): boolean {\n if (missingTokens.includes(value)) {\n return true;\n }\n\n // NDBC missing values are often 9s (e.g. 99, 999, 9999, 99.0).\n if (/^9{2,}(\\.0+|\\.9+)?$/.test(value)) {\n return true;\n }\n\n return false;\n}\n\nfunction coerceValue(\n raw: string,\n options: Required<ParseRowOptions>,\n): ParsedValue {\n if (isMissingToken(raw, options.missingTokens)) {\n return options.missingValue;\n }\n\n if (options.coerceNumbers) {\n const numeric = Number(raw);\n if (!Number.isNaN(numeric)) {\n return numeric;\n }\n }\n\n return raw;\n}\n\nexport function parseRow(\n rawRow: string,\n options: ParseRowOptions = {},\n): ParsedValue[] {\n const resolved: Required<ParseRowOptions> = {\n coerceNumbers: options.coerceNumbers ?? true,\n missingValue: options.missingValue ?? null,\n missingTokens: options.missingTokens ?? DEFAULT_MISSING_TOKENS,\n };\n\n return rawRow\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map(value => coerceValue(value, resolved));\n}\n\nfunction isCommentLine(line: string, commentPrefix: string): boolean {\n return line.startsWith(`${commentPrefix} `);\n}\n\nfunction normalizeLines(rawText: string): string[] {\n return rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n}\n\nexport function parseRealtimeTable(\n rawText: string,\n options: ParseRealtimeTableOptions = {},\n): RealtimeTable {\n const commentPrefix = options.commentPrefix ?? '#';\n const lines = normalizeLines(rawText).filter(\n line => !isCommentLine(line, commentPrefix),\n );\n\n if (lines.length === 0) {\n return { headers: [], units: [], rows: [], rawRows: [] };\n }\n\n const headerLine = lines[0] ?? '';\n const headerOptions: ParseRowOptions = {\n coerceNumbers: false,\n missingValue: null,\n missingTokens: [],\n };\n const headers = parseRow(headerLine, headerOptions).map(String);\n\n let units: string[] = [];\n let dataStartIndex = 1;\n\n const unitLine = lines[1];\n if (unitLine && unitLine.startsWith(commentPrefix)) {\n units = parseRow(unitLine, headerOptions).map(token => {\n const text = String(token);\n return text.startsWith(commentPrefix)\n ? text.slice(commentPrefix.length)\n : text;\n });\n dataStartIndex = 2;\n }\n\n const rowOptions: ParseRowOptions = {\n coerceNumbers: options.coerceNumbers,\n missingValue: options.missingValue,\n missingTokens: options.missingTokens,\n };\n\n const dataRows = lines.slice(dataStartIndex);\n const rows = dataRows.map(row => parseRow(row, rowOptions));\n\n return {\n headers,\n units,\n rows,\n rawRows: dataRows,\n };\n}\n\nexport function objectifyTable(table: RealtimeTable): RealtimeRecord[] {\n const { headers, rows } = table;\n return rows.map(row => {\n const record: RealtimeRecord = {};\n headers.forEach((header, index) => {\n record[header] = row[index] ?? null;\n });\n return record;\n });\n}\n\nexport function createMeasurement(): Measurement {\n return {\n airTemperature: Number.NaN,\n day: Number.NaN,\n dewpointTemperature: Number.NaN,\n hour: Number.NaN,\n minute: Number.NaN,\n month: Number.NaN,\n pressureTendancy: Number.NaN,\n seaLevelPressure: Number.NaN,\n stationVisibility: Number.NaN,\n water: {\n averagePeriod: Number.NaN,\n dominantDirection: Number.NaN,\n dominantPeriod: Number.NaN,\n significantHeight: Number.NaN,\n surfaceTemperature: Number.NaN,\n tide: Number.NaN,\n },\n wind: {\n averageSpeed: Number.NaN,\n direction: Number.NaN,\n peakGustSpeed: Number.NaN,\n },\n year: Number.NaN,\n };\n}\n\nconst FIELD_MAPPINGS: Record<string, (m: Measurement, value: ParsedValue) => void> = {\n '#YY': (m, value) => {\n m.year = Number(value);\n },\n YY: (m, value) => {\n m.year = Number(value);\n },\n MM: (m, value) => {\n m.month = Number(value);\n },\n DD: (m, value) => {\n m.day = Number(value);\n },\n hh: (m, value) => {\n m.hour = Number(value);\n },\n mm: (m, value) => {\n m.minute = Number(value);\n },\n APD: (m, value) => {\n m.water.averagePeriod = Number(value);\n },\n ATMP: (m, value) => {\n m.airTemperature = Number(value);\n },\n DEWP: (m, value) => {\n m.dewpointTemperature = Number(value);\n },\n DPD: (m, value) => {\n m.water.dominantPeriod = Number(value);\n },\n GST: (m, value) => {\n m.wind.peakGustSpeed = Number(value);\n },\n MWD: (m, value) => {\n m.water.dominantDirection = Number(value);\n },\n PRES: (m, value) => {\n m.seaLevelPressure = Number(value);\n },\n PTDY: (m, value) => {\n m.pressureTendancy = Number(value);\n },\n TIDE: (m, value) => {\n m.water.tide = Number(value);\n },\n VIS: (m, value) => {\n m.stationVisibility = Number(value);\n },\n WDIR: (m, value) => {\n m.wind.direction = Number(value);\n },\n WSPD: (m, value) => {\n m.wind.averageSpeed = Number(value);\n },\n WTMP: (m, value) => {\n m.water.surfaceTemperature = Number(value);\n },\n WVHT: (m, value) => {\n m.water.significantHeight = Number(value);\n },\n};\n\nfunction toMeasurement(record: RealtimeRecord): Measurement {\n const measurement = createMeasurement();\n Object.entries(record).forEach(([field, value]) => {\n const mapper = FIELD_MAPPINGS[field];\n if (mapper) {\n mapper(measurement, value);\n }\n });\n return measurement;\n}\n\nexport function parseRealtimeData(\n buoyId: string,\n rawText: string,\n options: ParseRealtimeDataOptions = {},\n): BuoyData {\n const table = parseRealtimeTable(rawText, {\n ...options,\n missingValue: options.missingValue ?? Number.NaN,\n });\n const records = objectifyTable(table);\n\n const measurements = records.map(record => toMeasurement(record));\n\n if (options.includeUnknownFields) {\n const measurementsWithUnknowns = measurements.map((measurement, index) => {\n const record = records[index];\n return { ...measurement, ...record } as Measurement;\n });\n\n return {\n id: buoyId,\n measurements: measurementsWithUnknowns,\n };\n }\n\n return {\n id: buoyId,\n measurements,\n };\n}\n","import { Measurement } from '../models/measurement';\n\n/**\n * Returns a UTC Date instance for a buoy measurement.\n */\nexport function getMeasurementDate(measurement: Measurement): Date {\n const { year, month, day, hour, minute } = measurement;\n return new Date(Date.UTC(year, month - 1, day, hour, minute));\n}\n"],"names":["formatQueryParams","params","searchParams","key","value","item","query","buildURL","base","path","normalizedBase","url","DEFAULT_BASE_URL","buildRealtimeUrl","buoyId","type","baseUrl","filename","fetchRealtimeData","options","fetchImpl","requestInit","response","DEFAULT_MISSING_TOKENS","isMissingToken","missingTokens","coerceValue","raw","numeric","parseRow","rawRow","resolved","isCommentLine","line","commentPrefix","normalizeLines","rawText","parseRealtimeTable","lines","headerLine","headerOptions","headers","units","dataStartIndex","unitLine","token","text","rowOptions","dataRows","rows","row","objectifyTable","table","record","header","index","createMeasurement","FIELD_MAPPINGS","m","toMeasurement","measurement","field","mapper","parseRealtimeData","records","measurements","measurementsWithUnknowns","getMeasurementDate","year","month","day","hour","minute"],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Real-time water measurement data.
3
+ */
4
+ export interface WaterMeasurement {
5
+ /**
6
+ * Average wave period (seconds) of all waves during the 20-minute period.
7
+ */
8
+ averagePeriod: number;
9
+ /**
10
+ * The direction from which the waves at the dominant period (DPD) are coming.
11
+ * The units are degrees from true North, increasing clockwise, with North as 0 (zero) degrees and East as 90 degrees.
12
+ */
13
+ dominantDirection: number;
14
+ /**
15
+ * Dominant wave period (seconds) is the period with the maximum wave energy.
16
+ */
17
+ dominantPeriod: number;
18
+ /**
19
+ * Significant wave height (meters) is calculated as the average of the highest one-third of all of the wave heights
20
+ * during the 20-minute sampling period.
21
+ */
22
+ significantHeight: number;
23
+ /**
24
+ * Sea surface temperature (Celsius).
25
+ */
26
+ surfaceTemperature: number;
27
+ /**
28
+ * The water level in feet above or below Mean Lower Low Water (MLLW).
29
+ */
30
+ tide: number;
31
+ }
32
+ /**
33
+ * Real-time wind measurement data.
34
+ */
35
+ export interface WindMeasurement {
36
+ /**
37
+ * Wind direction (degrees clockwise from true North).
38
+ */
39
+ direction: number;
40
+ /**
41
+ * Wind speed (m/s) averaged over the buoy reporting interval.
42
+ */
43
+ averageSpeed: number;
44
+ /**
45
+ * Peak gust speed (m/s) during the reporting interval.
46
+ */
47
+ peakGustSpeed: number;
48
+ }
49
+ /**
50
+ * A single real-time buoy data measurement.
51
+ */
52
+ export interface Measurement {
53
+ year: number;
54
+ month: number;
55
+ day: number;
56
+ hour: number;
57
+ minute: number;
58
+ /**
59
+ * Air temperature (Celsius).
60
+ */
61
+ airTemperature: number;
62
+ /**
63
+ * Dewpoint temperature (Celsius).
64
+ */
65
+ dewpointTemperature: number;
66
+ /**
67
+ * Pressure tendency (hPa over the last three hours).
68
+ */
69
+ pressureTendancy: number;
70
+ /**
71
+ * Sea level pressure (hPa).
72
+ */
73
+ seaLevelPressure: number;
74
+ /**
75
+ * Station visibility (nautical miles).
76
+ */
77
+ stationVisibility: number;
78
+ water: WaterMeasurement;
79
+ wind: WindMeasurement;
80
+ }
81
+ export interface BuoyData {
82
+ id: string;
83
+ /**
84
+ * Measurements ordered from latest to oldest as reported by NDBC.
85
+ */
86
+ measurements: Measurement[];
87
+ }
@@ -0,0 +1,8 @@
1
+ export type ParsedValue = string | number | null;
2
+ export interface RealtimeTable {
3
+ headers: string[];
4
+ units: string[];
5
+ rows: ParsedValue[][];
6
+ rawRows: string[];
7
+ }
8
+ export type RealtimeRecord = Record<string, ParsedValue>;
@@ -0,0 +1,9 @@
1
+ export interface FetchRealtimeOptions {
2
+ buoyId: string;
3
+ type?: string;
4
+ fetch?: typeof fetch;
5
+ requestInit?: RequestInit;
6
+ baseUrl?: string;
7
+ }
8
+ export declare function buildRealtimeUrl(buoyId: string, type?: string, baseUrl?: string): string;
9
+ export declare function fetchRealtimeData(options: FetchRealtimeOptions): Promise<string>;
@@ -0,0 +1,18 @@
1
+ import { BuoyData, Measurement } from '../models/measurement';
2
+ import { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';
3
+ export interface ParseRowOptions {
4
+ coerceNumbers?: boolean;
5
+ missingValue?: number | null;
6
+ missingTokens?: string[];
7
+ }
8
+ export interface ParseRealtimeTableOptions extends ParseRowOptions {
9
+ commentPrefix?: string;
10
+ }
11
+ export interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {
12
+ includeUnknownFields?: boolean;
13
+ }
14
+ export declare function parseRow(rawRow: string, options?: ParseRowOptions): ParsedValue[];
15
+ export declare function parseRealtimeTable(rawText: string, options?: ParseRealtimeTableOptions): RealtimeTable;
16
+ export declare function objectifyTable(table: RealtimeTable): RealtimeRecord[];
17
+ export declare function createMeasurement(): Measurement;
18
+ export declare function parseRealtimeData(buoyId: string, rawText: string, options?: ParseRealtimeDataOptions): BuoyData;
@@ -0,0 +1,5 @@
1
+ import { Measurement } from '../models/measurement';
2
+ /**
3
+ * Returns a UTC Date instance for a buoy measurement.
4
+ */
5
+ export declare function getMeasurementDate(measurement: Measurement): Date;
@@ -0,0 +1,4 @@
1
+ export type QueryParamValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
2
+ export type QueryParams = Record<string, QueryParamValue>;
3
+ export declare function formatQueryParams(params?: QueryParams): string;
4
+ export declare function buildURL(base: string, path?: string, params?: QueryParams): string;
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "buoydata",
3
+ "version": "0.1.0",
4
+ "description": "Modern TypeScript SDK for NDBC realtime buoy data.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "devDependencies": {
21
+ "@typescript-eslint/eslint-plugin": "^8.4.0",
22
+ "@typescript-eslint/parser": "^8.4.0",
23
+ "@vitest/coverage-v8": "^2.1.1",
24
+ "eslint": "^9.9.0",
25
+ "eslint-config-prettier": "^9.1.0",
26
+ "prettier": "^3.3.3",
27
+ "typescript": "^5.5.4",
28
+ "vite": "^5.4.2",
29
+ "vitest": "^2.1.1"
30
+ },
31
+ "scripts": {
32
+ "dev": "vite",
33
+ "build": "vite build && tsc -p tsconfig.build.json",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "lint": "eslint . --ext .ts",
37
+ "format": "prettier -w ."
38
+ }
39
+ }