1 /*
  2  * DateRngFmt.js - Date formatter definition
  3  *
  4  * Copyright © 2012-2015, 2018, 2020 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("../index.js");
 23 var JSUtils = require("./JSUtils.js");
 24 
 25 var Locale = require("./Locale.js");
 26 
 27 var CalendarFactory = require("./CalendarFactory.js");
 28 
 29 var DateFmt = require("./DateFmt.js");
 30 var IString = require("./IString.js");
 31 var GregorianCal = require("./GregorianCal.js");
 32 
 33 var DateFactory = require("./DateFactory.js");
 34 
 35 /**
 36  * @class
 37  * Create a new date range formatter instance. The date range formatter is immutable once
 38  * it is created, but can format as many different date ranges as needed with the same
 39  * options. Create different date range formatter instances for different purposes
 40  * and then keep them cached for use later if you have more than one range to
 41  * format.<p>
 42  *
 43  * The options may contain any of the following properties:
 44  *
 45  * <ul>
 46  * <li><i>locale</i> - locale to use when formatting the date/times in the range. If the
 47  * locale is not specified, then the default locale of the app or web page will be used.
 48  *
 49  * <li><i>calendar</i> - the type of calendar to use for this format. The value should
 50  * be a sting containing the name of the calendar. Currently, the supported
 51  * types are "gregorian", "julian", "arabic", "hebrew", or "chinese". If the
 52  * calendar is not specified, then the default calendar for the locale is used. When the
 53  * calendar type is specified, then the format method must be called with an instance of
 54  * the appropriate date type. (eg. Gregorian calendar means that the format method must
 55  * be called with a GregDate instance.)
 56  *
 57  * <li><i>timezone</i> - time zone to use when formatting times. This may be a time zone
 58  * instance or a time zone specifier string in RFC 822 format. If not specified, the
 59  * default time zone for the locale is used.
 60  *
 61  * <li><i>length</i> - Specify the length of the format to use as a string. The length
 62  * is the approximate size of the formatted string.
 63  *
 64  * <ul>
 65  * <li><i>short</i> - use a short representation of the time. This is the most compact format possible for the locale.
 66  * <li><i>medium</i> - use a medium length representation of the time. This is a slightly longer format.
 67  * <li><i>long</i> - use a long representation of the time. This is a fully specified format, but some of the textual
 68  * components may still be abbreviated. (eg. "Tue" instead of "Tuesday")
 69  * <li><i>full</i> - use a full representation of the time. This is a fully specified format where all the textual
 70  * components are spelled out completely.
 71  * </ul>
 72  *
 73  * eg. The "short" format for an en_US range may be "MM/yy - MM/yy", whereas the long format might be
 74  * "MMM, yyyy - MMM, yyyy". In the long format, the month name is textual instead of numeric
 75  * and is longer, the year is 4 digits instead of 2, and the format contains slightly more
 76  * spaces and formatting characters.<p>
 77  *
 78  * Note that the length parameter does not specify which components are to be formatted. The
 79  * components that are formatted depend on the length of time in the range.
 80  *
 81  * <li><i>clock</i> - specify that formatted times should use a 12 or 24 hour clock if the
 82  * format happens to include times. Valid values are "12" and "24".<p>
 83  *
 84  * In some locales, both clocks are used. For example, in en_US, the general populace uses
 85  * a 12 hour clock with am/pm, but in the US military or in nautical or aeronautical or
 86  * scientific writing, it is more common to use a 24 hour clock. This property allows you to
 87  * construct a formatter that overrides the default for the locale.<p>
 88  *
 89  * If this property is not specified, the default is to use the most widely used convention
 90  * for the locale.
 91  * <li>onLoad - a callback function to call when the date range format object is fully
 92  * loaded. When the onLoad option is given, the DateRngFmt object will attempt to
 93  * load any missing locale data using the ilib loader callback.
 94  * When the constructor is done (even if the data is already preassembled), the
 95  * onLoad function is called with the current instance as a parameter, so this
 96  * callback can be used with preassembled or dynamic loading or a mix of the two.
 97  *
 98  * <li>sync - tell whether to load any missing locale data synchronously or
 99  * asynchronously. If this option is given as "false", then the "onLoad"
100  * callback must be given, as the instance returned from this constructor will
101  * not be usable for a while.
102  *
103  * <li><i>loadParams</i> - an object containing parameters to pass to the
104  * loader callback function when locale data is missing. The parameters are not
105  * interpretted or modified in any way. They are simply passed along. The object
106  * may contain any property/value pairs as long as the calling code is in
107  * agreement with the loader callback function as to what those parameters mean.
108  * </ul>
109  * <p>
110  *
111  *
112  * @constructor
113  * @param {Object} options options governing the way this date range formatter instance works
114  */
115 var DateRngFmt = function(options) {
116     var sync = true;
117     var loadParams = undefined;
118     this.locale = new Locale();
119     this.length = "s";
120 
121     if (options) {
122         if (options.locale) {
123             this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
124         }
125 
126         if (options.calendar) {
127             this.calName = options.calendar;
128         }
129 
130         if (options.length) {
131             if (options.length === 'short' ||
132                 options.length === 'medium' ||
133                 options.length === 'long' ||
134                 options.length === 'full') {
135                 // only use the first char to save space in the json files
136                 this.length = options.length.charAt(0);
137             }
138         }
139 
140         if (options.timezone) {
141             this.timezone = options.timezone;
142         }
143 
144         if (typeof(options.sync) !== 'undefined') {
145             sync = !!options.sync;
146         }
147 
148         loadParams = options.loadParams;
149     }
150 
151     var opts = {};
152     JSUtils.shallowCopy(options, opts);
153     opts.sync = sync;
154     opts.loadParams = loadParams;
155 
156     /**
157      * @private
158      */
159     opts.onLoad = ilib.bind(this, function (fmt) {
160         this.dateFmt = fmt;
161         if (fmt) {
162             this.locinfo = this.dateFmt.locinfo;
163 
164             // get the default calendar name from the locale, and if the locale doesn't define
165             // one, use the hard-coded gregorian as the last resort
166             this.calName = this.calName || this.locinfo.getCalendar() || "gregorian";
167             CalendarFactory({
168                 type: this.calName,
169                 sync: sync,
170                 loadParams: loadParams,
171                 onLoad: ilib.bind(this, function(cal) {
172                     this.cal = cal;
173 
174                     if (!this.cal) {
175                         // always synchronous
176                         this.cal = new GregorianCal();
177                     }
178 
179                     this.timeTemplate = this.dateFmt._getFormat(this.dateFmt.formats.time[this.dateFmt.clock], this.dateFmt.timeComponents, this.length) || "hh:mm";
180                     this.timeTemplateArr = this.dateFmt._tokenize(this.timeTemplate);
181 
182                     if (options && typeof(options.onLoad) === 'function') {
183                         options.onLoad(this);
184                     }
185                 })
186             });
187         } else {
188             if (options && typeof(options.sync) === "boolean" && !options.sync && typeof(options.onLoad) === 'function') {
189                 options.onLoad(undefined);
190             } else {
191                 throw "No formats available for calendar " + this.calName + " in locale " + this.locale.getSpec();
192             }
193         }
194     });
195 
196     // delegate a bunch of the formatting to this formatter
197     new DateFmt(opts);
198 };
199 
200 DateRngFmt.prototype = {
201     /**
202      * Return the locale used with this formatter instance.
203      * @return {Locale} the Locale instance for this formatter
204      */
205     getLocale: function() {
206         return this.locale;
207     },
208 
209     /**
210      * Return the name of the calendar used to format date/times for this
211      * formatter instance.
212      * @return {string} the name of the calendar used by this formatter
213      */
214     getCalendar: function () {
215         return this.dateFmt.getCalendar();
216     },
217 
218     /**
219      * Return the length used to format date/times in this formatter. This is either the
220      * value of the length option to the constructor, or the default value.
221      *
222      * @return {string} the length of formats this formatter returns
223      */
224     getLength: function () {
225         return DateFmt.lenmap[this.length] || "";
226     },
227 
228     /**
229      * Return the time zone used to format date/times for this formatter
230      * instance.
231      * @return {TimeZone} a string naming the time zone
232      */
233     getTimeZone: function () {
234         return this.dateFmt.getTimeZone();
235     },
236 
237     /**
238      * Return the clock option set in the constructor. If the clock option was
239      * not given, the default from the locale is returned instead.
240      * @return {string} "12" or "24" depending on whether this formatter uses
241      * the 12-hour or 24-hour clock
242      */
243     getClock: function () {
244         return this.dateFmt.getClock();
245     },
246 
247     /**
248      * Format a date/time range according to the settings of the current
249      * formatter. The range is specified as being from the "start" date until
250      * the "end" date. <p>
251      *
252      * The template that the date/time range uses depends on the
253      * length of time between the dates, on the premise that a long date range
254      * which is too specific is not useful. For example, when giving
255      * the dates of the 100 Years War, in most situations it would be more
256      * appropriate to format the range as "1337 - 1453" than to format it as
257      * "10:37am November 9, 1337 - 4:37pm July 17, 1453", as the latter format
258      * is much too specific given the length of time that the range represents.
259      * If a very specific, but long, date range really is needed, the caller
260      * should format two specific dates separately and put them
261      * together as you might with other normal strings.<p>
262      *
263      * The format used for a date range contains the following date components,
264      * where the order of those components is rearranged and the component values
265      * are translated according to each locale:
266      *
267      * <ul>
268      * <li>within 3 days: the times of day, dates, months, and years
269      * <li>within 730 days (2 years): the dates, months, and years
270      * <li>within 3650 days (10 years): the months and years
271      * <li>longer than 10 years: the years only
272      * </ul>
273      *
274      * In general, if any of the date components share a value between the
275      * start and end date, that component is only given once. For example,
276      * if the range is from November 15, 2011 to November 26, 2011, the
277      * start and end dates both share the same month and year. The
278      * range would then be formatted as "November 15-26, 2011". <p>
279      *
280      * If you want to format a length of time instead of a particular range of
281      * time (for example, the length of an event rather than the specific start time
282      * and end time of that event), then use a duration formatter instance
283      * (DurationFmt) instead. The formatRange method will make sure that each component
284      * of the date/time is within the normal range for that component. For example,
285      * the minutes will always be between 0 and 59, no matter what is specified in
286      * the date to format, because that is the normal range for minutes. A duration
287      * format will allow the number of minutes to exceed 59. For example, if you
288      * were displaying the length of a movie that is 198 minutes long, the minutes
289      * component of a duration could be 198.<p>
290      *
291      * @param {IDate|Date|number|string} startDateLike the starting date/time of the range. The
292      * date may be given as an ilib IDate object, a javascript intrinsic Date object, a
293      * unix time, or a date string parsable by the javscript Date.
294      * @param {IDate|Date|number|string} endDateLike the ending date/time of the range. The
295      * date may be given as an ilib IDate object, a javascript intrinsic Date object, a
296      * unix time, or a date string parsable by the javscript Date.
297      * @throws "Wrong calendar type" when the start or end dates are not the same
298      * calendar type as the formatter itself
299      * @return {string} a date range formatted for the locale
300      */
301     format: function (startDateLike, endDateLike) {
302         var startRd, endRd, fmt = "", yearTemplate, monthTemplate, dayTemplate, formats;
303         var thisZoneName = this.dateFmt.tz && this.dateFmt.tz.getId() || "local";
304 
305         var start = DateFactory._dateToIlib(startDateLike, thisZoneName, this.locale);
306         var end = DateFactory._dateToIlib(endDateLike, thisZoneName, this.locale);
307         var tmp;
308 
309         if (typeof(start) !== 'object' || !start.getCalendar || start.getCalendar() !== this.calName || (this.timezone && start.timezone && start.timezone !== this.timezone)) {
310             start = DateFactory({
311                 type: this.calName,
312                 timezone: thisZoneName,
313                 julianday: start.getJulianDay()
314             });
315         }
316 
317         if (typeof(end) !== 'object' || !end.getCalendar || end.getCalendar() !== this.calName || (this.timezone && end.timezone && end.timezone !== this.timezone)) {
318             end = DateFactory({
319                 type: this.calName,
320                 timezone: thisZoneName,
321                 julianday: end.getJulianDay()
322             });
323         }
324 
325         startRd = start.getRataDie();
326         endRd = end.getRataDie();
327 
328         formats = this.dateFmt.formats.date;
329         // these are not stand-alone templates. We add stand-alone below if necessary for the locale and the format.
330         yearTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "y", this.length) || "yyyy");
331         monthTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "m", this.length) || "MM");
332         dayTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "d", this.length) || "dd");
333 
334         //
335         // legend:
336         // c00 - difference is less than 3 days. Year, month, and date are same, but time is different
337         // c01 - difference is less than 3 days. Year and month are same but date and time are different
338         // c02 - difference is less than 3 days. Year is same but month, date, and time are different. (ie. it straddles a month boundary)
339         // c03 - difference is less than 3 days. Year, month, date, and time are all different. (ie. it straddles a year boundary)
340         // c10 - difference is less than 2 years. Year and month are the same, but date is different.
341         // c11 - difference is less than 2 years. Year is the same, but month, date, and time are different.
342         // c12 - difference is less than 2 years. All fields are different. (ie. straddles a year boundary)
343         // c20 - difference is less than 10 years. All fields are different.
344         // c30 - difference is more than 10 years. All fields are different.
345         //
346 
347         if (endRd - startRd < 3) {
348             if (start.year === end.year) {
349                 if (start.month === end.month) {
350                     if (start.day === end.day) {
351                         fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c00", this.length));
352                     } else {
353                         fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c01", this.length));
354                     }
355                 } else {
356                     fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c02", this.length));
357                 }
358             } else {
359                 fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c03", this.length));
360             }
361         } else if (endRd - startRd < 730) {
362             if (start.year === end.year) {
363                 if (start.month === end.month) {
364                     fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c10", this.length));
365                 } else {
366                     fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c11", this.length));
367                 }
368             } else {
369                 fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c12", this.length));
370             }
371         } else if (endRd - startRd < 3650) {
372             fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c20", this.length));
373             // need to check for month/year stand-alone formats here
374             tmp = this.dateFmt._getFormatInternal(formats, "mys", this.length);
375             if (tmp) {
376                 if (tmp.indexOf('r') > -1) {
377                     var tmp2 = this.dateFmt._getFormatInternal(formats, "r", this.length);
378                     if (tmp2) {
379                         yearTemplate = this.dateFmt._tokenize(tmp2);
380                     }
381                 }
382                 if (tmp.indexOf('L') > -1) {
383                     var tmp3 = this.dateFmt._getFormatInternal(formats, "l", this.length);
384                     if (tmp3) {
385                         monthTemplate = this.dateFmt._tokenize(tmp3);
386                     }
387                 }
388             }
389         } else {
390             fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c30", this.length));
391             // need to check for stand-alone year formats here
392             tmp = this.dateFmt._getFormatInternal(formats, "r", this.length);
393             if (tmp) {
394                 yearTemplate = this.dateFmt._tokenize(tmp);
395             }
396         }
397 
398         /*
399         console.log("fmt is " + fmt.toString());
400         console.log("year template is " + yearTemplate);
401         console.log("month template is " + monthTemplate);
402         console.log("day template is " + dayTemplate);
403         */
404 
405         return fmt.format({
406             sy: this.dateFmt._formatTemplate(start, yearTemplate),
407             sm: this.dateFmt._formatTemplate(start, monthTemplate),
408             sd: this.dateFmt._formatTemplate(start, dayTemplate),
409             st: this.dateFmt._formatTemplate(start, this.timeTemplateArr),
410             ey: this.dateFmt._formatTemplate(end, yearTemplate),
411             em: this.dateFmt._formatTemplate(end, monthTemplate),
412             ed: this.dateFmt._formatTemplate(end, dayTemplate),
413             et: this.dateFmt._formatTemplate(end, this.timeTemplateArr)
414         });
415     }
416 };
417 
418 module.exports = DateRngFmt;
419