Source

DateRngFmt.js

/*
 * DateRngFmt.js - Date formatter definition
 *
 * Copyright © 2012-2015, 2018, 2020 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 JSUtils = require("./JSUtils.js");

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

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

var DateFmt = require("./DateFmt.js");
var IString = require("./IString.js");
var GregorianCal = require("./GregorianCal.js");

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

/**
 * @class
 * Create a new date range formatter instance. The date range formatter is immutable once
 * it is created, but can format as many different date ranges as needed with the same
 * options. Create different date range formatter instances for different purposes
 * and then keep them cached for use later if you have more than one range to
 * format.<p>
 *
 * The options may contain any of the following properties:
 *
 * <ul>
 * <li><i>locale</i> - locale to use when formatting the date/times in the range. 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 string in RFC 822 format. If not specified, the
 * default time zone for the locale is used.
 *
 * <li><i>length</i> - Specify the length of the format to use as a string. 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. (eg. "Tue" instead of "Tuesday")
 * <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 range may be "MM/yy - MM/yy", whereas the long format might be
 * "MMM, yyyy - 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. The
 * components that are formatted depend on the length of time in the range.
 *
 * <li><i>clock</i> - specify that formatted times should use a 12 or 24 hour clock if the
 * format happens to include times. 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>onLoad - a callback function to call when the date range format object is fully
 * loaded. When the onLoad option is given, the DateRngFmt 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>sync - 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>
 * <p>
 *
 *
 * @constructor
 * @param {Object} options options governing the way this date range formatter instance works
 */
