1 /*
  2  * DateRngFmt.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 
 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         if (typeof(options.sync) !== 'undefined') {
140             sync = !!options.sync;
141         }
142 
143         loadParams = options.loadParams;
144     }
145 
146     var opts = {};
147     JSUtils.shallowCopy(options, opts);
148     opts.sync = sync;
149     opts.loadParams = loadParams;
150 
151     /**
152      * @private
153      */
154     opts.onLoad = ilib.bind(this, function (fmt) {
155         this.dateFmt = fmt;
156         if (fmt) {
157             this.locinfo = this.dateFmt.locinfo;
158 
159             // get the default calendar name from the locale, and if the locale doesn't define
160             // one, use the hard-coded gregorian as the last resort
161             this.calName = this.calName || this.locinfo.getCalendar() || "gregorian";
162             CalendarFactory({
163                 type: this.calName,
164                 sync: sync,
165                 loadParams: loadParams,
166                 onLoad: ilib.bind(this, function(cal) {
167                     this.cal = cal;
168 
169                     if (!this.cal) {
170                         // always synchronous
171                         this.cal = new GregorianCal();
172                     }
173 
174                     this.timeTemplate = this.dateFmt._getFormat(this.dateFmt.formats.time[this.dateFmt.clock], this.dateFmt.timeComponents, this.length) || "hh:mm";
175                     this.timeTemplateArr = this.dateFmt._tokenize(this.timeTemplate);
176 
177                     if (options && typeof(options.onLoad) === 'function') {
178                         options.onLoad(this);
179                     }
180                 })
181             });
182         } else {
183             if (options && typeof(options.sync) === "boolean" && !options.sync && typeof(options.onLoad) === 'function') {
184                 options.onLoad(undefined);
185             } else {
186                 throw "No formats available for calendar " + this.calName + " in locale " + this.locale.getSpec();
187             }
188         }
189     });
190 
191     // delegate a bunch of the formatting to this formatter
192     new DateFmt(opts);
193 };
194 
195 DateRngFmt.prototype = {
196     /**
197      * Return the locale used with this formatter instance.
198      * @return {Locale} the Locale instance for this formatter
199      */
200     getLocale: function() {
201         return this.locale;
202     },
203 
204     /**
205      * Return the name of the calendar used to format date/times for this
206      * formatter instance.
207      * @return {string} the name of the calendar used by this formatter
208      */
209     getCalendar: function () {
210         return this.dateFmt.getCalendar();
211     },
212 
213     /**
214      * Return the length used to format date/times in this formatter. This is either the
215      * value of the length option to the constructor, or the default value.
216      *
217      * @return {string} the length of formats this formatter returns
218      */
219     getLength: function () {
220         return DateFmt.lenmap[this.length] || "";
221     },
222 
223     /**
224      * Return the time zone used to format date/times for this formatter
225      * instance.
226      * @return {TimeZone} a string naming the time zone
227      */
228     getTimeZone: function () {
229         return this.dateFmt.getTimeZone();
230     },
231 
232     /**
233      * Return the clock option set in the constructor. If the clock option was
234      * not given, the default from the locale is returned instead.
235      * @return {string} "12" or "24" depending on whether this formatter uses
236      * the 12-hour or 24-hour clock
237      */
238     getClock: function () {
239         return this.dateFmt.getClock();
240     },
241 
242     /**
243      * Format a date/time range according to the settings of the current
244      * formatter. The range is specified as being from the "start" date until
245      * the "end" date. <p>
246      *
247      * The template that the date/time range uses depends on the
248      * length of time between the dates, on the premise that a long date range
249      * which is too specific is not useful. For example, when giving
250      * the dates of the 100 Years War, in most situations it would be more
251      * appropriate to format the range as "1337 - 1453" than to format it as
252      * "10:37am November 9, 1337 - 4:37pm July 17, 1453", as the latter format
253      * is much too specific given the length of time that the range represents.
254      * If a very specific, but long, date range really is needed, the caller
255      * should format two specific dates separately and put them
256      * together as you might with other normal strings.<p>
257      *
258      * The format used for a date range contains the following date components,
259      * where the order of those components is rearranged and the component values
260      * are translated according to each locale:
261      *
262      * <ul>
263      * <li>within 3 days: the times of day, dates, months, and years
264      * <li>within 730 days (2 years): the dates, months, and years
265      * <li>within 3650 days (10 years): the months and years
266      * <li>longer than 10 years: the years only
267      * </ul>
268      *
269      * In general, if any of the date components share a value between the
270      * start and end date, that component is only given once. For example,
271      * if the range is from November 15, 2011 to November 26, 2011, the
272      * start and end dates both share the same month and year. The
273      * range would then be formatted as "November 15-26, 2011". <p>
274      *
275      * If you want to format a length of time instead of a particular range of
276      * time (for example, the length of an event rather than the specific start time
277      * and end time of that event), then use a duration formatter instance
278      * (DurationFmt) instead. The formatRange method will make sure that each component
279      * of the date/time is within the normal range for that component. For example,
280      * the minutes will always be between 0 and 59, no matter what is specified in
281      * the date to format, because that is the normal range for minutes. A duration
282      * format will allow the number of minutes to exceed 59. For example, if you
283      * were displaying the length of a movie that is 198 minutes long, the minutes
284      * component of a duration could be 198.<p>
285      *
286      * @param {IDate|Date|number|string} startDateLike the starting date/time of the range. The
287      * date may be given as an ilib IDate object, a javascript intrinsic Date object, a
288      * unix time, or a date string parsable by the javscript Date.
289      * @param {IDate|Date|number|string} endDateLike the ending date/time of the range. The
290      * date may be given as an ilib IDate object, a javascript intrinsic Date object, a
291      * unix time, or a date string parsable by the javscript Date.
292      * @throws "Wrong calendar type" when the start or end dates are not the same
293      * calendar type as the formatter itself
294      * @return {string} a date range formatted for the locale
295      */
296     format: function (startDateLike, endDateLike) {
297         var startRd, endRd, fmt = "", yearTemplate, monthTemplate, dayTemplate, formats;
298         var thisZoneName = this.dateFmt.tz && this.dateFmt.tz.getId() || "local";
299 
300         var start = DateFactory._dateToIlib(startDateLike, thisZoneName, this.locale);
301         var end = DateFactory._dateToIlib(endDateLike, thisZoneName, this.locale);
302 
303         if (typeof(start) !== 'object' || !start.getCalendar || start.getCalendar() !== this.calName ||
304             typeof(end) !== 'object' || !end.getCalendar || end.getCalendar() !== this.calName) {
305             throw "Wrong calendar type";
306         }
307 
308         startRd = start.getRataDie();
309         endRd = end.getRataDie();
310 
311         //
312         // legend:
313         // c00 - difference is less than 3 days. Year, month, and date are same, but time is different
314         // c01 - difference is less than 3 days. Year and month are same but date and time are different
315         // c02 - difference is less than 3 days. Year is same but month, date, and time are different. (ie. it straddles a month boundary)
316         // c03 - difference is less than 3 days. Year, month, date, and time are all different. (ie. it straddles a year boundary)
317         // c10 - difference is less than 2 years. Year and month are the same, but date is different.
318         // c11 - difference is less than 2 years. Year is the same, but month, date, and time are different.
319         // c12 - difference is less than 2 years. All fields are different. (ie. straddles a year boundary)
320         // c20 - difference is less than 10 years. All fields are different.
321         // c30 - difference is more than 10 years. All fields are different.
322         //
323 
324         if (endRd - startRd < 3) {
325             if (start.year === end.year) {
326                 if (start.month === end.month) {
327                     if (start.day === end.day) {
328                         fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c00", this.length));
329                     } else {
330                         fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c01", this.length));
331                     }
332                 } else {
333                     fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c02", this.length));
334                 }
335             } else {
336                 fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c03", this.length));
337             }
338         } else if (endRd - startRd < 730) {
339             if (start.year === end.year) {
340                 if (start.month === end.month) {
341                     fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c10", this.length));
342                 } else {
343                     fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c11", this.length));
344                 }
345             } else {
346                 fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c12", this.length));
347             }
348         } else if (endRd - startRd < 3650) {
349             fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c20", this.length));
350         } else {
351             fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c30", this.length));
352         }
353 
354         formats = this.dateFmt.formats.date;
355         yearTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "y", this.length) || "yyyy");
356         monthTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "m", this.length) || "MM");
357         dayTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "d", this.length) || "dd");
358 
359         /*
360         console.log("fmt is " + fmt.toString());
361         console.log("year template is " + yearTemplate);
362         console.log("month template is " + monthTemplate);
363         console.log("day template is " + dayTemplate);
364         */
365 
366         return fmt.format({
367             sy: this.dateFmt._formatTemplate(start, yearTemplate),
368             sm: this.dateFmt._formatTemplate(start, monthTemplate),
369             sd: this.dateFmt._formatTemplate(start, dayTemplate),
370             st: this.dateFmt._formatTemplate(start, this.timeTemplateArr),
371             ey: this.dateFmt._formatTemplate(end, yearTemplate),
372             em: this.dateFmt._formatTemplate(end, monthTemplate),
373             ed: this.dateFmt._formatTemplate(end, dayTemplate),
374             et: this.dateFmt._formatTemplate(end, this.timeTemplateArr)
375         });
376     }
377 };
378 
379 module.exports = DateRngFmt;
380