Source

DateFmt.js

/*
 * DateFmt.js - Date formatter definition
 *
 * Copyright © 2012-2015, 2018, 2020, 2023 JEDLSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// !data dateformats sysres

var ilib = require("../index.js");
var Utils = require("./Utils.js");
var JSUtils = require("./JSUtils.js");

var Locale = require("./Locale.js");
var LocaleInfo = require("./LocaleInfo.js");

var IDate = require("./IDate.js");
var DateFactory = require("./DateFactory.js");
var CalendarFactory = require("./CalendarFactory.js");

var ResBundle = require("./ResBundle.js");
var TimeZone = require("./TimeZone.js");
var GregorianCal = require("./GregorianCal.js");

var ISet = require("./ISet.js");

/**
 * @class
 * Create a new date formatter instance. The date formatter is immutable once
 * it is created, but can format as many different dates as needed with the same
 * options. Create different date formatter instances for different purposes
 * and then keep them cached for use later if you have more than one date to
 * format.<p>
 *
 * The options may contain any of the following properties:
 *
 * <ul>
 * <li><i>locale</i> - locale to use when formatting the date/time. If the locale is
 * not specified, then the default locale of the app or web page will be used.
 *
 * <li><i>calendar</i> - the type of calendar to use for this format. The value should
 * be a sting containing the name of the calendar. Currently, the supported
 * types are "gregorian", "julian", "arabic", "hebrew", or "chinese". If the
 * calendar is not specified, then the default calendar for the locale is used. When the
 * calendar type is specified, then the format method must be called with an instance of
 * the appropriate date type. (eg. Gregorian calendar means that the format method must
 * be called with a GregDate instance.)
 *
 * <li><i>timezone</i> - time zone to use when formatting times. This may be a time zone
 * instance or a time zone specifier from the IANA list of time zone database names
 * (eg. "America/Los_Angeles"),
 * the string "local", or a string specifying the offset in RFC 822 format. The IANA
 * list of time zone names can be viewed at
 * <a href="http://en.wikipedia.org/wiki/List_of_tz_database_time_zones">this page</a>.
 * If the time zone is given as "local", the offset from UTC as given by
 * the Javascript system is used. If the offset is given as an RFC 822 style offset
 * specifier, it will parse that string and use the resulting offset. If the time zone
 * is not specified, the
 * default time zone for the locale is used. If both the date object and this formatter
 * instance contain time zones and those time zones are different from each other, the
 * formatter will calculate the offset between the time zones and subtract it from the
 * date before formatting the result for the current time zone. The theory is that a date
 * object that contains a time zone specifies a specific instant in time that is valid
 * around the world, whereas a date object without one is a local time and can only be
 * used for doing things in the local time zone of the user.
 *
 * <li><i>type</i> - Specify whether this formatter should format times only, dates only, or
 * both times and dates together. Valid values are "time", "date", and "datetime". Note that
 * in some locales, the standard format uses the order "time followed by date" and in others,
 * the order is exactly opposite, so it is better to create a single "datetime" formatter
 * than it is to create a time formatter and a date formatter separately and concatenate the
 * results. A "datetime" formatter will get the order correct for the locale.<br><br>
 * The default type if none is specified in with the type option is "date". <br><br>
 *
 * <li><i>length</i> - Specify the length of the format to use. The length is the approximate size of the
 * formatted string.
 *
 * <ul>
 * <li><i>short</i> - use a short representation of the time. This is the most compact format possible for the locale.
 * <li><i>medium</i> - use a medium length representation of the time. This is a slightly longer format.
 * <li><i>long</i> - use a long representation of the time. This is a fully specified format, but some of the textual
 * components may still be abbreviated
 * <li><i>full</i> - use a full representation of the time. This is a fully specified format where all the textual
 * components are spelled out completely
 * </ul>
 * eg. The "short" format for an en_US date may be "MM/dd/yy", whereas the long format might be "d MMM, yyyy". In the long
 * format, the month name is textual instead of numeric and is longer, the year is 4 digits instead of 2, and the format
 * contains slightly more spaces and formatting characters.<br><br>
 * Note that the length parameter does not specify which components are to be formatted. Use the "date" and the "time"
 * properties to specify the components. Also, very few of the components of a time format differ according to the length,
 * so this property has little to no affect on time formatting. <br><br>
 *
 * <li><i>date</i> - This property tells
 * which components of a date format to use. For example,
 * sometimes you may wish to format a date that only contains the month and date
 * without the year, such as when displaying a person's yearly birthday. The value
 * of this property allows you to specify only those components you want to see in the
 * final output, ordered correctly for the locale. <br><br>
 * Valid values are:
 *
 * <ul>
 * <li><i>dmwy</i> - format all components, weekday, date, month, and year
 * <li><i>dmy</i> - format only date, month, and year
 * <li><i>dmw</i> - format only weekday, date, and month
 * <li><i>dm</i> - format only date and month
 * <li><i>my</i> - format only month and year
 * <li><i>dw</i> - format only the weekday and date
 * <li><i>d</i> - format only the date
 * <li><i>m</i> - format only the month, in numbers for shorter lengths, and letters for
 * longer lengths
 * <li><i>n</i> - format only the month, in letters only for all lengths
 * <li><i>y</i> - format only the year
 * </ul>
 * Default components, if this property is not specified, is "dmy". This property may be specified
 * but has no affect if the current formatter is for times only.<br><br>
 * As of ilib 12.0, you can now pass ICU style skeletons in this option similar to the ones you
 * get from <a href="https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/classicu_1_1DateTimePatternGenerator.html#af59552a2922795b494f82ec230208e2e">DateTimePatternGenerator.getSkeleton()</a>.
 * It will not extract the length from the skeleton so you still need to pass the length property,
 * but it will extract the date components. <br><br>
 *
 * <li><i>time</i> - This property gives which components of a time format to use. The time will be formatted
 * correctly for the locale with only the time components requested. For example, a clock might only display
 * the hour and minute and not need the seconds or the am/pm component. In this case, the time property should be set
 * to "hm". <br><br>
 * Valid values for this property are:
 *
 * <ul>
 * <li><i>ahmsz</i> - format the hours, minutes, seconds, am/pm (if using a 12 hour clock), and the time zone
 * <li><i>ahms</i> - format the hours, minutes, seconds, and am/pm (if using a 12 hour clock)
 * <li><i>hmsz</i> - format the hours, minutes, seconds, and the time zone
 * <li><i>hms</i> - format the hours, minutes, and seconds
 * <li><i>ahmz</i> - format the hours, minutes, am/pm (if using a 12 hour clock), and the time zone
 * <li><i>ahm</i> - format the hours, minutes, and am/pm (if using a 12 hour clock)
 * <li><i>hmz</i> - format the hours, minutes, and the time zone
 * <li><i>ah</i> - format only the hours and am/pm if using a 12 hour clock
 * <li><i>hm</i> - format only the hours and minutes
 * <li><i>ms</i> - format only the minutes and seconds
 * <li><i>h</i> - format only the hours
 * <li><i>m</i> - format only the minutes
 * <li><i>s</i> - format only the seconds
 * </ul>
 * If you want to format a length of time instead of a particular instant
 * in time, use the duration formatter object (DurationFmt) instead because this
 * formatter is geared towards instants. A date formatter will make sure that each component of the
 * time is within the normal range
 * for that component. That is, the minutes will always be between 0 and 59, no matter
 * what is specified in the date to format. A duration format will allow the number
 * of minutes to exceed 59 if, for example, you were displaying the length of
 * a movie of 198 minutes.<br><br>
 * Default value if this property is not specified is "hma".<br><br>
 * As of ilib 12.0, you can now pass ICU style skeletons in this option similar to the ones you
 * get from <a href="https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/classicu_1_1DateTimePatternGenerator.html#af59552a2922795b494f82ec230208e2e">DateTimePatternGenerator.getSkeleton()</a>.
 * It will not extract the length from the skeleton so you still need to pass the length property,
 * but it will extract the time components.
 *
 * <li><i>clock</i> - specify that the time formatter should use a 12 or 24 hour clock.
 * Valid values are "12" and "24".<br><br>
 * In some locales, both clocks are used. For example, in en_US, the general populace uses
 * a 12 hour clock with am/pm, but in the US military or in nautical or aeronautical or
 * scientific writing, it is more common to use a 24 hour clock. This property allows you to
 * construct a formatter that overrides the default for the locale.<br><br>
 * If this property is not specified, the default is to use the most widely used convention
 * for the locale.
 *
 * <li><i>template</i> - use the given template string as a fixed format when formatting
 * the date/time. Valid codes to use in a template string are as follows:
 *
 * <ul>
 * <li><i>a</i> - am/pm marker
 * <li><i>B</i> - the current day period
 * <li><i>d</i> - 1 or 2 digit date of month, not padded
 * <li><i>dd</i> - 1 or 2 digit date of month, 0 padded to 2 digits
 * <li><i>O</i> - ordinal representation of the date of month (eg. "1st", "2nd", etc.)
 * <li><i>D</i> - 1 to 3 digit day of year
 * <li><i>DD</i> - 1 to 3 digit day of year, 0 padded to 2 digits
 * <li><i>DDD</i> - 1 to 3 digit day of year, 0 padded to 3 digits
 * <li><i>M</i> - 1 or 2 digit month number, not padded
 * <li><i>MM</i> - 1 or 2 digit month number, 0 padded to 2 digits
 * <li><i>N</i> - 1 character month name abbreviation
 * <li><i>NN</i> - 2 character month name abbreviation
 * <li><i>MMM</i> - 3 character month name abbreviation
 * <li><i>MMMM</i> - fully spelled out month name
 * <li><i>L</i> - 1 character stand-alone month name abbreviation
 * <li><i>LL</i> - 2 character stand-alone month name abbreviation
 * <li><i>LLL</i> - 3 character stand-alone month name abbreviation
 * <li><i>LLLL</i> - fully spelled out stand-alone month name
 * <li><i>yy</i> - 2 digit year
 * <li><i>yyyy</i> - 4 digit year
 * <li><i>E</i> - day-of-week name, abbreviated to a single character
 * <li><i>EE</i> - day-of-week name, abbreviated to a max of 2 characters
 * <li><i>EEE</i> - day-of-week name, abbreviated to a max of 3 characters
 * <li><i>EEEE</i> - day-of-week name fully spelled out
 * <li><i>c</i> - stand-alone day-of-week name, abbreviated to a single character
 * <li><i>cc</i> - stand-alone day-of-week name, abbreviated to a max of 2 characters
 * <li><i>ccc</i> - stand-alone day-of-week name, abbreviated to a max of 3 characters
 * <li><i>cccc</i> - stand-alone day-of-week name fully spelled out
 * <li><i>G</i> - era designator
 * <li><i>w</i> - week number in year
 * <li><i>ww</i> - week number in year, 0 padded to 2 digits
 * <li><i>W</i> - week in month
 * <li><i>h</i> - hour (12 followed by 1 to 11)
 * <li><i>hh</i> - hour (12, followed by 1 to 11), 0 padded to 2 digits
 * <li><i>k</i> - hour (1 to 24)
 * <li><i>kk</i> - hour (1 to 24), 0 padded to 2 digits
 * <li><i>H</i> - hour (0 to 23)
 * <li><i>HH</i> - hour (0 to 23), 0 padded to 2 digits
 * <li><i>K</i> - hour (0 to 11)
 * <li><i>KK</i> - hour (0 to 11), 0 padded to 2 digits
 * <li><i>m</i> - minute in hour
 * <li><i>mm</i> - minute in hour, 0 padded to 2 digits
 * <li><i>s</i> - second in minute
 * <li><i>ss</i> - second in minute, 0 padded to 2 digits
 * <li><i>S</i> - millisecond (1 to 3 digits)
 * <li><i>SSS</i> - millisecond, 0 padded to 3 digits
 * <li><i>z</i> - general time zone
 * <li><i>Z</i> - RFC 822 time zone
 * </ul>
 *
 * <li><i>useNative</i> - the flag used to determine whether to use the native script settings
 * for formatting the numbers.
 *
 * <li><i>meridiems</i> - string that specifies what style of meridiems to use with this
 * format. The choices are "default", "gregorian", "ethiopic", and "chinese". The "default"
 * style is often the simple Gregorian AM/PM, but the actual style is chosen by the locale.
 * (For almost all locales, the Gregorian AM/PM style is most frequently used.)
 * The "ethiopic" style uses 5 different meridiems for "morning", "noon", "afternoon",
 * "evening", and "night". The "chinese" style uses 7 different meridiems corresponding
 * to the various parts of the day. N.B. Even for the Chinese locales, the default is "gregorian"
 * when formatting dates in the Gregorian calendar.
 *
 * <li><i>useIntl</i> - choose whether Intl.DateTimeFormat object for formatting.
 * When it is set to true, the Intl object is available, it supports the requested locale, and
 * the parameters can be converted to equivalent parameters for the Intl.DateTimeFormat object,
 * then it will format the date relatively quickly using Intl.
 * When they cannot be converted, the Intl object is not available, or the Intl object does not support
 * the requested locale, it will perform the relatively slow formatting using regular ilib code written in Javascript.
 * The code will often return different results depending on the platform and version of the Javascript engine
 * and which version of CLDR it supports. If you need consistency across versions and platforms,
 * do not use the useIntl flag. Just stick with the regular ilib formatting code.
 *
 * <li><i>onLoad</i> - a callback function to call when the date format object is fully
 * loaded. When the onLoad option is given, the DateFmt object will attempt to
 * load any missing locale data using the ilib loader callback.
 * When the constructor is done (even if the data is already preassembled), the
 * onLoad function is called with the current instance as a parameter, so this
 * callback can be used with preassembled or dynamic loading or a mix of the two.
 *
 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
 * asynchronously. If this option is given as "false", then the "onLoad"
 * callback must be given, as the instance returned from this constructor will
 * not be usable for a while.
 *
 * <li><i>loadParams</i> - an object containing parameters to pass to the
 * loader callback function when locale data is missing. The parameters are not
 * interpretted or modified in any way. They are simply passed along. The object
 * may contain any property/value pairs as long as the calling code is in
 * agreement with the loader callback function as to what those parameters mean.
 * </ul>
 *
 * Any substring containing letters within single or double quotes will be used
 * as-is in the final output and will not be interpretted for codes as above.<p>
 *
 * Example: a date format in Spanish might be given as: "'El' d. 'de' MMMM", where
 * the 'El' and the 'de' are left as-is in the output because they are quoted. Typical
 * output for this example template might be, "El 5. de Mayo".
 *
 * The following options will be used when formatting a date/time with an explicit
 * template:
 *
 * <ul>
 * <li>locale - the locale is only used for
 * translations of things like month names or day-of-week names.
 * <li>calendar - used to translate a date instance into date/time component values
 * that can be formatted into the template
 * <li>timezone - used to figure out the offset to add or subtract from the time to
 * get the final time component values
 * <li>clock - used to figure out whether to format times with a 12 or 24 hour clock.
 * If this option is specified, it will override the hours portion of a time format.
 * That is, "hh" is switched with "HH" and "kk" is switched with "KK" as appropriate.
 * If this option is not specified, the 12/24 code in the template will dictate whether
 * to use the 12 or 24 clock, and the 12/24 default in the locale will be ignored.
 * </ul>
 *
 * All other options will be ignored and their corresponding getter methods will
 * return the empty string.<p>
 *
 *
 * @constructor
 * @param {Object} options options governing the way this date formatter instance works
 */
