/*
* HebrewDate.js - Represent a date in the Hebrew calendar
*
* Copyright © 2012-2015, 2018, 2023 JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ilib = require("../index.js");
var MathUtils = require("./MathUtils.js");
var Locale = require("./Locale.js");
var LocaleInfo = require("./LocaleInfo.js");
var IDate = require("./IDate.js");
var TimeZone = require("./TimeZone.js");
var HebrewCal = require("./HebrewCal.js");
var HebrewRataDie = require("./HebrewRataDie.js");
/**
* @class
* Construct a new civil Hebrew date object. The constructor can be called
* with a params object that can contain the following properties:<p>
*
* <ul>
* <li><i>julianday</i> - the Julian Day to set into this date
* <li><i>year</i> - any integer except 0. Years go from -1 (BCE) to 1 (CE), skipping the zero year
* <li><i>month</i> - 1 to 12, where 1 means Nisan, 2 means Iyyar, etc.
* <li><i>day</i> - 1 to 30
* <li><i>hour</i> - 0 to 23. A formatter is used to display 12 hour clocks, but this representation
* is always done with an unambiguous 24 hour representation
* <li><i>parts</i> - 0 to 1079. Specify the halaqim parts of an hour. Either specify
* the parts or specify the minutes, seconds, and milliseconds, but not both.
* <li><i>minute</i> - 0 to 59
* <li><i>second</i> - 0 to 59
* <li><i>millisecond</i> - 0 to 999
* <li><i>locale</i> - the TimeZone instance or time zone name as a string
* of this julian date. The date/time is kept in the local time. The time zone
* is used later if this date is formatted according to a different time zone and
* the difference has to be calculated, or when the date format has a time zone
* component in it.
* <li><i>timezone</i> - the time zone of this instance. If the time zone is not
* given, it can be inferred from this locale. For locales that span multiple
* time zones, the one with the largest population is chosen as the one that
* represents the locale.
*
* <li><i>date</i> - use the given intrinsic Javascript date to initialize this one.
* </ul>
*
* If called with another Hebrew date argument, the date components of the given
* date are copied into the current one.<p>
*
* If the constructor is called with no arguments at all or if none of the
* properties listed above
* from <i>julianday</i> through <i>millisecond</i> are present, then the date
* components are
* filled in with the current date at the time of instantiation. Note that if
* you do not give the time zone when defaulting to the current time and the
* time zone for all of ilib was not set with <i>ilib.setTimeZone()</i>, then the
* time zone will default to UTC ("Universal Time, Coordinated" or "Greenwich
* Mean Time").<p>
*
*
* @constructor
* @extends IDate
* @param {Object=} params parameters that govern the settings and behaviour of this Hebrew date
*/
var HebrewDate = function(params) {
this.cal = new HebrewCal();
params = params || {};
if (params.timezone) {
this.timezone = params.timezone;
}
if (params.locale) {
this.locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale;
}
if (!this.timezone) {
if (this.locale) {
new LocaleInfo(this.locale, {
sync: params.sync,
loadParams: params.loadParams,
onLoad: ilib.bind(this, function(li) {
this.li = li;
this.timezone = li.getTimeZone();
this._init(params);
})
});
} else {
this.timezone = "local";
this._init(params);
}
} else {
this._init(params);
}
};
HebrewDate.prototype = new IDate({noinstance: true});
HebrewDate.prototype.parent = IDate;
HebrewDate.prototype.constructor = HebrewDate;
/**
* Initialize this date
* @private
*/
HebrewDate.prototype._init = function (params) {
if (params.year || params.month || params.day || params.hour ||
params.minute || params.second || params.millisecond || params.parts ) {
/**
* Year in the Hebrew calendar.
* @type number
*/
this.year = parseInt(params.year, 10) || 0;
/**
* The month number, ranging from 1 to 13.
* @type number
*/
this.month = parseInt(params.month, 10) || 1;
/**
* The day of the month. This ranges from 1 to 30.
* @type number
*/
this.day = parseInt(params.day, 10) || 1;
/**
* The hour of the day. This can be a number from 0 to 23, as times are
* stored unambiguously in the 24-hour clock.
* @type number
*/
this.hour = parseInt(params.hour, 10) || 0;
if (typeof(params.parts) !== 'undefined') {
/**
* The parts (halaqim) of the hour. This can be a number from 0 to 1079.
* @type number
*/
this.parts = parseInt(params.parts, 10);
var seconds = parseInt(params.parts, 10) * 3.333333333333;
this.minute = Math.floor(seconds / 60);
seconds -= this.minute * 60;
this.second = Math.floor(seconds);
this.millisecond = (seconds - this.second);
} else {
/**
* The minute of the hours. Ranges from 0 to 59.
* @type number
*/
this.minute = parseInt(params.minute, 10) || 0;
/**
* The second of the minute. Ranges from 0 to 59.
* @type number
*/
this.second = parseInt(params.second, 10) || 0;
/**
* The millisecond of the second. Ranges from 0 to 999.
* @type number
*/
this.millisecond = parseInt(params.millisecond, 10) || 0;
}
/**
* The day of the year. Ranges from 1 to 383.
* @type number
*/
this.dayOfYear = parseInt(params.dayOfYear, 10);
if (typeof(params.dst) === 'boolean') {
this.dst = params.dst;
}
this.rd = this.newRd(this);
// add the time zone offset to the rd to convert to UTC
new TimeZone({
id: this.timezone,
sync: params.sync,
loadParams: params.loadParams,
onLoad: ilib.bind(this, function(tz) {
this.tz = tz;
// getOffsetMillis requires that this.year, this.rd, and this.dst
// are set in order to figure out which time zone rules apply and
// what the offset is at that point in the year
this.offset = this.tz._getOffsetMillisWallTime(this) / 86400000;
if (this.offset !== 0) {
this.rd = this.newRd({
rd: this.rd.getRataDie() - this.offset
});
}
this._init2(params);
})
});
} else {
this._init2(params);
}
};
/**
* Finish initializing this date
* @private
*/
HebrewDate.prototype._init2 = function (params) {
if (!this.rd) {
this.rd = this.newRd(params);
this._calcDateComponents();
}
if (typeof(params.onLoad) === "function") {
params.onLoad(this);
}
};
/**
* the cumulative lengths of each month for a non-leap year, without new years corrections,
* that can be used in reverse to map days to months
* @private
* @const
* @type Array.<number>
*/
HebrewDate.cumMonthLengthsReverse = [
// [days, monthnumber],
[0, 7], /* Tishri - Jewish New Year (Rosh HaShanah) starts in month 7 */
[30, 8], /* Heshvan */
[59, 9], /* Kislev */
[88, 10], /* Teveth */
[117, 11], /* Shevat */
[147, 12], /* Adar I */
[176, 1], /* Nisan */
[206, 2], /* Iyyar */
[235, 3], /* Sivan */
[265, 4], /* Tammuz */
[294, 5], /* Av */
[324, 6], /* Elul */
[354, 7] /* end of year sentinel value */
];
/**
* the cumulative lengths of each month for a leap year, without new years corrections
* that can be used in reverse to map days to months
*
* @private
* @const
* @type Array.<number>
*/
HebrewDate.cumMonthLengthsLeapReverse = [
// [days, monthnumber],
[0, 7], /* Tishri - Jewish New Year (Rosh HaShanah) starts in month 7 */
[30, 8], /* Heshvan */
[59, 9], /* Kislev */
[88, 10], /* Teveth */
[117, 11], /* Shevat */
[147, 12], /* Adar I */
[177, 13], /* Adar II */
[206, 1], /* Nisan */
[236, 2], /* Iyyar */
[265, 3], /* Sivan */
[295, 4], /* Tammuz */
[324, 5], /* Av */
[354, 6], /* Elul */
[384, 7] /* end of year sentinel value */
];
/**
* Number of days difference between RD 0 of the Hebrew calendar
* (Jan 1, 1 Gregorian = JD 1721057.5) and RD 0 of the Hebrew calendar
* (September 7, -3760 Gregorian = JD 347997.25)
* @private
* @const
* @type number
*/
HebrewDate.GregorianDiff = 1373060.25;
/**
* Return a new RD for this date type using the given params.
* @private
* @param {Object=} params the parameters used to create this rata die instance
* @returns {RataDie} the new RD instance for the given params
*/
HebrewDate.prototype.newRd = function (params) {
return new HebrewRataDie(params);
};
/**
* Return the year for the given RD
* @private
* @param {number} rd RD to calculate from
* @returns {number} the year for the RD
*/
HebrewDate.prototype._calcYear = function(rd) {
var year, approximation, nextNewYear;
// divide by the average number of days per year in the Hebrew calendar
// to approximate the year, then tweak it to get the real year
approximation = Math.floor(rd / 365.246822206) + 1;
// console.log("HebrewDate._calcYear: approx is " + approximation);
// search forward from approximation-1 for the year that actually contains this rd
year = approximation;
nextNewYear = HebrewCal.newYear(year);
while (rd >= nextNewYear) {
year++;
nextNewYear = HebrewCal.newYear(year);
}
return year - 1;
};
/**
* Calculate date components for the given RD date.
* @private
*/
HebrewDate.prototype._calcDateComponents = function () {
var remainder,
i,
table,
target,
rd = this.rd.getRataDie();
// console.log("HebrewDate.calcComponents: calculating for rd " + rd);
if (typeof(this.offset) === "undefined") {
this.year = this._calcYear(rd);
// now offset the RD by the time zone, then recalculate in case we were
// near the year boundary
if (!this.tz) {
this.tz = new TimeZone({id: this.timezone});
}
this.offset = this.tz.getOffsetMillis(this) / 86400000;
}
if (this.offset !== 0) {
rd += this.offset;
this.year = this._calcYear(rd);
}
// console.log("HebrewDate.calcComponents: year is " + this.year + " with starting rd " + thisNewYear);
remainder = rd - HebrewCal.newYear(this.year);
// console.log("HebrewDate.calcComponents: remainder is " + remainder);
// take out new years corrections so we get the right month when we look it up in the table
if (remainder >= 59) {
if (remainder >= 88) {
if (HebrewCal.longKislev(this.year)) {
remainder--;
}
}
if (HebrewCal.longHeshvan(this.year)) {
remainder--;
}
}
// console.log("HebrewDate.calcComponents: after new years corrections, remainder is " + remainder);
table = this.cal.isLeapYear(this.year) ?
HebrewDate.cumMonthLengthsLeapReverse :
HebrewDate.cumMonthLengthsReverse;
i = 0;
target = Math.floor(remainder);
while (i+1 < table.length && target >= table[i+1][0]) {
i++;
}
this.month = table[i][1];
// console.log("HebrewDate.calcComponents: remainder is " + remainder);
remainder -= table[i][0];
// console.log("HebrewDate.calcComponents: month is " + this.month + " and remainder is " + remainder);
this.day = Math.floor(remainder);
remainder -= this.day;
this.day++; // days are 1-based
// console.log("HebrewDate.calcComponents: day is " + this.day + " and remainder is " + remainder);
// now convert to milliseconds for the rest of the calculation
remainder = Math.round(remainder * 86400000);
this.hour = Math.floor(remainder/3600000);
remainder -= this.hour * 3600000;
// the hours from 0 to 6 are actually 18:00 to midnight of the previous
// gregorian day, so we have to adjust for that
if (this.hour >= 6) {
this.hour -= 6;
} else {
this.hour += 18;
}
this.minute = Math.floor(remainder/60000);
remainder -= this.minute * 60000;
this.second = Math.floor(remainder/1000);
remainder -= this.second * 1000;
this.millisecond = Math.floor(remainder);
};
/**
* Return the day of the week of this date. The day of the week is encoded
* as number from 0 to 6, with 0=Sunday, 1=Monday, etc., until 6=Saturday.
*
* @return {number} the day of the week
*/
HebrewDate.prototype.getDayOfWeek = function() {
var rd = Math.floor(this.rd.getRataDie() + (this.offset || 0));
return MathUtils.mod(rd+1, 7);
};
/**
* Get the Halaqim (parts) of an hour. There are 1080 parts in an hour, which means
* each part is 3.33333333 seconds long. This means the number returned may not
* be an integer.
*
* @return {number} the halaqim parts of the current hour
*/
HebrewDate.prototype.getHalaqim = function() {
if (this.parts < 0) {
// convert to ms first, then to parts
var h = this.minute * 60000 + this.second * 1000 + this.millisecond;
this.parts = (h * 0.0003);
}
return this.parts;
};
/**
* Return the rd number of the first Sunday of the given ISO year.
* @protected
* @return the rd of the first Sunday of the ISO year
*/
HebrewDate.prototype.firstSunday = function (year) {
var tishri1 = this.newRd({
year: year,
month: 7,
day: 1,
hour: 18,
minute: 0,
second: 0,
millisecond: 0,
cal: this.cal
});
var firstThu = this.newRd({
rd: tishri1.onOrAfter(4),
cal: this.cal
});
return firstThu.before(0);
};
/**
* Return the ordinal day of the year. Days are counted from 1 and proceed linearly up to
* 385, regardless of months or weeks, etc. That is, Tishri 1st is day 1, and
* Elul 29 is 385 for a leap year with a long Heshvan and long Kislev.
* @return {number} the ordinal day of the year
*/
HebrewDate.prototype.getDayOfYear = function() {
var table = this.cal.isLeapYear(this.year) ?
HebrewRataDie.cumMonthLengthsLeap :
HebrewRataDie.cumMonthLengths;
var days = table[this.month-1];
if ((this.month < 7 || this.month > 8) && HebrewCal.longHeshvan(this.year)) {
days++;
}
if ((this.month < 7 || this.month > 9) && HebrewCal.longKislev(this.year)) {
days++;
}
return days + this.day;
};
/**
* Return the ordinal number of the week within the month. The first week of a month is
* the first one that contains 4 or more days in that month. If any days precede this
* first week, they are marked as being in week 0. This function returns values from 0
* through 6.<p>
*
* The locale is a required parameter because different locales that use the same
* Hebrew calendar consider different days of the week to be the beginning of
* the week. This can affect the week of the month in which some days are located.
*
* @param {Locale|string} locale the locale or locale spec to use when figuring out
* the first day of the week
* @return {number} the ordinal number of the week within the current month
*/
HebrewDate.prototype.getWeekOfMonth = function(locale) {
var li = new LocaleInfo(locale),
first = this.newRd({
year: this.year,
month: this.month,
day: 1,
hour: 18,
minute: 0,
second: 0,
millisecond: 0
}),
rd = this.rd.getRataDie(),
weekStart = first.onOrAfter(li.getFirstDayOfWeek());
if (weekStart - first.getRataDie() > 3) {
// if the first week has 4 or more days in it of the current month, then consider
// that week 1. Otherwise, it is week 0. To make it week 1, move the week start
// one week earlier.
weekStart -= 7;
}
return (rd < weekStart) ? 0 : Math.floor((rd - weekStart) / 7) + 1;
};
/**
* Return the era for this date as a number. The value for the era for Hebrew
* calendars is -1 for "before the Hebrew era" and 1 for "the Hebrew era".
* Hebrew era dates are any date after Tishri 1, 1, which is the same as
* September 7, 3760 BC in the Gregorian calendar.
*
* @return {number} 1 if this date is in the Hebrew era, -1 if it is before the
* Hebrew era
*/
HebrewDate.prototype.getEra = function() {
return (this.year < 1) ? -1 : 1;
};
/**
* Return the name of the calendar that governs this date.
*
* @return {string} a string giving the name of the calendar
*/
HebrewDate.prototype.getCalendar = function() {
return "hebrew";
};
// register with the factory method
IDate._constructors["hebrew"] = HebrewDate;
module.exports = HebrewDate;
Source