Skip to content

Commit 35518a6

Browse files
authored
Merge pull request #20 from TextureHQ/ENG-3658-v-0-standardize-conditions-in-weather-plus-project
Standardize weather conditions across providers
2 parents b5ddaf0 + 2137a16 commit 35518a6

File tree

13 files changed

+377
-11
lines changed

13 files changed

+377
-11
lines changed

README.md

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ An awesome TypeScript weather client for fetching weather data from various weat
1212
- [Tomorrow.io](https://www.tomorrow.io/) (coming soon!)
1313
- [WeatherKit](https://developer.apple.com/weatherkit/) (coming soon!)
1414
- **Clean and Standardized API**: Fetch weather data using a consistent interface, regardless of the underlying provider.
15+
- Standardized weather conditions across providers
1516
- **Built-in Caching**: Supports in-memory and Redis caching to optimize API usage and reduce latency.
1617
- **TypeScript Support**: Enjoy a type-safe API out of the box for better development experience.
1718

@@ -231,6 +232,73 @@ try {
231232
}
232233
```
233234

235+
### Standardized Weather Conditions
236+
237+
One of the key features of this library is the standardization of weather conditions across different providers.
238+
This ensures that your application receives consistent condition values regardless of which provider is used.
239+
240+
For OpenWeather, the library uses the Weather Condition ID codes provided by the API to derive standardized conditions.
241+
For NWS, the library uses the textDescription field for standardization.
242+
243+
#### Available Standardized Conditions
244+
245+
The following standardized condition values are available:
246+
247+
- `Blizzard` - Blizzard conditions
248+
- `Breezy` - Breezy conditions
249+
- `Clear` - Clear, sunny conditions
250+
- `Cloudy` - Cloudy conditions
251+
- `Cold` - Cold conditions
252+
- `Drizzle` - Light rain or drizzle
253+
- `Dust` - Dusty conditions
254+
- `Fair` - Fair weather
255+
- `Flurries` - Snow flurries
256+
- `Fog` - Foggy conditions
257+
- `FreezingDrizzle` - Freezing drizzle
258+
- `FreezingRain` - Freezing rain
259+
- `Hail` - Hail precipitation
260+
- `Haze` - Hazy conditions
261+
- `HeavyRain` - Heavy rain
262+
- `HeavySnow` - Heavy snow
263+
- `Hot` - Hot conditions
264+
- `Hurricane` - Hurricane conditions
265+
- `IsolatedThunderstorms` - Isolated thunderstorms
266+
- `LightRain` - Light rain
267+
- `LightSnow` - Light snow
268+
- `Mist` - Misty conditions
269+
- `Mixed` - Mixed precipitation (rain and snow)
270+
- `MostlyClear` - Mostly clear conditions
271+
- `MostlyCloudy` - Mostly cloudy conditions
272+
- `Overcast` - Overcast conditions
273+
- `PartlyCloudy` - Partly cloudy conditions
274+
- `Rain` - Rain (moderate)
275+
- `Sandstorm` - Sandstorm conditions
276+
- `Showers` - Rain showers
277+
- `Sleet` - Sleet
278+
- `Smoke` - Smoky conditions
279+
- `Snow` - Snow (moderate)
280+
- `Storm` - Stormy conditions
281+
- `Thunderstorms` - Thunderstorms
282+
- `Tornado` - Tornado conditions
283+
- `TropicalStorm` - Tropical storm conditions
284+
- `Windy` - Windy conditions
285+
- `Unknown` - Condition that couldn't be mapped to a standard value
286+
287+
#### Accessing Standardized Conditions
288+
289+
The weather data returned includes both the original provider-specific condition text and the standardized condition:
290+
291+
```ts
292+
const weather = await weatherPlus.getWeather(40.748817, -73.985428);
293+
294+
console.log(weather.conditions.value); // Standardized value (e.g., "Clear")
295+
console.log(weather.conditions.original); // Original provider text (e.g., "clear sky")
296+
```
297+
298+
For OpenWeather, the standardization is based on the API's Weather Condition ID codes as documented at [OpenWeather Weather Conditions](https://openweathermap.org/weather-conditions).
299+
300+
This allows you to display either the original detailed condition from the provider or use the standardized value for consistent UI elements or logic across your application.
301+
234302
### TypeScript Support
235303

236304
This library is built with TypeScript and includes type-safe interfaces for weather data and errors.
@@ -253,8 +321,9 @@ interface IWeatherData {
253321
unit: string;
254322
};
255323
conditions: {
256-
value: string;
257-
unit: string;
324+
value: string; // Standardized condition across providers
325+
unit: string; // Always "string"
326+
original: string; // Original provider condition text
258327
};
259328
}
260329
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "weather-plus",
3-
"version": "1.0.3",
3+
"version": "1.0.4",
44
"description": "Weather Plus is a powerful wrapper around various Weather APIs that simplifies adding weather data to your application",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

src/index.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ describe('WeatherPlus Library', () => {
126126
unit: 'C',
127127
},
128128
conditions: {
129-
value: 'Sunny',
129+
value: 'Clear',
130130
unit: 'string',
131+
original: 'Sunny'
131132
},
132133
provider: 'nws',
133134
cached: false,
@@ -190,7 +191,11 @@ describe('WeatherPlus Library', () => {
190191
expect(response).toBeDefined();
191192
expect(response.temperature).toEqual({ value: 15, unit: 'C' });
192193
expect(response.humidity).toEqual({ value: 82, unit: 'percent' });
193-
expect(response.conditions).toEqual({ value: 'light rain', unit: 'string' });
194+
expect(response.conditions).toEqual({
195+
value: 'Light Rain',
196+
unit: 'string',
197+
original: 'light rain'
198+
});
194199
});
195200

196201
it('should throw an error if all providers fail', async () => {

src/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@ export type IBaseWeatherProperty<T, U extends IWeatherUnits> = {
3232

3333
export type IRelativeHumidity = IBaseWeatherProperty<number, IWeatherUnits.percent>;
3434
export type IDewPoint = IBaseWeatherProperty<number, IWeatherUnits.C | IWeatherUnits.F>;
35-
export type IConditions = IBaseWeatherProperty<string, IWeatherUnits.string>;
35+
export type IConditions = IBaseWeatherProperty<string, IWeatherUnits.string> & {
36+
original?: string; // Original provider-specific condition value
37+
}
3638
export type ITemperature = IBaseWeatherProperty<number, IWeatherUnits.C | IWeatherUnits.F>;

src/providers/nws/client.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ describe('NWSProvider', () => {
6464
dewPoint: { value: 10, unit: 'C' },
6565
humidity: { value: 80, unit: 'percent' },
6666
temperature: { value: 20, unit: 'C' },
67-
conditions: { value: 'Clear', unit: 'string' },
67+
conditions: {
68+
value: 'Clear',
69+
unit: 'string',
70+
original: 'Clear'
71+
},
6872
});
6973
});
7074

@@ -131,7 +135,11 @@ describe('NWSProvider', () => {
131135
dewPoint: { value: 10, unit: 'C' },
132136
humidity: { value: 80, unit: 'percent' },
133137
temperature: { value: 20, unit: 'C' },
134-
conditions: { value: 'Clear', unit: 'string' },
138+
conditions: {
139+
value: 'Clear',
140+
unit: 'string',
141+
original: 'Clear'
142+
},
135143
});
136144
});
137145
});

src/providers/nws/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { IWeatherProvider } from '../IWeatherProvider';
1111
import { InvalidProviderLocationError } from '../../errors'; // Import the error class
1212
import { isLocationInUS } from '../../utils/locationUtils';
13+
import { standardizeCondition } from './condition';
1314

1415
const log = debug('weather-plus:nws:client');
1516

@@ -151,6 +152,7 @@ async function fetchLatestObservation(
151152

152153
function convertToWeatherData(observation: any): IWeatherProviderWeatherData {
153154
const properties = observation.properties;
155+
154156
return {
155157
dewPoint: {
156158
value: properties.dewpoint.value,
@@ -171,8 +173,9 @@ function convertToWeatherData(observation: any): IWeatherProviderWeatherData {
171173
: IWeatherUnits.F,
172174
},
173175
conditions: {
174-
value: properties.textDescription,
176+
value: standardizeCondition(properties.textDescription),
175177
unit: IWeatherUnits.string,
178+
original: properties.textDescription
176179
},
177180
};
178181
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { standardizeCondition } from './condition';
2+
import { StandardWeatherCondition } from '../../weatherCondition';
3+
4+
describe('NWS Conditions', () => {
5+
it('should standardize NWS conditions correctly', () => {
6+
// Test a sample of NWS conditions
7+
expect(standardizeCondition('Clear')).toEqual(StandardWeatherCondition.Clear);
8+
expect(standardizeCondition('Sunny')).toEqual(StandardWeatherCondition.Clear);
9+
expect(standardizeCondition('Mostly Clear')).toEqual(StandardWeatherCondition.MostlyClear);
10+
expect(standardizeCondition('Partly Cloudy')).toEqual(StandardWeatherCondition.PartlyCloudy);
11+
expect(standardizeCondition('Mostly Cloudy')).toEqual(StandardWeatherCondition.MostlyCloudy);
12+
expect(standardizeCondition('Overcast')).toEqual(StandardWeatherCondition.Overcast);
13+
expect(standardizeCondition('Light Rain')).toEqual(StandardWeatherCondition.LightRain);
14+
expect(standardizeCondition('Heavy Rain')).toEqual(StandardWeatherCondition.HeavyRain);
15+
expect(standardizeCondition('Fog')).toEqual(StandardWeatherCondition.Fog);
16+
expect(standardizeCondition('Mist')).toEqual(StandardWeatherCondition.Mist);
17+
expect(standardizeCondition('Haze')).toEqual(StandardWeatherCondition.Haze);
18+
expect(standardizeCondition('Thunderstorm')).toEqual(StandardWeatherCondition.Thunderstorms);
19+
expect(standardizeCondition('Hail')).toEqual(StandardWeatherCondition.Hail);
20+
expect(standardizeCondition('Mixed Precipitation')).toEqual(StandardWeatherCondition.Mixed);
21+
expect(standardizeCondition('Hail')).toEqual(StandardWeatherCondition.Hail);
22+
expect(standardizeCondition('Mixed Precipitation')).toEqual(StandardWeatherCondition.Mixed);
23+
});
24+
25+
it('should return unknown for unrecognized conditions', () => {
26+
expect(standardizeCondition('SomeUnknownCondition')).toEqual(StandardWeatherCondition.Unknown);
27+
});
28+
});

src/providers/nws/condition.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import debug from 'debug';
2+
import { StandardWeatherCondition } from '../../weatherCondition';
3+
4+
const log = debug('weather-plus:nws:conditions');
5+
6+
// Mapping table for NWS condition texts
7+
const nwsConditionsMap: Record<string, StandardWeatherCondition> = {
8+
'Clear': StandardWeatherCondition.Clear,
9+
'Sunny': StandardWeatherCondition.Clear,
10+
'Mostly Clear': StandardWeatherCondition.MostlyClear,
11+
'Mostly Sunny': StandardWeatherCondition.MostlyClear,
12+
'Partly Cloudy': StandardWeatherCondition.PartlyCloudy,
13+
'Partly Sunny': StandardWeatherCondition.PartlyCloudy,
14+
'Mostly Cloudy': StandardWeatherCondition.MostlyCloudy,
15+
'Cloudy': StandardWeatherCondition.Cloudy,
16+
'Overcast': StandardWeatherCondition.Overcast,
17+
'Light Rain': StandardWeatherCondition.LightRain,
18+
'Rain': StandardWeatherCondition.Rain,
19+
'Heavy Rain': StandardWeatherCondition.HeavyRain,
20+
'Light Snow': StandardWeatherCondition.LightSnow,
21+
'Snow': StandardWeatherCondition.Snow,
22+
'Heavy Snow': StandardWeatherCondition.HeavySnow,
23+
'Fog': StandardWeatherCondition.Fog,
24+
'Haze': StandardWeatherCondition.Haze,
25+
'Mist': StandardWeatherCondition.Mist,
26+
'Thunderstorm': StandardWeatherCondition.Thunderstorms,
27+
'Thunderstorms': StandardWeatherCondition.Thunderstorms,
28+
'Windy': StandardWeatherCondition.Windy,
29+
'Breezy': StandardWeatherCondition.Breezy,
30+
'Sleet': StandardWeatherCondition.Sleet,
31+
'Freezing Rain': StandardWeatherCondition.FreezingRain,
32+
'Hail': StandardWeatherCondition.Hail,
33+
'Rain/Snow': StandardWeatherCondition.Mixed,
34+
'Mixed Precipitation': StandardWeatherCondition.Mixed,
35+
'Tornado': StandardWeatherCondition.Tornado,
36+
'Hurricane': StandardWeatherCondition.Hurricane,
37+
'Tropical Storm': StandardWeatherCondition.TropicalStorm,
38+
};
39+
40+
/**
41+
* Standardizes an NWS condition string to a StandardWeatherCondition
42+
* @param condition The raw condition from NWS
43+
* @returns Standardized weather condition string
44+
*/
45+
export function standardizeCondition(condition: string): string {
46+
if (condition in nwsConditionsMap) {
47+
return nwsConditionsMap[condition];
48+
} else {
49+
log(`Unknown NWS condition: "${condition}". Returning Unknown condition.`);
50+
return StandardWeatherCondition.Unknown;
51+
}
52+
}

src/providers/openweather/client.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ describe('OpenWeatherProvider', () => {
2626
temp: 20,
2727
weather: [
2828
{
29+
id: 800,
30+
main: 'Clear',
2931
description: 'clear sky',
32+
icon: '01d',
3033
},
3134
],
3235
},
@@ -49,7 +52,11 @@ describe('OpenWeatherProvider', () => {
4952
dewPoint: { value: 10, unit: 'C' },
5053
humidity: { value: 80, unit: 'percent' },
5154
temperature: { value: 20, unit: 'C' },
52-
conditions: { value: 'clear sky', unit: 'string' },
55+
conditions: {
56+
value: 'Clear',
57+
unit: 'string',
58+
original: 'clear sky'
59+
},
5360
});
5461
});
5562

src/providers/openweather/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import debug from 'debug';
33
import { IWeatherUnits, IWeatherProviderWeatherData } from '../../interfaces';
44
import { IOpenWeatherResponse } from './interfaces';
55
import { IWeatherProvider } from '../IWeatherProvider';
6+
import { standardizeCondition} from './condition';
67

78
const log = debug('weather-plus:openweather:client');
89

@@ -40,6 +41,8 @@ export class OpenWeatherProvider implements IWeatherProvider {
4041
}
4142

4243
function convertToWeatherData(data: IOpenWeatherResponse): IWeatherProviderWeatherData {
44+
const weatherData = data.current.weather[0];
45+
4346
return {
4447
dewPoint: {
4548
value: data.current.dew_point,
@@ -54,8 +57,9 @@ function convertToWeatherData(data: IOpenWeatherResponse): IWeatherProviderWeath
5457
unit: IWeatherUnits.C,
5558
},
5659
conditions: {
57-
value: data.current.weather[0].description,
60+
value: standardizeCondition(weatherData.id),
5861
unit: IWeatherUnits.string,
62+
original: weatherData.description,
5963
},
6064
};
6165
}

0 commit comments

Comments
 (0)