Source

DurationFmt.js

/*
 * DurationFmt.js - Date formatter definition
 *
 * Copyright © 2012-2015, 2018, 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 LocaleInfo = require("./LocaleInfo.js");
var DateFmt = require("./DateFmt.js");
var IString = require("./IString.js");
var ResBundle = require("./ResBundle.js");
var ScriptInfo = require("./ScriptInfo.js");

/**
 * @class
 * Create a new duration formatter instance. The duration formatter is immutable once
 * it is created, but can format as many different durations as needed with the same
 * options. Create different duration formatter instances for different purposes
 * and then keep them cached for use later if you have more than one duration to
 * format.<p>
 *
 * Duration formatters format lengths of time. The duration formatter is meant to format
 * durations of such things as the length of a song or a movie or a meeting, or the
 * current position in that song or movie while playing it. If you wish to format a
 * period of time that has a specific start and end date/time, then use a
 * [DateRngFmt] instance instead and call its format method.<p>
 *
 * The options may contain any of the following properties:
 *
 * <ul>
 * <li><i>locale</i> - locale to use when formatting the duration. If the locale is
 * not specified, then the default locale of the app or web page will be used.
 *
 * <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 duration. This is the most compact format possible for the locale. eg. 1y 1m 1w 1d 1:01:01
 * <li><i>medium</i> - use a medium length representation of the duration. This is a slightly longer format. eg. 1 yr 1 mo 1 wk 1 dy 1 hr 1 mi 1 se
 * <li><i>long</i> - use a long representation of the duration. This is a fully specified format, but some of the textual
 * parts may still be abbreviated. eg. 1 yr 1 mo 1 wk 1 day 1 hr 1 min 1 sec
 * <li><i>full</i> - use a full representation of the duration. This is a fully specified format where all the textual
 * parts are spelled out completely. eg. 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute and 1 second
 * </ul>
 *
 * <li><i>style<i> - whether hours, minutes, and seconds should be formatted as a text string
 * or as a regular time as on a clock. eg. text is "1 hour, 15 minutes", whereas clock is "1:15:00". Valid
 * values for this property are "text" or "clock". Default if this property is not specified
 * is "text".
 *
 *<li><i>useNative</i> - the flag used to determaine whether to use the native script settings
 * for formatting the numbers .
 *
 * <li><i>onLoad</i> - a callback function to call when the format data is fully
 * loaded. When the onLoad option is given, this class 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 formatter instance works
 */
