1 /*
  2  * IslamicDate.js - Represent a date in the Islamic 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 SearchUtils = require("./SearchUtils.js");
 22 var MathUtils = require("./MathUtils.js");
 23 
 24 var Locale = require("./Locale.js");
 25 var LocaleInfo = require("./LocaleInfo.js");
 26 var TimeZone = require("./TimeZone.js");
 27 var IDate = require("./IDate.js");
 28 
 29 var IslamicRataDie = require("./IslamicRataDie.js");
 30 var IslamicCal = require("./IslamicCal.js");
 31 
 32 /**
 33  * @class
 34  * Construct a new civil Islamic date object. The constructor can be called
 35  * with a params object that can contain the following properties:<p>
 36  *
 37  * <ul>
 38  * <li><i>julianday</i> - the Julian Day to set into this date
 39  * <li><i>year</i> - any integer except 0. Years go from -1 (BCE) to 1 (CE), skipping the zero year
 40  * <li><i>month</i> - 1 to 12, where 1 means Muharram, 2 means Saffar, etc.
 41  * <li><i>day</i> - 1 to 30
 42  * <li><i>hour</i> - 0 to 23. A formatter is used to display 12 hour clocks, but this representation
 43  * is always done with an unambiguous 24 hour representation
 44  * <li><i>minute</i> - 0 to 59
 45  * <li><i>second</i> - 0 to 59
 46  * <li><i>millisecond</i> - 0 to 999
 47  * <li><i>locale</i> - the TimeZone instance or time zone name as a string
 48  * of this julian date. The date/time is kept in the local time. The time zone
 49  * is used later if this date is formatted according to a different time zone and
 50  * the difference has to be calculated, or when the date format has a time zone
 51  * component in it.
 52  * <li><i>timezone</i> - the time zone of this instance. If the time zone is not
 53  * given, it can be inferred from this locale. For locales that span multiple
 54  * time zones, the one with the largest population is chosen as the one that
 55  * represents the locale.
 56  *
 57  * <li><i>date</i> - use the given intrinsic Javascript date to initialize this one.
 58  * </ul>
 59  *
 60  * If called with another Islamic date argument, the date components of the given
 61  * date are copied into the current one.<p>
 62  *
 63  * If the constructor is called with no arguments at all or if none of the
 64  * properties listed above
 65  * from <i>julianday</i> through <i>millisecond</i> are present, then the date
 66  * components are
 67  * filled in with the current date at the time of instantiation. Note that if
 68  * you do not give the time zone when defaulting to the current time and the
 69  * time zone for all of ilib was not set with <i>ilib.setTimeZone()</i>, then the
 70  * time zone will default to UTC ("Universal Time, Coordinated" or "Greenwich
 71  * Mean Time").<p>
 72  *
 73  *
 74  * @constructor
 75  * @extends IDate
 76  * @param {Object=} params parameters that govern the settings and behaviour of this Islamic date
 77  */
 78 var IslamicDate = function(params) {
 79     this.cal = new IslamicCal();
 80 
 81     params = params || {};
 82 
 83     if (params.timezone) {
 84         this.timezone = params.timezone;
 85     }
 86     if (params.locale) {
 87         this.locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale;
 88     }
 89 
 90     if (!this.timezone) {
 91         if (this.locale) {
 92             new LocaleInfo(this.locale, {
 93                 sync: params.sync,
 94                 loadParams: params.loadParams,
 95                 onLoad: ilib.bind(this, function(li) {
 96                     this.li = li;
 97                     this.timezone = li.getTimeZone();
 98                     this._init(params);
 99                 })
100             });
101         } else {
102             this.timezone = "local";
103             this._init(params);
104         }
105     } else {
106         this._init(params);
107     }
108 };
109 
110 IslamicDate.prototype = new IDate({noinstance: true});
111 IslamicDate.prototype.parent = IDate;
112 IslamicDate.prototype.constructor = IslamicDate;
113 
114 /**
115  * Initialize the date
116  * @private
117  */
118 IslamicDate.prototype._init = function (params) {
119     if (params.year || params.month || params.day || params.hour ||
120         params.minute || params.second || params.millisecond ) {
121         /**
122          * Year in the Islamic calendar.
123          * @type number
124          */
125         this.year = parseInt(params.year, 10) || 0;
126 
127         /**
128          * The month number, ranging from 1 to 12 (December).
129          * @type number
130          */
131         this.month = parseInt(params.month, 10) || 1;
132 
133         /**
134          * The day of the month. This ranges from 1 to 30.
135          * @type number
136          */
137         this.day = parseInt(params.day, 10) || 1;
138 
139         /**
140          * The hour of the day. This can be a number from 0 to 23, as times are
141          * stored unambiguously in the 24-hour clock.
142          * @type number
143          */
144         this.hour = parseInt(params.hour, 10) || 0;
145 
146         /**
147          * The minute of the hours. Ranges from 0 to 59.
148          * @type number
149          */
150         this.minute = parseInt(params.minute, 10) || 0;
151 
152         /**
153          * The second of the minute. Ranges from 0 to 59.
154          * @type number
155          */
156         this.second = parseInt(params.second, 10) || 0;
157 
158         /**
159          * The millisecond of the second. Ranges from 0 to 999.
160          * @type number
161          */
162         this.millisecond = parseInt(params.millisecond, 10) || 0;
163 
164         /**
165          * The day of the year. Ranges from 1 to 355.
166          * @type number
167          */
168         this.dayOfYear = parseInt(params.dayOfYear, 10);
169 
170         if (typeof(params.dst) === 'boolean') {
171             this.dst = params.dst;
172         }
173 
174         this.rd = this.newRd(this);
175 
176         new TimeZone({
177             id: this.timezone,
178             sync: params.sync,
179             loadParams: params.loadParams,
180             onLoad: ilib.bind(this, function(tz) {
181                 this.tz = tz;
182                 // add the time zone offset to the rd to convert to UTC
183                 // getOffsetMillis requires that this.year, this.rd, and this.dst
184                 // are set in order to figure out which time zone rules apply and
185                 // what the offset is at that point in the year
186                 this.offset = this.tz._getOffsetMillisWallTime(this) / 86400000;
187                 if (this.offset !== 0) {
188                     this.rd = this.newRd({
189                         rd: this.rd.getRataDie() - this.offset
190                     });
191                 }
192                 this._init2(params);
193             })
194         });
195     } else {
196         this._init2(params);
197     }
198 };
199 
200 /**
201  * @private
202  * Finish initializing this date object
203  */
204 IslamicDate.prototype._init2 = function (params) {
205     if (!this.rd) {
206         this.rd = this.newRd(params);
207         this._calcDateComponents();
208     }
209 
210     if (typeof(params.onLoad) === "function") {
211         params.onLoad(this);
212     }
213 };
214 
215 /**
216  * the cumulative lengths of each month, for a non-leap year
217  * @private
218  * @const
219  * @type Array.<number>
220  */
221 IslamicDate.cumMonthLengths = [
222     0,  /* Muharram */
223     30,  /* Saffar */
224     59,  /* Rabi'I */
225     89,  /* Rabi'II */
226     118,  /* Jumada I */
227     148,  /* Jumada II */
228     177,  /* Rajab */
229     207,  /* Sha'ban */
230     236,  /* Ramadan */
231     266,  /* Shawwal */
232     295,  /* Dhu al-Qa'da */
233     325,  /* Dhu al-Hijja */
234     354
235 ];
236 
237 /**
238  * Number of days difference between RD 0 of the Gregorian calendar and
239  * RD 0 of the Islamic calendar.
240  * @private
241  * @const
242  * @type number
243  */
244 IslamicDate.GregorianDiff = 227015;
245 
246 /**
247  * Return a new RD for this date type using the given params.
248  * @protected
249  * @param {Object=} params the parameters used to create this rata die instance
250  * @returns {RataDie} the new RD instance for the given params
251  */
252 IslamicDate.prototype.newRd = function (params) {
253     return new IslamicRataDie(params);
254 };
255 
256 /**
257  * Return the year for the given RD
258  * @protected
259  * @param {number} rd RD to calculate from
260  * @returns {number} the year for the RD
261  */
262 IslamicDate.prototype._calcYear = function(rd) {
263     return Math.floor((30 * rd + 10646) / 10631);
264 };
265 
266 /**
267  * Calculate date components for the given RD date.
268  * @protected
269  */
270 IslamicDate.prototype._calcDateComponents = function () {
271     var remainder,
272         rd = this.rd.getRataDie();
273 
274     this.year = this._calcYear(rd);
275 
276     if (typeof(this.offset) === "undefined") {
277         this.year = this._calcYear(rd);
278 
279         // now offset the RD by the time zone, then recalculate in case we were
280         // near the year boundary
281         if (!this.tz) {
282             this.tz = new TimeZone({id: this.timezone});
283         }
284         this.offset = this.tz.getOffsetMillis(this) / 86400000;
285     }
286 
287     if (this.offset !== 0) {
288         rd += this.offset;
289         this.year = this._calcYear(rd);
290     }
291 
292     //console.log("IslamicDate.calcComponent: calculating for rd " + rd);
293     //console.log("IslamicDate.calcComponent: year is " + ret.year);
294     var yearStart = this.newRd({
295         year: this.year,
296         month: 1,
297         day: 1,
298         hour: 0,
299         minute: 0,
300         second: 0,
301         millisecond: 0
302     });
303     remainder = rd - yearStart.getRataDie() + 1;
304 
305     this.dayOfYear = remainder;
306 
307     //console.log("IslamicDate.calcComponent: remainder is " + remainder);
308 
309     this.month = SearchUtils.bsearch(remainder, IslamicDate.cumMonthLengths);
310     remainder -= IslamicDate.cumMonthLengths[this.month-1];
311 
312     //console.log("IslamicDate.calcComponent: month is " + this.month + " and remainder is " + remainder);
313 
314     this.day = Math.floor(remainder);
315     remainder -= this.day;
316 
317     //console.log("IslamicDate.calcComponent: day is " + this.day + " and remainder is " + remainder);
318 
319     // now convert to milliseconds for the rest of the calculation
320     remainder = Math.round(remainder * 86400000);
321 
322     this.hour = Math.floor(remainder/3600000);
323     remainder -= this.hour * 3600000;
324 
325     this.minute = Math.floor(remainder/60000);
326     remainder -= this.minute * 60000;
327 
328     this.second = Math.floor(remainder/1000);
329     remainder -= this.second * 1000;
330 
331     this.millisecond = remainder;
332 };
333 
334 /**
335  * Return the day of the week of this date. The day of the week is encoded
336  * as number from 0 to 6, with 0=Sunday, 1=Monday, etc., until 6=Saturday.
337  *
338  * @return {number} the day of the week
339  */
340 IslamicDate.prototype.getDayOfWeek = function() {
341     var rd = Math.floor(this.rd.getRataDie() + (this.offset || 0));
342     return MathUtils.mod(rd-2, 7);
343 };
344 
345 /**
346  * Return the ordinal day of the year. Days are counted from 1 and proceed linearly up to
347  * 354 or 355, regardless of months or weeks, etc. That is, Muharran 1st is day 1, and
348  * Dhu al-Hijja 29 is 354.
349  * @return {number} the ordinal day of the year
350  */
351 IslamicDate.prototype.getDayOfYear = function() {
352     return IslamicDate.cumMonthLengths[this.month-1] + this.day;
353 };
354 
355 /**
356  * Return the era for this date as a number. The value for the era for Islamic
357  * calendars is -1 for "before the Islamic era" and 1 for "the Islamic era".
358  * Islamic era dates are any date after Muharran 1, 1, which is the same as
359  * July 16, 622 CE in the Gregorian calendar.
360  *
361  * @return {number} 1 if this date is in the common era, -1 if it is before the
362  * common era
363  */
364 IslamicDate.prototype.getEra = function() {
365     return (this.year < 1) ? -1 : 1;
366 };
367 
368 /**
369  * Return the name of the calendar that governs this date.
370  *
371  * @return {string} a string giving the name of the calendar
372  */
373 IslamicDate.prototype.getCalendar = function() {
374     return "islamic";
375 };
376 
377 //register with the factory method
378 IDate._constructors["islamic"] = IslamicDate;
379 
380 module.exports = IslamicDate;
381