/*!
 * OpenUI5
 * (c) Copyright 2009-2026 SAP SE or an SAP affiliate company.
 * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
 */

// Provides class sap.ui.core.format.DateFormat
sap.ui.define([
	'sap/ui/core/CalendarType',
	'sap/ui/core/Locale',
	'sap/ui/core/LocaleData',
	'sap/ui/core/date/UniversalDate',
	'sap/ui/core/date/CalendarUtils',
	'sap/ui/core/date/CalendarWeekNumbering',
	"sap/base/util/deepEqual",
	'sap/base/util/values',
	"sap/base/strings/formatMessage",
	"sap/base/Log",
	"sap/base/util/extend"
],
	function(
		CalendarType,
		Locale,
		LocaleData,
		UniversalDate,
		CalendarUtils,
		CalendarWeekNumbering,
		deepEqual,
		values,
		formatMessage,
		Log,
		extend
	) {
	"use strict";

	/**
	 * Constructor for DateFormat - must not be used: To get a DateFormat instance, please use getDateInstance, getDateTimeInstance or getTimeInstance.
	 *
	 * @class
	 * The DateFormat is a static class for formatting and parsing single date and time values or date and time intervals according
	 * to a set of format options.
	 *
	 * Supported format options are pattern based on Unicode LDML Date Format notation. Please note that only a subset of the LDML date symbols
	 * is supported.
	 * If no pattern is specified a default pattern according to the locale settings is used.
	 *
	 * @public
	 * @hideconstructor
	 * @see http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
	 * @alias sap.ui.core.format.DateFormat
	 */
	var DateFormat = function() {
		// Do not use the constructor
		throw new Error();
	};

	// Cache for parsed CLDR DatePattern
	var mCldrDatePattern = {};

	DateFormat.oDateInfo = {
		oDefaultFormatOptions: {
			style: "medium",
			relativeScale: "day",
			relativeStyle: "wide"
		},
		aFallbackFormatOptions: [
			{style: "short"},
			{style: "medium"},
			{pattern: "yyyy-MM-dd"},
			{pattern: "yyyyMMdd", strictParsing: true}
		],
		bShortFallbackFormatOptions: true,
		bPatternFallbackWithoutDelimiter: true,
		getPattern: function(oLocaleData, sStyle, sCalendarType) {
			return oLocaleData.getDatePattern(sStyle, sCalendarType);
		},
		oRequiredParts: {
			"text": true, "year": true, "weekYear": true, "month": true, "day": true
		},
		aRelativeScales: ["year", "month", "week", "day"],
		aRelativeParseScales: ["year", "quarter", "month", "week", "day", "hour", "minute", "second"],
		aIntervalCompareFields: ["Era", "FullYear", "Quarter", "Month", "Week" ,"Date"]
	};

	DateFormat.oDateTimeInfo = {
		oDefaultFormatOptions: {
			style: "medium",
			relativeScale: "auto",
			relativeStyle: "wide"
		},
		aFallbackFormatOptions: [
			{style: "short"},
			{style: "medium"},
			{pattern: "yyyy-MM-dd'T'HH:mm:ss"},
			{pattern: "yyyyMMdd HHmmss"}
		],
		getPattern: function(oLocaleData, sStyle, sCalendarType) {
			// If style is mixed ("medium/short") split it and pass both parts separately
			var iSlashIndex = sStyle.indexOf("/");
			if (iSlashIndex > 0) {
				return oLocaleData.getCombinedDateTimePattern(sStyle.substr(0, iSlashIndex), sStyle.substr(iSlashIndex + 1), sCalendarType);
			} else {
				return oLocaleData.getCombinedDateTimePattern(sStyle, sStyle, sCalendarType);
			}
		},
		oRequiredParts: {
			"text": true, "year": true, "weekYear": true, "month": true, "day": true, "hour0_23": true,
			"hour1_24": true, "hour0_11": true, "hour1_12": true
		},
		aRelativeScales: ["year", "month", "week", "day", "hour", "minute", "second"],
		aRelativeParseScales: ["year", "quarter", "month", "week", "day", "hour", "minute", "second"],
		aIntervalCompareFields: ["Era", "FullYear", "Quarter", "Month", "Week", "Date", "DayPeriod", "Hours", "Minutes", "Seconds"]
	};

	DateFormat.oTimeInfo = {
		oDefaultFormatOptions: {
			style: "medium",
			relativeScale: "auto",
			relativeStyle: "wide"
		},
		aFallbackFormatOptions: [
			{style: "short"},
			{style: "medium"},
			{pattern: "HH:mm:ss"},
			{pattern: "HHmmss"}
		],
		getPattern: function(oLocaleData, sStyle, sCalendarType) {
			return oLocaleData.getTimePattern(sStyle, sCalendarType);
		},
		oRequiredParts: {
			"text": true, "hour0_23": true, "hour1_24": true, "hour0_11": true, "hour1_12": true
		},
		aRelativeScales: ["hour", "minute", "second"],
		aRelativeParseScales: ["year", "quarter", "month", "week", "day", "hour", "minute", "second"],
		aIntervalCompareFields: ["DayPeriod", "Hours", "Minutes", "Seconds"]
	};


	/**
	 * @see sap.ui.core.format.DateFormat.getDateInstance
	 */
	DateFormat.getInstance = function(oFormatOptions, oLocale) {
		return this.getDateInstance(oFormatOptions, oLocale);
	};


	/**
	 * Get a date instance of the DateFormat, which can be used for formatting.
	 *
	 * @param {object} [oFormatOptions] Object which defines the format options
	 * @param {sap.ui.core.date.CalendarWeekNumbering} [oFormatOptions.calendarWeekNumbering] @since 1.84.31 specifies the calendar week numbering.
	 *   Note: This API has been introduced with version 1.108 and downported to this release with
	 *   patch level 31.
	 * @param {string} [oFormatOptions.format] @since 1.34.0 contains pattern symbols (e.g. "yMMMd" or "Hms") which will be converted into the pattern in the used locale, which matches the wanted symbols best.
	 *  The symbols must be in canonical order, that is: Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D), Hour (h/H/k/K/j/J), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x)
	 *  See {@link http://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems}
	 * @param {string} [oFormatOptions.pattern] a data pattern in LDML format. It is not verified whether the pattern represents only a date.
	 * @param {string} [oFormatOptions.style] can be either 'short, 'medium', 'long' or 'full'. If no pattern is given, a locale dependent default date pattern of that style is used from the LocaleData class.
	 * @param {boolean} [oFormatOptions.strictParsing] if true, by parsing it is checked if the value is a valid date
	 * @param {boolean} [oFormatOptions.relative] if true, the date is formatted relatively to todays date if it is within the given day range, e.g. "today", "yesterday", "in 5 days"
	 * @param {int[]} [oFormatOptions.relativeRange] the day range used for relative formatting. If <code>oFormatOptions.relativeScale</code> is set to default value 'day', the relativeRange is by default [-6, 6], which means only the last 6 days, today and the next 6 days are formatted relatively. Otherwise when <code>oFormatOptions.relativeScale</code> is set to 'auto', all dates are formatted relatively.
	 * @param {string} [oFormatOptions.relativeScale="day"] if 'auto' is set, new relative time format is switched on for all Date/Time Instances. The relative scale is chosen depending on the difference between the given date and now.
	 * @param {string} [oFormatOptions.relativeStyle="wide"] @since 1.32.10, 1.34.4 the style of the relative format. The valid values are "wide", "short", "narrow"
	 * @param {boolean} [oFormatOptions.interval=false] @since 1.48.0 if true, the {@link sap.ui.core.format.DateFormat#format format} method expects an array with two dates as the first argument and formats them as interval. Further interval "Jan 10, 2008 - Jan 12, 2008" will be formatted as "Jan 10-12, 2008" if the 'format' option is set with necessary symbols.
	 *   Otherwise the two given dates are formatted separately and concatenated with local dependent pattern.
	 * @param {boolean} [oFormatOptions.singleIntervalValue=false] Only relevant if oFormatOptions.interval is set to 'true'. This allows to pass an array with only one date object to the {@link sap.ui.core.format.DateFormat#format format} method.
	 * @param {boolean} [oFormatOptions.UTC] if true, the date is formatted and parsed as UTC instead of the local timezone
	 * @param {sap.ui.core.CalendarType} [oFormatOptions.calendarType] The calender type which is used to format and parse the date. This value is by default either set in configuration or calculated based on current locale.
	 * @param {sap.ui.core.Locale} [oLocale] Locale to ask for locale specific texts/settings
	 * @return {sap.ui.core.format.DateFormat} date instance of the DateFormat
	 * @static
	 * @public
	 * @throws {TypeError} If the <code>calendarWeekNumbering</code> format option has an unsupported value
	 */
	DateFormat.getDateInstance = function(oFormatOptions, oLocale) {
		return this.createInstance(oFormatOptions, oLocale, this.oDateInfo);
	};

	/**
	 * Get a datetime instance of the DateFormat, which can be used for formatting.
	 *
	 * @param {object} [oFormatOptions] Object which defines the format options
	 * @param {sap.ui.core.date.CalendarWeekNumbering} [oFormatOptions.calendarWeekNumbering] @since 1.84.31 specifies the calendar week numbering.
	 *   Note: This API has been introduced with version 1.108 and downported to this release with
	 *   patch level 31.
	 * @param {string} [oFormatOptions.format] @since 1.34.0 contains pattern symbols (e.g. "yMMMd" or "Hms") which will be converted into the pattern in the used locale, which matches the wanted symbols best.
	 *  The symbols must be in canonical order, that is: Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D), Hour (h/H/k/K/j/J), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x)
	 *  See http://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems
	 * @param {string} [oFormatOptions.pattern] a datetime pattern in LDML format. It is not verified whether the pattern represents a full datetime.
	 * @param {string} [oFormatOptions.style] can be either 'short, 'medium', 'long' or 'full'. For datetime you can also define mixed styles, separated with a slash, where the first part is the date style and the second part is the time style (e.g. "medium/short"). If no pattern is given, a locale dependent default datetime pattern of that style is used from the LocaleData class.
	 * @param {boolean} [oFormatOptions.strictParsing] if true, by parsing it is checked if the value is a valid datetime
	 * @param {boolean} [oFormatOptions.relative] if true, the date is formatted relatively to todays date if it is within the given day range, e.g. "today", "yesterday", "in 5 days"@param {boolean} [oFormatOptions.UTC] if true, the date is formatted and parsed as UTC instead of the local timezone
	 * @param {int[]} [oFormatOptions.relativeRange] the day range used for relative formatting. If <code>oFormatOptions.relativeScale</code> is set to default value 'day', the relativeRange is by default [-6, 6], which means only the last 6 days, today and the next 6 days are formatted relatively. Otherwise when <code>oFormatOptions.relativeScale</code> is set to 'auto', all dates are formatted relatively.
	 * @param {string} [oFormatOptions.relativeScale="day"] if 'auto' is set, new relative time format is switched on for all Date/Time Instances. The relative scale is chosen depending on the difference between the given date and now.
	 * @param {string} [oFormatOptions.relativeStyle="wide"] @since 1.32.10, 1.34.4 the style of the relative format. The valid values are "wide", "short", "narrow"
	 * @param {boolean} [oFormatOptions.interval=false] @since 1.48.0 if true, the {@link sap.ui.core.format.DateFormat#format format} method expects an array with two dates as the first argument and formats them as interval. Further interval "Jan 10, 2008 - Jan 12, 2008" will be formatted as "Jan 10-12, 2008" if the 'format' option is set with necessary symbols.
	 *   Otherwise the two given dates are formatted separately and concatenated with local dependent pattern.
	 * @param {boolean} [oFormatOptions.singleIntervalValue=false] Only relevant if oFormatOptions.interval is set to 'true'. This allows to pass an array with only one date object to the {@link sap.ui.core.format.DateFormat#format format} method.
	 * @param {boolean} [oFormatOptions.UTC] if true, the date is formatted and parsed as UTC instead of the local timezone
	 * @param {sap.ui.core.CalendarType} [oFormatOptions.calendarType] The calender type which is used to format and parse the date. This value is by default either set in configuration or calculated based on current locale.
	 * @param {sap.ui.core.Locale} [oLocale] Locale to ask for locale specific texts/settings
	 * @return {sap.ui.core.format.DateFormat} datetime instance of the DateFormat
	 * @static
	 * @public
	 * @throws {TypeError} If the <code>calendarWeekNumbering</code> format option has an unsupported value
	 */
	DateFormat.getDateTimeInstance = function(oFormatOptions, oLocale) {
		return this.createInstance(oFormatOptions, oLocale, this.oDateTimeInfo);
	};

	/**
	 * Get a time instance of the DateFormat, which can be used for formatting.
	 *
	 * @param {object} [oFormatOptions] Object which defines the format options
	 * @param {sap.ui.core.date.CalendarWeekNumbering} [oFormatOptions.calendarWeekNumbering] @since 1.84.31 specifies the calendar week numbering.
	 *   Note: This API has been introduced with version 1.108 and downported to this release with
	 *   patch level 31.
	 * @param {string} [oFormatOptions.format] @since 1.34.0 contains pattern symbols (e.g. "yMMMd" or "Hms") which will be converted into the pattern in the used locale, which matches the wanted symbols best.
	 *  The symbols must be in canonical order, that is: Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D), Hour (h/H/k/K/j/J), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x)
	 *  See http://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems
	 * @param {string} [oFormatOptions.pattern] a time pattern in LDML format. It is not verified whether the pattern only represents a time.
	 * @param {string} [oFormatOptions.style] can be either 'short, 'medium', 'long' or 'full'. If no pattern is given, a locale dependent default time pattern of that style is used from the LocaleData class.
	 * @param {boolean} [oFormatOptions.strictParsing] if true, by parsing it is checked if the value is a valid time
	 * @param {boolean} [oFormatOptions.relative] if true, the date is formatted relatively to todays date if it is within the given day range, e.g. "today", "yesterday", "in 5 days"
	 * @param {int[]} [oFormatOptions.relativeRange] the day range used for relative formatting. If <code>oFormatOptions.relativeScale</code> is set to default value 'day', the relativeRange is by default [-6, 6], which means only the last 6 days, today and the next 6 days are formatted relatively. Otherwise when <code>oFormatOptions.relativeScale</code> is set to 'auto', all dates are formatted relatively.
	 * @param {string} [oFormatOptions.relativeScale="day"] if 'auto' is set, new relative time format is switched on for all Date/Time Instances. The relative scale is chosen depending on the difference between the given date and now.
	 * @param {string} [oFormatOptions.relativeStyle="wide"] @since 1.32.10, 1.34.4 the style of the relative format. The valid values are "wide", "short", "narrow"
	 * @param {boolean} [oFormatOptions.interval=false] @since 1.48.0 if true, the {@link sap.ui.core.format.DateFormat#format format} method expects an array with two dates as the first argument and formats them as interval. Further interval "Jan 10, 2008 - Jan 12, 2008" will be formatted as "Jan 10-12, 2008" if the 'format' option is set with necessary symbols.
	 *   Otherwise the two given dates are formatted separately and concatenated with local dependent pattern.
	 * @param {boolean} [oFormatOptions.singleIntervalValue=false] Only relevant if oFormatOptions.interval is set to 'true'. This allows to pass an array with only one date object to the {@link sap.ui.core.format.DateFormat#format format} method.
	 * @param {boolean} [oFormatOptions.UTC] if true, the time is formatted and parsed as UTC instead of the local timezone
	 * @param {sap.ui.core.CalendarType} [oFormatOptions.calendarType] The calender type which is used to format and parse the date. This value is by default either set in configuration or calculated based on current locale.
	 * @param {sap.ui.core.Locale} [oLocale] Locale to ask for locale specific texts/settings
	 * @return {sap.ui.core.format.DateFormat} time instance of the DateFormat
	 * @static
	 * @public
	 * @throws {TypeError} If the <code>calendarWeekNumbering</code> format option has an unsupported value
	 */
	DateFormat.getTimeInstance = function(oFormatOptions, oLocale) {
		return this.createInstance(oFormatOptions, oLocale, this.oTimeInfo);
	};

	function createIntervalPatternWithNormalConnector(oFormat) {
		var sPattern = oFormat.oLocaleData.getIntervalPattern("", oFormat.oFormatOptions.calendarType);

		sPattern = sPattern.replace(/[^\{\}01 ]/, "-");

		return sPattern.replace(/\{(0|1)\}/g, oFormat.oFormatOptions.pattern);
	}

	/**
	 * Create instance of the DateFormat.
	 *
	 * @param {object} [oFormatOptions] Object which defines the format options
	 * @param {sap.ui.core.Locale} [oLocale] Locale to ask for locale specific texts/settings
	 * @return {sap.ui.core.format.DateFormat} time instance of the DateFormat
	 * @static
	 * @private
	 * @throws {TypeError} If the <code>calendarWeekNumbering</code> format option has an unsupported value
	 */
	DateFormat.createInstance = function(oFormatOptions, oLocale, oInfo) {
		// Create an instance of the DateFormat
		var oFormat = Object.create(this.prototype);

		// Handle optional parameters
		if ( oFormatOptions instanceof Locale ) {
			oLocale = oFormatOptions;
			oFormatOptions = undefined;
		}

		// Get Locale and LocaleData to use
		if (!oLocale) {
			oLocale = sap.ui.getCore().getConfiguration().getFormatSettings().getFormatLocale();
		}
		oFormat.oLocale = oLocale;
		oFormat.oLocaleData = LocaleData.getInstance(oLocale);

		// Extend the default format options with custom format options and retrieve the pattern
		// from the LocaleData, in case it is not defined yet
		oFormat.oFormatOptions = extend({}, oInfo.oDefaultFormatOptions, oFormatOptions);

		if (!oFormat.oFormatOptions.calendarType) {
			oFormat.oFormatOptions.calendarType = sap.ui.getCore().getConfiguration().getCalendarType();
		}

		if (oFormat.oFormatOptions.calendarWeekNumbering && values(CalendarWeekNumbering).indexOf(oFormat.oFormatOptions.calendarWeekNumbering) < 0) {
			throw new TypeError("Illegal format option calendarWeekNumbering: '" + oFormat.oFormatOptions.calendarWeekNumbering + "'");
		}

		if (!oFormat.oFormatOptions.pattern) {
			if (oFormat.oFormatOptions.format) {
				oFormat.oFormatOptions.pattern = oFormat.oLocaleData.getCustomDateTimePattern(oFormat.oFormatOptions.format, oFormat.oFormatOptions.calendarType);
			} else {
				oFormat.oFormatOptions.pattern = oInfo.getPattern(oFormat.oLocaleData, oFormat.oFormatOptions.style, oFormat.oFormatOptions.calendarType);
			}
		}

		if (oFormat.oFormatOptions.interval) {

			if (oFormat.oFormatOptions.format) {
				// when 'format' option is set, generate the pattern based on the greatest difference
				oFormat.intervalPatterns = oFormat.oLocaleData.getCustomIntervalPattern(oFormat.oFormatOptions.format, null/*=no diff*/, oFormat.oFormatOptions.calendarType);

				// In case oFormat.intervalPatterns is a string, put the single string into array
				if (typeof oFormat.intervalPatterns === "string") {
					oFormat.intervalPatterns = [oFormat.intervalPatterns];
				}

				// Put the single date pattern, which is generated based on the oFormatOptions.format, into the array in case the date interval is formatted as a single date
				oFormat.intervalPatterns.push(oFormat.oLocaleData.getCustomDateTimePattern(oFormat.oFormatOptions.format, oFormat.oFormatOptions.calendarType));

			} else {
				oFormat.intervalPatterns = [
					// when 'format' option is not set, generate the combined interval pattern
					oFormat.oLocaleData.getCombinedIntervalPattern(oFormat.oFormatOptions.pattern, oFormat.oFormatOptions.calendarType),
					// Put the single date pattern into the array in case the date interval is formatted as a single date
					oFormat.oFormatOptions.pattern
				];
			}
			var sCommonConnectorPattern = createIntervalPatternWithNormalConnector(oFormat);
			oFormat.intervalPatterns.push(sCommonConnectorPattern);
		}

		// if the current format isn't a fallback format, create its fallback formats
		if (!oFormat.oFormatOptions.fallback) {
			// If fallback DateFormats have not been created yet, do it now
			if (!oInfo.oFallbackFormats) {
				oInfo.oFallbackFormats = {};
			}
			// Store fallback formats per locale and calendar type
			var sLocale = oLocale.toString(),
				sCalendarType = oFormat.oFormatOptions.calendarType,
				sKey = sLocale + "-" + sCalendarType,
				sPattern,
				aFallbackFormatOptions;

			if (oFormat.oFormatOptions.pattern && oInfo.bPatternFallbackWithoutDelimiter) {
				sKey = sKey + "-" + oFormat.oFormatOptions.pattern;
			}

			if (oFormat.oFormatOptions.interval) {
				sKey = sKey + "-" + "interval";
			}

			var oFallbackFormats = oInfo.oFallbackFormats[sKey] ? Object.assign({}, oInfo.oFallbackFormats[sKey]) : undefined;

			if (!oFallbackFormats) {
				aFallbackFormatOptions = oInfo.aFallbackFormatOptions;
				// Add two fallback patterns for locale-dependent short format without delimiters
				if (oInfo.bShortFallbackFormatOptions) {
					sPattern = oInfo.getPattern(oFormat.oLocaleData, "short");
					// add the options of fallback formats without delimiters to the fallback options array
					aFallbackFormatOptions = aFallbackFormatOptions.concat(DateFormat._createFallbackOptionsWithoutDelimiter(sPattern));
				}

				if (oFormat.oFormatOptions.pattern && oInfo.bPatternFallbackWithoutDelimiter) {
					// create options of fallback formats by removing delimiters from the given pattern
					// insert the new fallback format options to the front of the array
					aFallbackFormatOptions = DateFormat._createFallbackOptionsWithoutDelimiter(oFormat.oFormatOptions.pattern).concat(aFallbackFormatOptions);
				}

				oFallbackFormats = DateFormat._createFallbackFormat(aFallbackFormatOptions, sCalendarType, oLocale, oInfo, oFormat.oFormatOptions.interval);
			}

			oFormat.aFallbackFormats = oFallbackFormats;
		}

		oFormat.oRequiredParts = oInfo.oRequiredParts;
		oFormat.aRelativeScales = oInfo.aRelativeScales;
		oFormat.aRelativeParseScales = oInfo.aRelativeParseScales;
		oFormat.aIntervalCompareFields = oInfo.aIntervalCompareFields;

		oFormat.init();
		return oFormat;
	};

	/**
	 * Initialize date format
	 */
	DateFormat.prototype.init = function() {
		var sCalendarType = this.oFormatOptions.calendarType;

		this.aMonthsAbbrev = this.oLocaleData.getMonths("abbreviated", sCalendarType);
		this.aMonthsWide = this.oLocaleData.getMonths("wide", sCalendarType);
		this.aMonthsNarrow = this.oLocaleData.getMonths("narrow", sCalendarType);
		this.aMonthsAbbrevSt = this.oLocaleData.getMonthsStandAlone("abbreviated", sCalendarType);
		this.aMonthsWideSt = this.oLocaleData.getMonthsStandAlone("wide", sCalendarType);
		this.aMonthsNarrowSt = this.oLocaleData.getMonthsStandAlone("narrow", sCalendarType);
		this.aDaysAbbrev = this.oLocaleData.getDays("abbreviated", sCalendarType);
		this.aDaysWide = this.oLocaleData.getDays("wide", sCalendarType);
		this.aDaysNarrow = this.oLocaleData.getDays("narrow", sCalendarType);
		this.aDaysShort = this.oLocaleData.getDays("short", sCalendarType);
		this.aDaysAbbrevSt = this.oLocaleData.getDaysStandAlone("abbreviated", sCalendarType);
		this.aDaysWideSt = this.oLocaleData.getDaysStandAlone("wide", sCalendarType);
		this.aDaysNarrowSt = this.oLocaleData.getDaysStandAlone("narrow", sCalendarType);
		this.aDaysShortSt = this.oLocaleData.getDaysStandAlone("short", sCalendarType);
		this.aQuartersAbbrev = this.oLocaleData.getQuarters("abbreviated", sCalendarType);
		this.aQuartersWide = this.oLocaleData.getQuarters("wide", sCalendarType);
		this.aQuartersNarrow = this.oLocaleData.getQuarters("narrow", sCalendarType);
		this.aQuartersAbbrevSt = this.oLocaleData.getQuartersStandAlone("abbreviated", sCalendarType);
		this.aQuartersWideSt = this.oLocaleData.getQuartersStandAlone("wide", sCalendarType);
		this.aQuartersNarrowSt = this.oLocaleData.getQuartersStandAlone("narrow", sCalendarType);
		this.aErasNarrow = this.oLocaleData.getEras("narrow", sCalendarType);
		this.aErasAbbrev = this.oLocaleData.getEras("abbreviated", sCalendarType);
		this.aErasWide = this.oLocaleData.getEras("wide", sCalendarType);
		this.aDayPeriods = this.oLocaleData.getDayPeriods("abbreviated", sCalendarType);
		this.aFormatArray = this.parseCldrDatePattern(this.oFormatOptions.pattern);
		this.sAllowedCharacters = this.getAllowedCharacters(this.aFormatArray);
	};

	/**
	 * Creates DateFormat instances based on the given format options. The created
	 * instances are used as fallback formats of another DateFormat instances.
	 *
	 * All fallback formats are marked with 'bIsFallback' to make it distinguishable
	 * from the normal DateFormat instances.
	 *
	 * @param {Object[]} aFallbackFormatOptions the options for creating the fallback DateFormat
	 * @param {sap.ui.core.CalendarType} sCalendarType the type of the current calendarType
	 * @param {sap.ui.core.LocaleData} oLocale Locale to ask for locale specific texts/settings
	 * @param {Object} oInfo The default info object of the current date type
	 * @return {sap.ui.core.DateFormat[]} an array of fallback DateFormat instances
	 */
	DateFormat._createFallbackFormat = function(aFallbackFormatOptions, sCalendarType, oLocale, oInfo, bInterval) {
		return aFallbackFormatOptions.map(function(oOptions) {
			// The format options within the aFallbackFormatOptions array are static
			// and shouldn't be manipulated. Hence, cloning each format option is required.
			var oFormatOptions = Object.assign({}, oOptions);
			if (bInterval) {
				oFormatOptions.interval = true;
			}
			oFormatOptions.calendarType = sCalendarType;
			// mark the current format as a fallback in order to avoid endless recursive call of function 'createInstance'
			oFormatOptions.fallback = true;
			var oFallbackFormat = DateFormat.createInstance(oFormatOptions, oLocale, oInfo);
			oFallbackFormat.bIsFallback = true;
			return oFallbackFormat;
		});
	};

	/**
	 * Creates options for fallback DateFormat instance by removing all delimiters
	 * from the given base pattern.
	 *
	 * @param {string} sBasePattern The pattern where the result pattern will be
	 * generated by removing the delimiters
	 * @return {Object} Format option object which contains the new pattern
	 */
	DateFormat._createFallbackOptionsWithoutDelimiter = function(sBasePattern) {
		var rNonDateFields = /[^dMyGU]/g,
			oDayReplace = {
				regex: /d+/g,
				replace: "dd"
			},
			oMonthReplace = {
				regex: /M+/g,
				replace: "MM"
			},
			oYearReplace = {
				regex: /[yU]+/g,
				replace: ["yyyy", "yy"]
			};

		sBasePattern = sBasePattern.replace(rNonDateFields, ""); //remove all delimiters
		sBasePattern = sBasePattern.replace(oDayReplace.regex, oDayReplace.replace); // replace day entries with 2 digits
		sBasePattern = sBasePattern.replace(oMonthReplace.regex, oMonthReplace.replace); // replace month entries with 2 digits

		return oYearReplace.replace.map(function(sReplace) {
			return {
				pattern: sBasePattern.replace(oYearReplace.regex, sReplace),
				strictParsing: true
			};
		});
	};


	var oParseHelper = {
		isNumber: function (iCharCode) {
			return iCharCode >= 48 && iCharCode <= 57;
		},
		findNumbers: function (sValue, iMaxLength) {
			var iLength = 0;
			while (iLength < iMaxLength && this.isNumber(sValue.charCodeAt(iLength))) {
				iLength++;
			}

			if (typeof sValue !== "string") {
				sValue = sValue.toString();
			}

			return sValue.substr(0, iLength);
		},
		findEntry: function (sValue, aList) {
			var iFoundIndex = -1,
				iMatchedLength = 0;

			for (var j = 0; j < aList.length; j++) {
				if (aList[j] && aList[j].length > iMatchedLength && sValue.indexOf(aList[j]) === 0) {
					iFoundIndex = j;
					iMatchedLength = aList[j].length;
				}
			}
			return {
				index: iFoundIndex,
				value: iFoundIndex === -1 ? null : aList[iFoundIndex]
			};
		},
		/**
		 * Parses a given timezone
		 *
		 * @param {string} sValue, e.g. "-0800", "-08:00", "-08"
		 * @param {boolean} bColonSeparated whether or not the values are colon separated, e.g. "-08:00"
		 * @returns {{tzDiff: number, length: number}}
		 */
		parseTZ: function (sValue, bColonSeparated) {
			var iLength = 0;
			var iTZFactor = sValue.charAt(0) == "+" ? -1 : 1;
			var sPart;

			iLength++; //"+" or "-"
			sPart = this.findNumbers(sValue.substr(iLength), 2);

			var iTZDiffHour = parseInt(sPart);
			iLength += 2; //hh: 2 digits for hours

			if (bColonSeparated) {
				iLength++; //":"
			}
			sPart = this.findNumbers(sValue.substr(iLength), 2);
			var iTZDiff = 0;
			// timezone pattern "X": will produce only 2 digits: "-08"
			if (sPart) {
				iLength += 2; //mm: 2 digits for minutes
				iTZDiff = parseInt(sPart);
			}

			return {
				length: iLength,
				tzDiff: (iTZDiff + 60 * iTZDiffHour) * iTZFactor
			};
		},

		checkValid: function (sType, bPartInvalid, oFormat) {
			if (sType in oFormat.oRequiredParts && bPartInvalid) {
				return false;
			}
		}
	};

	/**
	 * Creates a pattern symbol object containing all needed functions to be used for formatting and parsing.
	 * Functions which are not given in <code>mParameters</code> are provided with a default implementation.
	 *
	 * @param {Object<string,any>} mParameters
	 *   A parameters map for creating a pattern symbol
	 * @param {string} mParameters.name
	 *   An internal unique name describing this symbol
	 * @param {function(Object<string, any>, sap.ui.core.date.UniversalDate, boolean, sap.ui.core.format.DateFormat,
	 *     string): string} [mParameters.format]
	 *   A function to format the symbol part based on a given date object
	 * @param {function(string, Object<string, any>, sap.ui.core.format.DateFormat, object, string): object}
	 *     [mParameters.parse]
	 *   A function to parse the symbol part based on a given date string
	 * @param {boolean|function(int): boolean} [mParameters.isNumeric]
	 *   A boolean to describe, or a function to evaluate, whether the pattern symbol expects a
	 *   numeric representation. For example pattern "MM" results in "02" and is therefore numeric,
	 *   but "MMM" results in "Feb" which is not numeric.
	 * @returns {{name: string, format: function, parse: function, isNumeric: function}}
	 *   A pattern symbol object
	 *
	 * @private
	 */
	DateFormat._createPatternSymbol = function (mParameters) {
		var fnIsNumeric = typeof mParameters.isNumeric === "function" && mParameters.isNumeric
				// Default: false
				|| function () { return mParameters.isNumeric || false; };

		return {
			name: mParameters.name,

			/**
			 * Formatter for a pattern symbol.
			 *
			 * @param {Object<string, any>} oField
			 *   The date pattern field as parsed by {@link DateFormat#parseCldrDatePattern}
			 * @param {sap.ui.core.date.UniversalDate} oDate
			 *   The date object to format
			 * @param {boolean} bUTC
			 *   Whether the UTC option is set
			 * @param {sap.ui.core.format.DateFormat} oFormat
			 *   The <code>DateFormat</code> instance
			 * @returns {string}
			 *   The formatted date information for this date pattern part
			 */
			format: mParameters.format
				// not supported, but reserved
				|| function () { return ""; },

			/**
			 * Parser for a pattern symbol.
			 *
			 * @param {string} sValue
			 *   The given input
			 * @param {Object<string, any>} oPart
			 *   The date pattern field as parsed by {@link DateFormat#parseCldrDatePattern}
			 * @param {sap.ui.core.format.DateFormat} oFormat
			 *   The <code>DateFormat</code> instance
			 * @param {object} oConfig
			 *   The configuration object for parsing the value
			 * @param {object} oConfig.dateValue
			 *   The already parsed date fields
			 * @param {boolean} oConfig.exactLength
			 *   Whether parsing with the exact length specified by {@link DateFormat#parseCldrDatePattern} is needed
			 * @param {object[]} oConfig.formatArray
			 *   The complete format array as parsed by {@link DateFormat#parseCldrDatePattern}
			 * @param {int} oConfig.index
			 *   The current index in the format array
			 * @param {boolean} oConfig.strict
			 *   Whether to disallow overflows for component values of a date (see
			 *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date).
			 *   Note that the corresponding checks are just for number ranges which are possible <em>at max</em>
			 *   without overflow; the check for day does not consider the month and year to compute the maximum
			 *   number of days allowed.
			 * @param {string} sTimezone
			 *   The IANA timezone ID
			 * @returns {object}
			 *   The parsed date information for this date pattern part; could contain the property <code>valid</code>
			 *   set to <code>false</code> if parsing was not successful
			 */
			parse: mParameters.parse
				// not supported, but reserved
				|| function () { return {}; },

			/**
			 * Evaluates whether this symbol has a numeric representation.
			 *
			 * @param {int} [iDigits] The number of repetitions of the pattern symbol, e.g. <code>3</code> for "aaa"
			 * @returns {boolean} Whether this symbol has a numeric representation
			 */
			isNumeric: fnIsNumeric
		};
	};

	/**
	 * Provides functionality to format and parse a given pattern symbol.
	 * @see https://unicode.org/reports/tr35/tr35-dates.html#table-date-field-symbol-table
	 */
	DateFormat.prototype.oSymbols = {
		"": DateFormat._createPatternSymbol({
			name: "text",
			format: function(oField, oDate, bUTC, oFormat) {
				return oField.value;
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sChar;
				var bValid = true;
				var iValueIndex = 0;
				var iPatternIndex = 0;
				// https://www.compart.com/en/unicode/category/Pd
				var sDelimiter = "\u002d\u007E\u2010\u2011\u2012\u2013\u2014\ufe58\ufe63\uff0d\uFF5E";

				// Compare the letters in oPart.value (the pattern) and sValue (the given string to parse)
				// one by one.
				// If the current letter in the pattern is " ", sValue is allowed to have no match, exact match
				// or multiple " ". This makes the parsing more tolerant.
				for (; iPatternIndex < oPart.value.length; iPatternIndex++) {
					sChar = oPart.value.charAt(iPatternIndex);

					if (sChar === " ") {
						// allows to have multiple spaces
						while (sValue.charAt(iValueIndex) === " ") {
							iValueIndex++;
						}
					} else if (sDelimiter.includes(sChar)) {
						if (!sDelimiter.includes(sValue.charAt(iValueIndex))) {
								bValid = false;
						}
						iValueIndex++;
					} else {
						if (sValue.charAt(iValueIndex) !== sChar) {
							// if it's not a space, there must be an exact match
							bValid = false;
						}
						iValueIndex++;
					}

					if (!bValid) {
						break;
					}
				}

				if (bValid) {
					return {
						length: iValueIndex
					};
				} else {
					var bPartInvalid = false;

					// only require text, if next part is also required
					if (oConfig.index < oConfig.formatArray.length - 1) {
						bPartInvalid = (oConfig.formatArray[oConfig.index + 1].type in oFormat.oRequiredParts);
					}
					return {
						valid: oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat)
					};
				}
			}
		}),
		"G": DateFormat._createPatternSymbol({
			name: "era",
			format: function(oField, oDate, bUTC, oFormat) {
				var iEra = bUTC ? oDate.getUTCEra() : oDate.getEra();
				if (oField.digits <= 3) {
					return oFormat.aErasAbbrev[iEra];
				} else if (oField.digits === 4) {
					return oFormat.aErasWide[iEra];
				} else {
					return oFormat.aErasNarrow[iEra];
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var aErasVariants = [oFormat.aErasWide, oFormat.aErasAbbrev, oFormat.aErasNarrow];

				for (var i = 0; i < aErasVariants.length; i++) {
					var aVariants = aErasVariants[i];
					var oFound = oParseHelper.findEntry(sValue, aVariants);
					if (oFound.index !== -1) {
						return {
							era: oFound.index,
							length: oFound.value.length
						};
					}
				}
				return {
					era: oFormat.aErasWide.length - 1,
					valid: oParseHelper.checkValid(oPart.type, true, oFormat)
				};
			}
		}),
		"y": DateFormat._createPatternSymbol({
			name: "year",
			format: function(oField, oDate, bUTC, oFormat) {
				var iYear = bUTC ? oDate.getUTCFullYear() : oDate.getFullYear();
				var sYear = String(iYear);
				var sCalendarType = oFormat.oFormatOptions.calendarType;

				if (oField.digits == 2 && sYear.length > 2) {
					sYear = sYear.substr(sYear.length - 2);
				}
				// When parsing we assume dates less than 100 to be in the current/last century,
				// so when formatting we have to make sure they are differentiable by prefixing with zeros
				if (sCalendarType != CalendarType.Japanese && oField.digits == 1 && iYear < 100) {
					sYear = sYear.padStart(4, "0");
				}
				return sYear.padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var iExpectedDigits, sPart, bPartInvalid,
					sCalendarType = oFormat.oFormatOptions.calendarType;

				if (oPart.digits == 1) {
					iExpectedDigits = 4;
				} else if (oPart.digits == 2) {
					iExpectedDigits = 2;
				} else {
					iExpectedDigits = oPart.digits;
				}
				sPart = oParseHelper.findNumbers(sValue, iExpectedDigits);
				bPartInvalid = sPart === ""
					|| oConfig.exactLength && sPart.length !== iExpectedDigits;

				var iYear = parseInt(sPart);
				// Find the right century for two-digit years
				// https://tc39.es/ecma262/#sec-date.parse
				// "The function first attempts to parse the String according to the format
				// described in Date Time String Format (https://tc39.es/ecma262/#sec-date-time-string-format),
				// including expanded years.
				// If the String does not conform to that format the function may fall back to
				// any implementation-specific heuristics or implementation-specific date formats."
				//
				// Since a two-digit year is not format conform, each JS implementations might differ.
				// Therefore we provide an own implementation:

				// current year: 1978
				// 1978: 08 = 1908 (diff: -70)
				// 1978: 07 = 2007 (diff: -71)

				// current year: 2018
				// 2018: 48 = 1948 (diff: 30)
				// 2018: 47 = 2047 (diff: 29)
				if (sCalendarType != CalendarType.Japanese && sPart.length <= 2) {
					var oCurrentDate = UniversalDate.getInstance(new Date(), sCalendarType),
						iCurrentYear = oCurrentDate.getFullYear(),
						iCurrentCentury = Math.floor(iCurrentYear / 100),
						iYearDiff = iCurrentCentury * 100 + iYear - iCurrentYear;
					if (iYearDiff < -70) {
						iYear += (iCurrentCentury + 1) * 100;
					} else if (iYearDiff < 30 ) {
						iYear += iCurrentCentury * 100;
					} else {
						iYear += (iCurrentCentury - 1) * 100;
					}
				}
				return {
					length: sPart.length,
					valid: oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat),
					year: iYear
				};
			},
			isNumeric: true
		}),
		"Y": DateFormat._createPatternSymbol({
			name: "weekYear",
			format: function(oField, oDate, bUTC, oFormat) {
				var oWeek = bUTC ? oDate.getUTCWeek(oFormat.oLocale, oFormat.oFormatOptions.calendarWeekNumbering) : oDate.getWeek(oFormat.oLocale, oFormat.oFormatOptions.calendarWeekNumbering);
				var iWeekYear = oWeek.year;
				var sWeekYear = String(iWeekYear);
				var sCalendarType = oFormat.oFormatOptions.calendarType;

				if (oField.digits == 2 && sWeekYear.length > 2) {
					sWeekYear = sWeekYear.substr(sWeekYear.length - 2);
				}
				// When parsing we assume dates less than 100 to be in the current/last century,
				// so when formatting we have to make sure they are differentiable by prefixing with zeros
				if (sCalendarType != CalendarType.Japanese && oField.digits == 1 && iWeekYear < 100) {
					sWeekYear = sWeekYear.padStart(4, "0");
				}
				return sWeekYear.padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var iExpectedDigits, sPart, bPartInvalid,
					sCalendarType = oFormat.oFormatOptions.calendarType;

				if (oPart.digits == 1) {
					iExpectedDigits = 4;
				} else if (oPart.digits == 2) {
					iExpectedDigits = 2;
				} else {
					iExpectedDigits = oPart.digits;
				}
				sPart = oParseHelper.findNumbers(sValue, iExpectedDigits);
				bPartInvalid = sPart === ""
					|| oConfig.exactLength && sPart.length !== iExpectedDigits;

				var iYear = parseInt(sPart);
				var iWeekYear;
				// Find the right century for two-digit years
				if (sCalendarType != CalendarType.Japanese && sPart.length <= 2) {
					var oCurrentDate = UniversalDate.getInstance(new Date(), sCalendarType),
						iCurrentYear = oCurrentDate.getFullYear(),
						iCurrentCentury = Math.floor(iCurrentYear / 100),
						iYearDiff = iCurrentCentury * 100 + iWeekYear - iCurrentYear;
					if (iYearDiff < -70) {
						iWeekYear += (iCurrentCentury + 1) * 100;
					} else if (iYearDiff < 30 ) {
						iWeekYear += iCurrentCentury * 100;
					} else {
						iWeekYear += (iCurrentCentury - 1) * 100;
					}
				}
				return {
					length: sPart.length,
					valid: oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat),
					year: iYear,
					weekYear: iWeekYear
				};
			},
			isNumeric: true
		}),
		"M": DateFormat._createPatternSymbol({
			name: "month",
			format: function(oField, oDate, bUTC, oFormat) {
				var iMonth = bUTC ? oDate.getUTCMonth() : oDate.getMonth();
				if (oField.digits == 3) {
					return oFormat.aMonthsAbbrev[iMonth];
				} else if (oField.digits == 4) {
					return oFormat.aMonthsWide[iMonth];
				} else if (oField.digits > 4) {
					return oFormat.aMonthsNarrow[iMonth];
				} else {
					return String(iMonth + 1).padStart(oField.digits, "0");
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var iMonth, sPart, bPartInvalid, bValid,
					aMonthsVariants = [
						oFormat.aMonthsWide, oFormat.aMonthsWideSt,
						oFormat.aMonthsAbbrev, oFormat.aMonthsAbbrevSt,
						oFormat.aMonthsNarrow, oFormat.aMonthsNarrowSt
					];

				if (oPart.digits < 3) {
					sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2));
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2;
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);
					iMonth = parseInt(sPart) - 1;
					if (oConfig.strict && (iMonth > 11 || iMonth < 0)) {
						bValid = false;
					}
				} else {
					for (var i = 0; i < aMonthsVariants.length; i++) {
						var aVariants = aMonthsVariants[i];
						var oFound = oParseHelper.findEntry(sValue, aVariants);
						if (oFound.index !== -1) {
							return {
								month: oFound.index,
								length: oFound.value.length
							};
						}
					}
					bValid = oParseHelper.checkValid(oPart.type, true, oFormat);
				}
				return {
					month: iMonth,
					length: sPart ? sPart.length : 0,
					valid: bValid
				};
			},
			isNumeric: function (iDigits) {
				return iDigits < 3;
			}
		}),
		"L": DateFormat._createPatternSymbol({
			name: "monthStandalone",
			format: function(oField, oDate, bUTC, oFormat) {
				var iMonth = bUTC ? oDate.getUTCMonth() : oDate.getMonth();
				if (oField.digits == 3) {
					return oFormat.aMonthsAbbrevSt[iMonth];
				} else if (oField.digits == 4) {
					return oFormat.aMonthsWideSt[iMonth];
				} else if (oField.digits > 4) {
					return oFormat.aMonthsNarrowSt[iMonth];
				} else {
					return String(iMonth + 1).padStart(oField.digits, "0");
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var iMonth, sPart, bPartInvalid, bValid,
					aMonthsVariants = [
						oFormat.aMonthsWide, oFormat.aMonthsWideSt,
						oFormat.aMonthsAbbrev, oFormat.aMonthsAbbrevSt,
						oFormat.aMonthsNarrow, oFormat.aMonthsNarrowSt
					];

				if (oPart.digits < 3) {
					sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2));
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2;
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);
					iMonth = parseInt(sPart) - 1;
					if (oConfig.strict && (iMonth > 11 || iMonth < 0)) {
						bValid = false;
					}
				} else {
					for (var i = 0; i < aMonthsVariants.length; i++) {
						var aVariants = aMonthsVariants[i];
						var oFound = oParseHelper.findEntry(sValue, aVariants);
						if (oFound.index !== -1) {
							return {
								month: oFound.index,
								length: oFound.value.length
							};
						}
					}
					bValid = oParseHelper.checkValid(oPart.type, true, oFormat);
				}
				return {
					month: iMonth,
					length: sPart ? sPart.length : 0,
					valid: bValid
				};
			},
			isNumeric: function (iDigits) {
				return iDigits < 3;
			}
		}),
		"w": DateFormat._createPatternSymbol({
			name: "weekInYear",
			format: function(oField, oDate, bUTC, oFormat) {
				var oWeek = bUTC ? oDate.getUTCWeek(oFormat.oLocale, oFormat.oFormatOptions.calendarWeekNumbering) : oDate.getWeek(oFormat.oLocale, oFormat.oFormatOptions.calendarWeekNumbering);
				var iWeek = oWeek.week;
				var sWeek = String(iWeek + 1);
				if (oField.digits < 3) {
					sWeek = sWeek.padStart(oField.digits, "0");
				} else {
					sWeek = oFormat.oLocaleData.getCalendarWeek(oField.digits === 3 ? "narrow" : "wide", sWeek.padStart(2, "0"));
				}
				return sWeek;
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart, bPartInvalid, bValid, iWeek,
					iLength = 0;

				if (oPart.digits < 3) {
					sPart = oParseHelper.findNumbers(sValue, 2);
					iLength = sPart.length;
					iWeek = parseInt(sPart) - 1;
					bPartInvalid = !sPart
						|| oConfig.exactLength && iLength < 2;
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);
				} else {
					sPart = oFormat.oLocaleData.getCalendarWeek(oPart.digits === 3 ? "narrow" : "wide");
					sPart = sPart.replace("{0}", "[0-9]+");
					var rWeekNumber = new RegExp(sPart),
						oResult = rWeekNumber.exec(sValue);
					if (oResult) {
						iLength = oResult[0].length;
						iWeek = parseInt(oResult[0]) - 1;
					} else {
						bValid = oParseHelper.checkValid(oPart.type, true, oFormat);
					}
				}

				return {
					length: iLength,
					valid: bValid,
					week: iWeek
				};
			},
			isNumeric: function (iDigits) {
				return iDigits < 3;
			}
		}),
		"W": DateFormat._createPatternSymbol({
			name: "weekInMonth"
		}),
		"D": DateFormat._createPatternSymbol({
			name: "dayInYear"
		}),
		"d": DateFormat._createPatternSymbol({
			name: "day",
			format: function(oField, oDate, bUTC, oFormat) {
				var iDate = bUTC ? oDate.getUTCDate() : oDate.getDate();
				return String(iDate).padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2)),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2,
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat),
					iDay = parseInt(sPart);

				if (oConfig.strict && (iDay > 31 || iDay < 1)) {
					bValid = false;
				}
				return {
					day: iDay,
					length: sPart.length,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"Q": DateFormat._createPatternSymbol({
			name: "quarter",
			format: function(oField, oDate, bUTC, oFormat) {
				var iQuarter = bUTC ? oDate.getUTCQuarter() : oDate.getQuarter();
				if (oField.digits == 3) {
					return oFormat.aQuartersAbbrev[iQuarter];
				} else if (oField.digits == 4) {
					return oFormat.aQuartersWide[iQuarter];
				} else if (oField.digits > 4) {
					return oFormat.aQuartersNarrow[iQuarter];
				} else {
					return String(iQuarter + 1).padStart(oField.digits, "0");
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart, bPartInvalid, iQuarter, bValid;
				var aQuartersVariants = [oFormat.aQuartersWide, oFormat.aQuartersWideSt, oFormat.aQuartersAbbrev, oFormat.aQuartersAbbrevSt, oFormat.aQuartersNarrow, oFormat.aQuartersNarrowSt];

				if (oPart.digits < 3) {
					sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2));
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2;
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);
					iQuarter = parseInt(sPart) - 1;
					if (oConfig.strict && iQuarter > 3) {
						bValid = false;
					}
				} else {
					for (var i = 0; i < aQuartersVariants.length; i++) {
						var aVariants = aQuartersVariants[i];
						var oFound = oParseHelper.findEntry(sValue, aVariants);
						if (oFound.index !== -1) {
							return {
								quarter: oFound.index,
								length: oFound.value.length
							};
						}
					}

					bValid = oParseHelper.checkValid(oPart.type, true, oFormat);
				}
				return {
					length: sPart ? sPart.length : 0,
					quarter: iQuarter,
					valid: bValid
				};
			},
			isNumeric: function (iDigits) {
				return iDigits < 3;
			}
		}),
		"q": DateFormat._createPatternSymbol({
			name: "quarterStandalone",
			format: function(oField, oDate, bUTC, oFormat) {
				var iQuarter = bUTC ? oDate.getUTCQuarter() : oDate.getQuarter();
				if (oField.digits == 3) {
					return oFormat.aQuartersAbbrevSt[iQuarter];
				} else if (oField.digits == 4) {
					return oFormat.aQuartersWideSt[iQuarter];
				} else if (oField.digits > 4) {
					return oFormat.aQuartersNarrowSt[iQuarter];
				} else {
					return String(iQuarter + 1).padStart(oField.digits, "0");
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart, bPartInvalid, iQuarter, bValid;
				var aQuartersVariants = [oFormat.aQuartersWide, oFormat.aQuartersWideSt, oFormat.aQuartersAbbrev, oFormat.aQuartersAbbrevSt, oFormat.aQuartersNarrow, oFormat.aQuartersNarrowSt];

				if (oPart.digits < 3) {
					sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2));
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2;
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);
					iQuarter = parseInt(sPart) - 1;
					if (oConfig.strict && iQuarter > 3) {
						bValid = false;
					}
				} else {
					for (var i = 0; i < aQuartersVariants.length; i++) {
						var aVariants = aQuartersVariants[i];
						var oFound = oParseHelper.findEntry(sValue, aVariants);
						if (oFound.index !== -1) {
							return {
								quarter: oFound.index,
								length: oFound.value.length
							};
						}
					}

					bValid = oParseHelper.checkValid(oPart.type, true, oFormat);
				}
				return {
					length: sPart ? sPart.length : 0,
					quarter: iQuarter,
					valid: bValid
				};
			},
			isNumeric: function (iDigits) {
				return iDigits < 3;
			}
		}),
		"F": DateFormat._createPatternSymbol({
			name: "dayOfWeekInMonth"
		}),
		"E": DateFormat._createPatternSymbol({
			name: "dayNameInWeek", //Day of week name, format style.
			format: function(oField, oDate, bUTC, oFormat) {
				var iDay = bUTC ? oDate.getUTCDay() : oDate.getDay();
				if (oField.digits < 4) {
					return oFormat.aDaysAbbrev[iDay];
				} else if (oField.digits == 4) {
					return oFormat.aDaysWide[iDay];
				} else if (oField.digits == 5) {
					return oFormat.aDaysNarrow[iDay];
				} else {
					return oFormat.aDaysShort[iDay];
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var aDaysVariants = [oFormat.aDaysWide, oFormat.aDaysWideSt, oFormat.aDaysAbbrev, oFormat.aDaysAbbrevSt, oFormat.aDaysShort, oFormat.aDaysShortSt, oFormat.aDaysNarrow, oFormat.aDaysNarrowSt];

				for (var i = 0; i < aDaysVariants.length; i++) {
					var aVariants = aDaysVariants[i];
					var oFound = oParseHelper.findEntry(sValue, aVariants);
					if (oFound.index !== -1) {
						return {
							// gets translated to dayOfWeek where the day of week is relative to the week
							dayOfWeek: oFound.index,
							length: oFound.value.length
						};
					}
				}
			}
		}),
		"c": DateFormat._createPatternSymbol({
			name: "dayNameInWeekStandalone",
			format: function(oField, oDate, bUTC, oFormat) {
				var iDay = bUTC ? oDate.getUTCDay() : oDate.getDay();
				if (oField.digits < 4) {
					return oFormat.aDaysAbbrevSt[iDay];
				} else if (oField.digits == 4) {
					return oFormat.aDaysWideSt[iDay];
				} else if (oField.digits == 5) {
					return oFormat.aDaysNarrowSt[iDay];
				} else {
					return oFormat.aDaysShortSt[iDay];
				}
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var aDaysVariants = [oFormat.aDaysWide, oFormat.aDaysWideSt, oFormat.aDaysAbbrev, oFormat.aDaysAbbrevSt, oFormat.aDaysShort, oFormat.aDaysShortSt, oFormat.aDaysNarrow, oFormat.aDaysNarrowSt];

				for (var i = 0; i < aDaysVariants.length; i++) {
					var aVariants = aDaysVariants[i];
					var oFound = oParseHelper.findEntry(sValue, aVariants);
					if (oFound.index !== -1) {
						return {
							day: oFound.index,
							length: oFound.value.length
						};
					}
				}
			}
		}),
		"u": DateFormat._createPatternSymbol({
			name: "dayNumberOfWeek",
			format: function(oField, oDate, bUTC, oFormat) {
				var iDay = bUTC ? oDate.getUTCDay() : oDate.getDay();
				return oFormat._adaptDayOfWeek(iDay);
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, oPart.digits),
					bPartInvalid = oConfig.exactLength && sPart.length !== oPart.digits;

				return {
					dayNumberOfWeek: parseInt(sPart),
					length: sPart.length,
					valid: oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat)
				};
			},
			isNumeric: true
		}),
		"a": DateFormat._createPatternSymbol({
			name: "amPmMarker",
			format: function(oField, oDate, bUTC, oFormat) {
				var iDayPeriod = bUTC ? oDate.getUTCDayPeriod() : oDate.getDayPeriod();

				return oFormat.aDayPeriods[iDayPeriod];
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var bPM;
				var iLength;
				var sAM = oFormat.aDayPeriods[0],
					sPM = oFormat.aDayPeriods[1];

				// check whether the value is one of the ASCII variants for AM/PM
				// for example: "am", "a.m.", "am.", "a. m." (and their case variants)
				// if true, remove the '.', the spaces and compare with the defined am/pm case
				// insensitive
				var rAMPM = /[aApP](?:\.)?[\x20\xA0]?[mM](?:\.)?/;
				var aMatch = sValue.match(rAMPM);
				var bVariant = (aMatch && aMatch.index === 0);

				if (bVariant) {
					sValue = aMatch[0];

					// Remove normal and non-breaking spaces
					sAM = sAM.replace(/[\x20\xA0]/g, "");
					sPM = sPM.replace(/[\x20\xA0]/g, "");
					sValue = sValue.replace(/[\x20\xA0]/g, "");

					// Remove dots and make it lowercase
					sAM = sAM.replace(/\./g, "").toLowerCase();
					sPM = sPM.replace(/\./g, "").toLowerCase();
					sValue = sValue.replace(/\./g, "").toLowerCase();
				}
				if (sValue.indexOf(sAM) === 0) {
					bPM = false;
					iLength = (bVariant ? aMatch[0].length : sAM.length);
				} else if (sValue.indexOf(sPM) === 0) {
					bPM = true;
					iLength = (bVariant ? aMatch[0].length : sPM.length);
				}
				return {
					pm: bPM,
					length: iLength
				};
			}
		}),
		"H": DateFormat._createPatternSymbol({
			name: "hour0_23",
			format: function(oField, oDate, bUTC, oFormat) {
				var iHours = bUTC ? oDate.getUTCHours() : oDate.getHours();
				return String(iHours).padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2)),
					iHours = parseInt(sPart),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2,
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);

				if (oConfig.strict && iHours > 23) {
					bValid = false;
				}

				return {
					hour: iHours,
					length: sPart.length,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"k": DateFormat._createPatternSymbol({
			name: "hour1_24",
			format: function(oField, oDate, bUTC, oFormat) {
				var iHours = bUTC ? oDate.getUTCHours() : oDate.getHours();
				var sHours = (iHours === 0 ? "24" : String(iHours));

				return sHours.padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2)),
					iHours = parseInt(sPart),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2,
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);

				if (iHours == 24) {
					iHours = 0;
				}
				if (oConfig.strict && iHours > 23) {
					bValid = false;
				}

				return {
					hour: iHours,
					length: sPart.length,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"K": DateFormat._createPatternSymbol({
			name: "hour0_11",
			format: function(oField, oDate, bUTC, oFormat) {
				var iHours = bUTC ? oDate.getUTCHours() : oDate.getHours();
				var sHours = String(iHours > 11 ? iHours - 12 : iHours);

				return sHours.padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2)),
					iHours = parseInt(sPart),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2,
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);

				if (oConfig.strict && iHours > 11) {
					bValid = false;
				}

				return {
					hour: iHours,
					length: sPart.length,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"h": DateFormat._createPatternSymbol({
			name: "hour1_12",
			format: function(oField, oDate, bUTC, oFormat) {
				var iHours = bUTC ? oDate.getUTCHours() : oDate.getHours();
				var sHours;

				if (iHours > 12) {
					sHours = String(iHours - 12);
				} else if (iHours == 0) {
					sHours = "12";
				} else {
					sHours = String(iHours);
				}
				return sHours.padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var bPM = oConfig.dateValue.pm,
					sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2)),
					iHours = parseInt(sPart),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2,
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);


				if (iHours == 12) {
					iHours = 0;
					// change the PM only when it's not yet parsed
					// 12:00 defaults to 12:00 PM
					 bPM = (bPM === undefined) ? true : bPM;
				}

				if (oConfig.strict && iHours > 11) {
					bValid = false;
				}

				return {
					hour: iHours,
					length: sPart.length,
					pm: bPM,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"m": DateFormat._createPatternSymbol({
			name: "minute",
			format: function(oField, oDate, bUTC, oFormat) {
				var iMinutes = bUTC ? oDate.getUTCMinutes() : oDate.getMinutes();
				return String(iMinutes).padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, Math.max(oPart.digits, 2)),
					iMinutes = parseInt(sPart),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < 2,
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);

				if (oConfig.strict && iMinutes > 59) {
					bValid = false;
				}

				return {
					length: sPart.length,
					minute: iMinutes,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"s": DateFormat._createPatternSymbol({
			name: "second",
			format: function(oField, oDate, bUTC, oFormat) {
				var iSeconds = bUTC ? oDate.getUTCSeconds() : oDate.getSeconds();
				return String(iSeconds).padStart(oField.digits, "0");
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var iExpectedDigits = Math.max(oPart.digits, 2),
					sPart = oParseHelper.findNumbers(sValue, iExpectedDigits),
					bPartInvalid = sPart === ""
						|| oConfig.exactLength && sPart.length < iExpectedDigits,
					iSeconds = parseInt(sPart),
					bValid = oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat);

				if (oConfig.strict && iSeconds > 59) {
					bValid = false;
				}

				return {
					length: sPart.length,
					second: iSeconds,
					valid: bValid
				};
			},
			isNumeric: true
		}),
		"S": DateFormat._createPatternSymbol({
			name: "fractionalsecond",
			format: function(oField, oDate, bUTC, oFormat) {
				var iMilliseconds = bUTC ? oDate.getUTCMilliseconds() : oDate.getMilliseconds();
				var sMilliseconds = String(iMilliseconds);
				var sFractionalseconds = sMilliseconds.padStart(3, "0");
				sFractionalseconds = sFractionalseconds.substr(0, oField.digits);
				sFractionalseconds = sFractionalseconds.padEnd(oField.digits, "0");
				return sFractionalseconds;
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var sPart = oParseHelper.findNumbers(sValue, oPart.digits),
					iLength = sPart.length,
					bPartInvalid = oConfig.exactLength && iLength < oPart.digits;

				sPart = sPart.substr(0, 3);
				sPart = sPart.padEnd(3, "0");

				var iMilliseconds = parseInt(sPart);

				return {
					length: iLength,
					millisecond: iMilliseconds,
					valid: oParseHelper.checkValid(oPart.type, bPartInvalid, oFormat)
				};
			},
			isNumeric: true
		}),
		"z": DateFormat._createPatternSymbol({
			name: "timezoneGeneral",
			format: function(oField, oDate, bUTC, oFormat) {
				//TODO getTimezoneLong and getTimezoneShort does not exist on Date object
				//-> this is a preparation for a future full timezone support (only used by unit test so far)
				if (oField.digits > 3 && oDate.getTimezoneLong && oDate.getTimezoneLong()) {
					return oDate.getTimezoneLong();
				} else if (oDate.getTimezoneShort && oDate.getTimezoneShort()) {
					return oDate.getTimezoneShort();
				}

				// valid for zzzz (fallback to OOOO)

				var sTimeZone = "GMT";
				var iTZOffset = Math.abs(oDate.getTimezoneOffset());
				var bPositiveOffset = oDate.getTimezoneOffset() > 0;
				var iHourOffset = Math.floor(iTZOffset / 60);
				var iMinuteOffset = iTZOffset % 60;

				if (!bUTC && iTZOffset != 0) {
					sTimeZone += (bPositiveOffset ? "-" : "+");
					sTimeZone += String(iHourOffset).padStart(2, "0");
					sTimeZone += ":";
					sTimeZone += String(iMinuteOffset).padStart(2, "0");
				} else {
					sTimeZone += "Z";
				}

				return sTimeZone;
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				var iLength = 0;
				var iTZDiff;
				var oTZ = sValue.substring(0, 3);

				if (oTZ === "GMT" || oTZ === "UTC") {
					iLength = 3;
				} else if (sValue.substring(0, 2) === "UT") {
					iLength = 2;
				} else if (sValue.charAt(0) === "Z") {
					iLength = 1;
					iTZDiff = 0;
				} else {
					return {
						error: "cannot be parsed correcly by sap.ui.core.format.DateFormat: The given timezone is not supported!"
					};
				}

				if (sValue.charAt(0) !== "Z") {
					var oParsedTZ = oParseHelper.parseTZ(sValue.substr(iLength), true);

					iLength += oParsedTZ.length;
					iTZDiff = oParsedTZ.tzDiff;
				}

				return {
					length: iLength,
					tzDiff: iTZDiff
				};
			}
		}),
		"Z": DateFormat._createPatternSymbol({
			name: "timezoneRFC822",
			format: function(oField, oDate, bUTC, oFormat) {
				var iTZOffset = Math.abs(oDate.getTimezoneOffset());
				var bPositiveOffset = oDate.getTimezoneOffset() > 0;
				var iHourOffset = Math.floor(iTZOffset / 60);
				var iMinuteOffset = iTZOffset % 60;
				var sTimeZone = "";

				// valid for Z-ZZZ
				// per RFC822 a timezone always has 4 digits
				// UTC+0: "+0000"
				// UTC-7: "-0700"
				// UTC+2: "+0200"
				// https://tools.ietf.org/html/rfc822 paragraph 5.1
				if (!bUTC) {
					sTimeZone += (bPositiveOffset ? "-" : "+");
					sTimeZone += String(iHourOffset).padStart(2, "0");
					sTimeZone += String(iMinuteOffset).padStart(2, "0");
				}

				return sTimeZone;
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				return oParseHelper.parseTZ(sValue, false);
			}
		}),
		"X": DateFormat._createPatternSymbol({
			name: "timezoneISO8601",
			format: function(oField, oDate, bUTC, oFormat) {
				/*
				 * Mountain Standard Time (MST, UTC-7)
				 * X:           "-07"
				 * XX, XXXX:    "-0700"
				 * XXX, XXXXX:  "-07:00"
				 */

				/*
				 * Central European Summer Time (CEST, UTC+2)
				 * X:           "+02"
				 * XX, XXXX:    "+0200"
				 * XXX, XXXXX:  "+02:00"
				 */

				/*
				 * Indian Standard Time (IST, UTC+5:30)
				 * X:           "+0530"
				 * XX, XXXX:    "+0530"
				 * XXX, XXXXX:  "+05:30"
				 */

				/*
				 * Greenwich Mean Time (GMT, UTC+0)
				 * X:           "Z"
				 * XX, XXXX:    "Z"
				 * XXX, XXXXX:  "Z"
				 */

				// @see http://www.unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Goals
				var iTZOffset = Math.abs(oDate.getTimezoneOffset());
				var bPositiveOffset = oDate.getTimezoneOffset() > 0;
				var iHourOffset = Math.floor(iTZOffset / 60);
				var iMinuteOffset = iTZOffset % 60;

				var sTimeZone = "";
				if (!bUTC && iTZOffset != 0) {
					sTimeZone += (bPositiveOffset ? "-" : "+");
					sTimeZone += String(iHourOffset).padStart(2, "0");
					if (oField.digits > 1 || iMinuteOffset > 0) {
						if (oField.digits === 3 || oField.digits === 5) {
							sTimeZone += ":";
						}
						sTimeZone += String(iMinuteOffset).padStart(2, "0");
					}
				} else {
					sTimeZone += "Z";
				}

				return sTimeZone;
			},
			parse: function(sValue, oPart, oFormat, oConfig) {
				if (sValue.charAt(0) === "Z") {
					return {
						length: 1,
						tzDiff: 0
					};
				} else {
					return oParseHelper.parseTZ(sValue, oPart.digits === 3 || oPart.digits === 5);
				}
			}
		})
	};

	DateFormat.prototype._format = function(oJSDate, bUTC) {
		if (this.oFormatOptions.relative) {
			var sRes = this.formatRelative(oJSDate, bUTC, this.oFormatOptions.relativeRange);
			if (sRes) { //Stop when relative formatting possible, else go on with standard formatting
				return sRes;
			}
		}

		var sCalendarType = this.oFormatOptions.calendarType;
		var oDate = UniversalDate.getInstance(oJSDate, sCalendarType);

		var aBuffer = [], oPart, sResult, sSymbol;

		for (var i = 0; i < this.aFormatArray.length; i++) {
			oPart = this.aFormatArray[i];
			sSymbol = oPart.symbol || "";

			aBuffer.push(this.oSymbols[sSymbol].format(oPart, oDate, bUTC, this));
		}

		sResult = aBuffer.join("");

		if (sap.ui.getCore().getConfiguration().getOriginInfo()) {
			sResult = new String(sResult);
			sResult.originInfo = {
				source: "Common Locale Data Repository",
				locale: this.oLocale.toString(),
				style: this.oFormatOptions.style,
				pattern: this.oFormatOptions.pattern
			};
		}

		return sResult;
	};

	/**
	 * Format a date according to the given format options.
	 *
	 * @param {Date|Date[]} vJSDate the value to format
	 * @param {boolean} bUTC whether to use UTC
	 * @return {string} the formatted output value. If an invalid date is given, an empty string is returned.
	 * @public
	 */
	DateFormat.prototype.format = function(vJSDate, bUTC) {
		var sCalendarType = this.oFormatOptions.calendarType,
			sResult;

		if (bUTC === undefined) {
			bUTC = this.oFormatOptions.UTC;
		}

		if (Array.isArray(vJSDate)) {
			if (!this.oFormatOptions.interval) {
				Log.error("Non-interval DateFormat can't format more than one date instance.");
				return "";
			}

			if (vJSDate.length !== 2) {
				Log.error("Interval DateFormat can only format with 2 date instances but " + vJSDate.length + " is given.");
				return "";
			}

			if (this.oFormatOptions.singleIntervalValue) {
				if (vJSDate[0] === null) {
					Log.error("First date instance which is passed to the interval DateFormat shouldn't be null.");
					return "";
				}

				if (vJSDate[1] === null) {
					sResult = this._format(vJSDate[0], bUTC);
				}
			}

			if (sResult === undefined) {
				var bValid = vJSDate.every(function(oJSDate) {
					return oJSDate && !isNaN(oJSDate.getTime());
				});

				if (!bValid) {
					Log.error("At least one date instance which is passed to the interval DateFormat isn't valid.");
					return "";
				}

				sResult = this._formatInterval(vJSDate, bUTC);
			}
		} else {
			if (!vJSDate || isNaN(vJSDate.getTime())) {
				Log.error("The given date instance isn't valid.");
				return "";
			}

			if (this.oFormatOptions.interval) {
				Log.error("Interval DateFormat expects an array with two dates for the first argument but only one date is given.");
				return "";
			}

			sResult = this._format(vJSDate, bUTC);
		}

		// Support Japanese Gannen instead of Ichinen for first year of the era
		if (sCalendarType === CalendarType.Japanese && this.oLocale.getLanguage() === "ja") {
			sResult = sResult.replace(/(^|\D)1\u5e74/g, "$1\u5143\u5e74");
		}

		return sResult;
	};

	DateFormat.prototype._formatInterval = function(aJSDates, bUTC) {
		var sCalendarType = this.oFormatOptions.calendarType;
		var oFromDate = UniversalDate.getInstance(aJSDates[0], sCalendarType);
		var oToDate = UniversalDate.getInstance(aJSDates[1], sCalendarType);
		var oDate;
		var oPart;
		var sSymbol;
		var aBuffer = [];
		var sPattern;
		var aFormatArray = [];

		var oDiffField = this._getGreatestDiffField([oFromDate, oToDate], bUTC);

		if (!oDiffField) {
			return this._format(aJSDates[0], bUTC);
		}

		if (this.oFormatOptions.format) {
			// when 'format' option is set, generate the pattern based on the greatest difference
			sPattern = this.oLocaleData.getCustomIntervalPattern(this.oFormatOptions.format, oDiffField, sCalendarType);

		} else {
			sPattern = this.oLocaleData.getCombinedIntervalPattern(this.oFormatOptions.pattern, sCalendarType);
		}

		aFormatArray = this.parseCldrDatePattern(sPattern);

		oDate = oFromDate;
		for (var i = 0; i < aFormatArray.length; i++) {
			oPart = aFormatArray[i];
			sSymbol = oPart.symbol || "";

			if (oPart.repeat) {
				oDate = oToDate;
			}

			aBuffer.push(this.oSymbols[sSymbol].format(oPart, oDate, bUTC, this));
		}

		return aBuffer.join("");
	};

	var mFieldToGroup = {
		Era: "Era",
		FullYear: "Year",
		Quarter: "Quarter",
		Month: "Month",
		Week: "Week",
		Date: "Day",
		DayPeriod: "DayPeriod",
		Hours: "Hour",
		Minutes: "Minute",
		Seconds: "Second"
	};

	DateFormat.prototype._getGreatestDiffField = function(aDates, bUTC) {
		var bDiffFound = false,
			mDiff = {};

		this.aIntervalCompareFields.forEach(function(sField) {
			var sGetterPrefix = "get" + (bUTC ? "UTC" : ""),
				sMethodName = sGetterPrefix + sField,
				sFieldGroup = mFieldToGroup[sField],
				vFromValue = aDates[0][sMethodName].apply(aDates[0]),
				vToValue = aDates[1][sMethodName].apply(aDates[1]);

			if (!deepEqual(vFromValue, vToValue)) {
				bDiffFound = true;
				mDiff[sFieldGroup] = true;
			}
		});

		if (bDiffFound) {
			return mDiff;
		}

		return null;
	};

	DateFormat.prototype._parse = function(sValue, aFormatArray, bUTC, bStrict) {
		var oNextPart, oPart, oPrevPart, oResult, sSubValue,
			oDateValue = {
				valid: true
			},
			iIndex = 0,
			oParseConf = {
				formatArray: aFormatArray,
				dateValue: oDateValue,
				strict: bStrict
			},
			that = this;

		function getSymbol(oPart0) { return that.oSymbols[oPart0.symbol || ""]; }
		function isNumeric(oPart0) { return !!oPart0 && getSymbol(oPart0).isNumeric(oPart0.digits); }

		for (var i = 0; i < aFormatArray.length; i++) {
			sSubValue = sValue.substr(iIndex);
			oPart = aFormatArray[i];
			oPrevPart = aFormatArray[i - 1];
			oNextPart = aFormatArray[i + 1];
			oParseConf.index = i;
			oParseConf.exactLength = isNumeric(oPart) && (isNumeric(oPrevPart) || isNumeric(oNextPart));

			oResult = getSymbol(oPart).parse(sSubValue, oPart, this, oParseConf) || {};

			oDateValue = extend(oDateValue, oResult);

			if (oResult.valid === false) {
				break;
			}

			iIndex += oResult.length || 0;
		}

		oDateValue.index = iIndex;

		if (oDateValue.pm) {
			oDateValue.hour += 12;
		}

		// use dayOfWeek (E) as dayNumberOfWeek (u) if dayNumberOfWeek (u) is not present
		if (oDateValue.dayNumberOfWeek === undefined && oDateValue.dayOfWeek !== undefined) {
			oDateValue.dayNumberOfWeek = this._adaptDayOfWeek(oDateValue.dayOfWeek);
		}

		if (oDateValue.quarter !== undefined && oDateValue.month === undefined && oDateValue.day === undefined) {
			oDateValue.month = 3 * oDateValue.quarter;
			oDateValue.day = 1;
		}

		return oDateValue;
	};

	DateFormat.prototype._parseInterval = function(sValue, sCalendarType, bUTC, bStrict) {
		var aDateValues,
			iRepeat,
			oDateValue;

		// Try out with all possible patterns until succesfully parse has been done or the end of the array is reached
		this.intervalPatterns.some(function(sPattern) {
			var aFormatArray = this.parseCldrDatePattern(sPattern);

			iRepeat = undefined;

			// loop through aFormatArray until we have found the repeated date symbol and get the index
			for (var i = 0; i < aFormatArray.length; i++) {
				if (aFormatArray[i].repeat) {
					iRepeat = i;
					break;
				}
			}
			if (iRepeat === undefined) {
				// In case of standard date pattern, parse string as single date and put the same date twice into the aDateValues array
				oDateValue = this._parse(sValue, aFormatArray, bUTC, bStrict);

				// If input value has not been completely parsed, mark it as invalid
				if (oDateValue.index === 0 || oDateValue.index < sValue.length) {
					oDateValue.valid = false;
				}

				if (oDateValue.valid === false) {
					return;
				}

				aDateValues = [oDateValue, oDateValue];

				return true;
			} else {
				aDateValues = [];

				// Call _parse function with start 0 and end index of repeated symbol
				oDateValue = this._parse(sValue, aFormatArray.slice(0, iRepeat), bUTC, bStrict);

				if (oDateValue.valid === false) {
					return;
				}
				aDateValues.push(oDateValue);

				var iLength = oDateValue.index;

				// Call _parse function with start iRepeat and end of array
				oDateValue = this._parse(sValue.substring(iLength), aFormatArray.slice(iRepeat), bUTC, bStrict);

				// If input value has not been completely parsed, mark it as invalid
				if (oDateValue.index === 0 || oDateValue.index + iLength < sValue.length) {
					oDateValue.valid = false;
				}

				if (oDateValue.valid === false) {
					return;
				}
				aDateValues.push(oDateValue);

				return true;
			}
		}.bind(this));

		return aDateValues;
	};

	// recreate javascript date object from the given oDateValues.
	// In case of oDateValue.valid == false, null value will be returned
	var fnCreateDate = function(oDateValue, sCalendarType, bUTC, bStrict, oFormatOptions, oLocale) {
		var oDate,
			iYear = typeof oDateValue.year === "number" ? oDateValue.year : 1970;

		if (oDateValue.valid) {
			if (bUTC || oDateValue.tzDiff !== undefined) {
				oDate = UniversalDate.getInstance(new Date(0), sCalendarType);
				oDate.setUTCEra(oDateValue.era || UniversalDate.getCurrentEra(sCalendarType));
				oDate.setUTCFullYear(iYear);
				oDate.setUTCMonth(oDateValue.month || 0);
				oDate.setUTCDate(oDateValue.day || 1);
				oDate.setUTCHours(oDateValue.hour || 0);
				oDate.setUTCMinutes(oDateValue.minute || 0);
				oDate.setUTCSeconds(oDateValue.second || 0);
				oDate.setUTCMilliseconds(oDateValue.millisecond || 0);
				if (bStrict && (oDateValue.day || 1) !== oDate.getUTCDate()) {
					// check if valid date given - if invalid, day is not the same (31.Apr -> 1.May)
					oDateValue.valid = false;
					oDate = undefined;
				} else {
					if (oDateValue.tzDiff) {
						// Set TZDiff after checking for valid day, as it may switch the day as well
						oDate.setUTCMinutes((oDateValue.minute || 0) + oDateValue.tzDiff);
					}
					if (oDateValue.week !== undefined  && (oDateValue.month === undefined || oDateValue.day === undefined)) {
						//check that the week is only set if the day/month has not been set, because day/month have higher precedence than week
						oDate.setUTCWeek({
							year: oDateValue.weekYear || oDateValue.year,
							week: oDateValue.week
						}, oLocale, oFormatOptions.calendarWeekNumbering);

						// add the dayNumberOfWeek to the current day
						if (oDateValue.dayNumberOfWeek !== undefined) {
							oDate.setUTCDate(oDate.getUTCDate() + oDateValue.dayNumberOfWeek - 1);
						}
					}
				}
			} else {
				oDate = UniversalDate.getInstance(new Date(1970, 0, 1, 0, 0, 0), sCalendarType);
				oDate.setEra(oDateValue.era || UniversalDate.getCurrentEra(sCalendarType));
				oDate.setFullYear(iYear);
				oDate.setMonth(oDateValue.month || 0);
				oDate.setDate(oDateValue.day || 1);
				oDate.setHours(oDateValue.hour || 0);
				oDate.setMinutes(oDateValue.minute || 0);
				oDate.setSeconds(oDateValue.second || 0);
				oDate.setMilliseconds(oDateValue.millisecond || 0);
				if (bStrict && (oDateValue.day || 1) !== oDate.getDate()) {
					// check if valid date given - if invalid, day is not the same (31.Apr -> 1.May)
					oDateValue.valid = false;
					oDate = undefined;
				} else if (oDateValue.week !== undefined && (oDateValue.month === undefined || oDateValue.day === undefined)) {
					//check that the week is only set if the day/month has not been set, because day/month have higher precedence than week
					oDate.setWeek({
						year: oDateValue.weekYear || oDateValue.year,
						week: oDateValue.week
					}, oLocale, oFormatOptions.calendarWeekNumbering);

					//add the dayNumberOfWeek to the current day
					if (oDateValue.dayNumberOfWeek !== undefined) {
						oDate.setDate(oDate.getDate() + oDateValue.dayNumberOfWeek - 1);
					}
				}
			}

			if (oDateValue.valid) {
				oDate = oDate.getJSDate();
				return oDate;
			}
		}

		return null;
	};

	// Copy the properties of object2 into object1 without
	// overwriting the existing properties in object1
	function mergeWithoutOverwrite(object1, object2) {
		if (object1 === object2) {
			return object1;
		}

		var oMergedObject = {};

		// Clone object1
		Object.keys(object1).forEach(function(sKey) {
			oMergedObject[sKey] = object1[sKey];
		});

		// merge
		Object.keys(object2).forEach(function(sKey) {
			if (!oMergedObject.hasOwnProperty(sKey)) {
				oMergedObject[sKey] = object2[sKey];
			}
		});

		return oMergedObject;
	}

	// Checks if the given start date is before the end date.
	function isValidDateRange(oStartDate, oEndDate) {
		if (oStartDate.getTime() > oEndDate.getTime()) {
			return false;
		}

		return true;
	}

	/**
	 * Parse a string which is formatted according to the given format options.
	 *
	 * @param {string} sValue the string containing a formatted date/time value
	 * @param {boolean} bUTC whether to use UTC, if no timezone is contained
	 * @param {boolean} bStrict to use strict value check
	 * @return {Date|Date[]} the parsed value(s)
	 * @public
	 */
	DateFormat.prototype.parse = function(sValue, bUTC, bStrict) {
		sValue = sValue == null ? "" : String(sValue).trim();

		var oDateValue;
		var sCalendarType = this.oFormatOptions.calendarType;

		if (bUTC === undefined) {
			bUTC = this.oFormatOptions.UTC;
		}

		if (bStrict === undefined) {
			bStrict = this.oFormatOptions.strictParsing;
		}

		// Support Japanese Gannen instead of Ichinen for first year of the era
		if (sCalendarType === CalendarType.Japanese && this.oLocale.getLanguage() === "ja") {
			sValue = sValue.replace(/\u5143\u5e74/g, "1\u5e74");
		}

		if (!this.oFormatOptions.interval) {
			var oJSDate = this.parseRelative(sValue, bUTC);
			if (oJSDate) { //Stop when relative parsing possible, else go on with standard parsing
				return oJSDate;
			}

			oDateValue = this._parse(sValue, this.aFormatArray, bUTC, bStrict);

			// If input value has not been completely parsed, mark it as invalid
			if (oDateValue.index === 0 || oDateValue.index < sValue.length) {
				oDateValue.valid = false;
			}


			oJSDate = fnCreateDate(oDateValue, sCalendarType, bUTC, bStrict, this.oFormatOptions, this.oLocale);

			if (oJSDate) {
				return oJSDate;
			}

		} else {
			var aDateValues = this._parseInterval(sValue, sCalendarType, bUTC, bStrict);
			var oJSDate1, oJSDate2;

			if (aDateValues && aDateValues.length == 2) {
				var oDateValue1 = mergeWithoutOverwrite(aDateValues[0], aDateValues[1]);
				var oDateValue2 = mergeWithoutOverwrite(aDateValues[1], aDateValues[0]);


				oJSDate1 = fnCreateDate(oDateValue1, sCalendarType, bUTC, bStrict, this.oFormatOptions, this.oLocale);
				oJSDate2 = fnCreateDate(oDateValue2, sCalendarType, bUTC, bStrict, this.oFormatOptions, this.oLocale);


				if (oJSDate1 && oJSDate2) {

					if (this.oFormatOptions.singleIntervalValue
						&& oJSDate1.getTime() === oJSDate2.getTime()) {

						return [oJSDate1, null];
					}

					var bValid = isValidDateRange(oJSDate1, oJSDate2);

					if (bStrict && !bValid) {
						Log.error("StrictParsing: Invalid date range. The given end date is before the start date.");
						return [null, null];
					}

					return [oJSDate1, oJSDate2];
				}
			}
		}

		if (!this.bIsFallback) {
			var vDate;

			this.aFallbackFormats.every(function(oFallbackFormat) {
				vDate = oFallbackFormat.parse(sValue, bUTC, bStrict);

				if (Array.isArray(vDate)) {
					return !(vDate[0] && vDate[1]);
				} else {
					return !vDate;
				}
			});

			return vDate;
		}

		if (!this.oFormatOptions.interval) {
			return null;
		} else {
			return [null, null];
		}
	};


	/**
	 * Parse the date pattern string and create a format array from it, which can be
	 * used for parsing and formatting the date
	 *
	 * @param {string} sPattern the CLDR date pattern string
	 * @returns {Array} format array
	 */
	DateFormat.prototype.parseCldrDatePattern = function(sPattern) {
		if (mCldrDatePattern[sPattern]) {
			return mCldrDatePattern[sPattern];
		}

		var aFormatArray = [],
			i,
			bQuoted = false,
			oCurrentObject = null,
			sState = "",
			sNewState = "",
			mAppeared = {},
			bIntervalStartFound = false;


		for (i = 0; i < sPattern.length; i++) {
			var sCurChar = sPattern.charAt(i), sNextChar, sPrevChar, sPrevPrevChar;
			if (bQuoted) {
				if (sCurChar == "'") {
					sPrevChar = sPattern.charAt(i - 1);
					sPrevPrevChar = sPattern.charAt(i - 2);
					sNextChar = sPattern.charAt(i + 1);
					// handle abc''def correctly
					if (sPrevChar == "'" && sPrevPrevChar != "'") {
						bQuoted = false;
					} else if (sNextChar == "'") {
						// handle 'abc''def' correctly

						i += 1;
					} else {
						//  normal quote 'abcdef'
						bQuoted = false;
						continue;
					}
				}
				if (sState == "text") {
					oCurrentObject.value += sCurChar;
				} else {
					oCurrentObject = {
						type: "text",
						value: sCurChar
					};
					aFormatArray.push(oCurrentObject);
					sState = "text";
				}

			} else {
				if (sCurChar == "'") {
					bQuoted = true;
				} else if (this.oSymbols[sCurChar]) {
					sNewState = this.oSymbols[sCurChar].name;
					if (sState == sNewState) {
						oCurrentObject.digits++;
					} else {
						oCurrentObject = {
							type: sNewState,
							symbol: sCurChar,
							digits: 1
						};
						aFormatArray.push(oCurrentObject);
						sState = sNewState;

						if (!bIntervalStartFound) {
							if (mAppeared[sNewState]) {
								oCurrentObject.repeat = true;
								bIntervalStartFound = true;
							} else {
								mAppeared[sNewState] = true;
							}
						}

					}
				} else {
					if (sState == "text") {
						oCurrentObject.value += sCurChar;
					} else {
						oCurrentObject = {
							type: "text",
							value: sCurChar
						};
						aFormatArray.push(oCurrentObject);
						sState = "text";
					}
				}
			}

		}

		mCldrDatePattern[sPattern] = aFormatArray;

		return aFormatArray;
	};

	/**
	 * Parse a date string relative to the current date.
	 *
	 * @param {string} sValue the string containing a formatted date/time value
	 * @param {boolean} bUTC whether to use UTC, if no timezone is contained
	 * @param {boolean} bStrict to use strict value check
	 * @return {Date} the parsed value or null if relative parsing not possible
	 * @private
	 */
	DateFormat.prototype.parseRelative = function(sValue, bUTC) {
		var aPatterns, oEntry, rPattern, oResult, iValue;

		if (!sValue) {
			return null;
		}

		aPatterns = this.oLocaleData.getRelativePatterns(this.aRelativeParseScales, this.oFormatOptions.relativeStyle);
		for (var i = 0; i < aPatterns.length; i++) {
			oEntry = aPatterns[i];
			rPattern = new RegExp("^\\s*" + oEntry.pattern.replace(/\{0\}/, "(\\d+)") + "\\s*$", "i");
			oResult = rPattern.exec(sValue);
			if (oResult) {
				if (oEntry.value !== undefined) {
					return computeRelativeDate(oEntry.value, oEntry.scale);
				} else {
					iValue = parseInt(oResult[1]);
					return computeRelativeDate(iValue * oEntry.sign, oEntry.scale);
				}
			}
		}

		function computeRelativeDate(iDiff, sScale){
			var iToday,
				oToday = new Date(),
				oJSDate;

			if (bUTC) {
				iToday = oToday.getTime();
			} else {
				iToday = Date.UTC(oToday.getFullYear(), oToday.getMonth(), oToday.getDate(), oToday.getHours(), oToday.getMinutes(), oToday.getSeconds(), oToday.getMilliseconds());
			}

			oJSDate = new Date(iToday);

			switch (sScale) {
				case "second": oJSDate.setUTCSeconds(oJSDate.getUTCSeconds() + iDiff); break;
				case "minute": oJSDate.setUTCMinutes(oJSDate.getUTCMinutes() + iDiff); break;
				case "hour": oJSDate.setUTCHours(oJSDate.getUTCHours() + iDiff); break;
				case "day": oJSDate.setUTCDate(oJSDate.getUTCDate() + iDiff); break;
				case "week": oJSDate.setUTCDate(oJSDate.getUTCDate() + iDiff * 7); break;
				case "month": oJSDate.setUTCMonth(oJSDate.getUTCMonth() + iDiff); break;
				case "quarter": oJSDate.setUTCMonth(oJSDate.getUTCMonth() + iDiff * 3); break;
				case "year": oJSDate.setUTCFullYear(oJSDate.getUTCFullYear() + iDiff); break;
			}

			if (bUTC) {
				return oJSDate;
			} else {
				return new Date(oJSDate.getUTCFullYear(), oJSDate.getUTCMonth(), oJSDate.getUTCDate(), oJSDate.getUTCHours(), oJSDate.getUTCMinutes(), oJSDate.getUTCSeconds(), oJSDate.getUTCMilliseconds());
			}
		}
	};

	/**
	 * Format a date relative to the current date.
	 *
	 * @param {Date} oJSDate the value to format
	 * @param {boolean} bUTC whether to use UTC
	 * @return {string} the formatted output value or null if relative formatting not possible
	 * @private
	 */
	DateFormat.prototype.formatRelative = function(oJSDate, bUTC, aRange) {

		var oToday = new Date(), oDateUTC,
			sScale = this.oFormatOptions.relativeScale || "day",
			iDiff, sPattern, iDiffSeconds;

		iDiffSeconds = (oJSDate.getTime() - oToday.getTime()) / 1000;
		if (this.oFormatOptions.relativeScale == "auto") {
			sScale = this._getScale(iDiffSeconds, this.aRelativeScales);
		}

		if (!aRange) {
			aRange = this._mRanges[sScale];
		}

		// For dates normalize to UTC to avoid issues with summer-/wintertime
		if (sScale == "year" || sScale == "month" || sScale == "day") {
			oToday = new Date(Date.UTC(oToday.getFullYear(), oToday.getMonth(), oToday.getDate()));

			oDateUTC = new Date(0);

			if (bUTC) {
				// The Date.UTC function doesn't accept years before 1900 (converts years before 100 into 1900 + years).
				// Using setUTCFullYear to workaround this issue.
				oDateUTC.setUTCFullYear(oJSDate.getUTCFullYear(), oJSDate.getUTCMonth(), oJSDate.getUTCDate());
			} else {
				oDateUTC.setUTCFullYear(oJSDate.getFullYear(), oJSDate.getMonth(), oJSDate.getDate());
			}

			oJSDate = oDateUTC;
		}

		iDiff = this._getDifference(sScale, [oToday, oJSDate]);

		if (this.oFormatOptions.relativeScale != "auto" && (iDiff < aRange[0] || iDiff > aRange[1])) {
			//Relative parsing only in range +/- x days
			return null;
		}

		sPattern = this.oLocaleData.getRelativePattern(sScale, iDiff, iDiffSeconds > 0, this.oFormatOptions.relativeStyle);
		return formatMessage(sPattern, [Math.abs(iDiff)]);

	};

	DateFormat.prototype._mRanges = {
		second: [-60, 60],
		minute: [-60, 60],
		hour: [-24, 24],
		day: [-6, 6],
		week: [-4, 4],
		month: [-12, 12],
		year: [-10, 10]
	};

	DateFormat.prototype._mScales = {
		second: 1,
		minute: 60,
		hour: 3600,
		day: 86400,
		week: 604800,
		month: 2592000,
		quarter: 7776000,
		year: 31536000
	};

	DateFormat.prototype._getScale = function(iDiffSeconds, aScales) {
		// Determines the correct time scale
		var	sScale, sTestScale;

		iDiffSeconds = Math.abs(iDiffSeconds);

		for (var i = 0; i < aScales.length; i++) {
			sTestScale = aScales[i];
			if (iDiffSeconds >= this._mScales[sTestScale]) {
				sScale = sTestScale;
				break;
			}
		}
		if (!sScale) {
			sScale = aScales[aScales.length - 1];
		}

		return sScale;
	};

	/**
	 * Modifies the Date and sets the values with a higher index to <code>0</code>
	 *
	 * @param {Date} oDate input date
	 * @param {number} iStartIndex index of the value to set to <code>0</code>. Higher indices will also be set to <code>0</code>.
	 * 0: FullYear
	 * 1: Month
	 * 2: Date
	 * 3: Hours
	 * 4: Minutes
	 * 5: Seconds
	 * 6: Milliseconds
	 * e.g. iStartIndex <code>4</code> will set Minutes, Seconds and Milliseconds to <code>0</code>
	 * @returns {Date} copy of the date with the modified values
	 */
	function cutDateFields(oDate, iStartIndex) {
		var aFields = [
			"FullYear",
			"Month",
			"Date",
			"Hours",
			"Minutes",
			"Seconds",
			"Milliseconds"
		], sMethodName;
		var oDateCopy = new Date(oDate.getTime());

		for (var i = iStartIndex; i < aFields.length; i++) {
			sMethodName = "set" + aFields[iStartIndex];
			oDateCopy[sMethodName].apply(oDateCopy, [0]);
		}
		return oDateCopy;
	}

	var mRelativeDiffs = {
		year: function(oFromDate, oToDate) {
			return oToDate.getFullYear() - oFromDate.getFullYear();
		},
		month: function(oFromDate, oToDate) {
			return oToDate.getMonth() - oFromDate.getMonth() + (this.year(oFromDate, oToDate) * 12);
		},
		week: function(oFromDate, oToDate, oFormat) {
			var iFromDay = oFormat._adaptDayOfWeek(oFromDate.getDay());
			var iToDay = oFormat._adaptDayOfWeek(oToDate.getDay());

			oFromDate = cutDateFields(oFromDate, 3);
			oToDate = cutDateFields(oToDate, 3);

			return (oToDate.getTime() - oFromDate.getTime() - (iToDay - iFromDay) * oFormat._mScales.day * 1000) / (oFormat._mScales.week * 1000);
		},
		day: function(oFromDate, oToDate, oFormat) {
			oFromDate = cutDateFields(oFromDate, 3);
			oToDate = cutDateFields(oToDate, 3);

			return (oToDate.getTime() - oFromDate.getTime()) / (oFormat._mScales.day * 1000);
		},
		hour: function(oFromDate, oToDate, oFormat) {
			oFromDate = cutDateFields(oFromDate, 4);
			oToDate = cutDateFields(oToDate, 4);

			return (oToDate.getTime() - oFromDate.getTime()) / (oFormat._mScales.hour * 1000);
		},
		minute: function(oFromDate, oToDate, oFormat) {
			oFromDate = cutDateFields(oFromDate, 5);
			oToDate = cutDateFields(oToDate, 5);

			return (oToDate.getTime() - oFromDate.getTime()) / (oFormat._mScales.minute * 1000);
		},
		second: function(oFromDate, oToDate, oFormat) {
			oFromDate = cutDateFields(oFromDate, 6);
			oToDate = cutDateFields(oToDate, 6);

			return (oToDate.getTime() - oFromDate.getTime()) / (oFormat._mScales.second * 1000);
		}
	};

	DateFormat.prototype._adaptDayOfWeek = function(iDayOfWeek) {
		// day of week depends on the format locale
		// the DateFormat's locale is independent
		var sCalendarWeekParameter = this.oFormatOptions.calendarWeekNumbering;
		var iFirstDayOfWeek = CalendarUtils.getWeekConfigurationValues(sCalendarWeekParameter, this.oLocale).firstDayOfWeek;

		var iDayNumberOfWeek = iDayOfWeek - (iFirstDayOfWeek - 1);

		if (iDayNumberOfWeek <= 0) {
			iDayNumberOfWeek += 7;
		}
		return iDayNumberOfWeek;
	};

	DateFormat.prototype._getDifference = function(sScale, aDates) {
		var oFromDate = aDates[0];
		var oToDate = aDates[1];

		return Math.round(mRelativeDiffs[sScale](oFromDate, oToDate, this));
	};


	DateFormat.prototype.getAllowedCharacters = function(aFormatArray) {

		if (this.oFormatOptions.relative) {
			return ""; //Allow all
		}

		var sAllowedCharacters = "";
		var bNumbers = false;
		var bAll = false;
		var oPart;

		for (var i = 0; i < aFormatArray.length; i++) {
			oPart = aFormatArray[i];
			switch (oPart.type) {
			case "text":
				if (sAllowedCharacters.indexOf(oPart.value) < 0) {
					sAllowedCharacters += oPart.value;
				}
				break;
			case "day":
			case "year":
			case "weekYear":
			case "dayNumberOfWeek":
			case "weekInYear":
			case "hour0_23":
			case "hour1_24":
			case "hour0_11":
			case "hour1_12":
			case "minute":
			case "second":
			case "fractionalsecond":
				if (!bNumbers) {
					sAllowedCharacters += "0123456789";
					bNumbers = true;
				}
				break;
			case "month":
			case "monthStandalone":
				if (oPart.digits < 3) {
					if (!bNumbers) {
						sAllowedCharacters += "0123456789";
						bNumbers = true;
					}
				}else {
					bAll = true;
				}
				break;

			default:
				bAll = true;
				break;
			}
		}

		if (bAll) {
			sAllowedCharacters = "";
		}

		return sAllowedCharacters;

	};

	/**
	 * Returns a language-dependent placeholder text according to this instance's format options, for example
	 * "e.g. 12/31/2023".
	 *
	 * @returns {string} The language-dependent placeholder text
	 *
	 * @private
	 * @ui5-restricted sap.m
	 */
	DateFormat.prototype.getPlaceholderText = function() {
		var oResourceBundle = sap.ui.getCore().getLibraryResourceBundle();

		return oResourceBundle.getText("date.placeholder", [this.format.apply(this, this.getSampleValue())]);
	};

	/**
	 * Returns a sample date value.
	 *
	 * @returns {array}
	 *   A sample date value as an array of parameter values as expected by {@link #format}
	 *
	 * @private
	 */
	DateFormat.prototype.getSampleValue = function() {
		var oDate,
			iFullYear = new Date().getFullYear(),
			bUTC = this.oFormatOptions.UTC;

		function getDate(iYear, iMonth, iDay, iHours, iMinutes, iSeconds, iMilliseconds) {
			return bUTC
				? new Date(Date.UTC(iYear, iMonth, iDay, iHours, iMinutes, iSeconds, iMilliseconds))
				: new Date(iYear, iMonth, iDay, iHours, iMinutes, iSeconds, iMilliseconds);
		}

		oDate = getDate(iFullYear, 11, 31, 23, 59, 58, 123);

		if (this.oFormatOptions.interval) {
			return [[getDate(iFullYear, 11, 22, 9, 12, 34, 567), oDate]];
		}

		return [oDate];
	};

	return DateFormat;

}, /* bExport= */ true);
