Source: polyfill/Intl/DateTimeFormat.js

// Licensed Materials - Property of IBM
//
// IBM Watson Analytics
//
// (C) Copyright IBM Corp. 2018
//
// US Government Users Restricted Rights - Use, duplication or
// disclosure restricted by GSA ADP Schedule Contract with IBM Corp.

module.exports = ( function( ObjectPolyfill, Format )
{
"use strict";
/**
 * These classes are NOT a polyfill, and are not meant to be!
 * They provide a poor man's fallback for the Intl formatters, for non-compliant environments.
 */

var NAME        = [ "short", "long" ];
var DATE_NUMBER = [ "numeric", "2-digit" ];
var DATE_TEXT   = [ "narrow" ].concat( NAME );
var DATE_BOTH   = DATE_NUMBER.concat( DATE_TEXT );
var BOOLEAN     = [ true, false ];

var padStart = Function.prototype.call.bind( String.prototype.padStart );
// This regex will match a sequence of possible formatter items
var reReplace   = /A+|a+|D+|d+|E+|e+|F+|G+|g+|H+|h+|K+|k+|M+|m+|S+|s+|u+|W+|w+|Y+|y+|Z+|z+/g;
// This date is re-used for processing
var d1 = new Date();

/**
 * Object that parses an Options object and normalizes it to options.
 * @class module:barejs/polyfill/Intl.DateTimeFormat~DateTimeFormatOptions
 */
function DateTimeFormatOptions( _options )
{
    // Date
    this._conditionalSet( _options, "year",         DATE_NUMBER );
    this._conditionalSet( _options, "month",        DATE_BOTH );
    this._conditionalSet( _options, "day",          DATE_NUMBER );
    this._conditionalSet( _options, "weekday",      DATE_TEXT );

    // Time
    this._conditionalSet( _options, "hour",         DATE_NUMBER );
    this._conditionalSet( _options, "minute",       DATE_NUMBER );
    this._conditionalSet( _options, "second",       DATE_NUMBER );

    this._conditionalSet( _options, "weekday",      DATE_TEXT );
    this._conditionalSet( _options, "hour12",       BOOLEAN );
    this._conditionalSet( _options, "timeZoneName", NAME );
}

ObjectPolyfill.defineProperties( DateTimeFormatOptions.prototype,
/** @lends module:barejs/polyfill/Intl.DateTimeFormat~DateTimeFormatOptions# */
{
    localeMatcher: { writable: true, value: null },
    timeZone: { writable: true, value: null },
    hour12: { writable: true, value: null },
    formatMatcher: { writable: true, value: null },
    weekday: { writable: true, value: null },
    era: { writable: true, value: null },
    year: { writable: true, value: null },
    month: { writable: true, value: null },
    day: { writable: true, value: null },
    hour: { writable: true, value: null },
    minute: { writable: true, value: null },
    second: { writable: true, value: null },
    timeZoneName: { writable: true, value: null },

    /**
     * Helper function. If _property is present on _options, will assign the value to this (with the
     * same _property name), and validate the value against _validValues while doing so.
     * @param _options {Object} The options to validate
     * @param _property {String} The name of the property
     * @param _validValues {Array} The list of valid values
     */
    _conditionalSet:
    {
        value: function( _options, _property, _validValues )
        {
            if ( typeof _options[ _property ] !== "undefined" )
            {
                if ( !_validValues.includes( this[ _property ] = _options[ _property ] ) )
                    throw new RangeError( "Invalid value for " + _property + ": " + _options[ _property ] );
            }
        }
    }
} );

/**
 * Supports NUMERICAL formatting of dates, according to the formatting patterns described on
 * http://www.unicode.org/reports/tr35/tr35-4.html#Date_Format_Patterns
 * Warning: does NOT support formats like MMMM which should be localized. This method should only
 * be used to produce a fixed date format. It also doesn't support identifiers that require
 * knowledge of a specific calendar system, like "day of the week" or "era".
 * @param _date {Date} The date to format
 * @param _formatString {String} The formatString to use.
 * @param _utc {Boolean} Optional: set to true to use the UTC time zone instead of local.
 * @returns {String} _date formatted as String using _pattern.
 * @memberof module:barejs/polyfill/Intl.DateTimeFormat~
 */
function formatDateTime( _date, _formatString, _utc )
{
    // Always assign to d1 so we convert a number to a date, and can safely modify the
    // date object to adjust for the time zone
    d1.setTime( _date || Date.now() );
    _utc = _utc === true; // ensure boolean

    return String( _formatString ).replace( reReplace, function( _match )
    {
        var v; // value; used by most format specifiers
        var c = _match.charAt( 0 ); // The first (and every) character of the matched pattern
        var l = _match.length; // short alias for _match.length.

        // First, grab values. Some values early out by returning immediately or throwing an error.
        switch ( c )
        {
            //
            // Values
            //
            case "y": // Year
                v = _utc ? d1.getUTCFullYear() : d1.getFullYear();
                // So two-digit years are a stupid idea (where's the cut-off point?!).
                // As a formatter, we just take the last two digits and don't care about a cut-off.
                // This does mean that 1894 and 1994 will both produce 94, which can't be distinguished.
                // The alternative would be a configurable cut-off year, but the best alternative
                // would be for people to stop chopping away significant digits.
                if ( l === 2 )
                    v %= 100;
                break;
            case "M": // Month
                v = ( _utc ? d1.getUTCMonth() : d1.getMonth() ) + 1; // compensate for 0 based
                break;
            case "d": // Day in month
                v = _utc ? d1.getUTCDate() : d1.getDate();
                break;
            case "D": // Day of year
                // Start by making v a new UTC date on January 1st of the same year.
                // Using UTC will avoid DST differences.
                v = new Date( Date.UTC( d1.getUTCFullYear(), 0, 1, d1.getUTCHours(), d1.getUTCMinutes(), d1.getUTCSeconds(), d1.getUTCMilliseconds() ) );
                // Calculate the number of days and make the value 1 based.
                // Although we expect an integer result (due to the usage of UTC), round the result
                // in case the browser included leap seconds, or some other unforeseen difference.
                v = Math.round( ( d1.getTime() - v.getTime() ) / 86400000 ) + 1;
                break;
            case "E": // Day of week - Sunday is always day 1
                v = ( _utc ? d1.getUTCDay() : d1.getDay() ) + 1;
                break;
            case "H": // Hour [0-23]
            case "h": // Hour [1-12]
            case "K": // Hour [0-11]
            case "k": // Hour [1-24]
                v = _utc ? d1.getUTCHours() : d1.getHours();
                // v is now [0-23]
                switch ( c )
                {
                    case 'h':
                        v = ( v % 12 ) || 12; // translate to [1-12]
                        break;
                    case 'K':
                        v = ( v % 12 );       // translate to [0-11]
                        break;
                    case 'k':
                        v = v || 24;          // translate to [1-24]
                        break;
                }
                break;
            case "m": // Minute
                v = _utc ? d1.getUTCMinutes() : d1.getMinutes();
                break;
            case "s": // Second
                v = _utc ? d1.getUTCSeconds() : d1.getSeconds();
                break;
            case "S": // Fractional Second - rounds to the count of letters
                v = Math.round( ( _utc ? d1.getUTCMilliseconds() : d1.getMilliseconds() ) * Math.pow( 10, l - 3 ) );
                break;

            //
            // Instant return
            //
            case "a": // AM or PM
                if ( l > 1 )
                    throw new RangeError( "Invalid pattern: " + _match + ", a maximum of 1 character is allowed" );
                // Note: am/pm this should actually be localised. However, we're allowing this
                // since the 12 hour format would be useless otherwise.
                v = _utc ? d1.getUTCHours() : d1.getHours();
                return v < 12 ? "am" : "pm";
            case "Z": // Time zone. 1: GMT format, 2: RFC 822
                if ( l > 2 )
                    throw new RangeError( "Invalid pattern: " + _match + ", a maximum of 2 characters is allowed" );
                if ( _utc && ( l === 2 ) )
                    return "Z";
                v = _utc ? 0 : -d1.getTimezoneOffset();
                // Translate v to +-00:00 syntax
                v = ( v < 0 ? "-" : "+" ) +
                    padStart( Math.floor( v / 60 ), 2, "0" ) +
                    ":" + padStart( v % 60, 2, "0" );
                // And immediately return
                return ( l === 1 ? "GMT" + v : v );

            //
            // Unsupported
            //
            /*
            case "A": // Milliseconds in day
            case "e": // Day of week - Local (calendar based)
            case "F": // Day of Week in Month.
            case "G": // Era
            case "g": // Modified Julian day.
            case "u": // Extended year
            case "W": // Week of month
            case "w": // Week of year
            case "Y": // Year (of "Week of Year"), used in ISO year-week calendar. May differ from calendar year.
            case "z": // Timezone. 1: short wall (generic), 2: long wall, 3: short time zone (i.e. PST) 4: full name (Pacific Standard Time).
            */
            default:
                throw new Error( "format identifier " + _match.charAt( 0 ) + "is not supported by this method" );
        }

        // Process values
        switch ( c )
        {
            // Two digit maximum, more is invalid
            case "H": // Hour [0-23]
            case "h": // Hour [1-12]
            case "K": // Hour [0-11]
            case "k": // Hour [1-24]
            case "m": // Minute
            case "s": // Second
                if ( l > 2 )
                    throw new RangeError( "Invalid pattern: " + _match + ", a maximum of 2 characters is allowed" );
                break;

            // Three digit maximum, more is invalid
            case "D": // Day of year
                if ( l > 3 )
                    throw new RangeError( "Invalid pattern: " + _match + ", a maximum of 3 characters is allowed" );
                break;

            // Five digit maximum, but only two digits are not localised.
            case "M": // Month
            case "d": // Day in month
            case "E": // Day of week - Sunday is always day 1
                if ( l > 5 )
                    throw new RangeError( "Invalid pattern: " + _match + ", a maximum of 5 characters is allowed" );
                if ( l > 2 )
                    throw new RangeError( "Pattern: " + _match + ", requires localisation, which is not supported by format" );
                break;

            // Unlimited repeat allowed (no validation needed)
            /*
            case "y": // Year
            case "S": // Fractional Second
                break;
            */
        }

        /*
        if ( ( maxLen > 0 ) && ( l > maxLen ) )
            throw new RangeError( "Invalid pattern: " + _match + ", a maximum of " + maxLen + " characters is allowed" );
        */

        // If we get here, we can just return value (possibly padding it to length).
        return l > 1 ? padStart( v, l, "0" ) : v;
    } );
}

/**
 * Provides Date/Time formatting
 * @class module:barejs/polyfill/Intl.DateTimeFormat
 * @extends module:barejs/polyfill/Intl~Format
 */
function DateTimeFormat( _locales, _options )
{
    Format.call( this, _locales, new DateTimeFormatOptions( Object( _options ) ) );
}

DateTimeFormat.prototype = Object.create( Format.prototype,
/** @lends module:barejs/polyfill/Intl.DateTimeFormat# */
{
    format:
    {
        enumerable: true,
        value: function format( _value )
        {
            var parts = []; // used to build the date and/or time part
            var fmt = []; // used to build the end format

            switch ( this._options.year )
            {
                case "2-digit":
                    parts.push( "yy" );
                    break;
                case "numeric":
                    parts.push( "yyyy" );
                    break;
            }

            switch ( this._options.month )
            {
                case "narrow":
                case "short":
                case "long":
                    // No actual implementation, default to 2-digit
                case "2-digit":
                    parts.push( "MM" );
                    break;
                case "numeric":
                    parts.push( "M" );
                    break;
            }

            switch ( this._options.day )
            {
                case "2-digit":
                    parts.push( "dd" );
                    break;
                case "numeric":
                    parts.push( "d" );
                    break;
            }

            if ( parts.length > 0 )
            {
                fmt.push( parts.join( "-" ) );
                parts.length = 0; // reset for time part
            }

            switch ( this._options.hour )
            {
                case "2-digit":
                    parts.push( "HH" );
                    break;
                case "numeric":
                    parts.push( "H" );
                    break;
            }

            switch ( this._options.minute )
            {
                case "2-digit":
                    parts.push( "mm" );
                    break;
                case "numeric":
                    parts.push( "m" );
                    break;
            }

            switch ( this._options.second )
            {
                case "2-digit":
                    parts.push( "ss" );
                    break;
                case "numeric":
                    parts.push( "s" );
                    break;
            }

            if ( parts.length > 0 )
                fmt.push( parts.join( ":" ) );

            // DateTimeFormat defaults to formatting year/month/day if no options where specified.
            if ( fmt.length < 1 )
                fmt.push( "yyyy-MM-dd" );

            return formatDateTime( _value, fmt.join( " " ) );
        }
    }
} );

return DateTimeFormat;

}( require( "../Object" ), require( "./Format" ) ) );