var DateFmt = function(options) {
    var arr, i, bad, c, comps,
        sync = true,
        loadParams = undefined;

    this.locale = new Locale();
    this.type = "date";
    this.length = "s";
    this.dateComponents = "dmy";
    this.timeComponents = "ahm";
    this.meridiems = "default";
    this.useIntl = false;

    options = options || {sync: true};
    if (options.locale) {
        this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
    }

    if (options.type) {
        if (options.type === 'date' || options.type === 'time' || options.type === 'datetime') {
            this.type = options.type;
        }
    }

    if (options.calendar) {
        this.calName = options.calendar;
    }

    if (options.length) {
        if (options.length === 'short' ||
            options.length === 'medium' ||
            options.length === 'long' ||
            options.length === 'full') {
            // only use the first char to save space in the json files
            this.length = options.length.charAt(0);
        }
    }

    if (options.date) {
        arr = options.date.split("");
        var dateComps = new ISet();
        bad = false;
        for (i = 0; i < arr.length; i++) {
            c = arr[i].toLowerCase();
            if (c === "e") c = "w"; // map ICU -> ilib
            if (c !== 'd' && c !== 'm' && c !== 'y' && c !== 'w' && c !== 'n') {
                // ignore time components and the era
                if (c !== 'h' && c !== 'm'  && c !== 's' && c !== 'a' && c !== 'z' && c !== 'g') {
                    bad = true;
                    break;
                }
            } else {
                dateComps.add(c);
            }
        }
        if (!bad) {
            comps = dateComps.asArray().sort(function (left, right) {
                return (left < right) ? -1 : ((right < left) ? 1 : 0);
            });
            this.dateComponents = comps.join("");
        }
    }

    if (options.time) {
        arr = options.time.split("");
        var timeComps = new ISet();
        this.badTime = false;
        for (i = 0; i < arr.length; i++) {
            c = arr[i].toLowerCase();
            if (c !== 'h' && c !== 'm' && c !== 's' && c !== 'a' && c !== 'z') {
                // ignore the date components
                if (c !== 'd' && c !== 'm' && c !== 'y' && c !== 'w' && c !== 'e' && c !== 'n' && c !== 'g') {
                    this.badTime = true;
                    break;
                }
            } else {
                timeComps.add(c);
            }
        }
        if (!this.badTime) {
            comps = timeComps.asArray().sort(function (left, right) {
                return (left < right) ? -1 : ((right < left) ? 1 : 0);
            });
            this.timeComponents = comps.join("");
        }
    }

    if (options.clock && (options.clock === '12' || options.clock === '24')) {
        this.clock = options.clock;
    }

    if (options.template) {
        // many options are not useful when specifying the template directly, so zero
        // them out.
        this.type = "";
        this.length = "";
        this.dateComponents = "";
        this.timeComponents = "";

        this.template = options.template;
    }

    if (options.timezone) {
        if (options.timezone instanceof TimeZone) {
            this.tz = options.timezone;
            this.timezone = this.tz.getId();
        } else {
            this.timezone = options.timezone;
        }
    }

    if (typeof(options.useNative) === 'boolean') {
        this.useNative = options.useNative;
    }

    if (typeof(options.meridiems) !== 'undefined' &&
        (options.meridiems === "chinese" ||
            options.meridiems === "gregorian" ||
            options.meridiems === "ethiopic")) {
        this.meridiems = options.meridiems;
    }

    if (typeof(options.sync) !== 'undefined') {
        sync = (options.sync === true);
    }

    if (typeof(options.useIntl) !== 'undefined') {
        this.useIntl = options.useIntl;
    }

    loadParams = options.loadParams;

    new LocaleInfo(this.locale, {
        sync: sync,
        loadParams: loadParams,
        onLoad: ilib.bind(this, function (li) {
            this.locinfo = li;

            // get the default calendar name from the locale, and if the locale doesn't define
            // one, use the hard-coded gregorian as the last resort
            this.calName = this.calName || this.locinfo.getCalendar() || "gregorian";

            if(this.useIntl && typeof(Intl) !== 'undefined' && Intl.DateTimeFormat.supportedLocalesOf(this.locale.getSpec()).length > 0 &&
            (this.locinfo.getDigitsStyle() === "western" && (!options.template) && this.calName === "gregorian")){
                var len = DateFmt.lenmap[this.length];
                if(this.type === "date" &&
                    ((this.dateComponents === "dmy" && len !== "full") || (this.dateComponents === "dmwy" && len === "full"))){
                    this.IntlDateTimeObj = new Intl.DateTimeFormat(this.locale.getSpec(), {
                        dateStyle: len
                    });
                } else if (this.type === "time" &&
                    this.timeComponents === "ahm" || this.timeComponents === "ahms"){
                    var timeMap = {
                        "ahm": "short",
                        "ahms": "medium"
                    }
                    this.IntlDateTimeObj = new Intl.DateTimeFormat(this.locale.getSpec(), {
                        timeStyle: timeMap[this.timeComponents]
                    });
                } else if (this.type === "date" && this.dateComponents === "m" && len === "full") {
                    this.IntlDateTimeObj = new Intl.DateTimeFormat(this.locale.getSpec(), {
                        month: "long"
                    });

                } else if (this.type === "date" && this.dateComponents === "w" && len === "full") {
                    this.IntlDateTimeObj = new Intl.DateTimeFormat(this.locale.getSpec(), {
                       weekday: "long"
                    });
                } else {
                    this.useIntl = false;
                }
            }
            if(!this.useIntl){
                if (!this.IntlDateTimeObj && ilib.isDynCode()) {
                    // If we are running in the dynamic code loading assembly of ilib, the following
                    // will attempt to dynamically load the calendar date class for this calendar. If
                    // it doesn't work, this just goes on and it will use Gregorian instead.
                    DateFactory._init(this.calName);
                }

                CalendarFactory({
                    type: this.calName,
                    sync: sync,
                    loadParams: loadParams,
                    onLoad: ilib.bind(this, function(cal) {
                        this.cal = cal;

                        if (!this.cal) {
                            // can be synchronous
                            this.cal = new GregorianCal();
                        }
                        if (this.meridiems === "default") {
                            this.meridiems = li.getMeridiemsStyle();
                        }

                        // load the strings used to translate the components
                        new ResBundle({
                            locale: this.locale,
                            name: "sysres",
                            sync: sync,
                            loadParams: loadParams,
                            onLoad: ilib.bind(this, function (rb) {
                                this.sysres = rb;

                                if (!this.tz) {
                                    var timezone = options.timezone;
                                    if (!timezone && !options.locale) {
                                        timezone = "local";
                                    }

                                    new TimeZone({
                                        locale: this.locale,
                                        id: timezone,
                                        sync: sync,
                                        loadParams: loadParams,
                                        onLoad: ilib.bind(this, function(tz) {
                                            this.tz = tz;
                                            this._init(options);
                                        })
                                    });
                                } else {
                                    this._init(options);
                                }
                            })
                        });
                    })
                });
            }
            else {
                if (typeof(options.onLoad) === 'function') {
                    options.onLoad(this);
                }
            }
        })
    });
};

