diff --git a/dailyAverage.js b/dailyAverage.js index ee42fb2..df80ec2 100644 --- a/dailyAverage.js +++ b/dailyAverage.js @@ -5,8 +5,8 @@ import { trimDate } from "./trimDate.js"; * Calculates daily averages for the time series specified by `datetime` and `x`. * * The returned object contains two properties: - * * datetime -- an array of date objects specifying the starting hour of each day - * * avg -- an array daily average values for each day + * * datetime -- Array of date objects specifying the starting hour of each day + * * average -- Array or daily average values for each day * @param {...Date} datetime Regular hourly axis representing the time associated * with each measurement. * @param {...Number} x Array of hourly measurements. @@ -21,19 +21,19 @@ export function dailyAverage(datetime, x, timezone) { let dayCount = trimmed.datetime.length / 24; let daily_datetime = []; - let daily_avg = []; + let daily_average = []; for (let i = 0; i < dayCount; i++) { let start = i * 24; let end = i * 24 + 24; // NOTE: Average is assigned to the start of the day. daily_datetime[i] = trimmed.datetime[start]; - daily_avg[i] = trimmed.x.slice(start, end).reduce((a, o) => a + o) / 24; + daily_average[i] = trimmed.x.slice(start, end).reduce((a, o) => a + o) / 24; } // Round to one decimal place and ensure null is the missing value - daily_avg = daily_avg.map((o) => + daily_average = daily_average.map((o) => o === null || o === undefined || isNaN(o) ? null : Math.round(10 * o) / 10 ); - return { datetime: daily_datetime, avg: daily_avg }; + return { datetime: daily_datetime, average: daily_average }; } diff --git a/diurnalAverage.js b/diurnalAverage.js index 2796d9b..af4c8b0 100644 --- a/diurnalAverage.js +++ b/diurnalAverage.js @@ -6,7 +6,7 @@ import { trimDate } from "./trimDate.js"; * * The returned object contains two properties: * * hour -- Array of local time hours [0-24] - * * avg -- Array of time-of-day averages for each hour + * * average -- Array of time-of-day averages for each hour * * By default, the averages are calculated using data from the most recent 7 days * in the `datetime` array. @@ -28,7 +28,7 @@ export function diurnalAverage(datetime, x, timezone, dayCount = 7) { let hours = localTime.map((o) => o.hours()); let hour = []; - let hourly_avg = []; + let hourly_average = []; // For each hour, average together the contributions from each day for (let h = 0; h < 24; h++) { @@ -37,13 +37,13 @@ export function diurnalAverage(datetime, x, timezone, dayCount = 7) { sum += x[h + d * 24]; } hour[h] = h; - hourly_avg[h] = sum / dayCount; + hourly_average[h] = sum / dayCount; } // Round to one decimal place and ensure null is the missing value - hourly_avg = hourly_avg.map((o) => + hourly_average = hourly_average.map((o) => o === null || o === undefined || isNaN(o) ? null : Math.round(10 * o) / 10 ); - return { hour: hour, avg: hourly_avg }; + return { hour: hour, average: hourly_average }; } diff --git a/tests/dailyAverage.test.js b/tests/dailyAverage.test.js new file mode 100644 index 0000000..ed7deb4 --- /dev/null +++ b/tests/dailyAverage.test.js @@ -0,0 +1,49 @@ +import { test } from "uvu"; +import * as assert from "uvu/assert"; + +import moment from "moment-timezone"; +import { dailyAverage } from "../index.js"; + +// Start of Valentine's Day in Greenwich +let start = moment.tz("2023-02-14 00:00:00", "UTC"); +let datetime = []; +let x = []; + +// Precisely 10 days worth of data +let day = 0; +for (var i = 0; i < 240; i++) { + if (i % 24 === 0) day++; + datetime[i] = new Date(start + i * 3600 * 1000); + let val = day * 10 + 5 * Math.sin((i * Math.PI) / 12); + x[i] = Math.round(val * 10) / 10; +} + +test("daily averages work for 'UTC'", () => { + let timezone = "UTC"; + let daily = dailyAverage(datetime, x, timezone); + let day0 = new Date(moment.tz("2023-02-14 00:00:00", "UTC")); + let day1 = new Date(moment.tz("2023-02-15 00:00:00", "UTC")); + assert.equal(daily.datetime.slice(0, 2), [day0, day1]); + assert.equal(daily.average.slice(0, 2), [10, 20]); +}); + +test("daily averages work for 'America/Chicago'", () => { + let timezone = "America/Chicago"; + let daily = dailyAverage(datetime, x, timezone); + let day0 = new Date(moment.tz("2023-02-14 06:00:00", "UTC")); + let day1 = new Date(moment.tz("2023-02-15 06:00:00", "UTC")); + assert.equal(daily.datetime.slice(0, 2), [day0, day1]); + assert.equal(daily.average.slice(0, 2), [12.5, 22.5]); +}); + +// ----- Run all tests --------------------------------------------------------- + +test.run(); + +// ----- Notes ----------------------------------------------------------------- + +// // Manual testing for "America/Chicago" +// x.slice(6,30).reduce((a,o) => a + o) / 24 +// // 12.5 +// x.slice(30,54).reduce((a,o) => a + o) / 24 +// // 22.5 diff --git a/tests/demo.js b/tests/demo.js deleted file mode 100644 index 4432ab4..0000000 --- a/tests/demo.js +++ /dev/null @@ -1,23 +0,0 @@ -// tests/demo.js -import { test } from "uvu"; -import * as assert from "uvu/assert"; - -test("Math.sqrt()", () => { - assert.is(Math.sqrt(4), 2); - assert.is(Math.sqrt(144), 12); - assert.is(Math.sqrt(2), Math.SQRT2); -}); - -test("JSON", () => { - const input = { - foo: "hello", - bar: "world", - }; - - const output = JSON.stringify(input); - - assert.snapshot(output, `{"foo":"hello","bar":"world"}`); - assert.equal(JSON.parse(output), input, "matches original"); -}); - -test.run(); diff --git a/tests/diurnalAverage.test.js b/tests/diurnalAverage.test.js new file mode 100644 index 0000000..1569d6f --- /dev/null +++ b/tests/diurnalAverage.test.js @@ -0,0 +1,59 @@ +import { test } from "uvu"; +import * as assert from "uvu/assert"; + +import moment from "moment-timezone"; +import { diurnalAverage } from "../index.js"; + +// Start of Valentine's Day in Greenwich +let start = moment.tz("2023-02-14 00:00:00", "UTC"); +let datetime = []; +let x = []; + +// Precisely 10 days worth of data +let day = 0; +for (var i = 0; i < 240; i++) { + if (i % 24 === 0) day++; + datetime[i] = new Date(start + i * 3600 * 1000); + let val = 10 + 5 * Math.sin((i * Math.PI) / 12); + x[i] = Math.round(val * 10) / 10; +} + +let hours = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, +]; + +test("diurnal averages work for 'UTC'", () => { + let timezone = "UTC"; + let diurnal = diurnalAverage(datetime, x, timezone); + let averages = [ + 10, 11.3, 12.5, 13.5, 14.3, 14.8, 15, 14.8, 14.3, 13.5, 12.5, 11.3, 10, 8.7, + 7.5, 6.5, 5.7, 5.2, 5, 5.2, 5.7, 6.5, 7.5, 8.7, + ]; + assert.equal(diurnal.hour, hours); + assert.equal(diurnal.averages); +}); + +test("diurnal averages work for 'America/Chicago'", () => { + let timezone = "America/Chicago"; + let diurnal = diurnalAverage(datetime, x, timezone); + let averages = [ + 15, 14.8, 14.3, 13.5, 12.5, 11.3, 10, 8.7, 7.5, 6.5, 5.7, 5.2, 5, 5.2, 5.7, + 6.5, 7.5, 8.7, 10, 11.3, 12.5, 13.5, 14.3, 14.8, + ]; + assert.equal(diurnal.hour, hours); + assert.equal(diurnal.averages); +}); + +// ----- Run all tests --------------------------------------------------------- + +test.run(); + +// ----- Notes ----------------------------------------------------------------- + +// Manual testing for "America/Chicago" + +// x.slice(0,24) +// // (24) [10, 11.3, 12.5, 13.5, 14.3, 14.8, 15, 14.8, 14.3, 13.5, 12.5, 11.3, 10, 8.7, 7.5, 6.5, 5.7, 5.2, 5, 5.2, 5.7, 6.5, 7.5, 8.7] +// x.slice(6,30) +// // (24) [15, 14.8, 14.3, 13.5, 12.5, 11.3, 10, 8.7, 7.5, 6.5, 5.7, 5.2, 5, 5.2, 5.7, 6.5, 7.5, 8.7, 10, 11.3, 12.5, 13.5, 14.3, 14.8] diff --git a/tests/interactive_tests.js b/tests/interactive_tests.js index 4b2d999..c8f628d 100644 --- a/tests/interactive_tests.js +++ b/tests/interactive_tests.js @@ -29,20 +29,4 @@ let z = 1; // ----- PLAY AREA ------------------------------------------------------------- -let start = moment.tz("2023-02-14 00:00:00", "UTC"); -datetime = []; -x = []; - -for (var i = 0; i < 240; i++) { - datetime[i] = new Date(start + i * 3600 * 1000); - let val = 10 + 5 * Math.sin((i * Math.PI) / 12) + Math.random() * 6 - 3; - x[i] = Math.round(val * 10) / 10; -} - -for (var i = 0; i < 240; i++) { - datetime[i] = moment.tz(start + i * 3600 * 1000, "UTC"); - let val = 10 + 5 * Math.sin((i * Math.PI) / 12) + Math.random() * 6 - 3; - x[i] = Math.round(val * 10) / 10; -} - let zzz = 1; diff --git a/tests/nowcast.test.js b/tests/nowcast.test.js new file mode 100644 index 0000000..5f62dce --- /dev/null +++ b/tests/nowcast.test.js @@ -0,0 +1,21 @@ +import { test } from "uvu"; +import * as assert from "uvu/assert"; + +import { pm_nowcast } from "../index.js"; + +test("nowcast works properly", () => { + // Data from: https://forum.airnowtech.org/t/the-nowcast-for-pm2-5-and-pm10/172 + // Answer should be 28.4 ug/m3 or 85 AQI + let pm25 = [34.9, 43, 50, 64.9, 69.2, 66.2, 53.7, 48.6, 49.2, 35, null, 21]; + let nowcast = pm_nowcast(pm25); + assert.is(nowcast.length, pm25.length); + assert.is(nowcast[0], null); + assert.is(nowcast[11], 28.4); +}); + +test("nowcast handles missing values properly", () => { + let pm25 = [34.9, 43, 50, 64.9, 69.2, 66.2, 53.7, 48.6, 49.2, 35, null, null]; + assert.is(pm_nowcast(pm25)[11], null); +}); + +test.run(); diff --git a/tests/trimDate.test.js b/tests/trimDate.test.js index e766128..6af15ca 100644 --- a/tests/trimDate.test.js +++ b/tests/trimDate.test.js @@ -18,6 +18,19 @@ for (var i = 0; i < 240; i++) { x[i] = Math.round(val * 10) / 10; } +test("trimming Date objects works for 'UTC'", () => { + let timezone = "UTC"; + let trimmed = trimDate(datetime, x, timezone); + let tz_start = moment.tz(trimmed.datetime[0], timezone); + let tz_end = moment.tz(trimmed.datetime[239], timezone); + assert.is(trimmed.datetime.length, 240); + assert.is(tz_start.hours(), 0); + assert.is(tz_end.hours(), 23); + // UTC is 8 hours ahead of "America/Los_Angeles" during the winter + assert.is(trimmed.datetime[0].valueOf(), datetime[0].valueOf()); + assert.is(trimmed.x[0], x[0]); +}); + test("trimming Date objects works for 'America/Los_Angeles'", () => { let timezone = "America/Los_Angeles"; let trimmed = trimDate(datetime, x, timezone); @@ -28,6 +41,7 @@ test("trimming Date objects works for 'America/Los_Angeles'", () => { assert.is(tz_end.hours(), 23); // UTC is 8 hours ahead of "America/Los_Angeles" during the winter assert.is(trimmed.datetime[0].valueOf(), datetime[8].valueOf()); + assert.is(trimmed.x[0], x[8]); }); test("trimming Date objects works for 'America/New_York'", () => { @@ -40,6 +54,7 @@ test("trimming Date objects works for 'America/New_York'", () => { assert.is(tz_end.hours(), 23); // UTC is 5 hours ahead of "America/New_York" during the winter assert.is(trimmed.datetime[0].valueOf(), datetime[5].valueOf()); + assert.is(trimmed.x[0], x[5]); }); // ----- Test passing in moment objects ---------------------------------------- @@ -61,6 +76,7 @@ test("trimming moment.tz objects works for 'America/Los_Angeles'", () => { assert.is(tz_end.hours(), 23); // UTC is 8 hours ahead of "America/Los_Angeles" during the winter assert.is(trimmed.datetime[0].valueOf(), datetime[8].valueOf()); + assert.is(trimmed.x[0], x[8]); }); test("trimming Date objects works for 'America/New_York'", () => { @@ -73,6 +89,7 @@ test("trimming Date objects works for 'America/New_York'", () => { assert.is(tz_end.hours(), 23); // UTC is 5 hours ahead of "America/New_York" during the winter assert.is(trimmed.datetime[0].valueOf(), datetime[5].valueOf()); + assert.is(trimmed.x[0], x[5]); }); // ----- Run all tests --------------------------------------------------------- diff --git a/trimDate.js b/trimDate.js index 15e92e1..2c74720 100644 --- a/trimDate.js +++ b/trimDate.js @@ -19,7 +19,7 @@ export function trimDate(datetime, x, timezone) { let start = hours[0] === 0 ? 0 : 24 - hours[0]; let end = hours[hours.length - 1] === 23 - ? hours.length - 1 + ? hours.length : hours.length - hours[hours.length - 1] - 1; let trimmed_datetime = datetime.slice(start, end);