Source

UnitFmt.js

/*
 * UnitFmt.js - Unit formatter class
 *
 * Copyright © 2014-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.
 */

/*
!depends
ilib.js
Locale.js
IString.js
NumFmt.js
Utils.js
ListFmt.js
Measurement.js
*/

// !data unitfmt

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

var Locale = require("./Locale.js");
var IString = require("./IString.js");
var NumFmt = require("./NumFmt.js");
var ListFmt = require("./ListFmt.js");
var Measurement = require("./Measurement.js");

// for converting ilib lengths to the ones that are supported in cldr
var lenMap = {
  "full": "long",
  "long": "long",
  "medium": "short",
  "short": "short"
};

/**
 * @class
 * Create a new unit formatter instance. The unit formatter is immutable once
 * it is created, but can format as many different strings with different values
 * as needed with the same options. Create different unit formatter instances
 * for different purposes and then keep them cached for use later if you have
 * more than one unit string to format.<p>
 *
 * The options may contain any of the following properties:
 *
 * <ul>
 * <li><i>locale</i> - locale to use when formatting the units. The locale also
 * controls the translation of the names of the units. If the locale is
 * not specified, then the default locale of the app or web page will be used.
 *
 * <li><i>autoScale</i> - when true, automatically scale the amount to get the smallest
 * number greater than 1, where possible, possibly by converting units within the locale's
 * measurement system. For example, if the current locale is "en-US", and we have
 * a measurement containing 278 fluid ounces, then the number "278" can be scaled down
 * by converting the units to a larger one such as gallons. The scaled size would be
 * 2.17188 gallons. Since iLib does not have a US customary measure larger than gallons,
 * it cannot scale it down any further. If the amount is less than the smallest measure
 * already, it cannot be scaled down any further and no autoscaling will be applied.
 * Default for the autoScale property is "true", so it only needs to be specified when
 * you want to turn off autoscaling.
 *
 * <li><i>autoConvert</i> - automatically convert the units to the nearest appropriate
 * measure of the same type in the measurement system used by the locale. For example,
 * if a measurement of length is given in meters, but the current locale is "en-US"
 * which uses the US Customary system, then the nearest appropriate measure would be
 * "yards", and the amount would be converted from meters to yards automatically before
 * being formatted. Default for the autoConvert property is "true", so it only needs to
 * be specified when you want to turn off autoconversion.
 *
 * <li><i>usage</i> - describe the reason for the measure. For example, the usage of
 * a formatter may be for a "person height", which implies that certain customary units
 * should be used, even though other measures in the same system may be more efficient.
 * In US Customary measures, a person's height is traditionally given in feet and inches,
 * even though yards, feet and inches would be more efficient and logical.<br><br>
 * Specifying a usage implies that the
 * autoScale is turned on so that the measure can be scaled to the level required for
 * the customary measures for the usage. Setting the usage can also implicitly set
 * the style, the max- and minFractionDigits, roundingMode, length, etc. if those
 * options are not explicitly given in this options object. If they are given, the
 * explicit settings override the defaults of the usage.<br><br>
 * Usages imply that the formatter should be used with a specific type of measurement.
 * If the format method is called on a measurement that is of the wrong type for the
 * usage, it will be formatted as a regular measurement with default options.<br><br>
 * List of usages currently supported:
 *   <ul>
 *   <li><i>general</i> no specific usage with no preselected measures. (Default which does not
 *   restrict the units used for any type of measurement.)
 *   <li><i>floorSpace</i> area of the floor of a house or building
 *   <li><i>landArea</i> area of a piece of plot of land
 *   <li><i>networkingSpeed</i> speed of transfer of data over a network
 *   <li><i>audioSpeed</i> speed of transfer of audio data
 *   <li><i>interfaceSpeed</i> speed of transfer of data over a computer interface such as a USB or SATA bus
 *   <li><i>foodEnergy</i> amount of energy contains in food
 *   <li><i>electricalEnergy</i> amount of energy in electricity
 *   <li><i>heatingEnergy</i> amount of energy required to heat things such as water or home interiors
 *   <li><i>babyHeight</i> length of a baby
 *   <li><i>personHeight</i> height of an adult or child (not a baby)
 *   <li><i>vehicleDistance</i> distance traveled by a vehicle or aircraft (except a boat)
 *   <li><i>nauticalDistance</i> distance traveled by a boat
 *   <li><i>personWeight</i> weight/mass of an adult human or larger child
 *   <li><i>babyWeight</i> weight/mass of a baby or of small animals such as cats and dogs
 *   <li><i>vehicleWeight</i> weight/mass of a vehicle (including a boat)
 *   <li><i>drugWeight</i> weight/mass of a medicinal drug
 *   <li><i>vehicleSpeed</i> speed of travel of a vehicle or aircraft (except a boat)
 *   <li><i>nauticalSpeed</i> speed of travel of a boat
 *   <li><i>dryFoodVolume</i> volume of a dry food substance in a recipe such as flour
 *   <li><i>liquidFoodVolume</i> volume of a liquid food substance in a recipe such as milk
 *   <li><i>drinkVolume</i> volume of a drink
 *   <li><i>fuelVolume</i> volume of a vehicular fuel
 *   <li><i>engineVolume</i> volume of an engine's combustion space
 *   <li><i>storageVolume</i> volume of a mass storage tank
 *   <li><i>gasVolume</i> volume of a gas such as natural gas used in a home
 *   </ul>
 *
 * <li><i>style</i> - give the style of this formatter. This is used to
 * decide how to format the number and units when the number is not whole, or becomes
 * not whole after auto conversion and scaling. There are two basic styles
 * supported so far:
 *
 *   <ul>
 *   <li><i>numeric</i> - only the largest unit is used and the number is given as
 *   decimals. Example: "5.25 lbs"
 *   <li><i>list</i> - display the measure with a list of successively smaller-sized
 *   units. Example: "5 lbs 4 oz"
 *   </ul>
 * The style is most useful for units which are not powers of 10 greater than the
 * smaller units as in the metric system, though it can be useful for metric measures
 * as well. Example: "2kg 381g".<br><br>
 * The style may be set implicitly when you set the usage. For example, if the usage is
 * "personWeight", the style will be "numeric" and the maxFractionDigits will be 0. That
 * is, weight of adults and children are most often given in whole pounds. (eg. "172 lbs").
 * If the usage is "babyWeight", the style will be "list", and the measures will be pounds
 * and ounces. (eg. "7 lbs 2 oz").
 *
 * <li><i>length</i> - the length of the units text. This can be either "short" or "long"
 * with the default being "long". Example: a short units text might be "kph" and the
 * corresponding long units text would be "kilometers per hour". Typically, it is the
 * long units text that is translated per locale, though the short one may be as well.
 * Plurals are taken care of properly per locale as well.
 *
 * <li><i>maxFractionDigits</i> - the maximum number of digits that should appear in the
 * formatted output after the decimal. A value of -1 means unlimited, and 0 means only print
 * the integral part of the number.
 *
 * <li><i>minFractionDigits</i> - the minimum number of fractional digits that should
 * appear in the formatted output. If the number does not have enough fractional digits
 * to reach this minimum, the number will be zero-padded at the end to get to the limit.
 *
 * <li><i>significantDigits</i> - the number of significant digits that should appear
 * in the formatted output. If the given number is less than 1, this option will be ignored.
 *
 * <li><i>roundingMode</i> - When the maxFractionDigits or maxIntegerDigits is specified,
 * this property governs how the least significant digits are rounded to conform to that
 * maximum. The value of this property is a string with one of the following values:
 * <ul>
 *   <li><i>up</i> - round away from zero
 *   <li><i>down</i> - round towards zero. This has the effect of truncating the number
 *   <li><i>ceiling</i> - round towards positive infinity
 *   <li><i>floor</i> - round towards negative infinity
 *   <li><i>halfup</i> - round towards nearest neighbour. If equidistant, round up.
 *   <li><i>halfdown</i> - round towards nearest neighbour. If equidistant, round down.
 *   <li><i>halfeven</i> - round towards nearest neighbour. If equidistant, round towards the even neighbour
 *   <li><i>halfodd</i> - round towards nearest neighbour. If equidistant, round towards the odd neighbour
 * </ul>
 * Default if this is not specified is "halfup".
 *
 * <li><i>onLoad</i> - a callback function to call when the date format object is fully
 * loaded. When the onLoad option is given, the UnitFmt 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>
 *
 *
 * @constructor
 * @param {Object} options options governing the way this date formatter instance works
 */