var DurationFmt = function(options) {
    var sync = true;
    var loadParams = undefined;

    this.locale = new Locale();
    this.length = "short";
    this.style = "text";

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

        if (options.length) {
            if (options.length === 'short' ||
                options.length === 'medium' ||
                options.length === 'long' ||
                options.length === 'full') {
                this.length = options.length;
            }
        }

        if (options.style) {
            if (options.style === 'text' || options.style === 'clock') {
                this.style = options.style;
            }
        }

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

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

        loadParams = options.loadParams;
    }
    options = options || {sync: true};

    new LocaleInfo(this.locale, {
        sync: sync,
        loadParams: loadParams,
        onLoad: ilib.bind(this, function (li) {
            this.script = li.getScript();
            new ResBundle({
                locale: this.locale,
                name: "sysres",
                sync: sync,
                loadParams: loadParams,
                onLoad: ilib.bind(this, function (sysres) {
                    IString.loadPlurals(sync, this.locale, loadParams, ilib.bind(this, function() {
                        if (this.length === 'medium' && !(this.script === 'Latn' || this.script ==='Grek' || this.script ==='Cyrl')) {
                            this.length = 'short';
                        }
                        switch (this.length) {
                            case 'short':
                                this.components = {
                                year: sysres.getString("#{num}y"),
                                month: sysres.getString("#{num}m", "durationShortMonths"),
                                week: sysres.getString("#{num}w"),
                                day: sysres.getString("#{num}d"),
                                hour: sysres.getString("#{num}h"),
                                minute: sysres.getString("#{num}m", "durationShortMinutes"),
                                second: sysres.getString("#{num}s"),
                                millisecond: sysres.getString("#{num}m", "durationShortMillis"),
                                separator: sysres.getString(" ", "separatorShort"),
                                finalSeparator: "" // not used at this length
                            };
                                break;

                            case 'medium':
                                this.components = {
                                year: sysres.getString("1#1 yr|#{num} yrs", "durationMediumYears"),
                                month: sysres.getString("1#1 mo|#{num} mos"),
                                week: sysres.getString("1#1 wk|#{num} wks", "durationMediumWeeks"),
                                day: sysres.getString("1#1 dy|#{num} dys"),
                                hour: sysres.getString("1#1 hr|#{num} hrs", "durationMediumHours"),
                                minute: sysres.getString("1#1 mi|#{num} min"),
                                second: sysres.getString("1#1 se|#{num} sec"),
                                millisecond: sysres.getString("#{num} ms", "durationMediumMillis"),
                                separator: sysres.getString(" ", "separatorMedium"),
                                finalSeparator: "" // not used at this length
                            };
                                break;

                            case 'long':
                                this.components = {
                                year: sysres.getString("1#1 yr|#{num} yrs"),
                                month: sysres.getString("1#1 mon|#{num} mons"),
                                week: sysres.getString("1#1 wk|#{num} wks"),
                                day: sysres.getString("1#1 day|#{num} days", "durationLongDays"),
                                hour: sysres.getString("1#1 hr|#{num} hrs"),
                                minute: sysres.getString("1#1 min|#{num} min"),
                                second: sysres.getString("1#1 sec|#{num} sec"),
                                millisecond: sysres.getString("#{num} ms"),
                                separator: sysres.getString(", ", "separatorLong"),
                                finalSeparator: "" // not used at this length
                            };
                                break;

                            case 'full':
                                this.components = {
                                year: sysres.getString("1#1 year|#{num} years"),
                                month: sysres.getString("1#1 month|#{num} months"),
                                week: sysres.getString("1#1 week|#{num} weeks"),
                                day: sysres.getString("1#1 day|#{num} days"),
                                hour: sysres.getString("1#1 hour|#{num} hours"),
                                minute: sysres.getString("1#1 minute|#{num} minutes"),
                                second: sysres.getString("1#1 second|#{num} seconds"),
                                millisecond: sysres.getString("1#1 millisecond|#{num} milliseconds"),
                                separator: sysres.getString(", ", "separatorFull"),
                                finalSeparator: sysres.getString(" and ", "finalSeparatorFull")
                            };
                                break;
                        }

                        if (this.style === 'clock') {
                            new DateFmt({
                                locale: this.locale,
                                calendar: "gregorian",
                                type: "time",
                                time: "ms",
                                sync: sync,
                                loadParams: loadParams,
                                useNative: this.useNative,
                                onLoad: ilib.bind(this, function (fmtMS) {
                                    this.timeFmtMS = fmtMS;
                                    new DateFmt({
                                        locale: this.locale,
                                        calendar: "gregorian",
                                        type: "time",
                                        time: "hm",
                                        sync: sync,
                                        loadParams: loadParams,
                                        useNative: this.useNative,
                                        onLoad: ilib.bind(this, function (fmtHM) {
                                            this.timeFmtHM = fmtHM;
                                            new DateFmt({
                                                locale: this.locale,
                                                calendar: "gregorian",
                                                type: "time",
                                                time: "hms",
                                                sync: sync,
                                                loadParams: loadParams,
                                                useNative: this.useNative,
                                                onLoad: ilib.bind(this, function (fmtHMS) {
                                                    this.timeFmtHMS = fmtHMS;

                                                    // munge with the template to make sure that the hours are not formatted mod 12
                                                    this.timeFmtHM.template = this.timeFmtHM.template.replace(/hh?/, 'H');
                                                    this.timeFmtHM.templateArr = this.timeFmtHM._tokenize(this.timeFmtHM.template);
                                                    this.timeFmtHMS.template = this.timeFmtHMS.template.replace(/hh?/, 'H');
                                                    this.timeFmtHMS.templateArr = this.timeFmtHMS._tokenize(this.timeFmtHMS.template);

                                                    this._init(this.timeFmtHM.locinfo, options);
                                                })
                                            });
                                        })
                                    });
                                })
                            });
                            return;
                        }
                        this._init(li, options);
                    }));
                })
            });
        })
    });
};

/**
 * @private
 * @static
 */
DurationFmt.complist = {
    "text": ["year", "month", "week", "day", "hour", "minute", "second", "millisecond"],
    "clock": ["year", "month", "week", "day"]
};

/**
 * @private
 */