// used in getLength
DateFmt.lenmap = {
    "s": "short",
    "m": "medium",
    "l": "long",
    "f": "full"
};

DateFmt.defaultFmt = {
    "gregorian": {
        "order": "{date} {time}",
        "date": {
            "dmwy": "EEE d/MM/yyyy",
            "dmy": "d/MM/yyyy",
            "dmw": "EEE d/MM",
            "dm": "d/MM",
            "my": "MM/yyyy",
            "dw": "EEE d",
            "d": "dd",
            "m": "MM",
            "y": "yyyy",
            "n": "NN",
            "w": "EEE"
        },
        "time": {
            "12": "h:mm:ssa",
            "24": "H:mm:ss"
        },
        "range": {
            "c00": "{st} - {et}, {sd}/{sm}/{sy}",
            "c01": "{sd}/{sm} {st} - {ed}/{em} {et}, {sy}",
            "c02": "{sd}/{sm} {st} - {ed}/{em} {et}, {sy}",
            "c03": "{sd}/{sm}/{sy} {st} - {ed}/{em}/{ey} {et}",
            "c10": "{sd}-{ed}/{sm}/{sy}",
            "c11": "{sd}/{sm} - {ed}/{em} {sy}",
            "c12": "{sd}/{sm}/{sy} - {ed}/{em}/{ey}",
            "c20": "{sm}/{sy} - {em}/{ey}",
            "c30": "{sy} - {ey}"
        }
    },
    "islamic": "gregorian",
    "hebrew": "gregorian",
    "julian": "gregorian",
    "buddhist": "gregorian",
    "persian": "gregorian",
    "persian-algo": "gregorian",
    "han": "gregorian"
};