var UnitFmt = function(options) {
    var sync = true,
        loadParams = undefined;

    this.length = "long";
    this.scale  = true;
    this.measurementType = 'undefined';
    this.convert = true;
    this.locale = new Locale();

    options = options || {sync: true};

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

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

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

    if (options.length) {
        this.length = lenMap[options.length] || "long";
    }

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

    if (typeof(options.style) === 'string') {
        this.style = options.style;
    }

    if (typeof(options.usage) === 'string') {
        this.usage = options.usage;
    }

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

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

    if (options.measurementSystem) {
        this.measurementSystem = options.measurementSystem;
    }

    if (typeof (options.maxFractionDigits) !== 'undefined') {
        /**
         * @private
         * @type {number|undefined}
         */
        this.maxFractionDigits = Number(options.maxFractionDigits);
    }
    if (typeof (options.minFractionDigits) !== 'undefined') {
        /**
         * @private
         * @type {number|undefined}
         */
        this.minFractionDigits = Number(options.minFractionDigits);
    }

    if (typeof (options.significantDigits) !== 'undefined') {
        /**
         * @private
         * @type {number|undefined}
         */
        this.significantDigits = Number(options.significantDigits);
    }

    /**
     * @private
     * @type {string}
     */
    this.roundingMode = options.roundingMode || "halfup";

    // ensure that the plural rules are loaded before we proceed
    IString.loadPlurals(sync, this.locale, loadParams, ilib.bind(this, function() {
        Utils.loadData({
            object: "UnitFmt",
            locale: this.locale,
            name: "unitfmt.json",
            sync: sync,
            loadParams: loadParams,
            callback: ilib.bind(this, function (format) {
                this.template = format["unitfmt"][this.length];

                if (this.usage && format.usages && format.usages[this.usage]) {
                    // if usage is not recognized, usageInfo will be undefined, which we will use to indicate unknown usage
                    this.usageInfo = format.usages[this.usage];

                    // default settings for this usage, but don't override the options that were passed in
                    if (typeof(this.maxFractionDigits) !== 'number' && typeof(this.usageInfo.maxFractionDigits) === 'number') {
                        this.maxFractionDigits = this.usageInfo.maxFractionDigits;
                    }
                    if (typeof(this.minFractionDigits) !== 'number' && typeof(this.usageInfo.minFractionDigits) === 'number') {
                        this.minFractionDigits = this.usageInfo.minFractionDigits;
                    }
                    if (typeof(this.significantDigits) !== 'number' && typeof(this.usageInfo.significantDigits) === 'number') {
                        this.significantDigits = this.usageInfo.significantDigits;
                    }
                    if (!this.measurementSystem && this.usageInfo.system) {
                        this.measurementSystem = this.usageInfo.system;
                    }
                    this.units = this.usageInfo.units;
                    if (!this.style && this.usageInfo.style) {
                        this.style = this.usageInfo.style;
                    }

                    if (this.usageInfo.systems) {
                        this.units = {
                            metric: this.usageInfo.systems.metric.units,
                            uscustomary: this.usageInfo.systems.uscustomary.units,
                            imperial: this.usageInfo.systems.imperial.units
                        };
                        this.numFmt = {};
                        this._initNumFmt(sync, loadParams, this.usageInfo.systems.metric, ilib.bind(this, function(numfmt) {
                            this.numFmt.metric = numfmt;
                            this._initNumFmt(sync, loadParams, this.usageInfo.systems.uscustomary, ilib.bind(this, function(numfmt) {
                                this.numFmt.uscustomary = numfmt;
                                this._initNumFmt(sync, loadParams, this.usageInfo.systems.imperial, ilib.bind(this, function(numfmt) {
                                    this.numFmt.imperial = numfmt;
                                    this._init(sync, loadParams, ilib.bind(this, function () {
                                        if (options && typeof(options.onLoad) === 'function') {
                                            options.onLoad(this);
                                        }
                                    }));
                                }));
                            }));
                        }));
                    } else {
                        this._initFormatters(sync, loadParams, {}, ilib.bind(this, function() {
                            if (options && typeof(options.onLoad) === 'function') {
                                options.onLoad(this);
                            }
                        }));
                    }
                } else {
                    this._initFormatters(sync, loadParams, {}, ilib.bind(this, function() {
                        if (options && typeof(options.onLoad) === 'function') {
                            options.onLoad(this);
                        }
                    }));
                }
            })
        });
    }));
};

