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