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