yr.no-forecast
Advanced tools
Comparing version 1.0.1 to 2.0.0
@@ -5,4 +5,16 @@ # CHANGELOG | ||
## 2.0.0 - 24/05/17 | ||
* Increase performance by approximately 10x due to use of `pixl-xml` and | ||
improved algorithm for getting weather nodes. | ||
* Simplify codebase. | ||
* Update JSON output format to be closer to the original XML content. This gives | ||
users of this module more flexibility and information but is a breaking change. | ||
* Add `getValidTimes` function. | ||
* Fix security `request` and `qs` module vulnerabilities by updating to `yr.no-interface@1.0.1`. | ||
# 1.0.0 26/04/2017 | ||
## 1.0.1 - 28/04/2017 | ||
* Mitigate security vulnerabilities. | ||
* Use new yr.no-interface@1.0.0 internally. | ||
## 1.0.0 - 27/04/2017 | ||
* Change to Promise based interface. | ||
@@ -17,3 +29,3 @@ * Make the module a factory function. | ||
# < 1.0.0 | ||
## < 1.0.0 | ||
* Undocumented. Sorry. |
331
index.js
@@ -7,6 +7,33 @@ 'use strict'; | ||
const VError = require('verror'); | ||
const filter = require('lodash.filter'); | ||
const each = require('lodash.foreach'); | ||
const Promise = require('bluebird'); | ||
/** | ||
* "simple" nodes are those with very basic detail. 1 to 4 of these follow a | ||
* node with more details | ||
* @param {Object} node | ||
* @return {Boolean} | ||
*/ | ||
function isSimpleNode (node) { | ||
return node.location.symbol !== undefined; | ||
} | ||
/** | ||
* Check if a node has a min and max temp range | ||
* @param {Object} node | ||
* @return {Boolean} | ||
*/ | ||
function hasTemperatureRange (node) { | ||
return node.location.minTemperature && node.location.maxTemperature; | ||
} | ||
/** | ||
* Convert a momentjs date object into an ISO string compatible with our XML | ||
* @param {Date} date | ||
* @return {String} | ||
*/ | ||
function dateToForecastISO (date) { | ||
return date.utc().format('YYYY-MM-DDTHH:mm:ss[Z]'); | ||
} | ||
module.exports = (config) => { | ||
@@ -65,5 +92,8 @@ // Make a default config, but extend it with the passed config | ||
this.xml = xml; | ||
this.basic = []; | ||
this.detail = []; | ||
// Map containing weather info for given utc times | ||
this.times = { | ||
// e.g '2017-04-29T01:00:00Z': { DATA HERE } | ||
}; | ||
log('building LocationForecast object by parsing xml to JSON'); | ||
@@ -92,7 +122,27 @@ | ||
each(this.json.weatherdata.product.time, function (time) { | ||
if (time.location.symbol) { | ||
self.basic.push(time); | ||
each(this.json.weatherdata.product.time, function (node) { | ||
const simple = isSimpleNode(node); | ||
const temps = hasTemperatureRange(node); | ||
if (!simple) { | ||
self.times[node.to] = node; | ||
} else { | ||
self.detail.push(time); | ||
// node is a small/simple node with format | ||
// <time datatype="forecast" from="2017-04-28T22:00:00Z" to="2017-04-28T23:00:00Z"> | ||
// <location altitude="17" latitude="34.0522" longitude="118.2437"> | ||
// <precipitation unit="mm" value="0.0"/> | ||
// <symbol id="Sun" number="1"/> | ||
// </location> | ||
// </time> | ||
const parent = self.times[node.to]; | ||
parent.icon = node.location.symbol.id; | ||
parent.rain = node.location.precipitation.value + ' ' + node.location.precipitation.unit; | ||
/* istanbul ignore else */ | ||
if (temps) { | ||
parent.minTemperature = node.location.minTemperature; | ||
parent.maxTemperature = node.location.maxTemperature; | ||
} | ||
} | ||
@@ -104,3 +154,4 @@ }); | ||
/** | ||
* Return JSON of weather | ||
* Returns the JSON representation of the parsed XML` | ||
* @return {Object} | ||
*/ | ||
@@ -113,3 +164,4 @@ getJson: function() { | ||
/** | ||
* Return XML of weather | ||
* Return the XML string that the met.no api returned | ||
* @return {String} | ||
*/ | ||
@@ -121,2 +173,6 @@ getXml: function() { | ||
/** | ||
* Returns the earliest ISO timestring available in the weather data | ||
* @return {String} | ||
*/ | ||
getFirstDateInPayload: function () { | ||
@@ -128,2 +184,20 @@ return this.json.weatherdata.product.time[0].from; | ||
/** | ||
* Returns the latest ISO timestring available in the weather data | ||
* @return {String} | ||
*/ | ||
getLastDateInPayload: function () { | ||
return this.json.weatherdata.product.time[this.json.weatherdata.product.time.length - 1].from; | ||
}, | ||
/** | ||
* Returns an array of all times that we have weather data for | ||
* @return {Array<String>} | ||
*/ | ||
getValidTimestamps: function () { | ||
return Object.keys(this.times); | ||
}, | ||
/** | ||
* Get five day weather. | ||
@@ -133,12 +207,23 @@ * @param {Function} callback | ||
getFiveDaySummary: function() { | ||
log('getting five day summary'); | ||
const startDate = moment.utc(this.getFirstDateInPayload()); | ||
const baseDate = startDate.clone().set('hour', 12).startOf('hour'); | ||
let firstDate = baseDate.clone(); | ||
var firstDate = moment.utc(this.getFirstDateInPayload()).hours(12); | ||
log(`five day summary is using ${baseDate.toString()} as a starting point`); | ||
/* istanbul ignore else */ | ||
if (firstDate.isBefore(startDate)) { | ||
// first date is unique since we may not have data back to midday so instead we | ||
// go with the earliest available | ||
firstDate = startDate.clone(); | ||
} | ||
log(`getting five day summary starting with ${firstDate.toISOString()}`); | ||
return Promise.all([ | ||
this.getForecastForTime(firstDate), | ||
this.getForecastForTime(firstDate.clone().add('days', 1)), | ||
this.getForecastForTime(firstDate.clone().add('days', 2)), | ||
this.getForecastForTime(firstDate.clone().add('days', 3)), | ||
this.getForecastForTime(firstDate.clone().add('days', 4)) | ||
this.getForecastForTime(baseDate.clone().add(1, 'days')), | ||
this.getForecastForTime(baseDate.clone().add(2, 'days')), | ||
this.getForecastForTime(baseDate.clone().add(3, 'days')), | ||
this.getForecastForTime(baseDate.clone().add(4, 'days')) | ||
]) | ||
@@ -153,2 +238,16 @@ .then(function (results) { | ||
/** | ||
* Verifies if the pased timestamp is a within range for the weather data | ||
* @param {String|Number|Date} time | ||
* @return {Boolean} | ||
*/ | ||
isInRange: function (time) { | ||
return moment.utc(time) | ||
.isBetween( | ||
moment(this.getFirstDateInPayload()), | ||
moment(this.getLastDateInPayload()) | ||
); | ||
}, | ||
/** | ||
* Returns a forecast for a given time. | ||
@@ -158,5 +257,3 @@ * @param {String|Date} time | ||
*/ | ||
getForecastForTime: function(time) { | ||
var self = this; | ||
getForecastForTime: function (time) { | ||
time = moment.utc(time); | ||
@@ -170,179 +267,59 @@ | ||
log('getting forecast for time %s', time); | ||
if (time.minute() > 30) { | ||
time.add('hours', 1).startOf('hour'); | ||
} else { | ||
time.startOf('hour'); | ||
} | ||
return Promise.resolve() | ||
.then(function () { | ||
return buildDetail( | ||
self.getDetailForTime(time), | ||
self.getBasicForTime(time) | ||
); | ||
}); | ||
}, | ||
log('getForecastForTime', dateToForecastISO(time)); | ||
/** | ||
* Get basic items for a time. | ||
* Find nearest hour on same day, or no result. | ||
* @param {String|Object} | ||
* @param {Function} | ||
*/ | ||
getBasicForTime: function (date) { | ||
date = moment.utc(date); | ||
let data = this.times[dateToForecastISO(time)] || null; | ||
log('getBasicForTime %s', date); | ||
/* istanbul ignore else */ | ||
if (!data && this.isInRange(time)) { | ||
data = this.fallbackSelector(time); | ||
} | ||
// Used to find closest time to one provided | ||
var maxDifference = Infinity; | ||
var res = null; | ||
var items = getItemsForDay(this.basic, date); | ||
var len = items.length; | ||
var i = 0; | ||
while (i < len) { | ||
var to = moment.utc(items[i].to); | ||
var from = moment.utc(items[i].from); | ||
// Check the date falls in range | ||
if ((from.isSame(date) || from.isBefore(date)) && (to.isSame(date) || to.isAfter(date))) { | ||
var diff = Math.abs(to.diff(from)); | ||
if (diff < maxDifference) { | ||
maxDifference = diff; | ||
res = items[i]; | ||
} | ||
} | ||
i++; | ||
/* istanbul ignore else */ | ||
if (data) { | ||
data = Object.assign({}, data, data.location); | ||
delete data.location; | ||
} | ||
return res || fallbackSelector(date, this.basic); | ||
return Promise.resolve(data); | ||
}, | ||
/** | ||
* Get detailed items for a time. | ||
* Find nearest hour on same day, or no result. | ||
* @param {String|Object} | ||
* @param {Function} | ||
*/ | ||
getDetailForTime: function (date) { | ||
date = moment.utc(date); | ||
log('getDetailForTime %s', date); | ||
fallbackSelector: function (date) { | ||
log('using fallbackSelector for date', date); | ||
// Used to find closest time to one provided | ||
var maxDifference = Infinity; | ||
var res = null; | ||
var itemsForDay = getItemsForDay(this.detail, date); | ||
var len = itemsForDay.length; | ||
var i = 0; | ||
const datetimes = Object.keys(this.times); | ||
while (i < len) { | ||
var diff = Math.abs(moment.utc(itemsForDay[i].to).diff(date)); | ||
if (diff < maxDifference) { | ||
maxDifference = diff; | ||
res = itemsForDay[i]; | ||
} | ||
let closest = null; | ||
let curnode, curTo; | ||
let len = datetimes.length - 1; | ||
i++; | ||
} | ||
while (len) { | ||
curnode = this.times[datetimes[len]]; | ||
curTo = moment(curnode.to); | ||
return res || fallbackSelector(date, this.detail); | ||
} | ||
}; | ||
/** | ||
* Build the detailed info for forecast object. | ||
* @param {Object} detail | ||
* @param {Object} obj | ||
*/ | ||
function buildDetail(detail, basic) { | ||
// <location altitude="48" latitude="59.3758" longitude="10.7814"> | ||
// <temperature id="TTT" unit="celcius" value="6.3"/> | ||
// <windDirection id="dd" deg="223.7" name="SW"/> | ||
// <windSpeed id="ff" mps="4.2" beaufort="3" name="Lett bris"/> | ||
// <humidity value="87.1" unit="percent"/> | ||
// <pressure id="pr" unit="hPa" value="1010.5"/> | ||
// <cloudiness id="NN" percent="0.0"/> | ||
// <fog id="FOG" percent="0.0"/> | ||
// <lowClouds id="LOW" percent="0.0"/> | ||
// <mediumClouds id="MEDIUM" percent="0.0"/> | ||
// <highClouds id="HIGH" percent="0.0"/> | ||
// <dewpointTemperature id="TD" unit="celcius" value="4.2"/> | ||
// </location> | ||
var obj = { | ||
icon: basic.location.symbol.id, | ||
to: basic.to, | ||
from: basic.from, | ||
rain: basic.location.precipitation.value + ' ' + basic.location.precipitation.unit | ||
}; | ||
// Corresponds to XML 'location' element | ||
var location = detail.location; | ||
var cur = null; | ||
for (var key in location) { | ||
cur = location[key]; | ||
// Based on field type build the response | ||
// Type 1: Has "value" and "unit", combine for result | ||
// Type 2: Has only "percent" | ||
// Type 3: Has multiple properties where the name is the value | ||
if (cur.hasOwnProperty('value')) { | ||
obj[key] = cur.value + ' ' + cur.unit; | ||
} else if (cur.hasOwnProperty('percent')) { | ||
obj[key] = cur.percent + '%'; | ||
} else if (typeof cur === 'object') { | ||
obj[key] = {}; | ||
for (var nestedKey in cur) { | ||
if (nestedKey !== 'id') { | ||
obj[key][nestedKey] = cur[nestedKey]; | ||
if (date.isSame(curTo, 'day')) { | ||
if (!closest) { | ||
closest = curnode; | ||
} else { | ||
/* istanbul ignore else */ | ||
if (Math.abs(date.diff(curTo)) < Math.abs(date.diff(moment(closest.to)))) { | ||
closest = curnode; | ||
} | ||
} | ||
} else if (closest) { | ||
// we found a node, and no more nodes exist for the day we need...BAIL | ||
break; | ||
} | ||
} | ||
} | ||
return obj; | ||
} | ||
/** | ||
* Used when no matching element is found for a time of a day. | ||
* @param {Date} time | ||
* @param {Array} collection | ||
*/ | ||
function fallbackSelector(time, collection) { | ||
time = moment.utc(time); | ||
var isBefore = false; | ||
var len = collection.length; | ||
var i = 0; | ||
while (i < len) { | ||
if (time.isBefore(moment(collection[i].to))) { | ||
isBefore = true; | ||
i = len; | ||
len--; | ||
} | ||
i++; | ||
return closest; | ||
} | ||
return isBefore ? collection[0] : collection[collection.length - 1]; | ||
} | ||
/** | ||
* Provides detailed forecasts for a given date. | ||
* @param {Array} collection | ||
* @param {String|Object} date | ||
*/ | ||
function getItemsForDay(collection, date) { | ||
date = moment.utc(date); | ||
return filter(collection, function (item) { | ||
return ( | ||
moment.utc(item.from).isSame(date, 'day') || | ||
moment.utc(item.to).isSame(date, 'day') | ||
); | ||
}); | ||
} | ||
}; |
{ | ||
"name": "yr.no-forecast", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"description": "retrieve a weather forecast for a given time and location from met.no", | ||
"main": "./index.js", | ||
"scripts": { | ||
"test": "npm run lint && npm run unit && npm run coverage", | ||
"test": "date && npm run lint && npm run unit && npm run coverage", | ||
"lint": "npm run eslint && npm run linelint", | ||
@@ -14,3 +14,4 @@ "eslint": "eslint index.js test/*.js", | ||
"coverage": "NODE_PATH=. nyc mocha test/ && nyc report --reporter=lcov", | ||
"example": "node example/dublin-weather.js" | ||
"example": "node example/dublin-weather.js", | ||
"benchmark": "node benchmark/index.js" | ||
}, | ||
@@ -24,2 +25,3 @@ "files": [ | ||
"lodash.filter": "~4.6.0", | ||
"lodash.find": "~4.6.0", | ||
"lodash.foreach": "~4.5.0", | ||
@@ -29,3 +31,3 @@ "moment": "~2.18.1", | ||
"verror": "~1.9.0", | ||
"yr.no-interface": "~1.0.0" | ||
"yr.no-interface": "~1.0.1" | ||
}, | ||
@@ -55,2 +57,3 @@ "license": "MIT", | ||
"devDependencies": { | ||
"benchmark": "~2.1.4", | ||
"chai": "~3.5.0", | ||
@@ -57,0 +60,0 @@ "chai-truthy": "~1.0.0", |
@@ -77,6 +77,6 @@ yr.no-forecast | ||
### LocationForecast.getXml() | ||
Returns the XML string that the locationforecast API returned. | ||
Returns the raw XML string that the `locationforecast` API returned. | ||
### LocationForecast.getJson() | ||
Returns the JSON representation of a locationforecast response. | ||
Returns the JSON representation of the entire `locationforecast` response. | ||
@@ -87,29 +87,82 @@ ### LocationForecast.getFirstDateInPayload() | ||
### LocationForecast.getValidTimes() | ||
Returns an Array of ISO timestamps that represent points in time that we have | ||
weather data for. | ||
## Weather JSON Format | ||
Format is somewhat inspired by that of the | ||
[forecast.io](https://developer.forecast.io/) service. | ||
Some fields will be undefined depending on the weather conditions. Always | ||
verify the field you need exists by using `data.hasOwnProperty('fog')` | ||
or similar techniques. | ||
verify the field you need exists, e.g use `data.hasOwnProperty('fog')` or | ||
similar techniques. | ||
```js | ||
```json | ||
{ | ||
icon: 'PARTLYCLOUD', | ||
to: '2013-11-15T18:00:00Z', | ||
from: '2013-11-15T12:00:00Z', | ||
rain: '0.0 mm', | ||
temperature: '9.7 celcius', | ||
windDirection: { deg: '220.2', name: 'SW' }, | ||
windSpeed: { mps: '2.7', beaufort: '2', name: 'Svak vind' }, | ||
humidity: '27.9 percent', | ||
pressure: '1021.0 hPa', | ||
cloudiness: '0.0%', | ||
fog: '0.0%', | ||
lowClouds: '0.0%', | ||
mediumClouds: '0.0%', | ||
highClouds: '0.0%', | ||
dewpointTemperature: '-8.3 celcius' | ||
"datatype": "forecast", | ||
"from": "2017-04-18T03:00:00Z", | ||
"to": "2017-04-18T03:00:00Z", | ||
"icon": "PartlyCloud", | ||
"rain": "0.0 mm", | ||
"altitude": "0", | ||
"latitude": "59.8940", | ||
"longitude": "10.6450", | ||
"temperature": { | ||
"id": "TTT", | ||
"unit": "celsius", | ||
"value": "-0.9" | ||
}, | ||
"windDirection": { | ||
"id": "dd", | ||
"deg": "14.6", | ||
"name": "N" | ||
}, | ||
"windSpeed": { | ||
"id": "ff", | ||
"mps": "1.5", | ||
"beaufort": "1", | ||
"name": "Flau vind" | ||
}, | ||
"windGust": { | ||
"id": "ff_gust", | ||
"mps": "2.4" | ||
}, | ||
"humidity": { | ||
"value": "78.3", | ||
"unit": "percent" | ||
}, | ||
"pressure": { | ||
"id": "pr", | ||
"unit": "hPa", | ||
"value": "1030.1" | ||
}, | ||
"cloudiness": { | ||
"id": "NN", | ||
"percent": "15.4" | ||
}, | ||
"fog": { | ||
"id": "FOG", | ||
"percent": "0.0" | ||
}, | ||
"lowClouds": { | ||
"id": "LOW", | ||
"percent": "15.4" | ||
}, | ||
"mediumClouds": { | ||
"id": "MEDIUM", | ||
"percent": "0.8" | ||
}, | ||
"highClouds": { | ||
"id": "HIGH", | ||
"percent": "0.0" | ||
}, | ||
"dewpointTemperature": { | ||
"id": "TD", | ||
"unit": "celsius", | ||
"value": "-4.5" | ||
} | ||
} | ||
``` | ||
## CHANGELOG | ||
Can be found [at this link](https://github.com/evanshortiss/yr.no-forecast/blob/master/CHANGELOG.md ). |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
16672
167
9
11
251
1
+ Addedlodash.find@~4.6.0
+ Addedlodash.find@4.6.0(transitive)
Updatedyr.no-interface@~1.0.1