Source: utils/date-time-formatter.js

/**
 * Creates a JS native date object from input
 *
 * @param {string | number | Date} date Input using which date object to be created
 * @return {Date} : JS native date object
 */
function convertToNativeDate (date) {
    if (date instanceof Date) {
        return date;
    }

    return new Date(date);
}
/**
 * Apply padding before a number if its less than 1o. This is used when constant digit's number to be returned
 * between 0 - 99
 *
 * @param {number} n Input to be padded
 * @return {string} Padded number
 */
function pad (n) {
    return (n < 10) ? (`0${n}`) : n;
}
/*
 * DateFormatter utility to convert any date format to any other date format
 * DateFormatter parse a date time stamp specified by a user abiding by rules which are defined
 * by user in terms of token. It creates JS native date object from the user specified format.
 * That native date can also be displayed
 * in any specified format.
 * This utility class only takes care of format conversion only
 */

/*
 * Escapes all the special character that are used in regular expression.
 * Like
 * RegExp.escape('sgfd-$') // Output: sgfd\-\$
 *
 * @param text {String} : text which is to be escaped
 */
RegExp.escape = function (text) {
    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
};

/**
 * DateTimeFormatter class to convert any user format of date time stamp to any other format
 * of date time stamp.
 *
 * @param {string} format Format of the date given. For the above date,
 * 'year: %Y, month: %b, day: %d'.
 * @class
 */
/* istanbul ignore next */ function DateTimeFormatter (format) {
    this.format = format;
    this.dtParams = undefined;
    this.nativeDate = undefined;
}

// The identifier of the tokens
DateTimeFormatter.TOKEN_PREFIX = '%';

// JS native Date constructor takes the date params (year, month, etc) in a certail sequence.
// This defines the sequence of the date parameters in the constructor.
DateTimeFormatter.DATETIME_PARAM_SEQUENCE = {
    YEAR: 0,
    MONTH: 1,
    DAY: 2,
    HOUR: 3,
    MINUTE: 4,
    SECOND: 5,
    MILLISECOND: 6
};

/*
 * This is a default number parsing utility. It tries to parse a number in integer, if parsing is unsuccessful, it
 * gives back a default value.
 *
 * @param: defVal {Number} : Default no if the parsing to integer is not successful
 * @return {Function} : An closure function which is to be called by passing an the value which needs to be parsed.
 */
DateTimeFormatter.defaultNumberParser = function (defVal) {
    return function (val) {
        let parsedVal;
        if (isFinite(parsedVal = parseInt(val, 10))) {
            return parsedVal;
        }

        return defVal;
    };
};

/*
 * This is a default number range utility. It tries to find an element in the range. If not found it returns a
 * default no as an index.
 *
 * @param: range {Array} : The list which is to be serached
 * @param: defVal {Number} : Default no if the serach and find does not return anything
 * @return {Function} : An closure function which is to be called by passing an the value which needs to be found
 */
DateTimeFormatter.defaultRangeParser = function (range, defVal) {
    return (val) => {
        let i;
        let l;

        if (!val) { return defVal; }

        const nVal = val.toLowerCase();

        for (i = 0, l = range.length; i < l; i++) {
            if (range[i].toLowerCase() === nVal) {
                return i;
            }
        }

        if (i === undefined) {
            return defVal;
        }
        return null;
    };
};

/*
 * Defines the tokens which are supporter by the dateformatter. Using this definitation a value gets extracted from
 * the user specifed date string. This also formats the value for display purpose from native JS date.
 * The definition of each token contains the following named properties
 * {
 *     %token_name% : {
 *         name: name of the token, this is used in reverse lookup,
 *         extract: a function that returns the regular expression to extract that piece of information. All the
 *                  regex should be gouped by using ()
 *         parser: a function which receives value extracted by the above regex and parse it to get the date params
 *         formatter: a formatter function that takes milliseconds or JS Date object and format the param
 *                  represented by the token only.
 *     }
 * }
 *
 * @return {Object} : Definition of the all the supported tokens.
 */
