items/time-series.js

const time = require('../time');
const { _isInstant } = require('../helpers');

/**
 * @typedef { import('@js-joda/core').Instant} time.Instant
 * @private
 */
/**
 * @typedef { import('@js-joda/core').ZonedDateTime} time.ZonedDateTime
 * @private
 */
/**
 * @typedef {import('../quantity').Quantity} Quantity
 * @private
 */

/**
 * A TimeSeries is used to transport a set of states together with their timestamp.
 * It is usually used for persisting historic state or forecasts in a persistence service.
 *
 * @memberof items
 */
class TimeSeries {
  #states = [];
  #policy;

  /**
   * Creates a new TimeSeries.
   *
   * The TimeSeries policy defines how the TimeSeries is persisted in a persistence service:
   * <code>ADD</code> adds the content to the persistence,
   * <code>REPLACE</code> first removes all persisted elements in the timespan given by {@link begin} and {@link end}.
   *
   * @param {string} policy TimeSeries policy <code>ADD</code> or <code>REPLACE</code>
   */
  constructor (policy) {
    if (!['ADD', 'REPLACE'].includes(policy)) throw new TypeError(`Invalid TimeSeries policy: ${policy}. Expected 'ADD' or 'REPLACE'.`);
    this.#policy = policy;
  }

  /**
   * The persistence policy of this TimeSeries
   *
   * @type {string}
   */
  get policy () {
    return this.#policy;
  }

  /**
   * Timestamp of the first element in the TimeSeries
   * @type {time.Instant}
   */
  get begin () {
    return this.#states.reduce((min, [timestamp, state]) => min.isBefore(timestamp) ? min : timestamp, time.Instant.MAX);
  }

  /**
   * Timestamp of the last element in the TimeSeries
   * @type {time.Instant}
   */
  get end () {
    return this.#states.reduce((max, [timestamp, state]) => max.isAfter(timestamp) ? max : timestamp, time.Instant.MIN);
  }

  /**
   * Number of elements in the TimeSeries
   * @type {number}
   */
  get size () {
    return this.#states.length;
  }

  /**
   * States of the TimeSeries together with their timestamp and sorted by their timestamps
   *
   * Be aware that this method returns a reference to the internal state array, so changes to the array will affect the TimeSeries.
   *
   * @type {Array}
   */
  get states () {
    return this.#states.sort(([timestampA, stateA], [timestampB, stateB]) => timestampA.isBefore(timestampB) ? -1 : 1);
  }

  /**
   * Add a new element to the TimeSeries.
   *
   * Elements can be added in an arbitrary order and are sorted chronologically.
   *
   * @param {(time.Instant|time.ZonedDateTime|string|Date)} timestamp a timestamp for the given state
   * @param {string|number|Quantity|HostState} state the state at the given timestamp
   * @returns {TimeSeries} this TimeSeries instance
   */
  add (timestamp, state) {
    const ts = _isInstant(timestamp) ? timestamp : time.toZDT(timestamp).toInstant();
    this.#states.push([ts, state]);
    return this;
  }

  toString () {
    let states = '';
    for (const [timestamp, state] of this.states) {
      states += `[${timestamp} -> ${state}], `;
    }
    states = states.substring(0, states.length - 2);
    return `TimeSeries (Begin=${this.begin}, End=${this.end}, Size=${this.size}, States=${states})`;
  }
}

module.exports = TimeSeries;