1 /* 2 * Measurement.js - Measurement unit superclass 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 // !depends JSUtils.js MathUtils.js Locale.js 21 22 var JSUtils = require("./JSUtils.js"); 23 var MathUtils = require("./MathUtils.js"); 24 var Locale = require("./Locale.js"); 25 26 function round(number, precision) { 27 var factor = Math.pow(10, precision); 28 return MathUtils.halfdown(number * factor) / factor; 29 } 30 31 /** 32 * @class 33 * Superclass for measurement instances that contains shared functionality 34 * and defines the interface. <p> 35 * 36 * This class is never instantiated on its own. Instead, measurements should 37 * be created using the {@link MeasurementFactory} function, which creates the 38 * correct subclass based on the given parameters.<p> 39 * 40 * @param {Object=} options options controlling the construction of this instance 41 * @private 42 * @constructor 43 */ 44 var Measurement = function(options) { 45 if (options) { 46 if (typeof(options.unit) !== 'undefined') { 47 this.originalUnit = options.unit; 48 this.unit = this.normalizeUnits(options.unit) || options.unit; 49 } 50 51 if (typeof(options.amount) === 'object') { 52 if (options.amount.getMeasure() === this.getMeasure()) { 53 this.amount = options.amount.convert(this.unit); 54 } else { 55 throw "Cannot convert unit " + options.amount.unit + " to a " + this.getMeasure(); 56 } 57 } else if (typeof(options.amount) !== 'undefined') { 58 this.amount = Number(options.amount); 59 } 60 61 if (typeof(this.ratios[this.unit]) === 'undefined') { 62 throw "Unknown unit: " + options.unit; 63 } 64 } 65 }; 66 67 /** 68 * @private 69 */ 70 Measurement._constructors = {}; 71 72 Measurement.prototype = { 73 /** 74 * Return the normalized name of the given units. If the units are 75 * not recognized, this method returns its parameter unmodified.<p> 76 * 77 * Examples: 78 * 79 * <ui> 80 * <li>"metres" gets normalized to "meter"<br> 81 * <li>"ml" gets normalized to "milliliter"<br> 82 * <li>"foobar" gets normalized to "foobar" (no change because it is not recognized) 83 * </ul> 84 * 85 * @param {string} name name of the units to normalize. 86 * @returns {string} normalized name of the units 87 */ 88 normalizeUnits: function(name) { 89 return (this.constructor && (Measurement.getUnitId(this.constructor, name) || 90 Measurement.getUnitIdCaseInsensitive(this.constructor, name))) || 91 name; 92 }, 93 94 /** 95 * Return the normalized units used in this measurement. 96 * @return {string} name of the unit of measurement 97 */ 98 getUnit: function() { 99 return this.unit; 100 }, 101 102 /** 103 * Return the units originally used to construct this measurement 104 * before it was normalized. 105 * @return {string} name of the unit of measurement 106 */ 107 getOriginalUnit: function() { 108 return this.originalUnit; 109 }, 110 111 /** 112 * Return the numeric amount of this measurement. 113 * @return {number} the numeric amount of this measurement 114 */ 115 getAmount: function() { 116 return this.amount; 117 }, 118 119 /** 120 * Return the type of this measurement. Examples are "mass", 121 * "length", "speed", etc. Measurements can only be converted 122 * to measurements of the same type.<p> 123 * 124 * The type of the units is determined automatically from the 125 * units. For example, the unit "grams" is type "mass". Use the 126 * static call {@link Measurement.getAvailableUnits} 127 * to find out what units this version of ilib supports. 128 * 129 * @return {string} the name of the type of this measurement 130 */ 131 getMeasure: function() {}, 132 133 /** 134 * Return an array of all units that this measurement types supports. 135 * 136 * @return {Array.<string>} an array of all units that this measurement 137 * types supports 138 */ 139 getMeasures: function () { 140 return Object.keys(this.ratios); 141 }, 142 143 /** 144 * Return the name of the measurement system that the current 145 * unit is a part of. 146 * 147 * @returns {string} the name of the measurement system for 148 * the units of this measurement 149 */ 150 getMeasurementSystem: function() { 151 if (this.unit) { 152 if (JSUtils.indexOf(this.systems.uscustomary, this.unit) > -1) { 153 return "uscustomary"; 154 } 155 156 if (JSUtils.indexOf(this.systems.imperial, this.unit) > -1) { 157 return "imperial"; 158 } 159 } 160 return "metric"; 161 }, 162 163 /** 164 * Localize the measurement to the commonly used measurement in that locale. For example 165 * If a user's locale is "en-US" and the measurement is given as "60 kmh", 166 * the formatted number should be automatically converted to the most appropriate 167 * measure in the other system, in this case, mph. The formatted result should 168 * appear as "37.3 mph". 169 * 170 * @param {string} locale current locale string 171 * @returns {Measurement} a new instance that is converted to locale 172 */ 173 localize: function(locale) { 174 var to; 175 var toSystem = Measurement.getMeasurementSystemForLocale(locale); 176 var fromSystem = this.getMeasurementSystem(); 177 if (toSystem === fromSystem) return this; // already there 178 to = this.systems.conversions[fromSystem] && 179 this.systems.conversions[fromSystem][toSystem] && 180 this.systems.conversions[fromSystem][toSystem][this.unit]; 181 182 return to ? this.newUnit({ 183 unit: to, 184 amount: this.convert(to) 185 }) : this; 186 }, 187 188 /** 189 * Return the amount of the current measurement when converted to the 190 * given measurement unit. Measurements can only be converted 191 * to other measurements of the same type.<p> 192 * 193 * @param {string} to the name of the units to convert this measurement to 194 * @return {number|undefined} the amount corresponding to the requested unit 195 */ 196 convert: function(to) { 197 if (!to || typeof(this.ratios[this.normalizeUnits(to)]) === 'undefined') { 198 return undefined; 199 } 200 201 var from = this.getUnitIdCaseInsensitive(this.unit) || this.unit; 202 to = this.getUnitIdCaseInsensitive(to) || to; 203 if (typeof(from) === 'undefined' || typeof(to) === 'undefined') { 204 return undefined; 205 } 206 207 var fromRow = this.ratios[from]; 208 var toRow = this.ratios[to]; 209 return this.amount * fromRow[toRow[0]]; 210 }, 211 212 /** 213 * Return a new measurement instance that is converted to a different 214 * measurement system. Measurements can only be converted 215 * to other measurements of the same type.<p> 216 * 217 * @param {string} measurementSystem the name of the system to convert to 218 * @return {Measurement} a new measurement in the given system, or the 219 * current measurement if it is already in the given system or could not 220 * be converted 221 */ 222 convertSystem: function(measurementSystem) { 223 if (!measurementSystem || measurementSystem === this.getMeasurementSystem()) { 224 return this; 225 } 226 var map = this.systems.conversions[this.getMeasurementSystem()][measurementSystem]; 227 var newunit = map && map[this.unit]; 228 if (!newunit) return this; 229 230 return this.newUnit({ 231 unit: newunit, 232 amount: this.convert(newunit) 233 }); 234 }, 235 236 /** 237 * Scale the measurement unit to an acceptable level. The scaling 238 * happens so that the integer part of the amount is as small as 239 * possible without being below zero. This will result in the 240 * largest units that can represent this measurement without 241 * fractions. Measurements can only be scaled to other measurements 242 * of the same type. 243 * 244 * @param {string=} measurementsystem the name of the system to scale to 245 * @param {Object=} units mapping from the measurement system to the units to use 246 * for this scaling. If this is not defined, this measurement type will use the 247 * set of units that it knows about for the given measurement system 248 * @return {Measurement} a new instance that is scaled to the 249 * right level 250 */ 251 scale: function(measurementsystem, units) { 252 var systemName = this.getMeasurementSystem(); 253 var mSystem; 254 if (units) { 255 mSystem = (units[measurementsystem] && JSUtils.indexOf(units[measurementsystem], this.unit) > -1) ? 256 units[measurementsystem] : units[systemName]; 257 } 258 if (!mSystem) { 259 mSystem = (this.systems[measurementsystem] && JSUtils.indexOf(this.systems[measurementsystem], this.unit) > -1) ? 260 this.systems[measurementsystem] : this.systems[systemName]; 261 } 262 if (!mSystem) { 263 // cannot find the system to scale within... just return the measurement as is 264 return this; 265 } 266 267 return this.newUnit(this.scaleUnits(mSystem)); 268 }, 269 270 /** 271 * Expand the current measurement such that any fractions of the current unit 272 * are represented in terms of smaller units in the same system instead of fractions 273 * of the current unit. For example, "6.25 feet" may be represented as 274 * "6 feet 4 inches" instead. The return value is an array of measurements which 275 * are progressively smaller until the smallest unit in the system is reached 276 * or until there is a whole number of any unit along the way. 277 * 278 * @param {string=} measurementsystem system to use (uscustomary|imperial|metric), 279 * or undefined if the system can be inferred from the current measure 280 * @param {Array.<string>=} units object containing a mapping between the measurement system 281 * and an array of units to use to restrict the expansion to 282 * @param {function(number):number=} constrain a function that constrains 283 * a number according to the display options 284 * @param {boolean=} scale if true, rescale all of the units so that the 285 * largest unit is the largest one with a non-fractional number. If false, then 286 * the current unit stays the largest unit. 287 * @return {Array.<Measurement>} an array of new measurements in order from 288 * the current units to the smallest units in the system which together are the 289 * same measurement as this one 290 */ 291 expand: function(measurementsystem, units, constrain, scale) { 292 var systemName = this.getMeasurementSystem(); 293 var mSystem = (units && units[systemName]) ? units[systemName] : (this.systems[systemName] || this.systems.metric); 294 295 return this.list(mSystem, this.ratios, constrain, scale).map(function(item) { 296 return this.newUnit(item); 297 }.bind(this)); 298 }, 299 300 /** 301 * Convert the current measurement to a list of measures 302 * and amounts. This method will autoScale the current measurement 303 * to the largest measure in the given measures list such that the 304 * amount of that measure is still greater than or equal to 1. From 305 * there, it will truncate that measure to a whole 306 * number and then it will calculate the remainder in terms of 307 * each of the smaller measures in the given list.<p> 308 * 309 * For example, if a person's height is given as 70.5 inches, and 310 * the list of measures is ["mile", "foot", "inch"], then it will 311 * scale the amount to 5 feet, 10.5 inches. The amount is not big 312 * enough to have any whole miles, so that measure is not used. 313 * The first measure will be "foot" because it is the first one 314 * in the measure list where the there is an amount of them that 315 * is greater than or equal to 1. The return value in this example 316 * would be: 317 * 318 * <pre> 319 * [ 320 * { 321 * "unit": "foot", 322 * "amount": 5 323 * }, 324 * { 325 * "unit": "inch", 326 * "amount": 10.5 327 * } 328 * ] 329 * </pre> 330 * 331 * Note that all measures except the smallest will be returned 332 * as whole numbers. The smallest measure will contain any possible 333 * fractional remainder. 334 * 335 * @param {Array.<string>|undefined} measures array of measure names to 336 * convert this measure to 337 * @param {Object} ratios the conversion ratios 338 * table for the measurement type 339 * @param {function (number): number=} constrain a function that constrains 340 * a number according to the display options 341 * @param {boolean=} scale if true, rescale all of the units so that the 342 * largest unit is the largest one with a non-fractional number. If false, then 343 * the current unit stays the largest unit. 344 * @returns {Array.<{unit: String, amount: Number}>} the conversion 345 * of the current measurement into an array of unit names and 346 * their amounts 347 */ 348 list: function(measures, ratios, constrain, scale) { 349 var row = ratios[this.unit]; 350 var ret = []; 351 var scaled; 352 var unit = this.unit; 353 var amount = this.amount; 354 constrain = constrain || round; 355 356 var start = JSUtils.indexOf(measures, this.unit); 357 358 if (scale || start === -1) { 359 start = measures.length-1; 360 } 361 362 if (this.unit !== measures[0]) { 363 // if this unit is not the smallest measure in the system, we have to convert 364 unit = measures[0]; 365 amount = this.amount * row[ratios[unit][0]]; 366 row = ratios[unit]; 367 } 368 369 // convert to smallest measure 370 amount = constrain(amount); 371 // go backwards so we get from the largest to the smallest units in order 372 for (var j = start; j > 0; j--) { 373 unit = measures[j]; 374 scaled = amount * row[ratios[unit][0]]; 375 var xf = Math.floor(scaled); 376 if (xf) { 377 var item = { 378 unit: unit, 379 amount: xf 380 }; 381 ret.push(item); 382 383 amount -= xf * ratios[unit][ratios[measures[0]][0]]; 384 } 385 } 386 387 // last measure is rounded/constrained, not truncated 388 if (amount !== 0) { 389 ret.push({ 390 unit: measures[0], 391 amount: constrain(amount) 392 }); 393 } 394 395 return ret; 396 }, 397 398 /** 399 * @private 400 */ 401 scaleUnits: function(mSystem) { 402 var tmp, munit, amount = 18446744073709551999; 403 var fromRow = this.ratios[this.unit]; 404 405 for (var m = 0; m < mSystem.length; m++) { 406 tmp = this.amount * fromRow[this.ratios[mSystem[m]][0]]; 407 if ((tmp >= 1 && tmp < amount) || amount === 18446744073709551999) { 408 amount = tmp; 409 munit = mSystem[m]; 410 } 411 } 412 413 return { 414 unit: munit, 415 amount: amount 416 }; 417 }, 418 419 /** 420 * @private 421 * 422 * Return the normalized units identifier for the given unit. This looks up the units 423 * in the aliases list and returns the normalized unit id. 424 * 425 * @param {string} unit the unit to find 426 * @returns {string|undefined} the normalized identifier for the given unit, or 427 * undefined if there is no such unit in this type of measurement 428 */ 429 getUnitId: function(unit) { 430 if (!unit) return undefined; 431 432 if (this.aliases && typeof(this.aliases[unit]) !== 'undefined') { 433 return this.aliases[unit]; 434 } 435 436 if (this.ratios && typeof(this.ratios[unit]) !== 'undefined') { 437 return unit; 438 } 439 440 return undefined; 441 }, 442 443 /** 444 * Return the normalized units identifier for the given unit, searching case-insensitively. 445 * This has the risk that things may match erroneously because many short form unit strings 446 * are case-sensitive. This should method be used as a last resort if no case-sensitive match 447 * is found amongst all the different types of measurements. 448 * 449 * @param {string} unit the unit to find 450 * @returns {string|undefined} the normalized identifier for the given unit, or 451 * undefined if there is no such unit in this type of measurement 452 */ 453 getUnitIdCaseInsensitive: function(unit) { 454 if (!unit) return undefined; 455 456 // try with the original case first, just in case that works 457 var ret = this.getUnitId(unit); 458 if (ret) return ret; 459 460 var u = unit.toLowerCase(); 461 if (this.aliasesLower && typeof(this.aliasesLower[u]) !== 'undefined') { 462 return this.aliasesLower[u]; 463 } 464 465 return undefined; 466 } 467 }; 468 469 /** 470 * Return the normalized units identifier for the given unit. This looks up the units 471 * in the aliases list and returns the normalized unit id. 472 * 473 * @static 474 * @param {function(...)} measurement name of the the class of measure being searched 475 * @param {string} unit the unit to find 476 * @returns {string|undefined} the normalized identifier for the given unit, or 477 * undefined if there is no such unit in this type of measurement 478 */ 479 Measurement.getUnitId = function(measurement, unit) { 480 if (!unit) return undefined; 481 482 if (typeof(measurement.aliases[unit]) !== 'undefined') { 483 return measurement.aliases[unit]; 484 } 485 486 if (measurement.ratios && typeof(measurement.ratios[unit]) !== 'undefined') { 487 return unit; 488 } 489 490 return undefined; 491 }; 492 493 /** 494 * Return the normalized units identifier for the given unit, searching case-insensitively. 495 * This has the risk that things may match erroneously because many short form unit strings 496 * are case-sensitive. This should method be used as a last resort if no case-sensitive match 497 * is found amongst all the different types of measurements. 498 * 499 * @static 500 * @param {function(...)} measurement name of the class of measure being searched 501 * @param {string} unit the unit to find 502 * @returns {string|undefined} the normalized identifier for the given unit, or 503 * undefined if there is no such unit in this type of measurement 504 */ 505 Measurement.getUnitIdCaseInsensitive = function(measurement, unit) { 506 if (!unit) return undefined; 507 var u = unit.toLowerCase(); 508 509 // try this first, just in case 510 var ret = Measurement.getUnitId(measurement, unit); 511 if (ret) return ret; 512 513 if (measurement.aliases && !measurement.aliasesLower) { 514 measurement.aliasesLower = {}; 515 for (var a in measurement.aliases) { 516 measurement.aliasesLower[a.toLowerCase()] = measurement.aliases[a]; 517 } 518 } 519 520 if (typeof(measurement.aliasesLower[u]) !== 'undefined') { 521 return measurement.aliasesLower[u]; 522 } 523 524 return undefined; 525 }; 526 527 // Hard-code these because CLDR has incorrect data, plus this is small so we don't 528 // want to do an async load just to get it. 529 // Source: https://en.wikipedia.org/wiki/Metrication#Overview 530 var systems = { 531 "uscustomary": ["US", "FM", "MH", "LR", "PR", "PW", "GU", "WS", "AS", "VI", "MP"], 532 "imperial": ["GB", "MM"] 533 }; 534 535 // every other country in the world is metric. Myanmar (MM) is adopting metric by 2019 536 // supposedly, and Liberia is as well 537 538 /** 539 * Return the name of the measurement system in use in the given locale. 540 * 541 * @param {string|Locale} locale the locale spec or Locale instance of the 542 * 543 * @returns {string} the name of the measurement system 544 */ 545 Measurement.getMeasurementSystemForLocale = function(locale) { 546 var l = typeof(locale) === "object" ? locale : new Locale(locale); 547 var region = l.getRegion(); 548 549 if (region) { 550 if (JSUtils.indexOf(systems.uscustomary, region) > -1) { 551 return "uscustomary"; 552 } else if (JSUtils.indexOf(systems.imperial, region) > -1) { 553 return "imperial"; 554 } 555 } 556 557 return "metric"; 558 }; 559 560 module.exports = Measurement; 561