DateTimeFormatter.getTokenDefinitions = function () {
    const daysDef = {
        short: [
            'Sun',
            'Mon',
            'Tue',
            'Wed',
            'Thu',
            'Fri',
            'Sat'
        ],
        long: [
            'Sunday',
            'Monday',
            'Tuesday',
            'Wednesday',
            'Thursday',
            'Friday',
            'Saturday'
        ]
    };
    const monthsDef = {
        short: [
            'Jan',
            'Feb',
            'Mar',
            'Apr',
            'May',
            'Jun',
            'Jul',
            'Aug',
            'Sep',
            'Oct',
            'Nov',
            'Dec'
        ],
        long: [
            'January',
            'February',
            'March',
            'April',
            'May',
            'June',
            'July',
            'August',
            'September',
            'October',
            'November',
            'December'
        ]
    };

    const definitions = {
        H: {
            // 24 hours format
            name: 'H',
            index: 3,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);

                return d.getHours().toString();
            }
        },
        l: {
            // 12 hours format
            name: 'l',
            index: 3,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const hours = d.getHours() % 12;

                return (hours === 0 ? 12 : hours).toString();
            }
        },
        p: {
            // AM or PM
            name: 'p',
            index: 3,
            extract () { return '(AM|PM)'; },
            parser: (val) => {
                if (val) {
                    return val.toLowerCase();
                }
                return null;
            },
            formatter: (val) => {
                const d = convertToNativeDate(val);
                const hours = d.getHours();

                return (hours < 12 ? 'AM' : 'PM');
            }
        },
        P: {
            // am or pm
            name: 'P',
            index: 3,
            extract () { return '(am|pm)'; },
            parser: (val) => {
                if (val) {
                    return val.toLowerCase();
                }
                return null;
            },
            formatter: (val) => {
                const d = convertToNativeDate(val);
                const hours = d.getHours();

                return (hours < 12 ? 'am' : 'pm');
            }
        },
        M: {
            // Two digit minutes 00 - 59
            name: 'M',
            index: 4,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const mins = d.getMinutes();

                return pad(mins);
            }
        },
        S: {
            // Two digit seconds 00 - 59
            name: 'S',
            index: 5,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const seconds = d.getSeconds();

                return pad(seconds);
            }
        },
        K: {
            // Milliseconds
            name: 'K',
            index: 6,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const ms = d.getMilliseconds();

                return ms.toString();
            }
        },
        a: {
            // Short name of day, like Mon
            name: 'a',
            index: 2,
            extract () { return `(${daysDef.short.join('|')})`; },
            parser: DateTimeFormatter.defaultRangeParser(daysDef.short),
            formatter (val) {
                const d = convertToNativeDate(val);
                const day = d.getDay();

                return (daysDef.short[day]).toString();
            }
        },
        A: {
            // Long name of day, like Monday
            name: 'A',
            index: 2,
            extract () { return `(${daysDef.long.join('|')})`; },
            parser: DateTimeFormatter.defaultRangeParser(daysDef.long),
            formatter (val) {
                const d = convertToNativeDate(val);
                const day = d.getDay();

                return (daysDef.long[day]).toString();
            }
        },
        e: {
            // 8 of March, 11 of November
            name: 'e',
            index: 2,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const day = d.getDate();

                return day.toString();
            }
        },
        d: {
            // 08 of March, 11 of November
            name: 'd',
            index: 2,
            extract () { return '(\\d+)'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const day = d.getDate();

                return pad(day);
            }
        },
        b: {
            // Short month, like Jan
            name: 'b',
            index: 1,
            extract () { return `(${monthsDef.short.join('|')})`; },
            parser: DateTimeFormatter.defaultRangeParser(monthsDef.short),
            formatter (val) {
                const d = convertToNativeDate(val);
                const month = d.getMonth();

                return (monthsDef.short[month]).toString();
            }
        },
        B: {
            // Long month, like January
            name: 'B',
            index: 1,
            extract () { return `(${monthsDef.long.join('|')})`; },
            parser: DateTimeFormatter.defaultNumberParser(monthsDef.long),
            formatter (val) {
                const d = convertToNativeDate(val);
                const month = d.getMonth();

                return (monthsDef.long[month]).toString();
            }
        },
        m: {
            // Two digit month of year like 01 for January
            name: 'm',
            index: 1,
            extract () { return '(\\d+)'; },
            parser (val) { return DateTimeFormatter.defaultNumberParser()(val) - 1; },
            formatter (val) {
                const d = convertToNativeDate(val);
                const month = d.getMonth();

                return pad(month + 1);
            }
        },
        y: {
            // Short year like 90 for 1990
            name: 'y',
            index: 0,
            extract () { return '(\\d{4})'; },
            parser (val) {
                if (val) {
                    const l = val.length;
                    val = val.substring(l - 2, l);
                }

                return DateTimeFormatter.defaultNumberParser()(val);
            },
            formatter (val) {
                const d = convertToNativeDate(val);
                let year = d.getFullYear().toString();
                let l;

                if (year) {
                    l = year.length;
                    year = year.substring(l - 2, l);
                }

                return year;
            }
        },
        Y: {
            // Long year like 1990
            name: 'Y',
            index: 0,
            extract () { return '(\\d{4})'; },
            parser: DateTimeFormatter.defaultNumberParser(),
            formatter (val) {
                const d = convertToNativeDate(val);
                const year = d.getFullYear().toString();

                return year;
            }
        }
    };

    return definitions;
};

