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("../index.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