TimeUtils.js

/* global Utils */

/**
 * Created by Blake McBride on 6/18/18.
 */

'use strict';

/**
 * Time utilities.
 *
 * Utilities to deal (mostly) with times stored as a number in the form HHMM since midnight.
 * Also, some handling of Date objects.
 *
 */
class TimeUtils {

    /**
     * Returns the current time in HHMM format.
     *
     * @returns {number} HHMM
     */
    static current() {
        const dt = new Date();
        return dt.getHours() * 100 + dt.getMinutes();
    }

    /**
     * Format time with timezone.
     *
     * @param dt {Date}
     * @returns {string} hh:mm XM XST
     */
    static formatLong(dt) {
        if (typeof dt !== 'object')
            return '';
        const idt = TimeUtils.dateToTime(dt);
        return TimeUtils.format(idt) + ' ' + dt.toLocaleTimeString('en-us',{timeZoneName:'short'}).split(' ')[2];
    }

    /**
     * Format time.
     *
     * @param  {Date|number} val integer HHMM or Date object
     * @param {boolean} zero_fill
     * @returns {string} hh:mm XM
     */
    static format(val, zero_fill=false) {
        if (typeof val === 'string')
            val = Number(val);
        if (val === null || val === undefined || isNaN(val)  ||  val < 0)
            return '';
        if (typeof val === 'object')
            val = DateTimeUtils.dateToIntTime(val);
        const hours = Math.floor(val / 100);
        const minutes = val % 100;
        let width;
        let msk;
        if (zero_fill) {
            width = 2;
            msk = 'Z';
        } else {
            width = 0;
            msk = '';
        }
        if (hours === 0)
            return Utils.format(hours + 12, msk, width, 0) + ':' + Utils.format(minutes, 'Z', 2, 0) + ' AM';
        if (hours >= 13)
            return Utils.format(hours - 12, msk, width, 0) + ':' + Utils.format(minutes, 'Z', 2, 0) + ' PM';
        else
            return Utils.format(hours, msk, width, 0) + ':' + Utils.format(minutes, 'Z', 2, 0) + (hours === 12 ? ' PM' : ' AM');
    }

    /**
     * Convert a string time into an integer time.
     *
     * @param sval {string}  "11:30 AM", "1130", "11:30pm", "11.30", accepts military time too, etc.
     * @returns {null|number}   integer formatted as HHMM or null if not a time
     */
    static strToInt(sval) {
        if (!sval)
            return null;
        sval = sval.trim();
        if (!sval)
            return null;
        const isDigit = function (c) {
            return c >= '0'  &&  c <= '9';
        };
        let buf = '';
        let i = 0;
        let c;

        let ndigits;
        for (ndigits=0 ; ndigits < sval.length && isDigit(sval[ndigits]) ; ndigits++);

        // hours
        let ndh;
        switch (ndigits) {
            case 1:  ndh = 1;  break;
            case 2:  ndh = 2;  break;
            case 3:  ndh = 1;  break;
            default: ndh = 2;  break;
        }
        for (; i < ndh ; i++) {
            c = sval.charAt(i);
            buf += c;
        }
        let hours = buf ? parseInt(buf) : 0;

        // skip : and space
        for (; i < sval.length; i++) {
            c = sval.charAt(i);
            if (isDigit(c)  ||  c === 'a' || c === 'A' || c === 'p' || c === 'P')
                break;
        }

        let minutes = 0;
        if (c !== 'a' && c !== 'A' && c !== 'p' && c !== 'P') {
            if (i < sval.length && isDigit(sval.charAt(i))) {
                buf = '';
                for (let n = 0; i < sval.length; i++, n++) {
                    c = sval.charAt(i);
                    if (!isDigit(c) || n >= 2)
                        break;
                    buf += c;
                }
                minutes = parseInt(buf);
            }

            for (; i < sval.length && sval.charAt(i) === ' '; i++) ;
        }

        let part;
        if (i >= sval.length)
            part = null;
        else {
            c = sval.charAt(i);
            if (c === 'a'  ||  c === 'A')
                part = 'A';
            else if (c === 'p'  ||  c === 'P')
                part = 'P';
            else
                part = null;
        }

        if (!part)
            if (hours >= 6 && hours < 12)
                part = 'A';
            else if (hours < 13)
                part = 'P';

        if (part === 'A'  &&  hours === 12)
            hours -= 12;
        else if (part === 'P' && hours !== 12)
            hours += 12;
        return hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60 ? hours * 100 + minutes : null;
    };

