1 /* 2 * HebrewDate.js - Represent a date in the Hebrew calendar 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 var ilib = require("./ilib.js"); 21 var MathUtils = require("./MathUtils.js"); 22 23 var Locale = require("./Locale.js"); 24 var LocaleInfo = require("./LocaleInfo.js"); 25 var IDate = require("./IDate.js"); 26 var TimeZone = require("./TimeZone.js"); 27 28 var HebrewCal = require("./HebrewCal.js"); 29 var HebrewRataDie = require("./HebrewRataDie.js"); 30 31 /** 32 * @class 33 * Construct a new civil Hebrew date object. The constructor can be called 34 * with a params object that can contain the following properties:<p> 35 * 36 * <ul> 37 * <li><i>julianday</i> - the Julian Day to set into this date 38 * <li><i>year</i> - any integer except 0. Years go from -1 (BCE) to 1 (CE), skipping the zero year 39 * <li><i>month</i> - 1 to 12, where 1 means Nisan, 2 means Iyyar, etc. 40 * <li><i>day</i> - 1 to 30 41 * <li><i>hour</i> - 0 to 23. A formatter is used to display 12 hour clocks, but this representation 42 * is always done with an unambiguous 24 hour representation 43 * <li><i>parts</i> - 0 to 1079. Specify the halaqim parts of an hour. Either specify 44 * the parts or specify the minutes, seconds, and milliseconds, but not both. 45 * <li><i>minute</i> - 0 to 59 46 * <li><i>second</i> - 0 to 59 47 * <li><i>millisecond</i> - 0 to 999 48 * <li><i>locale</i> - the TimeZone instance or time zone name as a string 49 * of this julian date. The date/time is kept in the local time. The time zone 50 * is used later if this date is formatted according to a different time zone and 51 * the difference has to be calculated, or when the date format has a time zone 52 * component in it. 53 * <li><i>timezone</i> - the time zone of this instance. If the time zone is not 54 * given, it can be inferred from this locale. For locales that span multiple 55 * time zones, the one with the largest population is chosen as the one that 56 * represents the locale. 57 * 58 * <li><i>date</i> - use the given intrinsic Javascript date to initialize this one. 59 * </ul> 60 * 61 * If called with another Hebrew date argument, the date components of the given 62 * date are copied into the current one.<p> 63 * 64 * If the constructor is called with no arguments at all or if none of the 65 * properties listed above 66 * from <i>julianday</i> through <i>millisecond</i> are present, then the date 67 * components are 68 * filled in with the current date at the time of instantiation. Note that if 69 * you do not give the time zone when defaulting to the current time and the 70 * time zone for all of ilib was not set with <i>ilib.setTimeZone()</i>, then the 71 * time zone will default to UTC ("Universal Time, Coordinated" or "Greenwich 72 * Mean Time").<p> 73 * 74 * 75 * @constructor 76 * @extends IDate 77 * @param {Object=} params parameters that govern the settings and behaviour of this Hebrew date 78 */ 79 var HebrewDate = function(params) { 80 this.cal = new HebrewCal(); 81 82 params = params || {}; 83 84 if (params.timezone) { 85 this.timezone = params.timezone; 86 } 87 if (params.locale) { 88 this.locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale; 89 } 90 91 if (!this.timezone) { 92 if (this.locale) { 93 new LocaleInfo(this.locale, { 94 sync: params.sync, 95 loadParams: params.loadParams, 96 onLoad: ilib.bind(this, function(li) { 97 this.li = li; 98 this.timezone = li.getTimeZone(); 99 this._init(params); 100 }) 101 }); 102 } else { 103 this.timezone = "local"; 104 this._init(params); 105 } 106 } else { 107 this._init(params); 108 } 109 }; 110 111 HebrewDate.prototype = new IDate({noinstance: true}); 112 HebrewDate.prototype.parent = IDate; 113 HebrewDate.prototype.constructor = HebrewDate; 114 115 /** 116 * Initialize this date 117 * @private 118 */ 119 HebrewDate.prototype._init = function (params) { 120 if (params.year || params.month || params.day || params.hour || 121 params.minute || params.second || params.millisecond || params.parts ) { 122 /** 123 * Year in the Hebrew calendar. 124 * @type number 125 */ 126 this.year = parseInt(params.year, 10) || 0; 127 128 /** 129 * The month number, ranging from 1 to 13. 130 * @type number 131 */ 132 this.month = parseInt(params.month, 10) || 1; 133 134 /** 135 * The day of the month. This ranges from 1 to 30. 136 * @type number 137 */ 138 this.day = parseInt(params.day, 10) || 1; 139 140 /** 141 * The hour of the day. This can be a number from 0 to 23, as times are 142 * stored unambiguously in the 24-hour clock. 143 * @type number 144 */ 145 this.hour = parseInt(params.hour, 10) || 0; 146 147 if (typeof(params.parts) !== 'undefined') { 148 /** 149 * The parts (halaqim) of the hour. This can be a number from 0 to 1079. 150 * @type number 151 */ 152 this.parts = parseInt(params.parts, 10); 153 var seconds = parseInt(params.parts, 10) * 3.333333333333; 154 this.minute = Math.floor(seconds / 60); 155 seconds -= this.minute * 60; 156 this.second = Math.floor(seconds); 157 this.millisecond = (seconds - this.second); 158 } else { 159 /** 160 * The minute of the hours. Ranges from 0 to 59. 161 * @type number 162 */ 163 this.minute = parseInt(params.minute, 10) || 0; 164 165 /** 166 * The second of the minute. Ranges from 0 to 59. 167 * @type number 168 */ 169 this.second = parseInt(params.second, 10) || 0; 170 171 /** 172 * The millisecond of the second. Ranges from 0 to 999. 173 * @type number 174 */ 175 this.millisecond = parseInt(params.millisecond, 10) || 0; 176 } 177 178 /** 179 * The day of the year. Ranges from 1 to 383. 180 * @type number 181 */ 182 this.dayOfYear = parseInt(params.dayOfYear, 10); 183 184 if (typeof(params.dst) === 'boolean') { 185 this.dst = params.dst; 186 } 187 188 this.rd = this.newRd(this); 189 190 // add the time zone offset to the rd to convert to UTC 191 new TimeZone({ 192 id: this.timezone, 193 sync: params.sync, 194 loadParams: params.loadParams, 195 onLoad: ilib.bind(this, function(tz) { 196 this.tz = tz; 197 // getOffsetMillis requires that this.year, this.rd, and this.dst 198 // are set in order to figure out which time zone rules apply and 199 // what the offset is at that point in the year 200 this.offset = this.tz._getOffsetMillisWallTime(this) / 86400000; 201 if (this.offset !== 0) { 202 this.rd = this.newRd({ 203 rd: this.rd.getRataDie() - this.offset 204 }); 205 } 206 207 this._init2(params); 208 }) 209 }); 210 } else { 211 this._init2(params); 212 } 213 }; 214 215 /** 216 * Finish initializing this date 217 * @private 218 */ 219 HebrewDate.prototype._init2 = function (params) { 220 if (!this.rd) { 221 this.rd = this.newRd(params); 222 this._calcDateComponents(); 223 } 224 225 if (typeof(params.onLoad) === "function") { 226 params.onLoad(this); 227 } 228 }; 229 230 /** 231 * the cumulative lengths of each month for a non-leap year, without new years corrections, 232 * that can be used in reverse to map days to months 233 * @private 234 * @const 235 * @type Array.<number> 236 */ 237 HebrewDate.cumMonthLengthsReverse = [ 238 // [days, monthnumber], 239 [0, 7], /* Tishri - Jewish New Year (Rosh HaShanah) starts in month 7 */ 240 [30, 8], /* Heshvan */ 241 [59, 9], /* Kislev */ 242 [88, 10], /* Teveth */ 243 [117, 11], /* Shevat */ 244 [147, 12], /* Adar I */ 245 [176, 1], /* Nisan */ 246 [206, 2], /* Iyyar */ 247 [235, 3], /* Sivan */ 248 [265, 4], /* Tammuz */ 249 [294, 5], /* Av */ 250 [324, 6], /* Elul */ 251 [354, 7] /* end of year sentinel value */ 252 ]; 253 254 /** 255 * the cumulative lengths of each month for a leap year, without new years corrections 256 * that can be used in reverse to map days to months 257 * 258 * @private 259 * @const 260 * @type Array.<number> 261 */ 262 HebrewDate.cumMonthLengthsLeapReverse = [ 263 // [days, monthnumber], 264 [0, 7], /* Tishri - Jewish New Year (Rosh HaShanah) starts in month 7 */ 265 [30, 8], /* Heshvan */ 266 [59, 9], /* Kislev */ 267 [88, 10], /* Teveth */ 268 [117, 11], /* Shevat */ 269 [147, 12], /* Adar I */ 270 [177, 13], /* Adar II */ 271 [206, 1], /* Nisan */ 272 [236, 2], /* Iyyar */ 273 [265, 3], /* Sivan */ 274 [295, 4], /* Tammuz */ 275 [324, 5], /* Av */ 276 [354, 6], /* Elul */ 277 [384, 7] /* end of year sentinel value */ 278 ]; 279 280 /** 281 * Number of days difference between RD 0 of the Hebrew calendar 282 * (Jan 1, 1 Gregorian = JD 1721057.5) and RD 0 of the Hebrew calendar 283 * (September 7, -3760 Gregorian = JD 347997.25) 284 * @private 285 * @const 286 * @type number 287 */ 288 HebrewDate.GregorianDiff = 1373060.25; 289 290 /** 291 * Return a new RD for this date type using the given params. 292 * @private 293 * @param {Object=} params the parameters used to create this rata die instance 294 * @returns {RataDie} the new RD instance for the given params 295 */ 296 HebrewDate.prototype.newRd = function (params) { 297 return new HebrewRataDie(params); 298 }; 299 300 /** 301 * Return the year for the given RD 302 * @protected 303 * @param {number} rd RD to calculate from 304 * @returns {number} the year for the RD 305 */ 306 HebrewDate.prototype._calcYear = function(rd) { 307 var year, approximation, nextNewYear; 308 309 // divide by the average number of days per year in the Hebrew calendar 310 // to approximate the year, then tweak it to get the real year 311 approximation = Math.floor(rd / 365.246822206) + 1; 312 313 // console.log("HebrewDate._calcYear: approx is " + approximation); 314 315 // search forward from approximation-1 for the year that actually contains this rd 316 year = approximation; 317 nextNewYear = HebrewCal.newYear(year); 318 while (rd >= nextNewYear) { 319 year++; 320 nextNewYear = HebrewCal.newYear(year); 321 } 322 return year - 1; 323 }; 324 325 /** 326 * Calculate date components for the given RD date. 327 * @protected 328 */ 329 HebrewDate.prototype._calcDateComponents = function () { 330 var remainder, 331 i, 332 table, 333 target, 334 rd = this.rd.getRataDie(); 335 336 // console.log("HebrewDate.calcComponents: calculating for rd " + rd); 337 338 if (typeof(this.offset) === "undefined") { 339 this.year = this._calcYear(rd); 340 341 // now offset the RD by the time zone, then recalculate in case we were 342 // near the year boundary 343 if (!this.tz) { 344 this.tz = new TimeZone({id: this.timezone}); 345 } 346 this.offset = this.tz.getOffsetMillis(this) / 86400000; 347 } 348 349 if (this.offset !== 0) { 350 rd += this.offset; 351 this.year = this._calcYear(rd); 352 } 353 354 // console.log("HebrewDate.calcComponents: year is " + this.year + " with starting rd " + thisNewYear); 355 356 remainder = rd - HebrewCal.newYear(this.year); 357 // console.log("HebrewDate.calcComponents: remainder is " + remainder); 358 359 // take out new years corrections so we get the right month when we look it up in the table 360 if (remainder >= 59) { 361 if (remainder >= 88) { 362 if (HebrewCal.longKislev(this.year)) { 363 remainder--; 364 } 365 } 366 if (HebrewCal.longHeshvan(this.year)) { 367 remainder--; 368 } 369 } 370 371 // console.log("HebrewDate.calcComponents: after new years corrections, remainder is " + remainder); 372 373 table = this.cal.isLeapYear(this.year) ? 374 HebrewDate.cumMonthLengthsLeapReverse : 375 HebrewDate.cumMonthLengthsReverse; 376 377 i = 0; 378 target = Math.floor(remainder); 379 while (i+1 < table.length && target >= table[i+1][0]) { 380 i++; 381 } 382 383 this.month = table[i][1]; 384 // console.log("HebrewDate.calcComponents: remainder is " + remainder); 385 remainder -= table[i][0]; 386 387 // console.log("HebrewDate.calcComponents: month is " + this.month + " and remainder is " + remainder); 388 389 this.day = Math.floor(remainder); 390 remainder -= this.day; 391 this.day++; // days are 1-based 392 393 // console.log("HebrewDate.calcComponents: day is " + this.day + " and remainder is " + remainder); 394 395 // now convert to milliseconds for the rest of the calculation 396 remainder = Math.round(remainder * 86400000); 397 398 this.hour = Math.floor(remainder/3600000); 399 remainder -= this.hour * 3600000; 400 401 // the hours from 0 to 6 are actually 18:00 to midnight of the previous 402 // gregorian day, so we have to adjust for that 403 if (this.hour >= 6) { 404 this.hour -= 6; 405 } else { 406 this.hour += 18; 407 } 408 409 this.minute = Math.floor(remainder/60000); 410 remainder -= this.minute * 60000; 411 412 this.second = Math.floor(remainder/1000); 413 remainder -= this.second * 1000; 414 415 this.millisecond = Math.floor(remainder); 416 }; 417 418 /** 419 * Return the day of the week of this date. The day of the week is encoded 420 * as number from 0 to 6, with 0=Sunday, 1=Monday, etc., until 6=Saturday. 421 * 422 * @return {number} the day of the week 423 */ 424 HebrewDate.prototype.getDayOfWeek = function() { 425 var rd = Math.floor(this.rd.getRataDie() + (this.offset || 0)); 426 return MathUtils.mod(rd+1, 7); 427 }; 428 429 /** 430 * Get the Halaqim (parts) of an hour. There are 1080 parts in an hour, which means 431 * each part is 3.33333333 seconds long. This means the number returned may not 432 * be an integer. 433 * 434 * @return {number} the halaqim parts of the current hour 435 */ 436 HebrewDate.prototype.getHalaqim = function() { 437 if (this.parts < 0) { 438 // convert to ms first, then to parts 439 var h = this.minute * 60000 + this.second * 1000 + this.millisecond; 440 this.parts = (h * 0.0003); 441 } 442 return this.parts; 443 }; 444 445 /** 446 * Return the rd number of the first Sunday of the given ISO year. 447 * @protected 448 * @return the rd of the first Sunday of the ISO year 449 */ 450 HebrewDate.prototype.firstSunday = function (year) { 451 var tishri1 = this.newRd({ 452 year: year, 453 month: 7, 454 day: 1, 455 hour: 18, 456 minute: 0, 457 second: 0, 458 millisecond: 0, 459 cal: this.cal 460 }); 461 var firstThu = this.newRd({ 462 rd: tishri1.onOrAfter(4), 463 cal: this.cal 464 }); 465 return firstThu.before(0); 466 }; 467 468 /** 469 * Return the ordinal day of the year. Days are counted from 1 and proceed linearly up to 470 * 385, regardless of months or weeks, etc. That is, Tishri 1st is day 1, and 471 * Elul 29 is 385 for a leap year with a long Heshvan and long Kislev. 472 * @return {number} the ordinal day of the year 473 */ 474 HebrewDate.prototype.getDayOfYear = function() { 475 var table = this.cal.isLeapYear(this.year) ? 476 HebrewRataDie.cumMonthLengthsLeap : 477 HebrewRataDie.cumMonthLengths; 478 var days = table[this.month-1]; 479 if ((this.month < 7 || this.month > 8) && HebrewCal.longHeshvan(this.year)) { 480 days++; 481 } 482 if ((this.month < 7 || this.month > 9) && HebrewCal.longKislev(this.year)) { 483 days++; 484 } 485 486 return days + this.day; 487 }; 488 489 /** 490 * Return the ordinal number of the week within the month. The first week of a month is 491 * the first one that contains 4 or more days in that month. If any days precede this 492 * first week, they are marked as being in week 0. This function returns values from 0 493 * through 6.<p> 494 * 495 * The locale is a required parameter because different locales that use the same 496 * Hebrew calendar consider different days of the week to be the beginning of 497 * the week. This can affect the week of the month in which some days are located. 498 * 499 * @param {Locale|string} locale the locale or locale spec to use when figuring out 500 * the first day of the week 501 * @return {number} the ordinal number of the week within the current month 502 */ 503 HebrewDate.prototype.getWeekOfMonth = function(locale) { 504 var li = new LocaleInfo(locale), 505 first = this.newRd({ 506 year: this.year, 507 month: this.month, 508 day: 1, 509 hour: 18, 510 minute: 0, 511 second: 0, 512 millisecond: 0 513 }), 514 rd = this.rd.getRataDie(), 515 weekStart = first.onOrAfter(li.getFirstDayOfWeek()); 516 517 if (weekStart - first.getRataDie() > 3) { 518 // if the first week has 4 or more days in it of the current month, then consider 519 // that week 1. Otherwise, it is week 0. To make it week 1, move the week start 520 // one week earlier. 521 weekStart -= 7; 522 } 523 return (rd < weekStart) ? 0 : Math.floor((rd - weekStart) / 7) + 1; 524 }; 525 526 /** 527 * Return the era for this date as a number. The value for the era for Hebrew 528 * calendars is -1 for "before the Hebrew era" and 1 for "the Hebrew era". 529 * Hebrew era dates are any date after Tishri 1, 1, which is the same as 530 * September 7, 3760 BC in the Gregorian calendar. 531 * 532 * @return {number} 1 if this date is in the Hebrew era, -1 if it is before the 533 * Hebrew era 534 */ 535 HebrewDate.prototype.getEra = function() { 536 return (this.year < 1) ? -1 : 1; 537 }; 538 539 /** 540 * Return the name of the calendar that governs this date. 541 * 542 * @return {string} a string giving the name of the calendar 543 */ 544 HebrewDate.prototype.getCalendar = function() { 545 return "hebrew"; 546 }; 547 548 // register with the factory method 549 IDate._constructors["hebrew"] = HebrewDate; 550 551 module.exports = HebrewDate; 552