@verifyhash/wind-scale 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 +200 -0
- package/package.json +36 -0
- package/src/wind-scale.js +228 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <OWNER-NAME PLACEHOLDER>
|
|
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,200 @@
|
|
|
1
|
+
# wind-scale
|
|
2
|
+
|
|
3
|
+
<!-- publish-prep -->
|
|
4
|
+
> **TODO (owner): pick the final npm name/scope before publishing.**
|
|
5
|
+
> The npm package name is **not** finalized by this project. Convention used here:
|
|
6
|
+
> `package.json` keeps the working slug `wind-scale` as a placeholder — replace it
|
|
7
|
+
> (and the `npm install` line below) with the real published name/scope before
|
|
8
|
+
> running `npm publish`.
|
|
9
|
+
|
|
10
|
+
Zero-dependency, pure-JavaScript wind utilities: convert speeds between the five
|
|
11
|
+
units meteorologists actually use, and classify a wind on the three scales the
|
|
12
|
+
public sees on forecasts — the Beaufort force scale, the Saffir-Simpson
|
|
13
|
+
hurricane scale, and the official U.S. National Weather Service wind-chill
|
|
14
|
+
temperature.
|
|
15
|
+
|
|
16
|
+
No dependencies, no network, no I/O. Every function is pure: given the same
|
|
17
|
+
inputs it always returns the same value and mutates nothing. It runs anywhere
|
|
18
|
+
Node.js runs and is trivial to vendor into a browser bundle.
|
|
19
|
+
|
|
20
|
+
## Who it's for
|
|
21
|
+
|
|
22
|
+
- Weather / marine / aviation dashboards that receive wind in one unit (say
|
|
23
|
+
METAR knots) and must display another (mph, km/h).
|
|
24
|
+
- Anyone turning a raw wind speed into a human label ("Force 7, near gale" or
|
|
25
|
+
"Category 3 hurricane").
|
|
26
|
+
- Cold-climate apps computing the "feels-like" wind-chill temperature with the
|
|
27
|
+
correct, current NWS formula instead of the obsolete pre-2001 one.
|
|
28
|
+
|
|
29
|
+
## Install / use
|
|
30
|
+
|
|
31
|
+
It's a single file with no dependencies. Copy `src/wind-scale.js` into your
|
|
32
|
+
project or `require` it directly:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const { convert, beaufort, saffirSimpson, windChill } = require('./src/wind-scale.js');
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
### `convert(value, fromUnit, toUnit) -> number`
|
|
41
|
+
|
|
42
|
+
Convert a wind speed between any two of these units:
|
|
43
|
+
|
|
44
|
+
| code | unit |
|
|
45
|
+
|--------|------------------|
|
|
46
|
+
| `ms` | metres/second |
|
|
47
|
+
| `kmh` | kilometres/hour |
|
|
48
|
+
| `mph` | miles/hour |
|
|
49
|
+
| `kn` | knots |
|
|
50
|
+
| `fts` | feet/second |
|
|
51
|
+
|
|
52
|
+
Exact factors relative to 1 m/s:
|
|
53
|
+
`1 m/s = 3.6 km/h = 2.2369362920544 mph = 1.9438444924406 kn = 3.280839895 ft/s`.
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
convert(10, 'ms', 'kmh'); // 36
|
|
57
|
+
convert(100, 'kmh', 'mph'); // 62.137119223733...
|
|
58
|
+
convert(50, 'kn', 'mph'); // 57.539019...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Throws `TypeError` if `value` is not a finite number and `RangeError` for an
|
|
62
|
+
unknown unit code.
|
|
63
|
+
|
|
64
|
+
### `beaufort(speedMs) -> { force, description, minMs, maxMs }`
|
|
65
|
+
|
|
66
|
+
Classify a wind speed **in metres per second** on the 0–12 Beaufort scale. The
|
|
67
|
+
result gives the integer `force`, its standard WMO `description`, and the band's
|
|
68
|
+
bounds in m/s: `minMs` (inclusive lower bound) and `maxMs` (exclusive upper
|
|
69
|
+
bound; `Infinity` for Force 12). Bands are contiguous — the `maxMs` of one force
|
|
70
|
+
equals the `minMs` of the next.
|
|
71
|
+
|
|
72
|
+
Boundaries (m/s), Force 0 below 0.5 up to Force 12 at 32.7 and above:
|
|
73
|
+
|
|
74
|
+
| force | m/s band | description |
|
|
75
|
+
|-------|--------------|-----------------|
|
|
76
|
+
| 0 | < 0.5 | Calm |
|
|
77
|
+
| 1 | 0.5–1.6 | Light air |
|
|
78
|
+
| 2 | 1.6–3.4 | Light breeze |
|
|
79
|
+
| 3 | 3.4–5.5 | Gentle breeze |
|
|
80
|
+
| 4 | 5.5–8.0 | Moderate breeze |
|
|
81
|
+
| 5 | 8.0–10.8 | Fresh breeze |
|
|
82
|
+
| 6 | 10.8–13.9 | Strong breeze |
|
|
83
|
+
| 7 | 13.9–17.2 | Near gale |
|
|
84
|
+
| 8 | 17.2–20.8 | Gale |
|
|
85
|
+
| 9 | 20.8–24.5 | Strong gale |
|
|
86
|
+
| 10 | 24.5–28.5 | Storm |
|
|
87
|
+
| 11 | 28.5–32.7 | Violent storm |
|
|
88
|
+
| 12 | >= 32.7 | Hurricane force |
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
beaufort(9.0);
|
|
92
|
+
// { force: 5, description: 'Fresh breeze', minMs: 8, maxMs: 10.8 }
|
|
93
|
+
|
|
94
|
+
beaufort(40);
|
|
95
|
+
// { force: 12, description: 'Hurricane force', minMs: 32.7, maxMs: Infinity }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If your speed is in another unit, feed it through `convert` first:
|
|
99
|
+
`beaufort(convert(20, 'kn', 'ms'))`.
|
|
100
|
+
|
|
101
|
+
Throws `TypeError` for non-finite input and `RangeError` for negative speeds.
|
|
102
|
+
|
|
103
|
+
### `saffirSimpson(sustainedMph) -> { category, label }`
|
|
104
|
+
|
|
105
|
+
Classify a tropical cyclone by its **1-minute sustained wind in mph**. Uses the
|
|
106
|
+
Saffir-Simpson Hurricane Wind Scale plus the U.S. National Hurricane Center's
|
|
107
|
+
sub-hurricane designations. `category` is `'TD'`, `'TS'`, or the number `1`–`5`.
|
|
108
|
+
|
|
109
|
+
| category | sustained mph | label |
|
|
110
|
+
|----------|---------------|-----------------------|
|
|
111
|
+
| `'TD'` | < 39 | Tropical Depression |
|
|
112
|
+
| `'TS'` | 39–73 | Tropical Storm |
|
|
113
|
+
| `1` | 74–95 | Category 1 Hurricane |
|
|
114
|
+
| `2` | 96–110 | Category 2 Hurricane |
|
|
115
|
+
| `3` | 111–129 | Category 3 Hurricane |
|
|
116
|
+
| `4` | 130–156 | Category 4 Hurricane |
|
|
117
|
+
| `5` | >= 157 | Category 5 Hurricane |
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
saffirSimpson(35); // { category: 'TD', label: 'Tropical Depression' }
|
|
121
|
+
saffirSimpson(74); // { category: 1, label: 'Category 1 Hurricane' }
|
|
122
|
+
saffirSimpson(160); // { category: 5, label: 'Category 5 Hurricane' }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Throws `TypeError` for non-finite input and `RangeError` for negative speeds.
|
|
126
|
+
|
|
127
|
+
### `windChill(tempF, windMph) -> number`
|
|
128
|
+
|
|
129
|
+
Wind chill temperature in degrees Fahrenheit using the official 2001 NWS /
|
|
130
|
+
Environment Canada formula:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
WC = 35.74 + 0.6215*T - 35.75*V^0.16 + 0.4275*T*V^0.16
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
with `T` air temperature in °F and `V` wind speed in mph (at the standard 10 m
|
|
137
|
+
anemometer height).
|
|
138
|
+
|
|
139
|
+
**Validity range.** The NWS defines this formula only for **air temperature ≤ 50 °F
|
|
140
|
+
and wind speed > 3 mph**. Outside that domain wind chill is not meaningful, so
|
|
141
|
+
this function returns the input air temperature (`tempF`) unchanged when
|
|
142
|
+
`tempF > 50` or `windMph <= 3`. Inside the valid domain it returns the raw
|
|
143
|
+
formula value (not rounded); published NWS charts round to the nearest whole
|
|
144
|
+
degree, so expect sub-degree differences from a chart cell.
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
windChill(0, 35); // -27.40... (NWS chart cell: -27)
|
|
148
|
+
windChill(40, 5); // 36.47... (NWS chart cell: 36)
|
|
149
|
+
windChill(60, 20); // 60 (T > 50 F -> out of range, returns tempF)
|
|
150
|
+
windChill(20, 2); // 20 (V <= 3 mph -> out of range, returns tempF)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Throws `TypeError` if either argument is not a finite number, and `RangeError`
|
|
154
|
+
for negative wind speed.
|
|
155
|
+
|
|
156
|
+
## Running the tests
|
|
157
|
+
|
|
158
|
+
One command, no install step (uses only `node:assert`):
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
node test/wind-scale.test.js
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The suite checks round-trip and cross-unit conversions, every Beaufort force
|
|
165
|
+
boundary (and the value just below it), every Saffir-Simpson category boundary,
|
|
166
|
+
and ten published NWS wind-chill chart cells.
|
|
167
|
+
|
|
168
|
+
## Status / limitations
|
|
169
|
+
|
|
170
|
+
- Beaufort m/s thresholds follow the widely published WMO table rounded to one
|
|
171
|
+
decimal; different sources round the knot-derived cut points slightly
|
|
172
|
+
differently, so a value sitting exactly on a boundary may differ by one force
|
|
173
|
+
from another table.
|
|
174
|
+
- `saffirSimpson` expects 1-minute sustained wind in mph, the U.S. convention.
|
|
175
|
+
Agencies using 10-minute sustained winds (much of the world) will classify the
|
|
176
|
+
same storm differently — convert your averaging period first.
|
|
177
|
+
- Wind chill is only valid for cold, windy conditions (≤ 50 °F, > 3 mph); it is
|
|
178
|
+
not a general "feels like" index and says nothing about heat or humidity.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT.
|
|
183
|
+
|
|
184
|
+
## Install
|
|
185
|
+
|
|
186
|
+
> Placeholder name: the `wind-scale` below is the working slug, not a finalized
|
|
187
|
+
> npm name — see the owner TODO near the top of this README.
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
npm install wind-scale
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
const { convert, beaufort, saffirSimpson, windChill } = require('wind-scale');
|
|
195
|
+
|
|
196
|
+
convert(10, 'ms', 'kmh'); // 36
|
|
197
|
+
beaufort(9.0); // { force: 5, description: 'Fresh breeze', minMs: 8, maxMs: 10.8 }
|
|
198
|
+
saffirSimpson(100); // { category: 2, label: 'Category 2 Hurricane' } (sustained mph)
|
|
199
|
+
windChill(20, 15); // 6.22 (°F, from tempF=20, windMph=15)
|
|
200
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verifyhash/wind-scale",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency wind speed conversions plus Beaufort, Saffir-Simpson, and the official 2001 NWS wind-chill formula.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wind",
|
|
7
|
+
"beaufort",
|
|
8
|
+
"saffir-simpson",
|
|
9
|
+
"hurricane",
|
|
10
|
+
"wind-chill",
|
|
11
|
+
"knots",
|
|
12
|
+
"meteorology",
|
|
13
|
+
"conversion"
|
|
14
|
+
],
|
|
15
|
+
"main": "src/wind-scale.js",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node test/wind-scale.test.js"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"files": [
|
|
21
|
+
"src/wind-scale.js",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/verifyhash/libs.git",
|
|
30
|
+
"directory": "wind-scale"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/verifyhash/libs/tree/main/wind-scale#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/verifyhash/libs/issues"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* wind-scale — zero-dependency wind speed conversions and meteorological scales.
|
|
5
|
+
*
|
|
6
|
+
* Provides four pure functions:
|
|
7
|
+
* - convert(value, fromUnit, toUnit) unit conversion among m/s, km/h, mph, kn, ft/s
|
|
8
|
+
* - beaufort(speedMs) Beaufort force (0-12) from a speed in m/s
|
|
9
|
+
* - saffirSimpson(sustainedMph) Saffir-Simpson hurricane category from sustained mph
|
|
10
|
+
* - windChill(tempF, windMph) 2001 NWS wind chill temperature (deg F)
|
|
11
|
+
*
|
|
12
|
+
* No I/O, no network, no external dependencies. Every function is pure and
|
|
13
|
+
* deterministic: same inputs always produce the same output, and nothing is
|
|
14
|
+
* mutated or observed outside the return value.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// --- unit conversion -------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Conversion factors expressed as "how many of this unit equal 1 metre/second".
|
|
21
|
+
* These are exact per the task specification:
|
|
22
|
+
* 1 m/s = 3.6 km/h = 2.2369362920544 mph = 1.9438444924406 kn = 3.280839895 ft/s
|
|
23
|
+
*
|
|
24
|
+
* (1 knot = 1 nautical mile/hour = 1852 m / 3600 s; 1 mph = 1609.344 m / 3600 s;
|
|
25
|
+
* 1 ft/s = 0.3048 m / s.)
|
|
26
|
+
*/
|
|
27
|
+
const FACTORS = Object.freeze({
|
|
28
|
+
ms: 1,
|
|
29
|
+
kmh: 3.6,
|
|
30
|
+
mph: 2.2369362920544,
|
|
31
|
+
kn: 1.9438444924406,
|
|
32
|
+
fts: 3.280839895,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const UNIT_NAMES = Object.freeze(Object.keys(FACTORS));
|
|
36
|
+
|
|
37
|
+
function isFiniteNumber(x) {
|
|
38
|
+
return typeof x === 'number' && Number.isFinite(x);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a wind speed between any two supported units.
|
|
43
|
+
*
|
|
44
|
+
* @param {number} value the numeric speed to convert (finite number)
|
|
45
|
+
* @param {string} fromUnit one of 'ms','kmh','mph','kn','fts'
|
|
46
|
+
* @param {string} toUnit one of 'ms','kmh','mph','kn','fts'
|
|
47
|
+
* @returns {number} the value expressed in toUnit
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* convert(10, 'ms', 'kmh') // => 36
|
|
51
|
+
* convert(100, 'kmh', 'mph') // => 62.137... (100 km/h in mph)
|
|
52
|
+
* convert(50, 'kn', 'kn') // => 50 (round-trip identity)
|
|
53
|
+
*/
|
|
54
|
+
function convert(value, fromUnit, toUnit) {
|
|
55
|
+
if (!isFiniteNumber(value)) {
|
|
56
|
+
throw new TypeError(`convert: value must be a finite number, got ${value}`);
|
|
57
|
+
}
|
|
58
|
+
if (!Object.prototype.hasOwnProperty.call(FACTORS, fromUnit)) {
|
|
59
|
+
throw new RangeError(
|
|
60
|
+
`convert: unknown fromUnit '${fromUnit}', expected one of ${UNIT_NAMES.join(', ')}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(FACTORS, toUnit)) {
|
|
64
|
+
throw new RangeError(
|
|
65
|
+
`convert: unknown toUnit '${toUnit}', expected one of ${UNIT_NAMES.join(', ')}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
// Convert to the m/s base, then out to the target unit.
|
|
69
|
+
const ms = value / FACTORS[fromUnit];
|
|
70
|
+
return ms * FACTORS[toUnit];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- Beaufort wind force scale --------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Beaufort force table. Each entry gives the inclusive lower bound in m/s
|
|
77
|
+
* (`minMs`) and the standard WMO description. Boundaries follow the widely
|
|
78
|
+
* published m/s thresholds: Force 0 is below 0.5 m/s, Force 12 (hurricane
|
|
79
|
+
* force) begins at 32.7 m/s. A given force f spans [minMs[f], minMs[f+1]).
|
|
80
|
+
*/
|
|
81
|
+
const BEAUFORT = Object.freeze([
|
|
82
|
+
{ force: 0, minMs: 0.0, description: 'Calm' },
|
|
83
|
+
{ force: 1, minMs: 0.5, description: 'Light air' },
|
|
84
|
+
{ force: 2, minMs: 1.6, description: 'Light breeze' },
|
|
85
|
+
{ force: 3, minMs: 3.4, description: 'Gentle breeze' },
|
|
86
|
+
{ force: 4, minMs: 5.5, description: 'Moderate breeze' },
|
|
87
|
+
{ force: 5, minMs: 8.0, description: 'Fresh breeze' },
|
|
88
|
+
{ force: 6, minMs: 10.8, description: 'Strong breeze' },
|
|
89
|
+
{ force: 7, minMs: 13.9, description: 'Near gale' },
|
|
90
|
+
{ force: 8, minMs: 17.2, description: 'Gale' },
|
|
91
|
+
{ force: 9, minMs: 20.8, description: 'Strong gale' },
|
|
92
|
+
{ force: 10, minMs: 24.5, description: 'Storm' },
|
|
93
|
+
{ force: 11, minMs: 28.5, description: 'Violent storm' },
|
|
94
|
+
{ force: 12, minMs: 32.7, description: 'Hurricane force' },
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Classify a wind speed (in metres per second) on the Beaufort scale.
|
|
99
|
+
*
|
|
100
|
+
* @param {number} speedMs wind speed in m/s (finite, >= 0)
|
|
101
|
+
* @returns {{force:number, description:string, minMs:number, maxMs:number}}
|
|
102
|
+
* force is 0..12; minMs/maxMs are the inclusive lower and exclusive upper
|
|
103
|
+
* bounds of that force band in m/s. For Force 12 maxMs is Infinity.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* beaufort(0.2) // => { force: 0, description: 'Calm', minMs: 0, maxMs: 0.5 }
|
|
107
|
+
* beaufort(9.0) // => { force: 5, description: 'Fresh breeze', minMs: 8, maxMs: 10.8 }
|
|
108
|
+
* beaufort(40) // => { force: 12, description: 'Hurricane force', minMs: 32.7, maxMs: Infinity }
|
|
109
|
+
*/
|
|
110
|
+
function beaufort(speedMs) {
|
|
111
|
+
if (!isFiniteNumber(speedMs)) {
|
|
112
|
+
throw new TypeError(`beaufort: speedMs must be a finite number, got ${speedMs}`);
|
|
113
|
+
}
|
|
114
|
+
if (speedMs < 0) {
|
|
115
|
+
throw new RangeError(`beaufort: speedMs must be >= 0, got ${speedMs}`);
|
|
116
|
+
}
|
|
117
|
+
// Find the highest force whose lower bound is <= speedMs.
|
|
118
|
+
let idx = 0;
|
|
119
|
+
for (let i = 0; i < BEAUFORT.length; i++) {
|
|
120
|
+
if (speedMs >= BEAUFORT[i].minMs) idx = i;
|
|
121
|
+
else break;
|
|
122
|
+
}
|
|
123
|
+
const entry = BEAUFORT[idx];
|
|
124
|
+
const next = BEAUFORT[idx + 1];
|
|
125
|
+
return {
|
|
126
|
+
force: entry.force,
|
|
127
|
+
description: entry.description,
|
|
128
|
+
minMs: entry.minMs,
|
|
129
|
+
maxMs: next ? next.minMs : Infinity,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Saffir-Simpson hurricane wind scale ----------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Classify a tropical cyclone by its 1-minute sustained wind in mph on the
|
|
137
|
+
* Saffir-Simpson Hurricane Wind Scale, extended downward with the U.S.
|
|
138
|
+
* National Hurricane Center's sub-hurricane designations:
|
|
139
|
+
* TD (tropical depression) < 39 mph
|
|
140
|
+
* TS (tropical storm) 39-73 mph
|
|
141
|
+
* Category 1 74-95 mph
|
|
142
|
+
* Category 2 96-110 mph
|
|
143
|
+
* Category 3 111-129 mph (major hurricane)
|
|
144
|
+
* Category 4 130-156 mph (major hurricane)
|
|
145
|
+
* Category 5 >= 157 mph (major hurricane)
|
|
146
|
+
*
|
|
147
|
+
* @param {number} sustainedMph sustained wind speed in mph (finite, >= 0)
|
|
148
|
+
* @returns {{category:('TD'|'TS'|1|2|3|4|5), label:string}}
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* saffirSimpson(35) // => { category: 'TD', label: 'Tropical Depression' }
|
|
152
|
+
* saffirSimpson(74) // => { category: 1, label: 'Category 1 Hurricane' }
|
|
153
|
+
* saffirSimpson(160) // => { category: 5, label: 'Category 5 Hurricane' }
|
|
154
|
+
*/
|
|
155
|
+
function saffirSimpson(sustainedMph) {
|
|
156
|
+
if (!isFiniteNumber(sustainedMph)) {
|
|
157
|
+
throw new TypeError(
|
|
158
|
+
`saffirSimpson: sustainedMph must be a finite number, got ${sustainedMph}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (sustainedMph < 0) {
|
|
162
|
+
throw new RangeError(`saffirSimpson: sustainedMph must be >= 0, got ${sustainedMph}`);
|
|
163
|
+
}
|
|
164
|
+
if (sustainedMph < 39) return { category: 'TD', label: 'Tropical Depression' };
|
|
165
|
+
if (sustainedMph < 74) return { category: 'TS', label: 'Tropical Storm' };
|
|
166
|
+
if (sustainedMph < 96) return { category: 1, label: 'Category 1 Hurricane' };
|
|
167
|
+
if (sustainedMph < 111) return { category: 2, label: 'Category 2 Hurricane' };
|
|
168
|
+
if (sustainedMph < 130) return { category: 3, label: 'Category 3 Hurricane' };
|
|
169
|
+
if (sustainedMph < 157) return { category: 4, label: 'Category 4 Hurricane' };
|
|
170
|
+
return { category: 5, label: 'Category 5 Hurricane' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- NWS wind chill --------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Wind chill temperature using the official 2001 U.S. National Weather Service
|
|
177
|
+
* / Environment Canada formula:
|
|
178
|
+
*
|
|
179
|
+
* WC = 35.74 + 0.6215*T - 35.75*V^0.16 + 0.4275*T*V^0.16
|
|
180
|
+
*
|
|
181
|
+
* where T is air temperature in degrees Fahrenheit and V is the wind speed in
|
|
182
|
+
* miles per hour (measured at the standard 10 m anemometer height).
|
|
183
|
+
*
|
|
184
|
+
* VALIDITY: the formula is only defined by the NWS for air temperature at or
|
|
185
|
+
* below 50 deg F AND wind speed above 3 mph. Outside that domain wind chill is
|
|
186
|
+
* not meaningful, so this function returns the input air temperature (tempF)
|
|
187
|
+
* unchanged when tempF > 50 or windMph <= 3. Within the valid domain it returns
|
|
188
|
+
* the raw formula result (not rounded); published NWS charts round to the
|
|
189
|
+
* nearest whole degree.
|
|
190
|
+
*
|
|
191
|
+
* @param {number} tempF air temperature in degrees Fahrenheit (finite)
|
|
192
|
+
* @param {number} windMph wind speed in mph (finite, >= 0)
|
|
193
|
+
* @returns {number} wind chill temperature in deg F, or tempF if out of range
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* windChill(0, 35) // => -27.4 (NWS chart cell: -27)
|
|
197
|
+
* windChill(40, 5) // => 36.4 (NWS chart cell: 36)
|
|
198
|
+
* windChill(60, 20) // => 60 (T > 50 F: out of range, returns tempF)
|
|
199
|
+
* windChill(20, 2) // => 20 (V <= 3 mph: out of range, returns tempF)
|
|
200
|
+
*/
|
|
201
|
+
function windChill(tempF, windMph) {
|
|
202
|
+
if (!isFiniteNumber(tempF)) {
|
|
203
|
+
throw new TypeError(`windChill: tempF must be a finite number, got ${tempF}`);
|
|
204
|
+
}
|
|
205
|
+
if (!isFiniteNumber(windMph)) {
|
|
206
|
+
throw new TypeError(`windChill: windMph must be a finite number, got ${windMph}`);
|
|
207
|
+
}
|
|
208
|
+
if (windMph < 0) {
|
|
209
|
+
throw new RangeError(`windChill: windMph must be >= 0, got ${windMph}`);
|
|
210
|
+
}
|
|
211
|
+
// Outside the NWS validity domain the formula is undefined; return tempF.
|
|
212
|
+
if (tempF > 50 || windMph <= 3) {
|
|
213
|
+
return tempF;
|
|
214
|
+
}
|
|
215
|
+
const v16 = Math.pow(windMph, 0.16);
|
|
216
|
+
return 35.74 + 0.6215 * tempF - 35.75 * v16 + 0.4275 * tempF * v16;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
convert,
|
|
221
|
+
beaufort,
|
|
222
|
+
saffirSimpson,
|
|
223
|
+
windChill,
|
|
224
|
+
// handy for callers / tests
|
|
225
|
+
FACTORS,
|
|
226
|
+
BEAUFORT,
|
|
227
|
+
UNITS: UNIT_NAMES,
|
|
228
|
+
};
|