    /**
     * Is argument a valid numeric or string time?
     *
     * Note:  This function should probably not be called if you are going to follow it up with a call to
     * strToInt().  The reason is that this function calls that function.  You would be calling it twice.
     * Just call strToInt() and compare it to null.
     *
     * @param time {number|string} numeric or string time
     * @returns {boolean}
     */
    static isValid(time) {
        if (isNaN(time) || time === null  ||  time === ''  ||  time === undefined)
            return false;
        if (typeof time === 'string')
            return TimeUtils.strToInt(time) !== null;
        if (typeof time !== 'number')
            return false;
        const hours = Math.floor(time / 100);
        const minutes = Math.floor(time - hours * 100);
        if (time !== hours * 100 + minutes)
            return false;
        return hours < 24 && hours >= 0 && minutes < 60 && minutes >= 0;
    }

    /**
     * Returns the hours portion of a time in an integer representation formatted HHMM
     *
     * @param time {number} HHMM
     * @returns {number}
     */
    static hours(time) {
        if (typeof time !== 'number')
            return 0;
        return Math.floor(time / 100);
    }

    /**
     * Returns the minutes portion of a time in an integer representation formatted HHMM
     *
     * @param time {number} HHMM
     * @returns {number}
     */
    static minutes(time) {
        if (typeof time !== 'number')
            return 0;
        return time - Math.floor(time / 100) * 100;
    }

    /**
     * Convert a number of milliseconds since 1970 UTC to an integer time HHMM.
     * This takes into account the local timezone.
     *
     * @param m {number} number of milliseconds since 1970 UTC
     * @returns {number} HHMM
     *
     * @see DateTimeUtils.toMilliseconds()
     * @see DateUtils.millsToInt()
     */
    static millsToInt(m) {
        if (!m)
            return 0;
        const dt = new Date(m);
        return TimeUtils.dateToTime(dt);
    }

    /**
     * Returns the total minutes in a time.
     *
     * @param time {number} HHMM
     * @returns {null|number}
     */
    static totalMinutes(time) {
        if (typeof time !== 'number')
            return null;
        return TimeUtils.hours(time) * 60 + TimeUtils.minutes(time);
    }

    /**
     * Returns the total minutes between two times.
     *
     * @param t1 {number} HHMM
     * @param t2 {number} HHMM
     * @returns {null|number}
     */
    static diff(t1, t2) {
        if (typeof t1 !== 'number' || typeof t2 !== 'number')
            return null;
        return TimeUtils.totalMinutes(t1) - TimeUtils.totalMinutes(t2);
    }

    /**
     * Convert a number of minutes into standard time format (HHMM).
     *
     * @param m {number} number of minutes
     * @returns {number} HHMM
     */
    static minutesToTime(m) {
        if (typeof m !== 'number')
            return null;
        const hours = Math.floor(m / 60);
        const minutes = m - hours * 60;
        return hours * 100 + minutes;
    }

    /**
     * Convert a Date object into a numeric time HHMM
     * dropping the date portion.
     *
     * @param dt {Date}
     * @returns {number} HHMM
     */
    static dateToTime(dt) {
        if (typeof dt !== 'object')
            return 0;
        return dt.getHours() * 100 + dt.getMinutes();
    }

    /**
     * Add hours to a time.
     *
     * @param time  HHMM
     * @param hoursToAdd
     * @returns {number} HHMM
     */
    static addHours(time, hoursToAdd) {
        let hours = Math.floor(time / 100);
        const minutes = time % 100;
        hours = (hours + hoursToAdd) % 24;
        if (hours < 0)
            hours += 24;
        return hours * 100 + minutes;
    }

    /**
     * Add minutes to a time
     *
     * @param time HHMM
     * @param minutesToAdd
     * @returns {number} HHMM
     */
    static addMinutes(time, minutesToAdd) {
        let totalMinutes = (Math.floor(time / 100) * 60) + (time % 100) + minutesToAdd;
        if (totalMinutes < 0)
            totalMinutes += 24 * 60;
        const hours = Math.floor(totalMinutes / 60) % 24;
        const minutes = totalMinutes % 60;
        return hours * 100 + minutes;
    }

}