// 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" ) ) );