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 +21 -0
- package/README.md +289 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/models/measurement.d.ts +87 -0
- package/dist/models/table.d.ts +8 -0
- package/dist/realtime/fetch.d.ts +9 -0
- package/dist/realtime/parser.d.ts +18 -0
- package/dist/utils/date.d.ts +5 -0
- package/dist/utils/url.d.ts +4 -0
- package/package.json +39 -0
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":"gFAUO,SAASA,EAAkBC,EAAsB,GAAY,CAClE,MAAMC,EAAe,IAAI,gBAEzB,SAAW,CAACC,EAAKC,CAAK,IAAK,OAAO,QAAQH,CAAM,EAC9C,GAAIG,GAAU,KAId,IAAI,MAAM,QAAQA,CAAK,EAAG,CACxBA,EAAM,QAAQC,GAAQ,CACpBH,EAAa,OAAOC,EAAK,OAAOE,CAAI,CAAC,CACvC,CAAC,EACD,QACF,CAEAH,EAAa,OAAOC,EAAK,OAAOC,CAAK,CAAC,EAGxC,MAAME,EAAQJ,EAAa,SAAA,EAC3B,OAAOI,EAAQ,IAAIA,CAAK,GAAK,EAC/B,CAEO,SAASC,EACdC,EACAC,EAAO,GACPR,EAAsB,CAAA,EACd,CACR,MAAMS,EAAiBF,EAAK,SAAS,GAAG,EAAIA,EAAO,GAAGA,CAAI,IACpDG,EAAM,IAAI,IAAIF,EAAMC,CAAc,EAClCJ,EAAQN,EAAkBC,CAAM,EACtC,OAAAU,EAAI,OAASL,EACNK,EAAI,SAAA,CACb,CChCA,MAAMC,EAAmB,4CAElB,SAASC,EACdC,EACAC,EAAO,MACPC,EAAUJ,EACF,CACR,MAAMK,EAAW,GAAGH,CAAM,IAAIC,CAAI,GAClC,OAAOR,EAASS,EAASC,CAAQ,CACnC,CAEA,eAAsBC,EACpBC,EACiB,CACjB,KAAM,CACJ,OAAAL,EACA,KAAAC,EAAO,MACP,MAAOK,EAAY,MACnB,YAAAC,EACA,QAAAL,EAAUJ,CAAA,EACRO,EAEJ,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,oCAAoC,EAGtD,MAAMT,EAAME,EAAiBC,EAAQC,EAAMC,CAAO,EAC5CM,EAAW,MAAMF,EAAUT,EAAKU,CAAW,EAEjD,GAAI,CAACC,EAAS,GACZ,MAAM,IAAI,MAAM,mBAAmBX,CAAG,KAAKW,EAAS,MAAM,EAAE,EAG9D,OAAOA,EAAS,KAAA,CAClB,CC3BA,MAAMC,EAAyB,CAAC,IAAI,EAEpC,SAASC,EAAepB,EAAeqB,EAAkC,CAMvE,MALI,GAAAA,EAAc,SAASrB,CAAK,GAK5B,sBAAsB,KAAKA,CAAK,EAKtC,CAEA,SAASsB,EACPC,EACAR,EACa,CACb,GAAIK,EAAeG,EAAKR,EAAQ,aAAa,EAC3C,OAAOA,EAAQ,aAGjB,GAAIA,EAAQ,cAAe,CACzB,MAAMS,EAAU,OAAOD,CAAG,EAC1B,GAAI,CAAC,OAAO,MAAMC,CAAO,EACvB,OAAOA,CAEX,CAEA,OAAOD,CACT,CAEO,SAASE,EACdC,EACAX,EAA2B,GACZ,CACf,MAAMY,EAAsC,CAC1C,cAAeZ,EAAQ,eAAiB,GACxC,aAAcA,EAAQ,cAAgB,KACtC,cAAeA,EAAQ,eAAiBI,CAAA,EAG1C,OAAOO,EACJ,KAAA,EACA,MAAM,KAAK,EACX,OAAO,OAAO,EACd,IAAI1B,GAASsB,EAAYtB,EAAO2B,CAAQ,CAAC,CAC9C,CAEA,SAASC,EAAcC,EAAcC,EAAgC,CACnE,OAAOD,EAAK,WAAW,GAAGC,CAAa,GAAG,CAC5C,CAEA,SAASC,EAAeC,EAA2B,CACjD,OAAOA,EACJ,MAAM,OAAO,EACb,IAAIH,GAAQA,EAAK,KAAA,CAAM,EACvB,OAAOA,GAAQA,EAAK,OAAS,CAAC,CACnC,CAEO,SAASI,EACdD,EACAjB,EAAqC,GACtB,CACf,MAAMe,EAAgBf,EAAQ,eAAiB,IACzCmB,EAAQH,EAAeC,CAAO,EAAE,OACpCH,GAAQ,CAACD,EAAcC,EAAMC,CAAa,CAAA,EAG5C,GAAII,EAAM,SAAW,EACnB,MAAO,CAAE,QAAS,CAAA,EAAI,MAAO,CAAA,EAAI,KAAM,CAAA,EAAI,QAAS,EAAC,EAGvD,MAAMC,EAAaD,EAAM,CAAC,GAAK,GACzBE,EAAiC,CACrC,cAAe,GACf,aAAc,KACd,cAAe,CAAA,CAAC,EAEZC,EAAUZ,EAASU,EAAYC,CAAa,EAAE,IAAI,MAAM,EAE9D,IAAIE,EAAkB,CAAA,EAClBC,EAAiB,EAErB,MAAMC,EAAWN,EAAM,CAAC,EACpBM,GAAYA,EAAS,WAAWV,CAAa,IAC/CQ,EAAQb,EAASe,EAAUJ,CAAa,EAAE,IAAIK,GAAS,CACrD,MAAMC,EAAO,OAAOD,CAAK,EACzB,OAAOC,EAAK,WAAWZ,CAAa,EAChCY,EAAK,MAAMZ,EAAc,MAAM,EAC/BY,CACN,CAAC,EACDH,EAAiB,GAGnB,MAAMI,EAA8B,CAClC,cAAe5B,EAAQ,cACvB,aAAcA,EAAQ,aACtB,cAAeA,EAAQ,aAAA,EAGnB6B,EAAWV,EAAM,MAAMK,CAAc,EACrCM,EAAOD,EAAS,OAAWnB,EAASqB,EAAKH,CAAU,CAAC,EAE1D,MAAO,CACL,QAAAN,EACA,MAAAC,EACA,KAAAO,EACA,QAASD,CAAA,CAEb,CAEO,SAASG,EAAeC,EAAwC,CACrE,KAAM,CAAE,QAAAX,EAAS,KAAAQ,CAAA,EAASG,EAC1B,OAAOH,EAAK,IAAIC,GAAO,CACrB,MAAMG,EAAyB,CAAA,EAC/B,OAAAZ,EAAQ,QAAQ,CAACa,EAAQC,IAAU,CACjCF,EAAOC,CAAM,EAAIJ,EAAIK,CAAK,GAAK,IACjC,CAAC,EACMF,CACT,CAAC,CACH,CAEO,SAASG,GAAiC,CAC/C,MAAO,CACL,eAAgB,OAAO,IACvB,IAAK,OAAO,IACZ,oBAAqB,OAAO,IAC5B,KAAM,OAAO,IACb,OAAQ,OAAO,IACf,MAAO,OAAO,IACd,iBAAkB,OAAO,IACzB,iBAAkB,OAAO,IACzB,kBAAmB,OAAO,IAC1B,MAAO,CACL,cAAe,OAAO,IACtB,kBAAmB,OAAO,IAC1B,eAAgB,OAAO,IACvB,kBAAmB,OAAO,IAC1B,mBAAoB,OAAO,IAC3B,KAAM,OAAO,GAAA,EAEf,KAAM,CACJ,aAAc,OAAO,IACrB,UAAW,OAAO,IAClB,cAAe,OAAO,GAAA,EAExB,KAAM,OAAO,GAAA,CAEjB,CAEA,MAAMC,EAA+E,CACnF,MAAO,CAACC,EAAGtD,IAAU,CACnBsD,EAAE,KAAO,OAAOtD,CAAK,CACvB,EACA,GAAI,CAACsD,EAAGtD,IAAU,CAChBsD,EAAE,KAAO,OAAOtD,CAAK,CACvB,EACA,GAAI,CAACsD,EAAGtD,IAAU,CAChBsD,EAAE,MAAQ,OAAOtD,CAAK,CACxB,EACA,GAAI,CAACsD,EAAGtD,IAAU,CAChBsD,EAAE,IAAM,OAAOtD,CAAK,CACtB,EACA,GAAI,CAACsD,EAAGtD,IAAU,CAChBsD,EAAE,KAAO,OAAOtD,CAAK,CACvB,EACA,GAAI,CAACsD,EAAGtD,IAAU,CAChBsD,EAAE,OAAS,OAAOtD,CAAK,CACzB,EACA,IAAK,CAACsD,EAAGtD,IAAU,CACjBsD,EAAE,MAAM,cAAgB,OAAOtD,CAAK,CACtC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,eAAiB,OAAOtD,CAAK,CACjC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,oBAAsB,OAAOtD,CAAK,CACtC,EACA,IAAK,CAACsD,EAAGtD,IAAU,CACjBsD,EAAE,MAAM,eAAiB,OAAOtD,CAAK,CACvC,EACA,IAAK,CAACsD,EAAGtD,IAAU,CACjBsD,EAAE,KAAK,cAAgB,OAAOtD,CAAK,CACrC,EACA,IAAK,CAACsD,EAAGtD,IAAU,CACjBsD,EAAE,MAAM,kBAAoB,OAAOtD,CAAK,CAC1C,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,iBAAmB,OAAOtD,CAAK,CACnC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,iBAAmB,OAAOtD,CAAK,CACnC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,MAAM,KAAO,OAAOtD,CAAK,CAC7B,EACA,IAAK,CAACsD,EAAGtD,IAAU,CACjBsD,EAAE,kBAAoB,OAAOtD,CAAK,CACpC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,KAAK,UAAY,OAAOtD,CAAK,CACjC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,KAAK,aAAe,OAAOtD,CAAK,CACpC,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,MAAM,mBAAqB,OAAOtD,CAAK,CAC3C,EACA,KAAM,CAACsD,EAAGtD,IAAU,CAClBsD,EAAE,MAAM,kBAAoB,OAAOtD,CAAK,CAC1C,CACF,EAEA,SAASuD,EAAcN,EAAqC,CAC1D,MAAMO,EAAcJ,EAAA,EACpB,cAAO,QAAQH,CAAM,EAAE,QAAQ,CAAC,CAACQ,EAAOzD,CAAK,IAAM,CACjD,MAAM0D,EAASL,EAAeI,CAAK,EAC/BC,GACFA,EAAOF,EAAaxD,CAAK,CAE7B,CAAC,EACMwD,CACT,CAEO,SAASG,EACdjD,EACAsB,EACAjB,EAAoC,CAAA,EAC1B,CACV,MAAMiC,EAAQf,EAAmBD,EAAS,CACxC,GAAGjB,EACH,aAAcA,EAAQ,cAAgB,OAAO,GAAA,CAC9C,EACK6C,EAAUb,EAAeC,CAAK,EAE9Ba,EAAeD,EAAQ,IAAIX,GAAUM,EAAcN,CAAM,CAAC,EAEhE,GAAIlC,EAAQ,qBAAsB,CAChC,MAAM+C,EAA2BD,EAAa,IAAI,CAACL,EAAaL,IAAU,CACxE,MAAMF,EAASW,EAAQT,CAAK,EAC5B,MAAO,CAAE,GAAGK,EAAa,GAAGP,CAAA,CAC9B,CAAC,EAED,MAAO,CACL,GAAIvC,EACJ,aAAcoD,CAAA,CAElB,CAEA,MAAO,CACL,GAAIpD,EACJ,aAAAmD,CAAA,CAEJ,CC3QO,SAASE,EAAmBP,EAAgC,CACjE,KAAM,CAAE,KAAAQ,EAAM,MAAAC,EAAO,IAAAC,EAAK,KAAAC,EAAM,OAAAC,GAAWZ,EAC3C,OAAO,IAAI,KAAK,KAAK,IAAIQ,EAAMC,EAAQ,EAAGC,EAAKC,EAAMC,CAAM,CAAC,CAC9D"}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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,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
|
+
}
|