/*
 * The tokens which works internally is not user friendly in terms of memorizing the names. This gives a formal
 * definition to the informal notations.
 *
 * @return {Object} : Formal definition of the tokens
 */
DateTimeFormatter.getTokenFormalNames = function () {
    const definitions = DateTimeFormatter.getTokenDefinitions();

    return {
        HOUR: definitions.H,
        HOUR_12: definitions.l,
        AMPM_UPPERCASE: definitions.p,
        AMPM_LOWERCASE: definitions.P,
        MINUTE: definitions.M,
        SECOND: definitions.S,
        SHORT_DAY: definitions.a,
        LONG_DAY: definitions.A,
        DAY_OF_MONTH: definitions.e,
        DAY_OF_MONTH_CONSTANT_WIDTH: definitions.d,
        SHORT_MONTH: definitions.b,
        LONG_MONTH: definitions.B,
        MONTH_OF_YEAR: definitions.m,
        SHORT_YEAR: definitions.y,
        LONG_YEAR: definitions.Y
    };
};

/*
 * This defines the rules and declares dependencies that resolves a date parameter (year, month etc) from
 * the date time parameter array.
 *
 * @return {Object} : An object that contains dependencies and a resolver function. The dependencies values are fed
 *                  to the resolver function in that particular sequence only.
 */
DateTimeFormatter.tokenResolver = function () {
    const definitions = DateTimeFormatter.getTokenDefinitions();
    const defaultResolver = (...args) => { // eslint-disable-line require-jsdoc
        let i = 0;
        let arg;
        let targetParam;
        const l = args.length;

        for (; i < l; i++) {
            arg = args[i];
            if (args[i]) {
                targetParam = arg;
            }
        }

        if (!targetParam) { return null; }

        return targetParam[0].parser(targetParam[1]);
    };

    return {
        YEAR: [definitions.y, definitions.Y,
            defaultResolver
        ],
        MONTH: [definitions.b, definitions.B, definitions.m,
            defaultResolver
        ],
        DAY: [definitions.a, definitions.A, definitions.e, definitions.d,
            defaultResolver
        ],
        HOUR: [definitions.H, definitions.l, definitions.p, definitions.P,
            function (hourFormat24, hourFormat12, ampmLower, ampmUpper) {
                let targetParam;
                let amOrpm;
                let isPM;
                let val;

                if (hourFormat12 && (amOrpm = (ampmLower || ampmUpper))) {
                    if (amOrpm[0].parser(amOrpm[1]) === 'pm') {
                        isPM = true;
                    }

                    targetParam = hourFormat12;
                } else if (hourFormat12) {
                    targetParam = hourFormat12;
                } else {
                    targetParam = hourFormat24;
                }

                if (!targetParam) { return null; }

                val = targetParam[0].parser(targetParam[1]);
                if (isPM) {
                    val += 12;
                }
                return val;
            }
        ],
        MINUTE: [definitions.M,
            defaultResolver
        ],
        SECOND: [definitions.S,
            defaultResolver
        ]
    };
};

/*
 * Finds token from the format rule specified by a user.
 * @param format {String} : The format of the input date specified by the user
 * @return {Array} : An array of objects which contains the available token and their occurence index in the format
 */
DateTimeFormatter.findTokens = function (format) {
    const tokenPrefix = DateTimeFormatter.TOKEN_PREFIX;
    const definitions = DateTimeFormatter.getTokenDefinitions();
    const tokenLiterals = Object.keys(definitions);
    const occurrence = [];
    let i;
    let forwardChar;

    while ((i = format.indexOf(tokenPrefix, i + 1)) >= 0) {
        forwardChar = format[i + 1];
        if (tokenLiterals.indexOf(forwardChar) === -1) { continue; }

        occurrence.push({
            index: i,
            token: forwardChar
        });
    }

    return occurrence;
};

/*
 * Format any JS date to a specified date given by user.
 *
 * @param date {Number | Date} : The date object which is to be formatted
 * @param format {String} : The format using which the date will be formatted for display
 */
DateTimeFormatter.formatAs = function (date, format) {
    const nDate = convertToNativeDate(date);
    const occurrence = DateTimeFormatter.findTokens(format);
    const definitions = DateTimeFormatter.getTokenDefinitions();
    let formattedStr = String(format);
    const tokenPrefix = DateTimeFormatter.TOKEN_PREFIX;
    let token;
    let formattedVal;
    let i;
    let l;

    for (i = 0, l = occurrence.length; i < l; i++) {
        token = occurrence[i].token;
        formattedVal = definitions[token].formatter(nDate);
        formattedStr = formattedStr.replace(new RegExp(tokenPrefix + token, 'g'), formattedVal);
    }

    return formattedStr;
};

