aggregateHelpers.mjs 12.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"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
19
  ) {
20
21
22
    throw new Error(
      `Check that the provided phenomenon sampling rate string is in this format: "15min" or "60min"`
    );
23
  }
24
  // 15 min sampling rate
25
  else if (phenomenonSamplingRate === fifteenMinutes) {
26
27
28
    return [startTime, endTimeFifteenMinutes];
  }
  // 60 min sampling rate
29
  else if (phenomenonSamplingRate === sixtyMinutes) {
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    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) {
63
  return inputTimestampArr.findIndex(
64
65
    (timestamp) => timestamp === timestampOfInterest
  );
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
};

/**
 * 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
  );
83
84

  // If the timestamp does not exist in the timestamp array
85
  if (timestampStartIndex === -1) {
86
    throw new Error(
87
      "A start timestamp could not be found in the timestamp array"
88
    );
89
  }
90
  // If the timestamp exists in the timestamp array
91
  else {
92
    return timestampStartIndex;
93
  }
94
95
};

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**
 * 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;
  }
};

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
 * 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
153
  const indexStartTimestamp = getIndexOfStartTimestamp(
154
155
156
    obsTimestampArr,
    startIso8601DateTimeString
  );
157
  const indexEndTimestamp = getIndexOfEndTimestamp(
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
    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
) {
  // 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`;

262
263
264
  if (monthNum < 1 || monthNum > 12) {
    throw new Error("The specified digit for the month of the year is invalid");
  }
265
  // February
266
  else if (monthNum === 2) {
267
    // Leap year
268
    if (checkIfLeapYear(yearNum)) {
269
270
271
272
273
274
      return extractObservationValuesWithinDatesInterval(
        obsArray,
        samplingRate,
        startDateStr,
        `${calendarMonthStr}-29`
      );
275
    }
276
    // Non-leap year
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
    else {
      return extractObservationValuesWithinDatesInterval(
        obsArray,
        samplingRate,
        startDateStr,
        `${calendarMonthStr}-28`
      );
    }
  }
  // Months with 30 days
  else if (
    monthNum === 4 ||
    monthNum === 6 ||
    monthNum === 9 ||
    monthNum === 11
  ) {
293
294
295
296
    return extractObservationValuesWithinDatesInterval(
      obsArray,
      samplingRate,
      startDateStr,
297
      `${calendarMonthStr}-30`
298
299
    );
  }
300
301
  // Months with 31 days
  else {
302
303
304
305
    return extractObservationValuesWithinDatesInterval(
      obsArray,
      samplingRate,
      startDateStr,
306
      `${calendarMonthStr}-31`
307
    );
308
  }
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
};

/**
 * 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];
};

export {
  extractObservationsWithinDatesInterval,
  extractObservationValuesWithinDatesInterval,
  extractObservationValuesWithinMonthInterval,
  extractUniqueCalendarDatesFromTimestamp,
  extractUniqueCalendarMonthsFromCalendarDates,
};