var DateRngFmt = function(options) {
    var sync = true;
    var loadParams = undefined;
    this.locale = new Locale();
    this.length = "s";

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

        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.timezone) {
            this.timezone = options.timezone;
        }

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

        loadParams = options.loadParams;
    }

    var opts = {};
    JSUtils.shallowCopy(options, opts);
    opts.sync = sync;
    opts.loadParams = loadParams;

    /**
     * @private
     */
    opts.onLoad = ilib.bind(this, function (fmt) {
        this.dateFmt = fmt;
        if (fmt) {
            this.locinfo = this.dateFmt.locinfo;

            // 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";
            CalendarFactory({
                type: this.calName,
                sync: sync,
                loadParams: loadParams,
                onLoad: ilib.bind(this, function(cal) {
                    this.cal = cal;

                    if (!this.cal) {
                        // always synchronous
                        this.cal = new GregorianCal();
                    }

                    this.timeTemplate = this.dateFmt._getFormat(this.dateFmt.formats.time[this.dateFmt.clock], this.dateFmt.timeComponents, this.length) || "hh:mm";
                    this.timeTemplateArr = this.dateFmt._tokenize(this.timeTemplate);

                    if (options && typeof(options.onLoad) === 'function') {
                        options.onLoad(this);
                    }
                })
            });
        } else {
            if (options && typeof(options.sync) === "boolean" && !options.sync && typeof(options.onLoad) === 'function') {
                options.onLoad(undefined);
            } else {
                throw "No formats available for calendar " + this.calName + " in locale " + this.locale.getSpec();
            }
        }
    });

    // delegate a bunch of the formatting to this formatter
    new DateFmt(opts);
};

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

    /**
     * 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.dateFmt.getCalendar();
    },

    /**
     * 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 time zone used to format date/times for this formatter
     * instance.
     * @return {TimeZone} a string naming the time zone
     */
    getTimeZone: function () {
        return this.dateFmt.getTimeZone();
    },

    /**
     * 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.dateFmt.getClock();
    },

    /**
     * Format a date/time range according to the settings of the current
     * formatter. The range is specified as being from the "start" date until
     * the "end" date. <p>
     *
     * The template that the date/time range uses depends on the
     * length of time between the dates, on the premise that a long date range
     * which is too specific is not useful. For example, when giving
     * the dates of the 100 Years War, in most situations it would be more
     * appropriate to format the range as "1337 - 1453" than to format it as
     * "10:37am November 9, 1337 - 4:37pm July 17, 1453", as the latter format
     * is much too specific given the length of time that the range represents.
     * If a very specific, but long, date range really is needed, the caller
     * should format two specific dates separately and put them
     * together as you might with other normal strings.<p>
     *
     * The format used for a date range contains the following date components,
     * where the order of those components is rearranged and the component values
     * are translated according to each locale:
     *
     * <ul>
     * <li>within 3 days: the times of day, dates, months, and years
     * <li>within 730 days (2 years): the dates, months, and years
     * <li>within 3650 days (10 years): the months and years
     * <li>longer than 10 years: the years only
     * </ul>
     *
     * In general, if any of the date components share a value between the
     * start and end date, that component is only given once. For example,
     * if the range is from November 15, 2011 to November 26, 2011, the
     * start and end dates both share the same month and year. The
     * range would then be formatted as "November 15-26, 2011". <p>
     *
     * If you want to format a length of time instead of a particular range of
     * time (for example, the length of an event rather than the specific start time
     * and end time of that event), then use a duration formatter instance
     * (DurationFmt) instead. The formatRange method will make sure that each component
     * of the date/time is within the normal range for that component. For example,
     * the minutes will always be between 0 and 59, no matter what is specified in
     * the date to format, because that is the normal range for minutes. A duration
     * format will allow the number of minutes to exceed 59. For example, if you
     * were displaying the length of a movie that is 198 minutes long, the minutes
     * component of a duration could be 198.<p>
     *
     * @param {IDate|Date|number|string} startDateLike the starting date/time of the range. The
     * date may be given as an ilib IDate object, a javascript intrinsic Date object, a
     * unix time, or a date string parsable by the javscript Date.
     * @param {IDate|Date|number|string} endDateLike the ending date/time of the range. The
     * date may be given as an ilib IDate object, a javascript intrinsic Date object, a
     * unix time, or a date string parsable by the javscript Date.
     * @throws "Wrong calendar type" when the start or end dates are not the same
     * calendar type as the formatter itself
     * @return {string} a date range formatted for the locale
     */
    format: function (startDateLike, endDateLike) {
        var startRd, endRd, fmt = "", yearTemplate, monthTemplate, dayTemplate, formats;
        var thisZoneName = this.dateFmt.tz && this.dateFmt.tz.getId() || "local";

        var start = DateFactory._dateToIlib(startDateLike, thisZoneName, this.locale);
        var end = DateFactory._dateToIlib(endDateLike, thisZoneName, this.locale);
        var tmp;

        if (typeof(start) !== 'object' || !start.getCalendar || start.getCalendar() !== this.calName || (this.timezone && start.timezone && start.timezone !== this.timezone)) {
            start = DateFactory({
                type: this.calName,
                timezone: thisZoneName,
                julianday: start.getJulianDay()
            });
        }

        if (typeof(end) !== 'object' || !end.getCalendar || end.getCalendar() !== this.calName || (this.timezone && end.timezone && end.timezone !== this.timezone)) {
            end = DateFactory({
                type: this.calName,
                timezone: thisZoneName,
                julianday: end.getJulianDay()
            });
        }

        startRd = start.getRataDie();
        endRd = end.getRataDie();

        formats = this.dateFmt.formats.date;
        // these are not stand-alone templates. We add stand-alone below if necessary for the locale and the format.
        yearTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "y", this.length) || "yyyy");
        monthTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "m", this.length) || "MM");
        dayTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "d", this.length) || "dd");

        //
        // legend:
        // c00 - difference is less than 3 days. Year, month, and date are same, but time is different
        // c01 - difference is less than 3 days. Year and month are same but date and time are different
        // c02 - difference is less than 3 days. Year is same but month, date, and time are different. (ie. it straddles a month boundary)
        // c03 - difference is less than 3 days. Year, month, date, and time are all different. (ie. it straddles a year boundary)
        // c10 - difference is less than 2 years. Year and month are the same, but date is different.
        // c11 - difference is less than 2 years. Year is the same, but month, date, and time are different.
        // c12 - difference is less than 2 years. All fields are different. (ie. straddles a year boundary)
        // c20 - difference is less than 10 years. All fields are different.
        // c30 - difference is more than 10 years. All fields are different.
        //

        if (endRd - startRd < 3) {
            if (start.year === end.year) {
                if (start.month === end.month) {
                    if (start.day === end.day) {
                        fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c00", this.length));
                    } else {
                        fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c01", this.length));
                    }
                } else {
                    fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c02", this.length));
                }
            } else {
                fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c03", this.length));
            }
        } else if (endRd - startRd < 730) {
            if (start.year === end.year) {
                if (start.month === end.month) {
                    fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c10", this.length));
                } else {
                    fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c11", this.length));
                }
            } else {
                fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c12", this.length));
            }
        } else if (endRd - startRd < 3650) {
            fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c20", this.length));
            // need to check for month/year stand-alone formats here
            tmp = this.dateFmt._getFormatInternal(formats, "mys", this.length);
            if (tmp) {
                if (tmp.indexOf('r') > -1) {
                    var tmp2 = this.dateFmt._getFormatInternal(formats, "r", this.length);
                    if (tmp2) {
                        yearTemplate = this.dateFmt._tokenize(tmp2);
                    }
                }
                if (tmp.indexOf('L') > -1) {
                    var tmp3 = this.dateFmt._getFormatInternal(formats, "l", this.length);
                    if (tmp3) {
                        monthTemplate = this.dateFmt._tokenize(tmp3);
                    }
                }
            }
        } else {
            fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c30", this.length));
            // need to check for stand-alone year formats here
            tmp = this.dateFmt._getFormatInternal(formats, "r", this.length);
            if (tmp) {
                yearTemplate = this.dateFmt._tokenize(tmp);
            }
        }

        /*
        console.log("fmt is " + fmt.toString());
        console.log("year template is " + yearTemplate);
        console.log("month template is " + monthTemplate);
        console.log("day template is " + dayTemplate);
        */

        return fmt.format({
            sy: this.dateFmt._formatTemplate(start, yearTemplate),
            sm: this.dateFmt._formatTemplate(start, monthTemplate),
            sd: this.dateFmt._formatTemplate(start, dayTemplate),
            st: this.dateFmt._formatTemplate(start, this.timeTemplateArr),
            ey: this.dateFmt._formatTemplate(end, yearTemplate),
            em: this.dateFmt._formatTemplate(end, monthTemplate),
            ed: this.dateFmt._formatTemplate(end, dayTemplate),
            et: this.dateFmt._formatTemplate(end, this.timeTemplateArr)
        });
    }
};

module.exports = DateRngFmt;