Home Identifier Source

src/range.js

import _ from "underscore";
import Immutable from "immutable";
import moment from "moment";

export default class TimeRange {

    /**
     * Builds a new TimeRange which may be of several different formats:
     *   - Another TimeRange (copy constructor)
     *   - An Immutable.List containing two Dates.
     *   - A Javascript array containing two Date or ms timestamps
     *   - Two arguments, begin and end, each of which may be a Data,
     *     a Moment, or a ms timestamp.
     */
    constructor(arg1, arg2) {
        if (arg1 instanceof TimeRange) {
            const other = arg1;
            this._range = other._range;
        } else if (arg1 instanceof Immutable.List) {
            const rangeList = arg1;
            this._range = rangeList;
        } else if (_.isArray(arg1)) {
            const rangeArray = arg1;
            this._range = new Immutable.List([new Date(rangeArray[0]),
                                              new Date(rangeArray[1])]);
        } else {
            const b = arg1;
            const e = arg2;
            if (_.isDate(b) && _.isDate(e)) {
                this._range = new Immutable.List([new Date(b.getTime()),
                                                  new Date(e.getTime())]);
            } else if (moment.isMoment(b) && moment.isMoment(e)) {
                this._range = new Immutable.List([new Date(b.valueOf()),
                                                  new Date(e.valueOf())]);
            } else if (_.isNumber(b) && _.isNumber(e)) {
                this._range = new Immutable.List([new Date(b), new Date(e)]);
            }
        }
    }

    /**
     * Returns the internal range, which is an Immutable List containing
     * begin and end keys
     */
    range() {
        return this._range;
    }

    //
    // Serialize
    //

    /**
     * Returns the TimeRange as JSON, which will be a Javascript array
     * of two ms timestamps.
     * @return {number[]} JSON representation of the TimeRange
     */
    toJSON() {
        return [this.begin().getTime(), this.end().getTime()];
    }

    /**
     * Returns the TimeRange as a string, useful for serialization.
     * @return {string} String representation of the TimeRange
     */
    toString() {
        return JSON.stringify(this.toJSON());
    }

    /**
     * Returns the TimeRange as a string expressed in local time
     * @return {string} String representation of the TimeRange
     */
    toLocalString() {
        return `[${this.begin()}, ${this.end()}]`;
    }

    /**
     * Returns the TimeRange as a string expressed in UTC time
     * @return {string} String representation of the TimeRange
     */
    toUTCString() {
        return `[${this.begin().toUTCString()}, ${this.end().toUTCString()}]`;
    }

    /**
     * Returns a human friendly version of the TimeRange, e.g.
     * "Aug 1, 2014 05:19:59 am to Aug 1, 2014 07:41:06 am"
     * @return {string} Human friendly string representation of the TimeRange
     */
    humanize() {
        const begin = moment(this.begin());
        const end = moment(this.end());
        const beginStr = begin.format("MMM D, YYYY hh:mm:ss a");
        const endStr = end.format("MMM D, YYYY hh:mm:ss a");

        return `${beginStr} to ${endStr}`;
    }

    /**
     * Returns a human friendly version of the TimeRange, e.g.
     * e.g. "a few seconds ago to a month ago"
     * @return {string} Human friendly string representation of the TimeRange
     */
    relativeString() {
        const begin = moment(this.begin());
        const end = moment(this.end());
        return `${begin.fromNow()} to ${end.fromNow()}`;
    }

    /**
     * Returns the begin time of the TimeRange.
     * @return {Date} Begin time
     */
    begin() {
        return this._range.get(0);
    }

    /**
     * Returns the end time of the TimeRange.
     * @return {Date} End time
     */
    end() {
        return this._range.get(1);
    }

    /**
     * Sets a new begin time on the TimeRange. The result will be
     * a new TimeRange.
     */
    setBegin(t) {
        return new TimeRange(this._range.set(0, t));
    }

    /**
     * Sets a new end time on the TimeRange. The result will be a new TimeRange.
     */
    setEnd(t) {
        return new TimeRange(this._range.set(1, t));
    }

    /**
     * Returns if the two TimeRanges can be considered equal,
     * in that they have the same times.
     */
    equals(other) {
        return this.begin().getTime() === other.begin().getTime() &&
               this.end().getTime() === other.end().getTime();
    }

    /**
     * Returns true if other is completely inside this.
     */
    contains(other) {
        if (_.isDate(other)) {
            return this.begin() <= other && this.end() >= other;
        } else {
            return this.begin() <= other.begin() &&
                   this.end() >= other.end();
        }
        return false;
    }

    /**
     * Returns true if this TimeRange is completely within the supplied
     * other TimeRange.
     */
    within(other) {
        return this.begin() >= other.begin() &&
               this.end() <= other.end();
    }

    /**
     * Returns true if the passed in other TimeRange overlaps this time Range.
     */
    overlaps(other) {
        if (this.contains(other.begin()) && !this.contains(other.end()) ||
            this.contains(other.end()) && !this.contains(other.begin())) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if the passed in other Range in no way
     * overlaps this time Range.
     */
    disjoint(other) {
        return (this.end() < other.begin() || this.begin() > other.end());
    }

    /**
    * Returns a new Timerange which covers the extents of this and
    * other combined.
    */
    extents(other) {
        const b = this.begin() < other.begin() ? this.begin() : other.begin();
        const e = this.end() > other.end() ? this.end() : other.end();
        return new TimeRange(new Date(b.getTime()), new Date(e.getTime()));
    }

    /**
    * Returns a new TimeRange which represents the intersection
    * (overlapping) part of this and other.
    */
    intersection(other) {
        if (this.disjoint(other)) {
            return undefined;
        }
        const b = this.begin() > other.begin() ? this.begin() : other.begin();
        const e = this.end() < other.end() ? this.end() : other.end();
        return new TimeRange(new Date(b.getTime()),
                             new Date(e.getTime()));
    }

    duration() {
        return this.end().getTime() - this.begin().getTime();
    }

    humanizeDuration() {
        return moment.duration(this.duration()).humanize();
    }

    //
    // Static TimeRange creators
    //

    static lastDay() {
        const beginTime = moment();
        const endTime = beginTime.clone().subtract(24, "hours");
        return new TimeRange(beginTime, endTime);
    }

    static lastSevenDays() {
        const beginTime = moment();
        const endTime = beginTime.clone().subtract(7, "days");
        return new TimeRange(beginTime, endTime);
    }

    static lastThirtyDays() {
        const beginTime = moment();
        const endTime = beginTime.clone().subtract(30, "days");
        return new TimeRange(beginTime, endTime);
    }

    static lastNinetyDays() {
        const beginTime = moment();
        const endTime = beginTime.clone().subtract(90, "days");
        return new TimeRange(beginTime, endTime);
    }
}