/*
 * Parses the user specified date string to extract the date time params.
 *
 * @return {Array} : Value of date time params in an array [year, month, day, hour, minutes, seconds, milli]
 */
DateTimeFormatter.prototype.parse = function (dateTimeStamp, options) {
    const tokenResolver = DateTimeFormatter.tokenResolver();
    const dtParams = this.extractTokenValue(dateTimeStamp);
    const dtParamSeq = DateTimeFormatter.DATETIME_PARAM_SEQUENCE;
    const noBreak = options && options.noBreak;
    const dtParamArr = [];
    const args = [];
    let resolverKey;
    let resolverParams;
    let resolverFn;
    let val;
    let i;
    let param;
    let resolvedVal;
    let l;

    for (resolverKey in tokenResolver) {
        if (!{}.hasOwnProperty.call(tokenResolver, resolverKey)) { continue; }

        args.length = 0;
        resolverParams = tokenResolver[resolverKey];
        resolverFn = resolverParams.splice(resolverParams.length - 1, 1)[0];

        for (i = 0, l = resolverParams.length; i < l; i++) {
            param = resolverParams[i];
            val = dtParams[param.name];

            if (val === undefined) {
                args.push(null);
            } else {
                args.push([param, val]);
            }
        }

        resolvedVal = resolverFn.apply(this, args);

        if ((resolvedVal === undefined || resolvedVal === null) && !noBreak) {
            break;
        }

        dtParamArr[dtParamSeq[resolverKey]] = resolvedVal;
    }

    return dtParamArr;
};

/*
 * Extract the value of the token from user specified date time string.
 *
 * @return {Object} : An key value pair which contains the tokens as key and value as pair
 */
DateTimeFormatter.prototype.extractTokenValue = function (dateTimeStamp) {
    const format = this.format;
    const definitions = DateTimeFormatter.getTokenDefinitions();
    const tokenPrefix = DateTimeFormatter.TOKEN_PREFIX;
    const occurrence = DateTimeFormatter.findTokens(format);
    const tokenObj = {};

    let lastOccurrenceIndex;
    let occObj;
    let occIndex;
    let targetText;
    let regexFormat;

    let l;
    let i;

    regexFormat = String(format);

    const tokenArr = occurrence.map(obj => obj.token);
    const occurrenceLength = occurrence.length;
    for (i = occurrenceLength - 1; i >= 0; i--) {
        occIndex = occurrence[i].index;

        if (occIndex + 1 === regexFormat.length - 1) {
            lastOccurrenceIndex = occIndex;
            continue;
        }

        if (lastOccurrenceIndex === undefined) {
            lastOccurrenceIndex = regexFormat.length;
        }

        targetText = regexFormat.substring(occIndex + 2, lastOccurrenceIndex);
        regexFormat = regexFormat.substring(0, occIndex + 2) +
            RegExp.escape(targetText) +
            regexFormat.substring(lastOccurrenceIndex, regexFormat.length);

        lastOccurrenceIndex = occIndex;
    }

    for (i = 0; i < occurrenceLength; i++) {
        occObj = occurrence[i];
        regexFormat = regexFormat.replace(tokenPrefix + occObj.token, definitions[occObj.token].extract());
    }

    const extractValues = dateTimeStamp.match(new RegExp(regexFormat)) || [];
    extractValues.shift();

    for (i = 0, l = tokenArr.length; i < l; i++) {
        tokenObj[tokenArr[i]] = extractValues[i];
    }
    return tokenObj;
};

/*
 * Give back the JS native date formed from  user specified date string
 *
 * @return {Date} : Native JS Date
 */
DateTimeFormatter.prototype.getNativeDate = function (dateTimeStamp) {
    if (dateTimeStamp instanceof Date) {
        return dateTimeStamp;
    } else if (isFinite(dateTimeStamp) && !!this.format) {
        return new Date(dateTimeStamp);
    }

    const dtParams = this.dtParams = this.parse(dateTimeStamp);

    dtParams.unshift(null);
    this.nativeDate = new (Function.prototype.bind.apply(Date, dtParams))();
    return this.nativeDate;
};

/*
 * Represents JS native date to a user specified format.
 *
 * @param format {String} : The format according to which the date is to be represented
 * @return {String} : The formatted date string
 */
DateTimeFormatter.prototype.formatAs = function (format, dateTimeStamp) {
    let nativeDate;

    if (dateTimeStamp) {
        nativeDate = this.nativeDate = this.getNativeDate(dateTimeStamp);
    } else if (!(nativeDate = this.nativeDate)) {
        nativeDate = this.getNativeDate(dateTimeStamp);
    }

    return DateTimeFormatter.formatAs(nativeDate, format);
};

export { DateTimeFormatter as default };