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