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