/**
* @static
* @private
*/
DateFmt.monthNameLenMap = {
    "short" : "N",
    "medium": "NN",
    "long":   "MMM",
    "full":   "MMMM"
};

/**
* @static
* @private
*/
DateFmt.weekDayLenMap = {
    "short" : "E",
    "medium": "EE",
    "long":   "EEE",
    "full":   "EEEE"
};

/**
 * Return the range of possible meridiems (times of day like "AM" or
 * "PM") in this date formatter.<p>
 *
 * The options may contain any of the following properties:
 *
 * <ul>
 * <li><i>locale</i> - locale to use when formatting the date/time. If the locale is
 * not specified, then the default locale of the app or web page will be used.
 *
 * <li><i>meridiems</i> - string that specifies what style of meridiems to use with this
 * format. The choices are "default", "gregorian", "ethiopic", and "chinese". The "default"
 * style is often the simple Gregorian AM/PM, but the actual style is chosen by the locale.
 * (For almost all locales, the Gregorian AM/PM style is most frequently used.)
 * The "ethiopic" style uses 5 different meridiems for "morning", "noon", "afternoon",
 * "evening", and "night". The "chinese" style uses 7 different meridiems corresponding
 * to the various parts of the day. N.B. Even for the Chinese locales, the default is "gregorian"
 * when formatting dates in the Gregorian calendar.
 * </ul>
 *
 * @static
 * @public
 * @param {Object} options options governing the way this date formatter instance works for getting meridiems range
 * @return {Array.<{name:string,start:string,end:string}>}
 */
DateFmt.getMeridiemsRange = function (options) {
    options = options || {sync: true};
    var args = JSUtils.merge({}, options);
    args.onLoad = function(fmt) {
        if (typeof(options.onLoad) === "function") {
            options.onLoad(fmt.getMeridiemsRange());
        }
    };
    var fmt = new DateFmt(args);

    return fmt.getMeridiemsRange();
};

/**
 * return true if the locale is supported in date and time formatting for Intl.DateTimeFormat Object
 * <ul>
 * <li><i>locale</i> - locale to check if it is available or not.
 * If the locale is not specified, then it returns false.
 *
 * </ul>
 *
 * @static
 * @public
 * @param {string} locale locale to check if it is available or not.
 * @return {Boolean} true if it is available to use, false otherwise
 */

DateFmt.isIntlDateTimeAvailable = function (locale) {
    if(!locale || !ilib._global("Intl")) return false;
    return (Intl.DateTimeFormat.supportedLocalesOf(locale).length > 0) ? true : false;
};

