modules/interval.js

import { OzTime } from '../core/core.js';
import { Duration } from './duration.js';

/**
 * Модуль интервалов времени.
 *
 * @module modules/interval
 */

/**
 * Проверяет, является ли значение экземпляром OzTime.
 *
 * @private
 * @param {*} value - Проверяемое значение.
 * @param {string} name - Имя параметра.
 * @throws {TypeError} Выбрасывается, если значение не является экземпляром OzTime.
 * @returns {void}
 */
function assertOzTime(value, name) {
    if (!(value instanceof OzTime)) {
        throw new TypeError(`${name} must be OzTime`);
    }
}

/**
 * Представляет замкнутый интервал между двумя значениями времени.
 *
 * @class
 * @example
 * import { Interval, fromISO } from '@alexstukovnikov/oz-time';
 *
 * const start = fromISO('2024-05-25T10:00:00Z');
 * const end = fromISO('2024-05-25T12:00:00Z');
 * const range = new Interval(start, end);
 * console.log(range.contains(fromISO('2024-05-25T11:00:00Z'))); // ожидаемый результат: true
 */
export class Interval {
    /**
     * Создаёт новый экземпляр Interval.
     *
     * @param {OzTime} start - Начало интервала.
     * @param {OzTime} end - Конец интервала.
     * @throws {TypeError} Выбрасывается, если start или end не являются экземплярами OzTime.
     * @throws {RangeError} Выбрасывается, если start больше end.
     */
    constructor(start, end) {
        assertOzTime(start, 'start');
        assertOzTime(end, 'end');

        if (start.getTimestamp() > end.getTimestamp()) {
            throw new RangeError('Interval: start must be before or equal to end');
        }

        this._start = start;
        this._end = end;
    }

    /**
     * Возвращает начало интервала.
     *
     * @returns {OzTime} Начальная граница интервала.
     * @example
     * import { Interval, fromISO } from '@alexstukovnikov/oz-time';
     *
     * const range = new Interval(
     *   fromISO('2024-05-25T10:00:00Z'),
     *   fromISO('2024-05-25T12:00:00Z')
     * );
     * console.log(range.getStart().toISOString()); // ожидаемый результат: 2024-05-25T10:00:00.000Z
     */
    getStart() {
        return this._start;
    }

    /**
     * Возвращает конец интервала.
     *
     * @returns {OzTime} Конечная граница интервала.
     * @example
     * import { Interval, fromISO } from '@alexstukovnikov/oz-time';
     *
     * const range = new Interval(
     *   fromISO('2024-05-25T10:00:00Z'),
     *   fromISO('2024-05-25T12:00:00Z')
     * );
     * console.log(range.getEnd().toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
     */
    getEnd() {
        return this._end;
    }

    /**
     * Проверяет, содержит ли интервал переданное значение времени.
     *
     * @param {OzTime} moment - Проверяемое значение.
     * @throws {TypeError} Выбрасывается, если moment не является экземпляром OzTime.
     * @returns {boolean} `true`, если значение входит в интервал.
     * @example
     * import { Interval, fromISO } from '@alexstukovnikov/oz-time';
     *
     * const range = new Interval(
     *   fromISO('2024-05-25T10:00:00Z'),
     *   fromISO('2024-05-25T12:00:00Z')
     * );
     * console.log(range.contains(fromISO('2024-05-25T11:00:00Z'))); // ожидаемый результат: true
     */
    contains(moment) {
        assertOzTime(moment, 'moment');

        const ts = moment.getTimestamp();
        return ts >= this._start.getTimestamp() && ts <= this._end.getTimestamp();
    }

    /**
     * Проверяет, пересекается ли текущий интервал с другим интервалом.
     *
     * @param {Interval} other - Второй интервал.
     * @throws {TypeError} Выбрасывается, если other не является экземпляром Interval.
     * @returns {boolean} `true`, если интервалы пересекаются.
     * @example
     * import { Interval, fromISO } from '@alexstukovnikov/oz-time';
     *
     * const a = new Interval(
     *   fromISO('2024-05-25T10:00:00Z'),
     *   fromISO('2024-05-25T12:00:00Z')
     * );
     * const b = new Interval(
     *   fromISO('2024-05-25T11:00:00Z'),
     *   fromISO('2024-05-25T13:00:00Z')
     * );
     * console.log(a.overlaps(b)); // ожидаемый результат: true
     */
    overlaps(other) {
        if (!(other instanceof Interval)) {
            throw new TypeError('other must be Interval');
        }

        const startA = this._start.getTimestamp();
        const endA = this._end.getTimestamp();
        const startB = other.getStart().getTimestamp();
        const endB = other.getEnd().getTimestamp();

        return startA <= endB && startB <= endA;
    }

    /**
     * Возвращает длительность интервала.
     *
     * @returns {Duration} Длительность интервала в виде экземпляра {@link Duration}.
     * @example
     * import { Interval, fromISO } from '@alexstukovnikov/oz-time';
     *
     * const range = new Interval(
     *   fromISO('2024-05-25T10:00:00Z'),
     *   fromISO('2024-05-25T12:00:00Z')
     * );
     *
     * console.log(range.duration().asHours()); // ожидаемый результат: 2
     */
    duration() {
        const diffMs = this._end.getTimestamp() - this._start.getTimestamp();
        return new Duration(diffMs);
    }
}

/**
 * Создаёт и возвращает экземпляр {@link Interval}.
 *
 * @param {OzTime} start - Начало интервала.
 * @param {OzTime} end - Конец интервала.
 * @returns {Interval} Новый экземпляр Interval.
 * @example
 * import { interval, fromISO } from '@alexstukovnikov/oz-time';
 *
 * const range = interval(
 *   fromISO('2024-05-25T10:00:00Z'),
 *   fromISO('2024-05-25T12:00:00Z')
 * );
 * console.log(range.duration('hour')); // ожидаемый результат: 2
 */
export function interval(start, end) {
    return new Interval(start, end);
}