1 /* 2 * DurationFmt.js - Date formatter definition 3 * 4 * Copyright © 2012-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 // !data dateformats sysres 21 22 var ilib = require("./ilib.js"); 23 var JSUtils = require("./JSUtils.js"); 24 var Locale = require("./Locale.js"); 25 var LocaleInfo = require("./LocaleInfo.js"); 26 var DateFmt = require("./DateFmt.js"); 27 var IString = require("./IString.js"); 28 var ResBundle = require("./ResBundle.js"); 29 var ScriptInfo = require("./ScriptInfo.js"); 30 31 /** 32 * @class 33 * Create a new duration formatter instance. The duration formatter is immutable once 34 * it is created, but can format as many different durations as needed with the same 35 * options. Create different duration formatter instances for different purposes 36 * and then keep them cached for use later if you have more than one duration to 37 * format.<p> 38 * 39 * Duration formatters format lengths of time. The duration formatter is meant to format 40 * durations of such things as the length of a song or a movie or a meeting, or the 41 * current position in that song or movie while playing it. If you wish to format a 42 * period of time that has a specific start and end date/time, then use a 43 * [DateRngFmt] instance instead and call its format method.<p> 44 * 45 * The options may contain any of the following properties: 46 * 47 * <ul> 48 * <li><i>locale</i> - locale to use when formatting the duration. If the locale is 49 * not specified, then the default locale of the app or web page will be used. 50 * 51 * <li><i>length</i> - Specify the length of the format to use. The length is the approximate size of the 52 * formatted string. 53 * 54 * <ul> 55 * <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 56 * <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 57 * <li><i>long</i> - use a long representation of the duration. This is a fully specified format, but some of the textual 58 * parts may still be abbreviated. eg. 1 yr 1 mo 1 wk 1 day 1 hr 1 min 1 sec 59 * <li><i>full</i> - use a full representation of the duration. This is a fully specified format where all the textual 60 * parts are spelled out completely. eg. 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute and 1 second 61 * </ul> 62 * 63 * <li><i>style<i> - whether hours, minutes, and seconds should be formatted as a text string 64 * or as a regular time as on a clock. eg. text is "1 hour, 15 minutes", whereas clock is "1:15:00". Valid 65 * values for this property are "text" or "clock". Default if this property is not specified 66 * is "text". 67 * 68 *<li><i>useNative</i> - the flag used to determaine whether to use the native script settings 69 * for formatting the numbers . 70 * 71 * <li><i>onLoad</i> - a callback function to call when the format data is fully 72 * loaded. When the onLoad option is given, this class will attempt to 73 * load any missing locale data using the ilib loader callback. 74 * When the constructor is done (even if the data is already preassembled), the 75 * onLoad function is called with the current instance as a parameter, so this 76 * callback can be used with preassembled or dynamic loading or a mix of the two. 77 * 78 * <li>sync - tell whether to load any missing locale data synchronously or 79 * asynchronously. If this option is given as "false", then the "onLoad" 80 * callback must be given, as the instance returned from this constructor will 81 * not be usable for a while. 82 * 83 * <li><i>loadParams</i> - an object containing parameters to pass to the 84 * loader callback function when locale data is missing. The parameters are not 85 * interpretted or modified in any way. They are simply passed along. The object 86 * may contain any property/value pairs as long as the calling code is in 87 * agreement with the loader callback function as to what those parameters mean. 88 * </ul> 89 * <p> 90 * 91 * 92 * @constructor 93 * @param {?Object} options options governing the way this date formatter instance works 94 */ 95 var DurationFmt = function(options) { 96 var sync = true; 97 var loadParams = undefined; 98 99 this.locale = new Locale(); 100 this.length = "short"; 101 this.style = "text"; 102 103 if (options) { 104 if (options.locale) { 105 this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale; 106 } 107 108 if (options.length) { 109 if (options.length === 'short' || 110 options.length === 'medium' || 111 options.length === 'long' || 112 options.length === 'full') { 113 this.length = options.length; 114 } 115 } 116 117 if (options.style) { 118 if (options.style === 'text' || options.style === 'clock') { 119 this.style = options.style; 120 } 121 } 122 123 if (typeof(options.sync) !== 'undefined') { 124 sync = !!options.sync; 125 } 126 127 if (typeof(options.useNative) === 'boolean') { 128 this.useNative = options.useNative; 129 } 130 131 loadParams = options.loadParams; 132 } 133 options = options || {sync: true}; 134 135 new LocaleInfo(this.locale, { 136 sync: sync, 137 loadParams: loadParams, 138 onLoad: ilib.bind(this, function (li) { 139 this.script = li.getScript(); 140 new ResBundle({ 141 locale: this.locale, 142 name: "sysres", 143 sync: sync, 144 loadParams: loadParams, 145 onLoad: ilib.bind(this, function (sysres) { 146 IString.loadPlurals(sync, this.locale, loadParams, ilib.bind(this, function() { 147 if (this.length === 'medium' && !(this.script === 'Latn' || this.script ==='Grek' || this.script ==='Cyrl')) { 148 this.length = 'short'; 149 } 150 switch (this.length) { 151 case 'short': 152 this.components = { 153 year: sysres.getString("#{num}y"), 154 month: sysres.getString("#{num}m", "durationShortMonths"), 155 week: sysres.getString("#{num}w"), 156 day: sysres.getString("#{num}d"), 157 hour: sysres.getString("#{num}h"), 158 minute: sysres.getString("#{num}m", "durationShortMinutes"), 159 second: sysres.getString("#{num}s"), 160 millisecond: sysres.getString("#{num}m", "durationShortMillis"), 161 separator: sysres.getString(" ", "separatorShort"), 162 finalSeparator: "" // not used at this length 163 }; 164 break; 165 166 case 'medium': 167 this.components = { 168 year: sysres.getString("1#1 yr|#{num} yrs", "durationMediumYears"), 169 month: sysres.getString("1#1 mo|#{num} mos"), 170 week: sysres.getString("1#1 wk|#{num} wks", "durationMediumWeeks"), 171 day: sysres.getString("1#1 dy|#{num} dys"), 172 hour: sysres.getString("1#1 hr|#{num} hrs", "durationMediumHours"), 173 minute: sysres.getString("1#1 mi|#{num} min"), 174 second: sysres.getString("1#1 se|#{num} sec"), 175 millisecond: sysres.getString("#{num} ms", "durationMediumMillis"), 176 separator: sysres.getString(" ", "separatorMedium"), 177 finalSeparator: "" // not used at this length 178 }; 179 break; 180 181 case 'long': 182 this.components = { 183 year: sysres.getString("1#1 yr|#{num} yrs"), 184 month: sysres.getString("1#1 mon|#{num} mons"), 185 week: sysres.getString("1#1 wk|#{num} wks"), 186 day: sysres.getString("1#1 day|#{num} days", "durationLongDays"), 187 hour: sysres.getString("1#1 hr|#{num} hrs"), 188 minute: sysres.getString("1#1 min|#{num} min"), 189 second: sysres.getString("1#1 sec|#{num} sec"), 190 millisecond: sysres.getString("#{num} ms"), 191 separator: sysres.getString(", ", "separatorLong"), 192 finalSeparator: "" // not used at this length 193 }; 194 break; 195 196 case 'full': 197 this.components = { 198 year: sysres.getString("1#1 year|#{num} years"), 199 month: sysres.getString("1#1 month|#{num} months"), 200 week: sysres.getString("1#1 week|#{num} weeks"), 201 day: sysres.getString("1#1 day|#{num} days"), 202 hour: sysres.getString("1#1 hour|#{num} hours"), 203 minute: sysres.getString("1#1 minute|#{num} minutes"), 204 second: sysres.getString("1#1 second|#{num} seconds"), 205 millisecond: sysres.getString("1#1 millisecond|#{num} milliseconds"), 206 separator: sysres.getString(", ", "separatorFull"), 207 finalSeparator: sysres.getString(" and ", "finalSeparatorFull") 208 }; 209 break; 210 } 211 212 if (this.style === 'clock') { 213 new DateFmt({ 214 locale: this.locale, 215 calendar: "gregorian", 216 type: "time", 217 time: "ms", 218 sync: sync, 219 loadParams: loadParams, 220 useNative: this.useNative, 221 onLoad: ilib.bind(this, function (fmtMS) { 222 this.timeFmtMS = fmtMS; 223 new DateFmt({ 224 locale: this.locale, 225 calendar: "gregorian", 226 type: "time", 227 time: "hm", 228 sync: sync, 229 loadParams: loadParams, 230 useNative: this.useNative, 231 onLoad: ilib.bind(this, function (fmtHM) { 232 this.timeFmtHM = fmtHM; 233 new DateFmt({ 234 locale: this.locale, 235 calendar: "gregorian", 236 type: "time", 237 time: "hms", 238 sync: sync, 239 loadParams: loadParams, 240 useNative: this.useNative, 241 onLoad: ilib.bind(this, function (fmtHMS) { 242 this.timeFmtHMS = fmtHMS; 243 244 // munge with the template to make sure that the hours are not formatted mod 12 245 this.timeFmtHM.template = this.timeFmtHM.template.replace(/hh?/, 'H'); 246 this.timeFmtHM.templateArr = this.timeFmtHM._tokenize(this.timeFmtHM.template); 247 this.timeFmtHMS.template = this.timeFmtHMS.template.replace(/hh?/, 'H'); 248 this.timeFmtHMS.templateArr = this.timeFmtHMS._tokenize(this.timeFmtHMS.template); 249 250 this._init(this.timeFmtHM.locinfo, options); 251 }) 252 }); 253 }) 254 }); 255 }) 256 }); 257 return; 258 } 259 this._init(li, options); 260 })); 261 }) 262 }); 263 }) 264 }); 265 }; 266 267 /** 268 * @private 269 * @static 270 */ 271 DurationFmt.complist = { 272 "text": ["year", "month", "week", "day", "hour", "minute", "second", "millisecond"], 273 "clock": ["year", "month", "week", "day"] 274 }; 275 276 /** 277 * @private 278 */ 279 DurationFmt.prototype._mapDigits = function(str) { 280 if (this.useNative && this.digits) { 281 return JSUtils.mapString(str.toString(), this.digits); 282 } 283 return str; 284 }; 285 286 /** 287 * @private 288 * @param {LocaleInfo} locinfo 289 * @param {Object|undefined} options 290 */ 291 DurationFmt.prototype._init = function(locinfo, options) { 292 var digits; 293 new ScriptInfo(locinfo.getScript(), { 294 sync: options.sync, 295 loadParams: options.loadParams, 296 onLoad: ilib.bind(this, function(scriptInfo) { 297 this.scriptDirection = scriptInfo.getScriptDirection(); 298 299 if (typeof(this.useNative) === 'boolean') { 300 // if the caller explicitly said to use native or not, honour that despite what the locale data says... 301 if (this.useNative) { 302 digits = locinfo.getNativeDigits(); 303 if (digits) { 304 this.digits = digits; 305 } 306 } 307 } else if (locinfo.getDigitsStyle() === "native") { 308 // else if the locale usually uses native digits, then use them 309 digits = locinfo.getNativeDigits(); 310 if (digits) { 311 this.useNative = true; 312 this.digits = digits; 313 } 314 } // else use western digits always 315 316 if (typeof(options.onLoad) === 'function') { 317 options.onLoad(this); 318 } 319 }) 320 }); 321 }; 322 323 /** 324 * Format a duration according to the format template of this formatter instance.<p> 325 * 326 * The components parameter should be an object that contains any or all of these 327 * numeric properties: 328 * 329 * <ul> 330 * <li>year 331 * <li>month 332 * <li>week 333 * <li>day 334 * <li>hour 335 * <li>minute 336 * <li>second 337 * </ul> 338 * <p> 339 * 340 * When a property is left out of the components parameter or has a value of 0, it will not 341 * be formatted into the output string, except for times that include 0 minutes and 0 seconds. 342 * 343 * This formatter will not ensure that numbers for each component property is within the 344 * valid range for that component. This allows you to format durations that are longer 345 * than normal range. For example, you could format a duration has being "33 hours" rather 346 * than "1 day, 9 hours". 347 * 348 * @param {Object} components date/time components to be formatted into a duration string 349 * @return {IString} a string with the duration formatted according to the style and 350 * locale set up for this formatter instance. If the components parameter is empty or 351 * undefined, an empty string is returned. 352 */ 353 DurationFmt.prototype.format = function (components) { 354 var i, list, fmt, secondlast = true, str = ""; 355 356 list = DurationFmt.complist[this.style]; 357 //for (i = 0; i < list.length; i++) { 358 for (i = list.length-1; i >= 0; i--) { 359 //console.log("Now dealing with " + list[i]); 360 if (typeof(components[list[i]]) !== 'undefined' && components[list[i]] != 0) { 361 if (str.length > 0) { 362 str = ((this.length === 'full' && secondlast) ? this.components.finalSeparator : this.components.separator) + str; 363 secondlast = false; 364 } 365 str = this.components[list[i]].formatChoice(components[list[i]], {num: this._mapDigits(components[list[i]])}) + str; 366 } 367 } 368 369 if (this.style === 'clock') { 370 if (typeof(components.hour) !== 'undefined') { 371 fmt = (typeof(components.second) !== 'undefined') ? this.timeFmtHMS : this.timeFmtHM; 372 } else { 373 fmt = this.timeFmtMS; 374 } 375 376 if (str.length > 0) { 377 str += this.components.separator; 378 } 379 str += fmt._formatTemplate(components, fmt.templateArr); 380 } 381 382 if (this.scriptDirection === 'rtl') { 383 str = "\u200F" + str; 384 } 385 return new IString(str); 386 }; 387 388 /** 389 * Return the locale that was used to construct this duration formatter object. If the 390 * locale was not given as parameter to the constructor, this method returns the default 391 * locale of the system. 392 * 393 * @return {Locale} locale that this duration formatter was constructed with 394 */ 395 DurationFmt.prototype.getLocale = function () { 396 return this.locale; 397 }; 398 399 /** 400 * Return the length that was used to construct this duration formatter object. If the 401 * length was not given as parameter to the constructor, this method returns the default 402 * length. Valid values are "short", "medium", "long", and "full". 403 * 404 * @return {string} length that this duration formatter was constructed with 405 */ 406 DurationFmt.prototype.getLength = function () { 407 return this.length; 408 }; 409 410 /** 411 * Return the style that was used to construct this duration formatter object. Returns 412 * one of "text" or "clock". 413 * 414 * @return {string} style that this duration formatter was constructed with 415 */ 416 DurationFmt.prototype.getStyle = function () { 417 return this.style; 418 }; 419 420 module.exports = DurationFmt; 421