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