1 /* 2 * IDate.js - Represent a date in any calendar. This class is subclassed for each 3 * calendar and includes some shared functionality. 4 * 5 * Copyright © 2012-2015, 2018, JEDLSoft 6 * 7 * Licensed under the Apache License, Version 2.0 (the "License"); 8 * you may not use this file except in compliance with the License. 9 * You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 21 var LocaleInfo = require("./LocaleInfo.js"); 22 23 /** 24 * @class 25 * Superclass for all the calendar date classes that contains shared 26 * functionality. This class is never instantiated on its own. Instead, 27 * you should use the {@link DateFactory} function to manufacture a new 28 * instance of a subclass of IDate. This class is called IDate for "ilib 29 * date" so that it does not conflict with the built-in Javascript Date 30 * class. 31 * 32 * @private 33 * @constructor 34 * @param {Object=} options The date components to initialize this date with 35 */ 36 var IDate = function(options) { 37 }; 38 39 /* place for the subclasses to put their constructors so that the factory method 40 * can find them. Do this to add your date after it's defined: 41 * IDate._constructors["mytype"] = IDate.MyTypeConstructor; 42 */ 43 IDate._constructors = {}; 44 45 IDate.prototype = { 46 getType: function() { 47 return "date"; 48 }, 49 50 /** 51 * Return the unix time equivalent to this date instance. Unix time is 52 * the number of milliseconds since midnight on Jan 1, 1970 UTC (Gregorian). This 53 * method only returns a valid number for dates between midnight, 54 * Jan 1, 1970 UTC (Gregorian) and Jan 19, 2038 at 3:14:07am UTC (Gregorian) when 55 * the unix time runs out. If this instance encodes a date outside of that range, 56 * this method will return -1. For date types that are not Gregorian, the point 57 * in time represented by this date object will only give a return value if it 58 * is in the correct range in the Gregorian calendar as given previously. 59 * 60 * @return {number} a number giving the unix time, or -1 if the date is outside the 61 * valid unix time range 62 */ 63 getTime: function() { 64 return this.rd.getTime(); 65 }, 66 67 /** 68 * Return the extended unix time equivalent to this Gregorian date instance. Unix time is 69 * the number of milliseconds since midnight on Jan 1, 1970 UTC. Traditionally unix time 70 * (or the type "time_t" in C/C++) is only encoded with an unsigned 32 bit integer, and thus 71 * runs out on Jan 19, 2038. However, most Javascript engines encode numbers well above 72 * 32 bits and the Date object allows you to encode up to 100 million days worth of time 73 * after Jan 1, 1970, and even more interestingly, 100 million days worth of time before 74 * Jan 1, 1970 as well. This method returns the number of milliseconds in that extended 75 * range. If this instance encodes a date outside of that range, this method will return 76 * NaN. 77 * 78 * @return {number} a number giving the extended unix time, or Nan if the date is outside 79 * the valid extended unix time range 80 */ 81 getTimeExtended: function() { 82 return this.rd.getTimeExtended(); 83 }, 84 85 /** 86 * Set the time of this instance according to the given unix time. Unix time is 87 * the number of milliseconds since midnight on Jan 1, 1970. 88 * 89 * @param {number} millis the unix time to set this date to in milliseconds 90 */ 91 setTime: function(millis) { 92 this.rd = this.newRd({ 93 unixtime: millis, 94 cal: this.cal 95 }); 96 this._calcDateComponents(); 97 }, 98 99 getDays: function() { 100 return this.day; 101 }, 102 getMonths: function() { 103 return this.month; 104 }, 105 getYears: function() { 106 return this.year; 107 }, 108 getHours: function() { 109 return this.hour; 110 }, 111 getMinutes: function() { 112 return this.minute; 113 }, 114 getSeconds: function() { 115 return this.second; 116 }, 117 getMilliseconds: function() { 118 return this.millisecond; 119 }, 120 getEra: function() { 121 return (this.year < 1) ? -1 : 1; 122 }, 123 124 setDays: function(day) { 125 this.day = parseInt(day, 10) || 1; 126 this.rd._setDateComponents(this); 127 }, 128 setMonths: function(month) { 129 this.month = parseInt(month, 10) || 1; 130 this.rd._setDateComponents(this); 131 }, 132 setYears: function(year) { 133 this.year = parseInt(year, 10) || 0; 134 this.rd._setDateComponents(this); 135 }, 136 137 setHours: function(hour) { 138 this.hour = parseInt(hour, 10) || 0; 139 this.rd._setDateComponents(this); 140 }, 141 setMinutes: function(minute) { 142 this.minute = parseInt(minute, 10) || 0; 143 this.rd._setDateComponents(this); 144 }, 145 setSeconds: function(second) { 146 this.second = parseInt(second, 10) || 0; 147 this.rd._setDateComponents(this); 148 }, 149 setMilliseconds: function(milli) { 150 this.millisecond = parseInt(milli, 10) || 0; 151 this.rd._setDateComponents(this); 152 }, 153 154 /** 155 * Return a new date instance in the current calendar that represents the first instance 156 * of the given day of the week before the current date. The day of the week is encoded 157 * as a number where 0 = Sunday, 1 = Monday, etc. 158 * 159 * @param {number} dow the day of the week before the current date that is being sought 160 * @return {IDate} the date being sought 161 */ 162 before: function (dow) { 163 return new this.constructor({ 164 rd: this.rd.before(dow, this.offset), 165 timezone: this.timezone 166 }); 167 }, 168 169 /** 170 * Return a new date instance in the current calendar that represents the first instance 171 * of the given day of the week after the current date. The day of the week is encoded 172 * as a number where 0 = Sunday, 1 = Monday, etc. 173 * 174 * @param {number} dow the day of the week after the current date that is being sought 175 * @return {IDate} the date being sought 176 */ 177 after: function (dow) { 178 return new this.constructor({ 179 rd: this.rd.after(dow, this.offset), 180 timezone: this.timezone 181 }); 182 }, 183 184 /** 185 * Return a new Gregorian date instance that represents the first instance of the 186 * given day of the week on or before the current date. The day of the week is encoded 187 * as a number where 0 = Sunday, 1 = Monday, etc. 188 * 189 * @param {number} dow the day of the week on or before the current date that is being sought 190 * @return {IDate} the date being sought 191 */ 192 onOrBefore: function (dow) { 193 return new this.constructor({ 194 rd: this.rd.onOrBefore(dow, this.offset), 195 timezone: this.timezone 196 }); 197 }, 198 199 /** 200 * Return a new Gregorian date instance that represents the first instance of the 201 * given day of the week on or after the current date. The day of the week is encoded 202 * as a number where 0 = Sunday, 1 = Monday, etc. 203 * 204 * @param {number} dow the day of the week on or after the current date that is being sought 205 * @return {IDate} the date being sought 206 */ 207 onOrAfter: function (dow) { 208 return new this.constructor({ 209 rd: this.rd.onOrAfter(dow, this.offset), 210 timezone: this.timezone 211 }); 212 }, 213 214 /** 215 * Return a Javascript Date object that is equivalent to this date 216 * object. 217 * 218 * @return {Date|undefined} a javascript Date object 219 */ 220 getJSDate: function() { 221 var unix = this.rd.getTimeExtended(); 222 return isNaN(unix) ? undefined : new Date(unix); 223 }, 224 225 /** 226 * Return the Rata Die (fixed day) number of this date. 227 * 228 * @protected 229 * @return {number} the rd date as a number 230 */ 231 getRataDie: function() { 232 return this.rd.getRataDie(); 233 }, 234 235 /** 236 * Set the date components of this instance based on the given rd. 237 * @protected 238 * @param {number} rd the rata die date to set 239 */ 240 setRd: function (rd) { 241 this.rd = this.newRd({ 242 rd: rd, 243 cal: this.cal 244 }); 245 this._calcDateComponents(); 246 }, 247 248 /** 249 * Return the Julian Day equivalent to this calendar date as a number. 250 * 251 * @return {number} the julian date equivalent of this date 252 */ 253 getJulianDay: function() { 254 return this.rd.getJulianDay(); 255 }, 256 257 /** 258 * Set the date of this instance using a Julian Day. 259 * @param {number|JulianDay} date the Julian Day to use to set this date 260 */ 261 setJulianDay: function (date) { 262 this.rd = this.newRd({ 263 julianday: (typeof(date) === 'object') ? date.getDate() : date, 264 cal: this.cal 265 }); 266 this._calcDateComponents(); 267 }, 268 269 /** 270 * Return the time zone associated with this date, or 271 * undefined if none was specified in the constructor. 272 * 273 * @return {string|undefined} the name of the time zone for this date instance 274 */ 275 getTimeZone: function() { 276 return this.timezone || "local"; 277 }, 278 279 /** 280 * Set the time zone associated with this date. 281 * @param {string=} tzName the name of the time zone to set into this date instance, 282 * or "undefined" to unset the time zone 283 */ 284 setTimeZone: function (tzName) { 285 if (!tzName || tzName === "") { 286 // same as undefining it 287 this.timezone = undefined; 288 this.tz = undefined; 289 } else if (typeof(tzName) === 'string') { 290 this.timezone = tzName; 291 this.tz = undefined; 292 // assuming the same UTC time, but a new time zone, now we have to 293 // recalculate what the date components are 294 this._calcDateComponents(); 295 } 296 }, 297 298 /** 299 * Return the rd number of the first Sunday of the given ISO year. 300 * @protected 301 * @param {number} year the year for which the first Sunday is being sought 302 * @return {number} the rd of the first Sunday of the ISO year 303 */ 304 firstSunday: function (year) { 305 var firstDay = this.newRd({ 306 year: year, 307 month: 1, 308 day: 1, 309 hour: 0, 310 minute: 0, 311 second: 0, 312 millisecond: 0, 313 cal: this.cal 314 }); 315 var firstThu = this.newRd({ 316 rd: firstDay.onOrAfter(4), 317 cal: this.cal 318 }); 319 return firstThu.before(0); 320 }, 321 322 /** 323 * Return the ISO 8601 week number in the current year for the current date. The week 324 * number ranges from 0 to 55, as some years have 55 weeks assigned to them in some 325 * calendars. 326 * 327 * @return {number} the week number for the current date 328 */ 329 getWeekOfYear: function() { 330 var rd = Math.floor(this.rd.getRataDie()); 331 var year = this._calcYear(rd + this.offset); 332 var yearStart = this.firstSunday(year); 333 var nextYear; 334 335 // if we have a January date, it may be in this ISO year or the previous year 336 if (rd < yearStart) { 337 yearStart = this.firstSunday(year-1); 338 } else { 339 // if we have a late December date, it may be in this ISO year, or the next year 340 nextYear = this.firstSunday(year+1); 341 if (rd >= nextYear) { 342 yearStart = nextYear; 343 } 344 } 345 346 return Math.floor((rd-yearStart)/7) + 1; 347 }, 348 349 /** 350 * Return the ordinal number of the week within the month. The first week of a month is 351 * the first one that contains 4 or more days in that month. If any days precede this 352 * first week, they are marked as being in week 0. This function returns values from 0 353 * through 6.<p> 354 * 355 * The locale is a required parameter because different locales that use the same 356 * Gregorian calendar consider different days of the week to be the beginning of 357 * the week. This can affect the week of the month in which some days are located. 358 * 359 * @param {Locale|string} locale the locale or locale spec to use when figuring out 360 * the first day of the week 361 * @return {number} the ordinal number of the week within the current month 362 */ 363 getWeekOfMonth: function(locale) { 364 var li = new LocaleInfo(locale); 365 366 var first = this.newRd({ 367 year: this._calcYear(this.rd.getRataDie()+this.offset), 368 month: this.getMonths(), 369 day: 1, 370 hour: 0, 371 minute: 0, 372 second: 0, 373 millisecond: 0, 374 cal: this.cal 375 }); 376 var weekStart = first.onOrAfter(li.getFirstDayOfWeek()); 377 378 if (weekStart - first.getRataDie() > 3) { 379 // if the first week has 4 or more days in it of the current month, then consider 380 // that week 1. Otherwise, it is week 0. To make it week 1, move the week start 381 // one week earlier. 382 weekStart -= 7; 383 } 384 return Math.floor((this.rd.getRataDie() - weekStart) / 7) + 1; 385 } 386 }; 387 388 module.exports = IDate;