"use strict"; /** * Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min) * @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15min", "60min" * @returns {Array} An array of two 24-hour strings representing the start time and end time */ const createTimeStringsForInterval = function (phenomenonSamplingRate) { const fifteenMinutes = "15min"; const sixtyMinutes = "60min"; const startTime = "00:00:00"; const endTimeFifteenMinutes = "23:45:00"; const endTimeSixtyMinutes = "23:00:00"; if ( phenomenonSamplingRate !== fifteenMinutes && phenomenonSamplingRate !== sixtyMinutes ) { throw new Error( `Check that the provided phenomenon sampling rate string is in this format: "15min" or "60min"` ); } // 15 min sampling rate else if (phenomenonSamplingRate === fifteenMinutes) { return [startTime, endTimeFifteenMinutes]; } // 60 min sampling rate else if (phenomenonSamplingRate === sixtyMinutes) { return [startTime, endTimeSixtyMinutes]; } }; /** * Create an ISO 8601 date and time string * @param {String} inputCalendarDate Calendar date string in "YYYY-MM-DD" format * @param {String} inputTwentyFourHourTime 24-hour time string in "hh:mm:ss" format * @returns {String} An ISO 8601 date and time string */ const createIso8601DateTimeString = function ( inputCalendarDate, inputTwentyFourHourTime ) { return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`; }; /** * Check whether a year is a leap year or not * @param {Number} year Integer representing the year * @returns {Boolean} true if leap year, false if not */ const checkIfLeapYear = function (year) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; }; /** * Calculate the index of a timestamp in an array of timestamps * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps */ const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) { return inputTimestampArr.findIndex( (timestamp) => timestamp === timestampOfInterest ); }; /** * Calculate the index of a start timestamp in an array of timestamps * * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps */ const getIndexOfStartTimestamp = function ( inputTimestampArr, timestampOfInterest ) { const timestampStartIndex = getIndexOfTimestamp( inputTimestampArr, timestampOfInterest ); // If the timestamp does not exist in the timestamp array if (timestampStartIndex === -1) { throw new Error( "A start timestamp could not be found in the timestamp array" ); } // If the timestamp exists in the timestamp array else { return timestampStartIndex; } }; /** * Calculate the index of an end timestamp in an array of timestamps * * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps */ const getIndexOfEndTimestamp = function ( inputTimestampArr, timestampOfInterest ) { const timestampEndIndex = getIndexOfTimestamp( inputTimestampArr, timestampOfInterest ); // If the timestamp does not exist in the timestamp array if (timestampEndIndex === -1) { throw new Error( "An end timestamp could not be found in the timestamp array" ); } // If the timestamp exists in the timestamp array else { return timestampEndIndex; } }; /** * Calculate the indexes of the start and end timestamps * @param {Array} obsTimestampArr An array of observations timestamps * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" * @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date * @returns {Array} A 1*2 array tht contains integers representing the start index and end index respectively */ const calculateIndexStartEndTimestamp = function ( obsTimestampArr, samplingRate, startDate, endDate ) { // Create and extract 24-hour strings for the start and end of interval const [startTimeString, endTimeString] = createTimeStringsForInterval(samplingRate); // Create ISO 8601 strings for the start and end of interval const startIso8601DateTimeString = createIso8601DateTimeString( startDate, startTimeString ); const endIso8601DateTimeString = createIso8601DateTimeString( endDate, endTimeString ); // Calculate the indexes of the timestamps for the start and end of interval const indexStartTimestamp = getIndexOfStartTimestamp( obsTimestampArr, startIso8601DateTimeString ); const indexEndTimestamp = getIndexOfEndTimestamp( obsTimestampArr, endIso8601DateTimeString ); return [indexStartTimestamp, indexEndTimestamp]; }; /** * Extract the set of observations that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date. * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" * @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date * @returns {Array} An array of observations (timestamp + value) that fall within our time interval */ const extractObservationsWithinDatesInterval = function ( obsArray, samplingRate, startDate, endDate ) { // Extract the timestamps from the observations const obsTimestampArr = obsArray.map((obs) => obs[0]); // Calculate the indexes of the timestamps for the start and end of interval const [indexStartTimestamp, indexEndTimestamp] = calculateIndexStartEndTimestamp( obsTimestampArr, samplingRate, startDate, endDate ); // Extract the observations that fall within our time interval return obsArray.slice(indexStartTimestamp, indexEndTimestamp + 1); }; /** * Extract the set of observation values that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date. * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" * @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date * @returns {Array} An array of observation values that fall within our time interval */ const extractObservationValuesWithinDatesInterval = function ( obsArray, samplingRate, startDate, endDate ) { // Extract the timestamps and values from the observations const obsTimestampArr = obsArray.map((obs) => obs[0]); const obsValuesArr = obsArray.map((obs) => obs[1]); // Calculate the indexes of the timestamps for the start and end of interval const [indexStartTimestamp, indexEndTimestamp] = calculateIndexStartEndTimestamp( obsTimestampArr, samplingRate, startDate, endDate ); // Extract the observation values that fall within our time interval return obsValuesArr.slice(indexStartTimestamp, indexEndTimestamp + 1); }; /** * Extract the year and month digits from a calendar month string * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format * @returns {Array} A 1*2 array tht contains integers representing the year and month respectively */ const extractMonthYearDigitsFromCalendarMonthString = function ( calendarMonthStr ) { // Extract year as integer const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10); // Extract month as integer const monthNum = parseInt(calendarMonthStr.slice(-2), 10); return [yearNum, monthNum]; }; /** * Extract the set of observation values that fall within a time interval delimited by the first day and last day of a calendar month * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format * @returns {Array} An array of observation values that fall within one calendar month */ const extractObservationValuesWithinMonthInterval = function ( obsArray, samplingRate, calendarMonthStr ) { try { // Extract the year and month digits from the calendar month string const [yearNum, monthNum] = extractMonthYearDigitsFromCalendarMonthString(calendarMonthStr); // All the months start on the first const startDateStr = `${calendarMonthStr}-01`; if (monthNum < 1 || monthNum > 12) { throw new Error( "The specified digit for the month of the year is invalid" ); } // February else if (monthNum === 2) { // Leap year if (checkIfLeapYear(yearNum)) { return extractObservationValuesWithinDatesInterval( obsArray, samplingRate, startDateStr, `${calendarMonthStr}-29` ); } // Non-leap year else { return extractObservationValuesWithinDatesInterval( obsArray, samplingRate, startDateStr, `${calendarMonthStr}-28` ); } } // Months with 30 days else if ( monthNum === 4 || monthNum === 6 || monthNum === 9 || monthNum === 11 ) { return extractObservationValuesWithinDatesInterval( obsArray, samplingRate, startDateStr, `${calendarMonthStr}-30` ); } // Months with 31 days else { return extractObservationValuesWithinDatesInterval( obsArray, samplingRate, startDateStr, `${calendarMonthStr}-31` ); } } catch (err) { // Rethrow errors from `getIndexOfStartTimestamp` or `getIndexOfEndTimestamp` functions throw new Error( `${err.message} \nCurrently, the monthly aggregation does not support partial calendar months` ); } }; /** * Extract unique calendar dates from date/time strings in ISO 8601 format * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format */ const extractUniqueCalendarDatesFromTimestamp = function (obsArray) { // The timestamp is the first element of the observation array const timestampArr = obsArray.map((obs) => obs[0]); // Extract the calendar date string from the timestamp string const calendarDateArr = timestampArr.map((timestamp) => timestamp.slice(0, 10) ); // Use a set to remove duplicates const uniqueCalendarDates = new Set(calendarDateArr); return [...uniqueCalendarDates]; }; /** * Extract unique calendar months from calendar date strings * @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format * @returns {Array} An array of unique calendar month strings in "YYYY-MM" format */ const extractUniqueCalendarMonthsFromCalendarDates = function ( calendarDatesArr ) { // Extract the calendar month strings const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7)); // Use a set to remove duplicates const uniqueCalendarMonths = new Set(calendarMonthsArr); return [...uniqueCalendarMonths]; }; /** * Format a computed aggregation result to make it suitable for a chart. Currently, only line and column charts are supported * @param {Array} calendarDatesMonthsStrArr An array of unique calendar dates strings (in "YYYY-MM-DD" fromat) or unique calendar months strings (in "YYYY-MM" format) * @param {Array} aggregatedValuesArr An array of aggregated values * @returns {Array} An array of formatted aggregation values suitable for use in a column chart */ const formatAggregationResultForChart = function ( calendarDatesMonthsStrArr, aggregatedValuesArr ) { if (!calendarDatesMonthsStrArr || !aggregatedValuesArr) return; // Create an array of Unix timestamp strings const timestampsArr = calendarDatesMonthsStrArr.map((calendarStr) => new Date(calendarStr).getTime() ); // Combine timestamp and value pairs // The timestamps array and values array have same lengths, use one for looping if (timestampsArr.length !== aggregatedValuesArr.length) { throw new Error( "The timestamps array and aggregated values array have different lengths" ); } else { return timestampsArr.map((timestamp, i) => [ timestamp, aggregatedValuesArr[i], ]); } }; export { extractObservationsWithinDatesInterval, extractObservationValuesWithinDatesInterval, extractObservationValuesWithinMonthInterval, extractUniqueCalendarDatesFromTimestamp, extractUniqueCalendarMonthsFromCalendarDates, formatAggregationResultForChart, };