DurationFmt.prototype._mapDigits = function(str) {
    if (this.useNative && this.digits) {
        return JSUtils.mapString(str.toString(), this.digits);
    }
    return str;
};

/**
 * @private
 * @param {LocaleInfo} locinfo
 * @param {Object|undefined} options
 */
DurationFmt.prototype._init = function(locinfo, options) {
    var digits;
    new ScriptInfo(locinfo.getScript(), {
        sync: options.sync,
        loadParams: options.loadParams,
        onLoad: ilib.bind(this, function(scriptInfo) {
            this.scriptDirection = scriptInfo.getScriptDirection();

            if (typeof(this.useNative) === 'boolean') {
                // if the caller explicitly said to use native or not, honour that despite what the locale data says...
                if (this.useNative) {
                    digits = locinfo.getNativeDigits();
                    if (digits) {
                        this.digits = digits;
                    }
                }
            } else if (locinfo.getDigitsStyle() === "native") {
                // else if the locale usually uses native digits, then use them
                digits = locinfo.getNativeDigits();
                if (digits) {
                    this.useNative = true;
                    this.digits = digits;
                }
            } // else use western digits always

            if (typeof(options.onLoad) === 'function') {
                options.onLoad(this);
            }
        })
    });
};

/**
 * Format a duration according to the format template of this formatter instance.<p>
 *
 * The components parameter should be an object that contains any or all of these
 * numeric properties:
 *
 * <ul>
 * <li>year
 * <li>month
 * <li>week
 * <li>day
 * <li>hour
 * <li>minute
 * <li>second
 * </ul>
 * <p>
 *
 * When a property is left out of the components parameter or has a value of 0, it will not
 * be formatted into the output string, except for times that include 0 minutes and 0 seconds.
 *
 * This formatter will not ensure that numbers for each component property is within the
 * valid range for that component. This allows you to format durations that are longer
 * than normal range. For example, you could format a duration has being "33 hours" rather
 * than "1 day, 9 hours".
 *
 * @param {Object} components date/time components to be formatted into a duration string
 * @return {IString} a string with the duration formatted according to the style and
 * locale set up for this formatter instance. If the components parameter is empty or
 * undefined, an empty string is returned.
 */
DurationFmt.prototype.format = function (components) {
    var i, list, fmt, secondlast = true, str = "";

    list = DurationFmt.complist[this.style];
    //for (i = 0; i < list.length; i++) {
    for (i = list.length-1; i >= 0; i--) {
        //console.log("Now dealing with " + list[i]);
        if (typeof(components[list[i]]) !== 'undefined' && components[list[i]] !== 0) {
            if (str.length > 0) {
                str = ((this.length === 'full' && secondlast) ? this.components.finalSeparator : this.components.separator) + str;
                secondlast = false;
            }
            str = this.components[list[i]].formatChoice(components[list[i]], {num: this._mapDigits(components[list[i]])}) + str;
        }
    }

    if (this.style === 'clock') {
        if (typeof(components.hour) !== 'undefined') {
            fmt = (typeof(components.second) !== 'undefined') ? this.timeFmtHMS : this.timeFmtHM;
        } else {
            fmt = this.timeFmtMS;
        }

        if (str.length > 0) {
            str += this.components.separator;
        }
        str += fmt._formatTemplate(components, fmt.templateArr);
    }

    if (this.scriptDirection === 'rtl') {
        str = "\u200F" + str;
    }
    return new IString(str);
};

/**
 * Return the locale that was used to construct this duration formatter object. If the
 * locale was not given as parameter to the constructor, this method returns the default
 * locale of the system.
 *
 * @return {Locale} locale that this duration formatter was constructed with
 */
DurationFmt.prototype.getLocale = function () {
    return this.locale;
};

/**
 * Return the length that was used to construct this duration formatter object. If the
 * length was not given as parameter to the constructor, this method returns the default
 * length. Valid values are "short", "medium", "long", and "full".
 *
 * @return {string} length that this duration formatter was constructed with
 */
DurationFmt.prototype.getLength = function () {
    return this.length;
};

/**
 * Return the style that was used to construct this duration formatter object. Returns
 * one of "text" or "clock".
 *
 * @return {string} style that this duration formatter was constructed with
 */
DurationFmt.prototype.getStyle = function () {
    return this.style;
};

module.exports = DurationFmt;