DateFmt.prototype = {
    /**
     * Finish initializing the formatter object
     * @private
     */
    _init: function(options) {
        if (typeof (options.sync) === 'undefined') {
            options.sync = true;
        }
        Utils.loadData({
            object: "DateFmt",
            locale: this.locale,
            name: "dateformats.json",
            sync: options.sync,
            loadParams: options.loadParams,
            callback: ilib.bind(this, function (formats) {
                if (!formats) {
                    formats = ilib.data.dateformats || DateFmt.defaultFmt;
                }

                this.info = formats;
                var ret = this;

                if (this.template) {
                    this._massageTemplate();
                } else {
                    if (typeof(this.clock) === 'undefined') {
                        // default to the locale instead
                        this.clock = this.locinfo.getClock();
                    }

                    if (typeof(options.sync) === "boolean" && !options.sync) {
                        // in async mode, capture the exception and call the callback with "undefined"
                        try {
                            this._initTemplate(formats);
                            this._massageTemplate();
                        } catch (e) {
                            ret = undefined;
                        }
                    } else {
                        // in sync mode, allow the exception to percolate upwards
                        this._initTemplate(formats);
                        this._massageTemplate();
                    }
                }

                if (typeof(options.onLoad) === 'function') {
                    options.onLoad(ret);
                }
           })
        });
    },
    /**
     * @private
     * @param {string|{
     *         order:(string|{
     *             s:string,
     *             m:string,
     *             l:string,
     *             f:string
     *         }),
     *         date:Object.<string, (string|{
     *             s:string,
     *             m:string,
     *             l:string,
     *             f:string
     *         })>,
     *         time:Object.<string,Object.<string,(string|{
     *             s:string,
     *             m:string,
     *             l:string,
     *             f:string
     *         })>>,
     *         range:Object.<string, (string|{
     *             s:string,
     *             m:string,
     *             l:string,
     *             f:string
     *         })>
     *     }} formats
     */
    _initTemplate: function (formats) {
        if (formats[this.calName]) {
            var name = formats[this.calName];
            // may be an alias to another calendar type
            this.formats = (typeof(name) === "string") ? formats[name] : name;

            this.template = "";

            switch (this.type) {
                case "datetime":
                    this.template = (this.formats && this._getLengthFormat(this.formats.order, this.length)) || "{date} {time}";
                    this.template = this.template.replace("{date}", this._getFormat(this.formats.date, this.dateComponents, this.length) || "");
                    this.template = this.template.replace("{time}", this._getFormat(this.formats.time[this.clock], this.timeComponents, this.length) || "");
                    break;
                case "date":
                    this.template = this._getFormat(this.formats.date, this.dateComponents, this.length);
                    break;
                case "time":
                    this.template = this._getFormat(this.formats.time[this.clock], this.timeComponents, this.length);
                    break;
            }

            // calculate what order the components appear in for this locale
            this.componentOrder = this._getFormat(this.formats.date, "dmy", "l").
                replace(/[^dMy]/g, "").
                replace(/y+/, "y").
                replace(/d+/, "d").
                replace(/M+/, "m");
        } else {
            throw "No formats available for calendar " + this.calName + " in locale " + this.locale.toString();
        }
    },

    /**
     * @private
     */
    _massageTemplate: function () {
        var i;

        if (this.clock && this.template) {
            // explicitly set the hours to the requested type
            var temp = "";
            switch (this.clock) {
                case "24":
                    for (i = 0; i < this.template.length; i++) {
                        if (this.template.charAt(i) == "'") {
                            temp += this.template.charAt(i++);
                            while (i < this.template.length && this.template.charAt(i) !== "'") {
                                temp += this.template.charAt(i++);
                            }
                            if (i < this.template.length) {
                                temp += this.template.charAt(i);
                            }
                        } else if (this.template.charAt(i) == 'K') {
                            temp += 'k';
                        } else if (this.template.charAt(i) == 'h') {
                            temp += 'H';
                        } else {
                            temp += this.template.charAt(i);
                        }
                    }
                    this.template = temp;
                    break;
                case "12":
                    for (i = 0; i < this.template.length; i++) {
                        if (this.template.charAt(i) == "'") {
                            temp += this.template.charAt(i++);
                            while (i < this.template.length && this.template.charAt(i) !== "'") {
                                temp += this.template.charAt(i++);
                            }
                            if (i < this.template.length) {
                                temp += this.template.charAt(i);
                            }
                        } else if (this.template.charAt(i) == 'k') {
                            temp += 'K';
                        } else if (this.template.charAt(i) == 'H') {
                            temp += 'h';
                        } else {
                            temp += this.template.charAt(i);
                        }
                    }
                    this.template = temp;
                    break;
            }
        }

        // tokenize it now for easy formatting
        this.templateArr = this._tokenize(this.template);

        var digits;
        // set up the mapping to native or alternate digits if necessary
        if (typeof(this.useNative) === "boolean") {
            if (this.useNative) {
                digits = this.locinfo.getNativeDigits();
                if (digits) {
                    this.digits = digits;
                }
            }
        } else if (this.locinfo.getDigitsStyle() === "native") {
            digits = this.locinfo.getNativeDigits();
            if (digits) {
                this.useNative = true;
                this.digits = digits;
            }
        }
    },

    /**
     * Convert the template into an array of date components separated by formatting chars.
     * @private
     * @param {string} template Format template to tokenize into components
     * @return {Array.<string>} a tokenized array of date format components
     */
    _tokenize: function (template) {
        var i = 0, start, ch, letter, arr = [];

        // console.log("_tokenize: tokenizing template " + template);
        if (template) {
            while (i < template.length) {
                ch = template.charAt(i);
                start = i;
                if (ch === "'") {
                    // console.log("found quoted string");
                    i++;
                    // escaped string - push as-is, then dequote later
                    while (i < template.length && template.charAt(i) !== "'") {
                        i++;
                    }
                    if (i < template.length) {
                        i++;    // grab the other quote too
                    }
                } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
                    letter = template.charAt(i);
                    // console.log("found letters " + letter);
                    while (i < template.length && ch === letter) {
                        ch = template.charAt(++i);
                    }
                } else {
                    // console.log("found other");
                    while (i < template.length && ch !== "'" && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) {
                        ch = template.charAt(++i);
                    }
                }
                arr.push(template.substring(start,i));
                // console.log("start is " + start + " i is " + i + " and substr is " + template.substring(start,i));
            }
        }
        return arr;
    },

    /**
     * @private
     * @param {Object.<string, (string|{s:string,m:string,l:string,f:string})>} obj Object to search
     * @param {string} components Format components to search
     * @param {string} length Length of the requested format
     * @return {string|undefined} the requested format
     */
    _getFormatInternal: function getFormatInternal(obj, components, length) {
        if (typeof(components) !== 'undefined' && obj && obj[components]) {
            return this._getLengthFormat(obj[components], length);
        }
        return undefined;
    },

    // stand-alone of m (month) is l
    // stand-alone of my (month year) is mys
    // stand-alone of d (day) is a
    // stand-alone of w (weekday) is e
    // stand-alone of y (year) is r
    _standAlones: {
        "m": "l",
        "my": "mys",
        "d": "a",
        "w": "e",
        "y": "r"
    },

    /**
     * @private
     * @param {Object.<string, (string|{s:string,m:string,l:string,f:string})>} obj Object to search
     * @param {string} components Format components to search
     * @param {string} length Length of the requested format
     * @return {string|undefined} the requested format
     */
    _getFormat: function getFormat(obj, components, length) {
        // handle some special cases for stand-alone formats
        if (components && this._standAlones[components]) {
            var tmp = this._getFormatInternal(obj, this._standAlones[components], length);
            if (tmp) {
                return tmp;
            }
        }

        // if no stand-alone format is available, fall back to the regular format
        return this._getFormatInternal(obj, components, length);
    },

    /**
     * @private
     * @param {(string|{s:string,m:string,l:string,f:string})} obj Object to search
     * @param {string} length Length of the requested format
     * @return {(string|undefined)} the requested format
     */
    _getLengthFormat: function getLengthFormat(obj, length) {
        if (typeof(obj) === 'string') {
            return obj;
        } else if (obj[length]) {
            return obj[length];
        }
        return undefined;
    },

    /**
     * Return the locale used with this formatter instance.
     * @return {Locale} the Locale instance for this formatter
     */
    getLocale: function() {
        return this.locale;
    },

    /**
     * Return the template string that is used to format date/times for this
     * formatter instance. This will work, even when the template property is not explicitly
     * given in the options to the constructor. Without the template option, the constructor
     * will build the appropriate template according to the options and use that template
     * in the format method.
     *
     * @return {string} the format template for this formatter
     */
    getTemplate: function() {
        return this.template;
    },

    /**
     * Return the order of the year, month, and date components for the current locale.<p>
     *
     * When implementing a date input widget in a UI, it would be useful to know what
     * order to put the year, month, and date input fields so that it conforms to the
     * user expectations for the locale. This method gives that order by returning a
     * string that has a single "y", "m", and "d" character in it in the correct
     * order.<p>
     *
     * For example, the return value "ymd" means that this locale formats the year first,
     * the month second, and the date third, and "mdy" means that the month is first,
     * the date is second, and the year is third. Four of the 6 possible permutations
     * of the three letters have at least one locale that uses that ordering, though some
     * combinations are far more likely than others. The ones that are not used by any
     * locales are "dym" and "myd", though new locales are still being added to
     * CLDR frequently, and possible orderings cannot be predicted. Your code should
     * support all 6 possibilities, just in case.
     *
     * @return {string} a string giving the date component order
     */
    getDateComponentOrder: function() {
        return this.componentOrder;
    },

    /**
     * Return the type of this formatter. The type is a string that has one of the following
     * values: "time", "date", "datetime".
     * @return {string} the type of the formatter
     */
    getType: function() {
        return this.type;
    },

    /**
     * Return the name of the calendar used to format date/times for this
     * formatter instance.
     * @return {string} the name of the calendar used by this formatter
     */
    getCalendar: function () {
        return this.cal.getType() || 'gregorian';
    },

    /**
     * Return the length used to format date/times in this formatter. This is either the
     * value of the length option to the constructor, or the default value.
     *
     * @return {string} the length of formats this formatter returns
     */
    getLength: function () {
        return DateFmt.lenmap[this.length] || "";
    },

    /**
     * Return the date components that this formatter formats. This is either the
     * value of the date option to the constructor, or the default value. If this
     * formatter is a time-only formatter, this method will return the empty
     * string. The date component letters may be specified in any order in the
     * constructor, but this method will reorder the given components to a standard
     * order.
     *
     * @return {string} the date components that this formatter formats
     */
    getDateComponents: function () {
        return this.dateComponents || "";
    },

    /**
     * Return the time components that this formatter formats. This is either the
     * value of the time option to the constructor, or the default value. If this
     * formatter is a date-only formatter, this method will return the empty
     * string. The time component letters may be specified in any order in the
     * constructor, but this method will reorder the given components to a standard
     * order.
     *
     * @return {string} the time components that this formatter formats
     */
    getTimeComponents: function () {
        return this.timeComponents || "";
    },

    /**
     * Return the time zone used to format date/times for this formatter
     * instance.
     * @return {TimeZone} a time zone object that this formatter is formatting for
     */
    getTimeZone: function () {
        return this.tz;
    },

    /**
     * Return the clock option set in the constructor. If the clock option was
     * not given, the default from the locale is returned instead.
     * @return {string} "12" or "24" depending on whether this formatter uses
     * the 12-hour or 24-hour clock
     */
    getClock: function () {
        return this.clock || this.locinfo.getClock();
    },

    /**
     * Return the meridiems range in current locale.
     * @return {Array.<{name:string,start:string,end:string}>} the range of available meridiems
     */
    getMeridiemsRange: function () {
        var result;
        var _getSysString = function (key) {
            return (this.sysres.getString(undefined, key + "-" + this.calName) || this.sysres.getString(undefined, key)).toString();
        };

        switch (this.meridiems) {
        case "chinese":
            result = [
                {
                    name: _getSysString.call(this, "azh0"),
                    start: "00:00",
                    end: "05:59"
                },
                {
                    name: _getSysString.call(this, "azh1"),
                    start: "06:00",
                    end: "08:59"
                },
                {
                    name: _getSysString.call(this, "azh2"),
                    start: "09:00",
                    end: "11:59"
                },
                {
                    name: _getSysString.call(this, "azh3"),
                    start: "12:00",
                    end: "12:59"
                },
                {
                    name: _getSysString.call(this, "azh4"),
                    start: "13:00",
                    end: "17:59"
                },
                {
                    name: _getSysString.call(this, "azh5"),
                    start: "18:00",
                    end: "20:59"
                },
                {
                    name: _getSysString.call(this, "azh6"),
                    start: "21:00",
                    end: "23:59"
                }
            ];
            break;
        case "ethiopic":
            result = [
                {
                    name: _getSysString.call(this, "a0-ethiopic"),
                    start: "00:00",
                    end: "05:59"
                },
                {
                    name: _getSysString.call(this, "a1-ethiopic"),
                    start: "06:00",
                    end: "06:00"
                },
                {
                    name: _getSysString.call(this, "a2-ethiopic"),
                    start: "06:01",
                    end: "11:59"
                },
                {
                    name: _getSysString.call(this, "a3-ethiopic"),
                    start: "12:00",
                    end: "17:59"
                },
                {
                    name: _getSysString.call(this, "a4-ethiopic"),
                    start: "18:00",
                    end: "23:59"
                }
            ];
            break;
        default:
            result = [
                {
                    name: _getSysString.call(this, "a0"),
                    start: "00:00",
                    end: "11:59"
                },
                {
                    name: _getSysString.call(this, "a1"),
                    start: "12:00",
                    end: "23:59"
                }
            ];
            break;
        }

        return result;
    },

    _findMeridiem: function(hours, minutes) {
        var range = this.info.dayPeriods;
        if (!range) {
            return "";
        }
        // find all day periods that apply, and then choose the shortest one
        var minuteOfDay = hours * 60 + minutes;
        var shortest = {
            name: "",
            length: 2000
        };
        for (var i = 0; i < range.length; i++) {
            var period = range[i];
            if (minuteOfDay === period.at || (minuteOfDay >= period.from && minuteOfDay < period.to)) {
                var periodCode = "B" + i;
                var length = typeof(period.at) !== "undefined" ? 0 : (period.to - period.from);

                if (length < shortest.length) {
                    shortest = {
                        name: this.sysres.getString(undefined, periodCode + "-" + this.calName) ||
                            this.sysres.getString(undefined, periodCode),
                        length: length
                    };
                }
            }
        }

        return shortest.name;
    },

    /**
     * @private
     */
    _getTemplate: function (prefix, calendar) {
        if (calendar !== "gregorian") {
            return prefix + "-" + calendar;
        }
        return prefix;
    },

    /**
     * Returns an array of the months of the year, formatted to the optional length specified.
     * i.e. ...getMonthsOfYear() OR ...getMonthsOfYear({length: "short"})
     * <p>
     * The options parameter may contain any of the following properties:
     *
     * <ul>
     * <li><i>length</i> - length of the names of the months being sought. This may be one of
     * "short", "medium", "long", or "full"
     * <li><i>date</i> - retrieve the names of the months in the date of the given date
     * <li><i>year</i> - retrieve the names of the months in the given year. In some calendars,
     * the months have different names depending if that year is a leap year or not.
     * </ul>
     *
     * @param  {Object=} options an object-literal that contains any of the above properties
     * @return {Array} an array of the names of all of the months of the year in the current calendar
     */
    getMonthsOfYear: function(options) {
        var length = (options && options.length) || this.getLength(),
            template = DateFmt.monthNameLenMap[length],
            months = [undefined],
            date,
            monthCount;

        if (options) {
            if (options.date) {
                date = DateFactory._dateToIlib(options.date);
            }

            if (options.year) {
                date = DateFactory({year: options.year, month: 1, day: 1, type: this.cal.getType()});
            }
        }

        if (!date) {
            date = DateFactory({
                calendar: this.cal.getType()
            });
        }

        monthCount = this.cal.getNumMonths(date.getYears());
        for (var i = 1; i <= monthCount; i++) {
            months[i] = this.sysres.getString(this._getTemplate(template + i, this.cal.getType())).toString();
        }
        return months;
    },

    /**
     * Returns an array of the days of the week, formatted to the optional length specified.
     * i.e. ...getDaysOfWeek() OR ...getDaysOfWeek({length: "short"})
     * <p>
     * The options parameter may contain any of the following properties:
     *
     * <ul>
     * <li><i>length</i> - length of the names of the months being sought. This may be one of
     * "short", "medium", "long", or "full"
     * </ul>
     * @param  {Object=} options an object-literal that contains one key
     *                   "length" with the standard length strings
     * @return {Array} an array of all of the names of the days of the week
     */
    getDaysOfWeek: function(options) {
        var length = (options && options.length) || this.getLength(),
            template = DateFmt.weekDayLenMap[length],
            days = [];
        for (var i = 0; i < 7; i++) {
            days[i] = this.sysres.getString(this._getTemplate(template + i, this.cal.getType())).toString();
        }
        return days;
    },


    /**
     * Convert this formatter to a string representation by returning the
     * format template. This method delegates to getTemplate.
     *
     * @return {string} the format template
     */
    toString: function() {
        return this.getTemplate();
    },

    /**
     * Format a date according to a sequence of components.
     * @private
     * @param {IDate} date a date/time object to format
     * @param {Array.<string>} templateArr an array of components to format
     * @return {string} the formatted date
     */
    _formatTemplate: function (date, templateArr) {
        var i, key, temp, tz, str = "";
        for (i = 0; i < templateArr.length; i++) {
            switch (templateArr[i]) {
                case 'd':
                    str += (date.day || 1);
                    break;
                case 'dd':
                    str += JSUtils.pad(date.day || "1", 2);
                    break;
                case 'yy':
                    temp = "" + ((date.year || 0) % 100);
                    str += JSUtils.pad(temp, 2);
                    break;
                case 'yyyy':
                    str += JSUtils.pad(date.year || "0", 4);
                    break;
                case 'M':
                    str += (date.month || 1);
                    break;
                case 'MM':
                    str += JSUtils.pad(date.month || "1", 2);
                    break;
                case 'h':
                    temp = (date.hour || 0) % 12;
                    if (temp == 0) {
                        temp = "12";
                    }
                    str += temp;
                    break;
                case 'hh':
                    temp = (date.hour || 0) % 12;
                    if (temp == 0) {
                        temp = "12";
                    }
                    str += JSUtils.pad(temp, 2);
                    break;
                /*
                case 'j':
                    temp = (date.hour || 0) % 12 + 1;
                    str += temp;
                    break;
                case 'jj':
                    temp = (date.hour || 0) % 12 + 1;
                    str += JSUtils.pad(temp, 2);
                    break;
                */
                case 'K':
                    temp = (date.hour || 0) % 12;
                    str += temp;
                    break;
                case 'KK':
                    temp = (date.hour || 0) % 12;
                    str += JSUtils.pad(temp, 2);
                    break;

                case 'H':
                    str += (date.hour || "0");
                    break;
                case 'HH':
                    str += JSUtils.pad(date.hour || "0", 2);
                    break;
                case 'k':
                    str += (date.hour == 0 ? "24" : date.hour);
                    break;
                case 'kk':
                    temp = (date.hour == 0 ? "24" : date.hour);
                    str += JSUtils.pad(temp, 2);
                    break;

                case 'm':
                    str += (date.minute || "0");
                    break;
                case 'mm':
                    str += JSUtils.pad(date.minute || "0", 2);
                    break;
                case 's':
                    str += (date.second || "0");
                    break;
                case 'ss':
                    str += JSUtils.pad(date.second || "0", 2);
                    break;
                case 'S':
                    str += (date.millisecond || "0");
                    break;
                case 'SSS':
                    str += JSUtils.pad(date.millisecond || "0", 3);
                    break;

                case 'N':
                case 'NN':
                case 'MMM':
                case 'MMMM':
                case 'L':
                case 'LL':
                case 'LLL':
                case 'LLLL':
                    key = templateArr[i] + (date.month || 1);
                    str += (this.sysres.getString(undefined, key + "-" + this.calName) || this.sysres.getString(undefined, key) ||
                           this.sysres.getString(undefined, key.replace(/L/g,"M") + "-" + this.calName) || this.sysres.getString(undefined, key.replace(/L/g,"M")));
                    break;

                case 'E':
                case 'EE':
                case 'EEE':
                case 'EEEE':
                case 'c':
                case 'cc':
                case 'ccc':
                case 'cccc':
                    key = templateArr[i] + date.getDayOfWeek();
                    //console.log("finding " + key + " in the resources");
                    str += (this.sysres.getString(undefined, key + "-" + this.calName) || this.sysres.getString(undefined, key));
                    break;

                case 'a':
                    switch (this.meridiems) {
                    case "chinese":
                        if (date.hour < 6) {
                            key = "azh0";    // before dawn
                        } else if (date.hour < 9) {
                            key = "azh1";    // morning
                        } else if (date.hour < 12) {
                            key = "azh2";    // late morning/day before noon
                        } else if (date.hour < 13) {
                            key = "azh3";    // noon hour/midday
                        } else if (date.hour < 18) {
                            key = "azh4";    // afternoon
                        } else if (date.hour < 21) {
                            key = "azh5";    // evening time/dusk
                        } else {
                            key = "azh6";    // night time
                        }
                        break;
                    case "ethiopic":
                        if (date.hour < 6) {
                            key = "a0-ethiopic";    // morning
                        } else if (date.hour === 6 && date.minute === 0) {
                            key = "a1-ethiopic";    // noon
                        } else if (date.hour >= 6 && date.hour < 12) {
                            key = "a2-ethiopic";    // afternoon
                        } else if (date.hour >= 12 && date.hour < 18) {
                            key = "a3-ethiopic";    // evening
                        } else if (date.hour >= 18) {
                            key = "a4-ethiopic";    // night
                        }
                        break;
                    default:
                        key = date.hour < 12 ? "a0" : "a1";
                        break;
                    }
                    //console.log("finding " + key + " in the resources");
                    str += (this.sysres.getString(undefined, key + "-" + this.calName) || this.sysres.getString(undefined, key));
                    break;

                case 'B':
                    str += this._findMeridiem(date.hour, date.minute);
                    break;

                case 'w':
                    str += date.getWeekOfYear();
                    break;
                case 'ww':
                    str += JSUtils.pad(date.getWeekOfYear(), 2);
                    break;

                case 'D':
                    str += date.getDayOfYear();
                    break;
                case 'DD':
                    str += JSUtils.pad(date.getDayOfYear(), 2);
                    break;
                case 'DDD':
                    str += JSUtils.pad(date.getDayOfYear(), 3);
                    break;
                case 'W':
                    str += date.getWeekOfMonth(this.locale);
                    break;

                case 'G':
                    key = "G" + date.getEra();
                    str += (this.sysres.getString(undefined, key + "-" + this.calName) || this.sysres.getString(undefined, key));
                    break;

                case 'O':
                    temp = this.sysres.getString("1#1st|2#2nd|3#3rd|21#21st|22#22nd|23#23rd|31#31st|#{num}th", "ordinalChoice");
                    str += temp.formatChoice(date.day, {num: date.day}, false);
                    break;

                case 'z': // general time zone
                    tz = this.getTimeZone(); // lazy-load the tz
                    str += tz.getDisplayName(date, "standard");
                    break;
                case 'Z': // RFC 822 time zone
                    tz = this.getTimeZone(); // lazy-load the tz
                    str += tz.getDisplayName(date, "rfc822");
                    break;

                default:
                    str += templateArr[i].replace(/'/g, "");
                    break;
            }
        }

        if (this.digits) {
            str = JSUtils.mapString(str, this.digits);
        }
        return str;
    },

    /**
     * Format a particular date instance according to the settings of this
     * formatter object. The type of the date instance being formatted must
     * correspond exactly to the calendar type with which this formatter was
     * constructed. If the types are not compatible, this formatter will
     * produce bogus results.
     *
     * @param {IDate|number|string|Date|JulianDay|null|undefined} dateLike a date-like object to format
     * @return {string} the formatted version of the given date instance
     */
    format: function (dateLike) {
        var thisZoneName = this.tz && this.tz.getId() || "local";

        var date = DateFactory._dateToIlib(dateLike, thisZoneName, this.locale);

        if (!date.getCalendar || !(date instanceof IDate)) {
            throw "Wrong date type passed to DateFmt.format()";
        }

        if(this.useIntl && this.IntlDateTimeObj){
            var jsDate = DateFactory._ilibToDate(date, thisZoneName, this.locale);
            return this.IntlDateTimeObj.format(jsDate);
        }

        var dateZoneName = date.timezone || "local";

        // convert to the time zone of this formatter before formatting
        if (dateZoneName !== thisZoneName || date.getCalendar() !== this.calName) {
            // console.log("Differing time zones date: " + dateZoneName + " and fmt: " + thisZoneName + ". Converting...");
            // this will recalculate the date components based on the new time zone
            // and/or convert a date in another calendar to the current calendar before formatting it
            var newDate = DateFactory({
                type: this.calName,
                timezone: thisZoneName,
                julianday: date.getJulianDay()
            });

            date = newDate;
        }
        return this._formatTemplate(date, this.templateArr);
    },

    /**
     * Return a string that describes a date relative to the given
     * reference date. The string returned is text that for the locale that
     * was specified when the formatter instance was constructed.<p>
     *
     * The date can be in the future relative to the reference date or in
     * the past, and the formatter will generate the appropriate string.<p>
     *
     * The text used to describe the relative reference depends on the length
     * of time between the date and the reference. If the time was in the
     * past, it will use the "ago" phrase, and in the future, it will use
     * the "in" phrase. Examples:<p>
     *
     * <ul>
     * <li>within a minute: either "X seconds ago" or "in X seconds"
     * <li>within an hour: either "X minutes ago" or "in X minutes"
     * <li>within a day: either "X hours ago" or "in X hours"
     * <li>within 2 weeks: either "X days ago" or "in X days"
     * <li>within 12 weeks (~3 months): either "X weeks ago" or "in X weeks"
     * <li>within two years: either "X months ago" or "in X months"
     * <li>longer than 2 years: "X years ago" or "in X years"
     * </ul>
     *
     * @param {IDate|number|string|Date|JulianDay|null|undefined} reference a date that the date parameter should be relative to
     * @param {IDate|number|string|Date|JulianDay|null|undefined} date a date being formatted
     * @throws "Wrong calendar type" when the start or end dates are not the same
     * calendar type as the formatter itself
     * @return {string} the formatted relative date
     */
    formatRelative: function(reference, date) {
        reference = DateFactory._dateToIlib(reference);
        date = DateFactory._dateToIlib(date);

        var referenceRd, dateRd, fmt, diff, absDiff, num;

        if (typeof(reference) !== 'object' || !reference.getCalendar || reference.getCalendar() !== this.calName ||
            typeof(date) !== 'object' || !date.getCalendar || date.getCalendar() !== this.calName) {
            throw "Wrong calendar type";
        }

        referenceRd = reference.getRataDie();
        dateRd = date.getRataDie();

        diff = referenceRd - dateRd;
        absDiff = Math.abs(diff);

        if (absDiff < 0.000694444) {
            num = Math.round(absDiff * 86400);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}s ago") : this.sysres.getString("#in {num}s");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 sec ago|#{num} sec ago") : this.sysres.getString("1#in 1 sec|#in {num} sec");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 second ago|#{num} seconds ago") : this.sysres.getString("1#in 1 second|#in {num} seconds");
                    break;
            }
        } else if (absDiff < 0.041666667) {
            num = Math.round(absDiff * 1440);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}mi ago") : this.sysres.getString("#in {num}mi");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 min ago|#{num} min ago") :  this.sysres.getString("1#in 1 min|#in {num} min");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 minute ago|#{num} minutes ago") : this.sysres.getString("1#in 1 minute|#in {num} minutes");
                    break;
            }
        } else if (absDiff < 1) {
            num = Math.round(absDiff * 24);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}h ago") : this.sysres.getString("#in {num}h");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 hr ago|#{num} hrs ago") : this.sysres.getString("1#in 1 hr|#in {num} hrs");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 hour ago|#{num} hours ago") : this.sysres.getString("1#in 1 hour|#in {num} hours");
                    break;
            }
        } else if (absDiff < 14) {
            num = Math.round(absDiff);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}d ago") : this.sysres.getString("#in {num}d");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 dy ago|#{num} dys ago") : this.sysres.getString("1#in 1 dy|#in {num} dys");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 day ago|#{num} days ago") : this.sysres.getString("1#in 1 day|#in {num} days");
                    break;
            }
        } else if (absDiff < 84) {
            num = Math.round(absDiff/7);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}w ago") : this.sysres.getString("#in {num}w");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 wk ago|#{num} wks ago") : this.sysres.getString("1#in 1 wk|#in {num} wks");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 week ago|#{num} weeks ago") : this.sysres.getString("1#in 1 week|#in {num} weeks");
                    break;
            }
        } else if (absDiff < 730) {
            num = Math.round(absDiff/30.4);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}mo ago") : this.sysres.getString("#in {num}mo");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 mon ago|#{num} mons ago") : this.sysres.getString("1#in 1 mon|#in {num} mons");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 month ago|#{num} months ago") : this.sysres.getString("1#in 1 month|#in {num} months");
                    break;
            }
        } else {
            num = Math.round(absDiff/365);
            switch (this.length) {
                case 's':
                    fmt = diff > 0 ? this.sysres.getString("#{num}y ago") : this.sysres.getString("#in {num}y");
                    break;
                case 'm':
                    fmt = diff > 0 ? this.sysres.getString("1#1 yr ago|#{num} yrs ago") : this.sysres.getString("1#in 1 yr|#in {num} yrs");
                    break;
                default:
                case 'f':
                case 'l':
                    fmt = diff > 0 ? this.sysres.getString("1#1 year ago|#{num} years ago") : this.sysres.getString("1#in 1 year|#in {num} years");
                    break;
            }
        }
        return fmt.formatChoice(num, {num: num});
    }
};

module.exports = DateFmt;