1 /*
  2  * UnitFmt.js - Unit formatter class
  3  *
  4  * Copyright © 2014-2015, 2018 JEDLSoft
  5  *
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  *
 10  *     http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *
 16  * See the License for the specific language governing permissions and
 17  * limitations under the License.
 18  */
 19 
 20 /*
 21 !depends
 22 ilib.js
 23 Locale.js
 24 IString.js
 25 NumFmt.js
 26 Utils.js
 27 ListFmt.js
 28 Measurement.js
 29 */
 30 
 31 // !data unitfmt
 32 
 33 var ilib = require("./ilib.js");
 34 var Utils = require("./Utils.js");
 35 
 36 var Locale = require("./Locale.js");
 37 var IString = require("./IString.js");
 38 var NumFmt = require("./NumFmt.js");
 39 var ListFmt = require("./ListFmt.js");
 40 var Measurement = require("./Measurement.js");
 41 
 42 // for converting ilib lengths to the ones that are supported in cldr
 43 var lenMap = {
 44   "full": "long",
 45   "long": "long",
 46   "medium": "short",
 47   "short": "short"
 48 };
 49 
 50 /**
 51  * @class
 52  * Create a new unit formatter instance. The unit formatter is immutable once
 53  * it is created, but can format as many different strings with different values
 54  * as needed with the same options. Create different unit formatter instances
 55  * for different purposes and then keep them cached for use later if you have
 56  * more than one unit string to format.<p>
 57  *
 58  * The options may contain any of the following properties:
 59  *
 60  * <ul>
 61  * <li><i>locale</i> - locale to use when formatting the units. The locale also
 62  * controls the translation of the names of the units. If the locale is
 63  * not specified, then the default locale of the app or web page will be used.
 64  *
 65  * <li><i>autoScale</i> - when true, automatically scale the amount to get the smallest
 66  * number greater than 1, where possible, possibly by converting units within the locale's
 67  * measurement system. For example, if the current locale is "en-US", and we have
 68  * a measurement containing 278 fluid ounces, then the number "278" can be scaled down
 69  * by converting the units to a larger one such as gallons. The scaled size would be
 70  * 2.17188 gallons. Since iLib does not have a US customary measure larger than gallons,
 71  * it cannot scale it down any further. If the amount is less than the smallest measure
 72  * already, it cannot be scaled down any further and no autoscaling will be applied.
 73  * Default for the autoScale property is "true", so it only needs to be specified when
 74  * you want to turn off autoscaling.
 75  *
 76  * <li><i>autoConvert</i> - automatically convert the units to the nearest appropriate
 77  * measure of the same type in the measurement system used by the locale. For example,
 78  * if a measurement of length is given in meters, but the current locale is "en-US"
 79  * which uses the US Customary system, then the nearest appropriate measure would be
 80  * "yards", and the amount would be converted from meters to yards automatically before
 81  * being formatted. Default for the autoConvert property is "true", so it only needs to
 82  * be specified when you want to turn off autoconversion.
 83  *
 84  * <li><i>usage</i> - describe the reason for the measure. For example, the usage of
 85  * a formatter may be for a "person height", which implies that certain customary units
 86  * should be used, even though other measures in the same system may be more efficient.
 87  * In US Customary measures, a person's height is traditionally given in feet and inches,
 88  * even though yards, feet and inches would be more efficient and logical.<p>
 89  *
 90  * Specifying a usage implies that the
 91  * autoScale is turned on so that the measure can be scaled to the level required for
 92  * the customary measures for the usage. Setting the usage can also implicitly set
 93  * the style, the max- and minFractionDigits, roundingMode, length, etc. if those
 94  * options are not explicitly given in this options object. If they are given, the
 95  * explicit settings override the defaults of the usage.<p>
 96  *
 97  * Usages imply that the formatter should be used with a specific type of measurement.
 98  * If the format method is called on a measurement that is of the wrong type for the
 99  * usage, it will be formatted as a regular measurement with default options.<p>
100  *
101  * List of usages currently supported:
102  *   <ul>
103  *   <li><i>general</i> no specific usage with no preselected measures. (Default which does not
104  *   restrict the units used for any type of measurement.)
105  *   <li><i>floorSpace</i> area of the floor of a house or building
106  *   <li><i>landArea</i> area of a piece of plot of land
107  *   <li><i>networkingSpeed</i> speed of transfer of data over a network
108  *   <li><i>audioSpeed</i> speed of transfer of audio data
109  *   <li><i>interfaceSpeed</i> speed of transfer of data over a computer interface such as a USB or SATA bus
110  *   <li><i>foodEnergy</i> amount of energy contains in food
111  *   <li><i>electricalEnergy</i> amount of energy in electricity
112  *   <li><i>heatingEnergy</i> amount of energy required to heat things such as water or home interiors
113  *   <li><i>babyHeight</i> length of a baby
114  *   <li><i>personHeight</i> height of an adult or child (not a baby)
115  *   <li><i>vehicleDistance</i> distance traveled by a vehicle or aircraft (except a boat)
116  *   <li><i>nauticalDistance</i> distance traveled by a boat
117  *   <li><i>personWeight</i> weight/mass of an adult human or larger child
118  *   <li><i>babyWeight</i> weight/mass of a baby or of small animals such as cats and dogs
119  *   <li><i>vehicleWeight</i> weight/mass of a vehicle (including a boat)
120  *   <li><i>drugWeight</i> weight/mass of a medicinal drug
121  *   <li><i>vehicleSpeed</i> speed of travel of a vehicle or aircraft (except a boat)
122  *   <li><i>nauticalSpeed</i> speed of travel of a boat
123  *   <li><i>dryFoodVolume</i> volume of a dry food substance in a recipe such as flour
124  *   <li><i>liquidFoodVolume</i> volume of a liquid food substance in a recipe such as milk
125  *   <li><i>drinkVolume</i> volume of a drink
126  *   <li><i>fuelVolume</i> volume of a vehicular fuel
127  *   <li><i>engineVolume</i> volume of an engine's combustion space
128  *   <li><i>storageVolume</i> volume of a mass storage tank
129  *   <li><i>gasVolume</i> volume of a gas such as natural gas used in a home
130  *   </ul>
131  *
132  * <li><i>style</i> - give the style of this formatter. This is used to
133  * decide how to format the number and units when the number is not whole, or becomes
134  * not whole after auto conversion and scaling. There are two basic styles
135  * supported so far:
136  *
137  *   <ul>
138  *   <li><i>numeric</i> - only the largest unit is used and the number is given as
139  *   decimals. Example: "5.25 lbs"
140  *   <li><i>list</i> - display the measure with a list of successively smaller-sized
141  *   units. Example: "5 lbs 4 oz"
142  *   </ul>
143  *
144  * The style is most useful for units which are not powers of 10 greater than the
145  * smaller units as in the metric system, though it can be useful for metric measures
146  * as well. Example: "2kg 381g".<p>
147  *
148  * The style may be set implicitly when you set the usage. For example, if the usage is
149  * "personWeight", the style will be "numeric" and the maxFractionDigits will be 0. That
150  * is, weight of adults and children are most often given in whole pounds. (eg. "172 lbs").
151  * If the usage is "babyWeight", the style will be "list", and the measures will be pounds
152  * and ounces. (eg. "7 lbs 2 oz").
153  *
154  * <li><i>length</i> - the length of the units text. This can be either "short" or "long"
155  * with the default being "long". Example: a short units text might be "kph" and the
156  * corresponding long units text would be "kilometers per hour". Typically, it is the
157  * long units text that is translated per locale, though the short one may be as well.
158  * Plurals are taken care of properly per locale as well.
159  *
160  * <li><i>maxFractionDigits</i> - the maximum number of digits that should appear in the
161  * formatted output after the decimal. A value of -1 means unlimited, and 0 means only print
162  * the integral part of the number.
163  *
164  * <li><i>minFractionDigits</i> - the minimum number of fractional digits that should
165  * appear in the formatted output. If the number does not have enough fractional digits
166  * to reach this minimum, the number will be zero-padded at the end to get to the limit.
167  *
168  * <li><i>significantDigits</i> - the number of significant digits that should appear
169  * in the formatted output. If the given number is less than 1, this option will be ignored.
170  *
171  * <li><i>roundingMode</i> - When the maxFractionDigits or maxIntegerDigits is specified,
172  * this property governs how the least significant digits are rounded to conform to that
173  * maximum. The value of this property is a string with one of the following values:
174  * <ul>
175  *   <li><i>up</i> - round away from zero
176  *   <li><i>down</i> - round towards zero. This has the effect of truncating the number
177  *   <li><i>ceiling</i> - round towards positive infinity
178  *   <li><i>floor</i> - round towards negative infinity
179  *   <li><i>halfup</i> - round towards nearest neighbour. If equidistant, round up.
180  *   <li><i>halfdown</i> - round towards nearest neighbour. If equidistant, round down.
181  *   <li><i>halfeven</i> - round towards nearest neighbour. If equidistant, round towards the even neighbour
182  *   <li><i>halfodd</i> - round towards nearest neighbour. If equidistant, round towards the odd neighbour
183  * </ul>
184  * Default if this is not specified is "halfup".
185  *
186  * <li><i>onLoad</i> - a callback function to call when the date format object is fully
187  * loaded. When the onLoad option is given, the UnitFmt object will attempt to
188  * load any missing locale data using the ilib loader callback.
189  * When the constructor is done (even if the data is already preassembled), the
190  * onLoad function is called with the current instance as a parameter, so this
191  * callback can be used with preassembled or dynamic loading or a mix of the two.
192  *
193  * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
194  * asynchronously. If this option is given as "false", then the "onLoad"
195  * callback must be given, as the instance returned from this constructor will
196  * not be usable for a while.
197  *
198  * <li><i>loadParams</i> - an object containing parameters to pass to the
199  * loader callback function when locale data is missing. The parameters are not
200  * interpretted or modified in any way. They are simply passed along. The object
201  * may contain any property/value pairs as long as the calling code is in
202  * agreement with the loader callback function as to what those parameters mean.
203  * </ul>
204  *
205  * Here is an example of how you might use the unit formatter to format a string with
206  * the correct units.<p>
207  *
208  *
209  * @constructor
210  * @param {Object} options options governing the way this date formatter instance works
211  */
212 var UnitFmt = function(options) {
213     var sync = true,
214         loadParams = undefined;
215 
216     this.length = "long";
217     this.scale  = true;
218     this.measurementType = 'undefined';
219     this.convert = true;
220     this.locale = new Locale();
221 
222     options = options || {sync: true};
223 
224     if (options.locale) {
225         this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
226     }
227 
228     if (typeof(options.sync) === 'boolean') {
229         sync = options.sync;
230     }
231 
232     if (typeof(options.loadParams) !== 'undefined') {
233         loadParams = options.loadParams;
234     }
235 
236     if (options.length) {
237         this.length = lenMap[options.length] || "long";
238     }
239 
240     if (typeof(options.autoScale) === 'boolean') {
241         this.scale = options.autoScale;
242     }
243 
244     if (typeof(options.style) === 'string') {
245         this.style = options.style;
246     }
247 
248     if (typeof(options.usage) === 'string') {
249         this.usage = options.usage;
250     }
251 
252     if (typeof(options.autoConvert) === 'boolean') {
253         this.convert = options.autoConvert;
254     }
255 
256     if (typeof(options.useNative) === 'boolean') {
257         this.useNative = options.useNative;
258     }
259 
260     if (options.measurementSystem) {
261         this.measurementSystem = options.measurementSystem;
262     }
263 
264     if (typeof (options.maxFractionDigits) !== 'undefined') {
265         /**
266          * @private
267          * @type {number|undefined}
268          */
269         this.maxFractionDigits = Number(options.maxFractionDigits);
270     }
271     if (typeof (options.minFractionDigits) !== 'undefined') {
272         /**
273          * @private
274          * @type {number|undefined}
275          */
276         this.minFractionDigits = Number(options.minFractionDigits);
277     }
278 
279     if (typeof (options.significantDigits) !== 'undefined') {
280         /**
281          * @private
282          * @type {number|undefined}
283          */
284         this.significantDigits = Number(options.significantDigits);
285     }
286 
287     /**
288      * @private
289      * @type {string}
290      */
291     this.roundingMode = options.roundingMode || "halfup";
292 
293     // ensure that the plural rules are loaded before we proceed
294     IString.loadPlurals(sync, this.locale, loadParams, ilib.bind(this, function() {
295         Utils.loadData({
296             object: "UnitFmt",
297             locale: this.locale,
298             name: "unitfmt.json",
299             sync: sync,
300             loadParams: loadParams,
301             callback: ilib.bind(this, function (format) {
302                 this.template = format["unitfmt"][this.length];
303 
304                 if (this.usage && format.usages && format.usages[this.usage]) {
305                     // if usage is not recognized, usageInfo will be undefined, which we will use to indicate unknown usage
306                     this.usageInfo = format.usages[this.usage];
307 
308                     // default settings for this usage, but don't override the options that were passed in
309                     if (typeof(this.maxFractionDigits) !== 'number' && typeof(this.usageInfo.maxFractionDigits) === 'number') {
310                         this.maxFractionDigits = this.usageInfo.maxFractionDigits;
311                     }
312                     if (typeof(this.minFractionDigits) !== 'number' && typeof(this.usageInfo.minFractionDigits) === 'number') {
313                         this.minFractionDigits = this.usageInfo.minFractionDigits;
314                     }
315                     if (typeof(this.significantDigits) !== 'number' && typeof(this.usageInfo.significantDigits) === 'number') {
316                         this.significantDigits = this.usageInfo.significantDigits;
317                     }
318                     if (!this.measurementSystem && this.usageInfo.system) {
319                         this.measurementSystem = this.usageInfo.system;
320                     }
321                     this.units = this.usageInfo.units;
322                     if (!this.style && this.usageInfo.style) {
323                         this.style = this.usageInfo.style;
324                     }
325 
326                     if (this.usageInfo.systems) {
327                         this.units = {
328                             metric: this.usageInfo.systems.metric.units,
329                             uscustomary: this.usageInfo.systems.uscustomary.units,
330                             imperial: this.usageInfo.systems.imperial.units
331                         };
332                         this.numFmt = {};
333                         this._initNumFmt(sync, loadParams, this.usageInfo.systems.metric, ilib.bind(this, function(numfmt) {
334                             this.numFmt.metric = numfmt;
335                             this._initNumFmt(sync, loadParams, this.usageInfo.systems.uscustomary, ilib.bind(this, function(numfmt) {
336                                 this.numFmt.uscustomary = numfmt;
337                                 this._initNumFmt(sync, loadParams, this.usageInfo.systems.imperial, ilib.bind(this, function(numfmt) {
338                                     this.numFmt.imperial = numfmt;
339                                     this._init(sync, loadParams, ilib.bind(this, function () {
340                                         if (options && typeof(options.onLoad) === 'function') {
341                                             options.onLoad(this);
342                                         }
343                                     }));
344                                 }));
345                             }));
346                         }));
347                     } else {
348                         this._initFormatters(sync, loadParams, {}, ilib.bind(this, function() {
349                             if (options && typeof(options.onLoad) === 'function') {
350                                 options.onLoad(this);
351                             }
352                         }));
353                     }
354                 } else {
355                     this._initFormatters(sync, loadParams, {}, ilib.bind(this, function() {
356                         if (options && typeof(options.onLoad) === 'function') {
357                             options.onLoad(this);
358                         }
359                     }));
360                 }
361             })
362         });
363     }));
364 };
365 
366 UnitFmt.prototype = {
367     /** @private */
368     _initNumFmt: function(sync, loadParams, options, callback) {
369         new NumFmt({
370             locale: this.locale,
371             useNative: this.useNative,
372             maxFractionDigits: typeof(this.maxFractionDigits) !== 'undefined' ? this.maxFractionDigits : options.maxFractionDigits,
373             minFractionDigits: typeof(this.minFractionDigits) !== 'undefined' ? this.minFractionDigits : options.minFractionDigits,
374             significantDigits: typeof(this.significantDigits) !== 'undefined' ? this.significantDigits : options.significantDigits,
375             roundingMode: this.roundingMode || options.roundingMode,
376             sync: sync,
377             loadParams: loadParams,
378             onLoad: ilib.bind(this, function (numfmt) {
379                 callback(numfmt);
380             })
381         });
382     },
383 
384     _initFormatters: function(sync, loadParams, options, callback) {
385         this._initNumFmt(sync, loadParams, {}, ilib.bind(this, function(numfmt) {
386             this.numFmt = {
387                 metric: numfmt,
388                 uscustomary: numfmt,
389                 imperial: numfmt
390             };
391 
392             this._init(sync, loadParams, callback);
393         }));
394     },
395 
396     /** @private */
397     _init: function(sync, loadParams, callback) {
398         if (this.style === "list" || (this.usageInfo && this.usageInfo.systems &&
399                 (this.usageInfo.systems.metric.style === "list" ||
400                 this.usageInfo.systems.uscustomary.style === "list" ||
401                 this.usageInfo.systems.imperial.style === "list"))) {
402             new ListFmt({
403                 locale: this.locale,
404                 style: "unit",
405                 sync: sync,
406                 loadParams: loadParams,
407                 onLoad: ilib.bind(this, function (listFmt) {
408                     this.listFmt = listFmt;
409                     callback();
410                 })
411             });
412         } else {
413             callback();
414         }
415     },
416 
417     /**
418      * Return the locale used with this formatter instance.
419      * @return {Locale} the Locale instance for this formatter
420      */
421     getLocale: function() {
422         return this.locale;
423     },
424 
425     /**
426      * Return the template string that is used to format date/times for this
427      * formatter instance. This will work, even when the template property is not explicitly
428      * given in the options to the constructor. Without the template option, the constructor
429      * will build the appropriate template according to the options and use that template
430      * in the format method.
431      *
432      * @return {string} the format template for this formatter
433      */
434     getTemplate: function() {
435         return this.template;
436     },
437 
438     /**
439      * Convert this formatter to a string representation by returning the
440      * format template. This method delegates to getTemplate.
441      *
442      * @return {string} the format template
443      */
444     toString: function() {
445         return this.getTemplate();
446     },
447 
448     /**
449      * Return whether or not this formatter will auto-scale the units while formatting.
450      * @returns {boolean} true if auto-scaling is turned on
451      */
452     getScale: function() {
453         return this.scale;
454     },
455 
456     /**
457      * Return the measurement system that is used for this formatter.
458      * @returns {string} the measurement system used in this formatter
459      */
460     getMeasurementSystem: function() {
461         return this.measurementSystem;
462     },
463 
464     /**
465      * @private
466      */
467     _format: function(u, system) {
468         var unit = u.getUnit() === "long-ton" ? "ton" : u.getUnit();
469         var formatted = new IString(this.template[unit]);
470         // make sure to use the right plural rules
471         formatted.setLocale(this.locale, true, undefined, undefined);
472         var rounded = this.numFmt[system].constrain(u.amount);
473         formatted = formatted.formatChoice(rounded, {n: this.numFmt[system].format(u.amount)});
474         return formatted.length > 0 ? formatted : rounded + " " + u.unit;
475     },
476 
477     /**
478      * Format a particular unit instance according to the settings of this
479      * formatter object.
480      *
481      * @param {Measurement} measurement measurement to format
482      * @return {string} the formatted version of the given date instance
483      */
484     format: function (measurement) {
485         var u = measurement, system, listStyle;
486         var doScale = this.scale;
487 
488         if (this.convert) {
489             if (this.measurementSystem) {
490                 if (this.measurementSystem !== measurement.getMeasurementSystem()) {
491                     u = u.convertSystem(this.measurementSystem);
492                 }
493             } else if (!this.usageInfo || Measurement.getMeasurementSystemForLocale(this.locale) !== u.getMeasurementSystem()) {
494                 u = measurement.localize(this.locale);
495             }
496 
497             doScale = (this.usageInfo && measurement.getMeasurementSystem() !== u.getMeasurementSystem()) || this.scale;
498         }
499 
500         system = u.getMeasurementSystem() || this.getMeasurementSystem() || "metric";
501         listStyle = (this.style === "list" || (this.usageInfo && this.usageInfo.systems && this.usageInfo.systems[system].style === "list"));
502 
503         if (doScale) {
504             if (this.usageInfo && measurement.getMeasure() === this.usageInfo.type && !listStyle) {
505                 // scaling with a restricted set of units
506                 u = u.scale(system, this.units);
507             } else {
508                 u = u.scale(); // scale within the current system
509             }
510         }
511 
512         if (listStyle) {
513             var numFmt = this.numFmt[system];
514             u = u.expand(undefined, this.units, ilib.bind(numFmt, numFmt.constrain), this.scale);
515             var formatted = u.map(ilib.bind(this, function(unit) {
516                 return this._format(unit, system);
517             }));
518             if (this.listFmt && formatted.length) {
519                 return this.listFmt.format(formatted);
520             } else {
521                 return formatted.join(' ');
522             }
523         } else {
524             return this._format(u, system);
525         }
526     }
527 };
528 
529 module.exports = UnitFmt;
530