UnitFmt.prototype = {
    /** @private */
    _initNumFmt: function(sync, loadParams, options, callback) {
        new NumFmt({
            locale: this.locale,
            useNative: this.useNative,
            maxFractionDigits: typeof(this.maxFractionDigits) !== 'undefined' ? this.maxFractionDigits : options.maxFractionDigits,
            minFractionDigits: typeof(this.minFractionDigits) !== 'undefined' ? this.minFractionDigits : options.minFractionDigits,
            significantDigits: typeof(this.significantDigits) !== 'undefined' ? this.significantDigits : options.significantDigits,
            roundingMode: this.roundingMode || options.roundingMode,
            sync: sync,
            loadParams: loadParams,
            onLoad: ilib.bind(this, function (numfmt) {
                callback(numfmt);
            })
        });
    },

    _initFormatters: function(sync, loadParams, options, callback) {
        this._initNumFmt(sync, loadParams, {}, ilib.bind(this, function(numfmt) {
            this.numFmt = {
                metric: numfmt,
                uscustomary: numfmt,
                imperial: numfmt
            };

            this._init(sync, loadParams, callback);
        }));
    },

    /** @private */
    _init: function(sync, loadParams, callback) {
        if (this.style === "list" || (this.usageInfo && this.usageInfo.systems &&
                (this.usageInfo.systems.metric.style === "list" ||
                this.usageInfo.systems.uscustomary.style === "list" ||
                this.usageInfo.systems.imperial.style === "list"))) {
            new ListFmt({
                locale: this.locale,
                style: "unit",
                sync: sync,
                loadParams: loadParams,
                onLoad: ilib.bind(this, function (listFmt) {
                    this.listFmt = listFmt;
                    callback();
                })
            });
        } else {
            callback();
        }
    },

    /**
     * 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;
    },

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

    /**
     * Return whether or not this formatter will auto-scale the units while formatting.
     * @returns {boolean} true if auto-scaling is turned on
     */
    getScale: function() {
        return this.scale;
    },

    /**
     * Return the measurement system that is used for this formatter.
     * @returns {string} the measurement system used in this formatter
     */
    getMeasurementSystem: function() {
        return this.measurementSystem;
    },

    /**
     * @private
     */
    _format: function(u, system) {
        var unit = u.getUnit() === "long-ton" ? "ton" : u.getUnit();
        var formatted = new IString(this.template[unit]);
        // make sure to use the right plural rules
        formatted.setLocale(this.locale, true, undefined, undefined);
        var rounded = this.numFmt[system].constrain(u.amount);
        formatted = formatted.formatChoice(rounded, {n: this.numFmt[system].format(u.amount)});
        return formatted.length > 0 ? formatted : rounded + " " + u.unit;
    },

    /**
     * Format a particular unit instance according to the settings of this
     * formatter object.
     *
     * @param {Measurement} measurement measurement to format
     * @return {string} the formatted version of the given date instance
     */
    format: function (measurement) {
        var u = measurement, system, listStyle;
        var doScale = this.scale;

        if (this.convert) {
            if (this.measurementSystem) {
                if (this.measurementSystem !== measurement.getMeasurementSystem()) {
                    u = u.convertSystem(this.measurementSystem);
                }
            } else if (!this.usageInfo || Measurement.getMeasurementSystemForLocale(this.locale) !== u.getMeasurementSystem()) {
                u = measurement.localize(this.locale);
            }

            doScale = (this.usageInfo && measurement.getMeasurementSystem() !== u.getMeasurementSystem()) || this.scale;
        }

        system = u.getMeasurementSystem() || this.getMeasurementSystem() || "metric";
        listStyle = (this.style === "list" || (this.usageInfo && this.usageInfo.systems && this.usageInfo.systems[system].style === "list"));

        if (doScale) {
            if (this.usageInfo && measurement.getMeasure() === this.usageInfo.type && !listStyle) {
                // scaling with a restricted set of units
                u = u.scale(system, this.units);
            } else {
                u = u.scale(); // scale within the current system
            }
        }

        if (listStyle) {
            var numFmt = this.numFmt[system];
            u = u.expand(undefined, this.units, ilib.bind(numFmt, numFmt.constrain), this.scale);
            var formatted = u.map(ilib.bind(this, function(unit) {
                return this._format(unit, system);
            }));
            if (this.listFmt && formatted.length) {
                return this.listFmt.format(formatted);
            } else {
                return formatted.join(' ');
            }
        } else {
            return this._format(u, system);
        }
    }
};

module.exports = UnitFmt;