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