1 /*
  2  * HanDate.js - Represent a date in the Han 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("../index.js");
 21 var JSUtils = require("./JSUtils.js");
 22 var MathUtils = require("./MathUtils.js");
 23 
 24 var Locale = require("./Locale.js");
 25 var LocaleInfo = require("./LocaleInfo.js");
 26 var IDate = require("./IDate.js");
 27 var TimeZone = require("./TimeZone.js");
 28 
 29 var Astro = require("./Astro.js");
 30 var HanCal = require("./HanCal.js");
 31 var GregorianDate = require("./GregorianDate.js");
 32 var HanRataDie = require("./HanRataDie.js");
 33 var RataDie = require("./RataDie.js");
 34 
 35 /**
 36  * @class
 37  *
 38  * Construct a new Han date object. The constructor parameters can
 39  * contain any of the following properties:
 40  *
 41  * <ul>
 42  * <li><i>unixtime<i> - sets the time of this instance according to the given
 43  * unix time. Unix time is the number of milliseconds since midnight on Jan 1, 1970, Gregorian
 44  *
 45  * <li><i>julianday</i> - sets the time of this instance according to the given
 46  * Julian Day instance or the Julian Day given as a float
 47  *
 48  * <li><i>cycle</i> - any integer giving the number of 60-year cycle in which the date is located.
 49  * If the cycle is not given but the year is, it is assumed that the year parameter is a fictitious
 50  * linear count of years since the beginning of the epoch, much like other calendars. This linear
 51  * count is never used. If both the cycle and year are given, the year is wrapped to the range 0
 52  * to 60 and treated as if it were a year in the regular 60-year cycle.
 53  *
 54  * <li><i>year</i> - any integer, including 0
 55  *
 56  * <li><i>month</i> - 1 to 12, where 1 means Farvardin, 2 means Ordibehesht, etc.
 57  *
 58  * <li><i>day</i> - 1 to 31
 59  *
 60  * <li><i>hour</i> - 0 to 23. A formatter is used to display 12 hour clocks, but this representation
 61  * is always done with an unambiguous 24 hour representation
 62  *
 63  * <li><i>minute</i> - 0 to 59
 64  *
 65  * <li><i>second</i> - 0 to 59
 66  *
 67  * <li><i>millisecond</i> - 0 to 999
 68  *
 69  * <li><i>timezone</i> - the TimeZone instance or time zone name as a string
 70  * of this han date. The date/time is kept in the local time. The time zone
 71  * is used later if this date is formatted according to a different time zone and
 72  * the difference has to be calculated, or when the date format has a time zone
 73  * component in it.
 74  *
 75  * <li><i>locale</i> - locale for this han date. If the time zone is not
 76  * given, it can be inferred from this locale. For locales that span multiple
 77  * time zones, the one with the largest population is chosen as the one that
 78  * represents the locale.
 79  *
 80  * <li><i>date</i> - use the given intrinsic Javascript date to initialize this one.
 81  * </ul>
 82  *
 83  * If the constructor is called with another Han date instance instead of
 84  * a parameter block, the other instance acts as a parameter block and its
 85  * settings are copied into the current instance.<p>
 86  *
 87  * If the constructor is called with no arguments at all or if none of the
 88  * properties listed above
 89  * from <i>unixtime</i> through <i>millisecond</i> are present, then the date
 90  * components are
 91  * filled in with the current date at the time of instantiation. Note that if
 92  * you do not give the time zone when defaulting to the current time and the
 93  * time zone for all of ilib was not set with <i>ilib.setTimeZone()</i>, then the
 94  * time zone will default to UTC ("Universal Time, Coordinated" or "Greenwich
 95  * Mean Time").<p>
 96  *
 97  * If any of the properties from <i>year</i> through <i>millisecond</i> are not
 98  * specified in the params, it is assumed that they have the smallest possible
 99  * value in the range for the property (zero or one).<p>
100  *
101  *
102  * @constructor
103  * @extends Date
104  * @param {Object=} params parameters that govern the settings and behaviour of this Han date
105  */
106 var HanDate = function(params) {
107     params = params || {};
108     if (params.locale) {
109         this.locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale;
110     }
111     if (params.timezone) {
112         this.timezone = params.timezone;
113     }
114 
115     if (!this.timezone) {
116         if (this.locale) {
117             new LocaleInfo(this.locale, {
118                 sync: params.sync,
119                 loadParams: params.loadParams,
120                 onLoad: ilib.bind(this, function(li) {
121                     this.li = li;
122                     this.timezone = li.getTimeZone();
123                     this._init(params);
124                 })
125             });
126         } else {
127             this.timezone = "local";
128             this._init(params);
129         }
130     } else {
131         this._init(params);
132     }
133 };
134 
135 HanDate.prototype = new IDate({noinstance: true});
136 HanDate.prototype.parent = IDate;
137 HanDate.prototype.constructor = HanDate;
138 
139 /**
140  * Initialize the han date
141  * @private
142  */
143 HanDate.prototype._init = function (params) {
144     new HanCal({
145         sync: params && typeof(params.sync) === 'boolean' ? params.sync : true,
146         loadParams: params && params.loadParams,
147         onLoad: ilib.bind(this, function (cal) {
148             this.cal = cal;
149 
150             if (params.year || params.month || params.day || params.hour ||
151                 params.minute || params.second || params.millisecond || params.cycle || params.cycleYear) {
152                 if (typeof(params.cycle) !== 'undefined') {
153                     /**
154                      * Cycle number in the Han calendar.
155                      * @type number
156                      */
157                     this.cycle = parseInt(params.cycle, 10) || 0;
158 
159                     var year = (typeof(params.year) !== 'undefined' ? parseInt(params.year, 10) : parseInt(params.cycleYear, 10)) || 0;
160 
161                     /**
162                      * Year in the Han calendar.
163                      * @type number
164                      */
165                     this.year = HanCal._getElapsedYear(year, this.cycle);
166                 } else {
167                     if (typeof(params.year) !== 'undefined') {
168                         this.year = parseInt(params.year, 10) || 0;
169                         this.cycle = Math.floor((this.year - 1) / 60);
170                     } else {
171                         this.year = this.cycle = 0;
172                     }
173                 }
174 
175                 /**
176                  * The month number, ranging from 1 to 13
177                  * @type number
178                  */
179                 this.month = parseInt(params.month, 10) || 1;
180 
181                 /**
182                  * The day of the month. This ranges from 1 to 30.
183                  * @type number
184                  */
185                 this.day = parseInt(params.day, 10) || 1;
186 
187                 /**
188                  * The hour of the day. This can be a number from 0 to 23, as times are
189                  * stored unambiguously in the 24-hour clock.
190                  * @type number
191                  */
192                 this.hour = parseInt(params.hour, 10) || 0;
193 
194                 /**
195                  * The minute of the hours. Ranges from 0 to 59.
196                  * @type number
197                  */
198                 this.minute = parseInt(params.minute, 10) || 0;
199 
200                 /**
201                  * The second of the minute. Ranges from 0 to 59.
202                  * @type number
203                  */
204                 this.second = parseInt(params.second, 10) || 0;
205 
206                 /**
207                  * The millisecond of the second. Ranges from 0 to 999.
208                  * @type number
209                  */
210                 this.millisecond = parseInt(params.millisecond, 10) || 0;
211 
212                 // derived properties
213 
214                 /**
215                  * Year in the cycle of the Han calendar
216                  * @type number
217                  */
218                 this.cycleYear = MathUtils.amod(this.year, 60);
219 
220                 /**
221                  * The day of the year. Ranges from 1 to 384.
222                  * @type number
223                  */
224                 this.dayOfYear = parseInt(params.dayOfYear, 10);
225 
226                 if (typeof(params.dst) === 'boolean') {
227                     this.dst = params.dst;
228                 }
229 
230                 this.newRd({
231                     cal: this.cal,
232                     cycle: this.cycle,
233                     year: this.year,
234                     month: this.month,
235                     day: this.day,
236                     hour: this.hour,
237                     minute: this.minute,
238                     second: this.second,
239                     millisecond: this.millisecond,
240                     sync: params.sync,
241                     loadParams: params.loadParams,
242                     callback: ilib.bind(this, function (rd) {
243                         if (rd) {
244                             this.rd = rd;
245 
246                             // add the time zone offset to the rd to convert to UTC
247                             new TimeZone({
248                                 id: this.timezone,
249                                 sync: params.sync,
250                                 loadParams: params.loadParams,
251                                 onLoad: ilib.bind(this, function(tz) {
252                                     this.tz = tz;
253                                     // getOffsetMillis requires that this.year, this.rd, and this.dst
254                                     // are set in order to figure out which time zone rules apply and
255                                     // what the offset is at that point in the year
256                                     this.offset = this.tz._getOffsetMillisWallTime(this) / 86400000;
257                                     if (this.offset !== 0) {
258                                         // this newRd can be called synchronously because we already called
259                                         // it asynchronously above, so all of the astro data should
260                                         // already be loaded.
261                                         this.rd = this.newRd({
262                                             cal: this.cal,
263                                             rd: this.rd.getRataDie() - this.offset
264                                         });
265                                         this._calcLeap();
266                                     } else {
267                                         // re-use the derived properties from the RD calculations
268                                         this.leapMonth = this.rd.leapMonth;
269                                         this.priorLeapMonth = this.rd.priorLeapMonth;
270                                         this.leapYear = this.rd.leapYear;
271                                     }
272 
273                                     this._init2(params);
274                                 })
275                             });
276                         } else {
277                             this._init2(params);
278                         }
279                     })
280                 });
281             } else {
282                 this._init2(params);
283             }
284         })
285     });
286 };
287 
288 /**
289  * Finish the initialization for the han date.
290  * @private
291  */
292 HanDate.prototype._init2 = function (params) {
293     if (!this.rd) {
294         // init2() may be called without newRd having been called before,
295         // so we cannot guarantee that the astro data is already loaded.
296         // That means, we have to treat this as a possibly asynchronous
297         // call.
298         this.newRd(JSUtils.merge(params || {}, {
299             cal: this.cal,
300             sync: params.sync,
301             loadParams: params.loadParams,
302             callback: ilib.bind(this, function(rd) {
303                 this.rd = rd;
304                 this._calcDateComponents();
305 
306                 if (params && typeof(params.onLoad) === 'function') {
307                     params.onLoad(this);
308                 }
309             })
310         }));
311     } else {
312         if (params && typeof(params.onLoad) === 'function') {
313             params.onLoad(this);
314         }
315     }
316 };
317 
318 /**
319  * Return a new RD for this date type using the given params.
320  * @protected
321  * @param {Object=} params the parameters used to create this rata die instance
322  * @returns {RataDie} the new RD instance for the given params
323  */
324 HanDate.prototype.newRd = function (params) {
325     return new HanRataDie(params);
326 };
327 
328 /**
329  * Return the year for the given RD
330  * @protected
331  * @param {number} rd RD to calculate from
332  * @returns {number} the year for the RD
333  */
334 HanDate.prototype._calcYear = function(rd) {
335     var gregdate = new GregorianDate({
336         rd: rd,
337         timezone: this.timezone
338     });
339     var hanyear = gregdate.year + 2697;
340     var newYears = this.cal.newYears(hanyear);
341     return hanyear - ((rd + RataDie.gregorianEpoch < newYears) ? 1 : 0);
342 };
343 
344 /**
345  * @private
346  * Calculate the leap year and months from the RD.
347  */
348 HanDate.prototype._calcLeap = function() {
349     var jd = this.rd.getRataDie() + RataDie.gregorianEpoch;
350 
351     var calc = HanCal._leapYearCalc(this.year);
352     var m2 = HanCal._newMoonOnOrAfter(calc.m1+1);
353     this.leapYear = Math.round((calc.m2 - calc.m1) / 29.530588853000001) === 12;
354 
355     var newYears = (this.leapYear &&
356         (HanCal._noMajorST(calc.m1) || HanCal._noMajorST(m2))) ?
357                 HanCal._newMoonOnOrAfter(m2+1) : m2;
358 
359     var m = HanCal._newMoonBefore(jd + 1);
360     this.priorLeapMonth = HanRataDie._priorLeapMonth(newYears, HanCal._newMoonBefore(m));
361     this.leapMonth = (this.leapYear && HanCal._noMajorST(m) && !this.priorLeapMonth);
362 };
363 
364 /**
365  * @private
366  * Calculate date components for the given RD date.
367  */
368 HanDate.prototype._calcDateComponents = function () {
369     var remainder,
370         jd = this.rd.getRataDie() + RataDie.gregorianEpoch;
371 
372     // console.log("HanDate._calcDateComponents: calculating for jd " + jd);
373 
374     if (typeof(this.offset) === "undefined") {
375         // now offset the jd by the time zone, then recalculate in case we were
376         // near the year boundary
377         if (!this.tz) {
378             this.tz = new TimeZone({id: this.timezone});
379         }
380         this.offset = this.tz.getOffsetMillis(this) / 86400000;
381     }
382 
383     if (this.offset !== 0) {
384         jd += this.offset;
385     }
386 
387     // use the Gregorian calendar objects as a convenient way to short-cut some
388     // of the date calculations
389 
390     var gregyear = GregorianDate._calcYear(this.rd.getRataDie());
391     this.year = gregyear + 2697;
392     var calc = HanCal._leapYearCalc(this.year);
393     var m2 = HanCal._newMoonOnOrAfter(calc.m1+1);
394     this.leapYear = Math.round((calc.m2 - calc.m1) / 29.530588853000001) === 12;
395     var newYears = (this.leapYear &&
396         (HanCal._noMajorST(calc.m1) || HanCal._noMajorST(m2))) ?
397                 HanCal._newMoonOnOrAfter(m2+1) : m2;
398 
399     // See if it's between Jan 1 and the Chinese new years of that Gregorian year. If
400     // so, then the Han year is actually the previous one
401     if (jd < newYears) {
402         this.year--;
403         calc = HanCal._leapYearCalc(this.year);
404         m2 = HanCal._newMoonOnOrAfter(calc.m1+1);
405         this.leapYear = Math.round((calc.m2 - calc.m1) / 29.530588853000001) === 12;
406         newYears = (this.leapYear &&
407             (HanCal._noMajorST(calc.m1) || HanCal._noMajorST(m2))) ?
408                     HanCal._newMoonOnOrAfter(m2+1) : m2;
409     }
410     // month is elapsed month, not the month number + leap month boolean
411     var m = HanCal._newMoonBefore(jd + 1);
412     this.month = Math.round((m - calc.m1) / 29.530588853000001);
413 
414     this.priorLeapMonth = HanRataDie._priorLeapMonth(newYears, HanCal._newMoonBefore(m));
415     this.leapMonth = (this.leapYear && HanCal._noMajorST(m) && !this.priorLeapMonth);
416 
417     this.cycle = Math.floor((this.year - 1) / 60);
418     this.cycleYear = MathUtils.amod(this.year, 60);
419     this.day = Astro._floorToJD(jd) - m + 1;
420 
421     /*
422     console.log("HanDate._calcDateComponents: year is " + this.year);
423     console.log("HanDate._calcDateComponents: isLeapYear is " + this.leapYear);
424     console.log("HanDate._calcDateComponents: cycle is " + this.cycle);
425     console.log("HanDate._calcDateComponents: cycleYear is " + this.cycleYear);
426     console.log("HanDate._calcDateComponents: month is " + this.month);
427     console.log("HanDate._calcDateComponents: isLeapMonth is " + this.leapMonth);
428     console.log("HanDate._calcDateComponents: day is " + this.day);
429     */
430 
431     // floor to the start of the julian day
432     remainder = jd - Astro._floorToJD(jd);
433 
434     // console.log("HanDate._calcDateComponents: time remainder is " + remainder);
435 
436     // now convert to milliseconds for the rest of the calculation
437     remainder = Math.round(remainder * 86400000);
438 
439     this.hour = Math.floor(remainder/3600000);
440     remainder -= this.hour * 3600000;
441 
442     this.minute = Math.floor(remainder/60000);
443     remainder -= this.minute * 60000;
444 
445     this.second = Math.floor(remainder/1000);
446     remainder -= this.second * 1000;
447 
448     this.millisecond = remainder;
449 };
450 
451 /**
452  * Return the year within the Chinese cycle of this date. Cycles are 60
453  * years long, and the value returned from this method is the number of the year
454  * within this cycle. The year returned from getYear() is the total elapsed
455  * years since the beginning of the Chinese epoch and does not include
456  * the cycles.
457  *
458  * @return {number} the year within the current Chinese cycle
459  */
460 HanDate.prototype.getCycleYears = function() {
461     return this.cycleYear;
462 };
463 
464 /**
465  * Return the Chinese cycle number of this date. Cycles are 60 years long,
466  * and the value returned from getCycleYear() is the number of the year
467  * within this cycle. The year returned from getYear() is the total elapsed
468  * years since the beginning of the Chinese epoch and does not include
469  * the cycles.
470  *
471  * @return {number} the current Chinese cycle
472  */
473 HanDate.prototype.getCycles = function() {
474     return this.cycle;
475 };
476 
477 /**
478  * Return whether the year of this date is a leap year in the Chinese Han
479  * calendar.
480  *
481  * @return {boolean} true if the year of this date is a leap year in the
482  * Chinese Han calendar.
483  */
484 HanDate.prototype.isLeapYear = function() {
485     return this.leapYear;
486 };
487 
488 /**
489  * Return whether the month of this date is a leap month in the Chinese Han
490  * calendar.
491  *
492  * @return {boolean} true if the month of this date is a leap month in the
493  * Chinese Han calendar.
494  */
495 HanDate.prototype.isLeapMonth = function() {
496     return this.leapMonth;
497 };
498 
499 /**
500  * Return the day of the week of this date. The day of the week is encoded
501  * as number from 0 to 6, with 0=Sunday, 1=Monday, etc., until 6=Saturday.
502  *
503  * @return {number} the day of the week
504  */
505 HanDate.prototype.getDayOfWeek = function() {
506     var rd = Math.floor(this.rd.getRataDie() + (this.offset || 0));
507     return MathUtils.mod(rd, 7);
508 };
509 
510 /**
511  * Return the ordinal day of the year. Days are counted from 1 and proceed linearly up to
512  * 365, regardless of months or weeks, etc. That is, Farvardin 1st is day 1, and
513  * December 31st is 365 in regular years, or 366 in leap years.
514  * @return {number} the ordinal day of the year
515  */
516 HanDate.prototype.getDayOfYear = function() {
517     var newYears = this.cal.newYears(this.year);
518     var priorNewMoon = HanCal._newMoonOnOrAfter(newYears + (this.month -1) * 29);
519     return priorNewMoon - newYears + this.day;
520 };
521 
522 /**
523  * Return the era for this date as a number. The value for the era for Han
524  * calendars is -1 for "before the han era" (BP) and 1 for "the han era" (anno
525  * persico or AP).
526  * BP dates are any date before Farvardin 1, 1 AP. In the proleptic Han calendar,
527  * there is a year 0, so any years that are negative or zero are BP.
528  * @return {number} 1 if this date is in the common era, -1 if it is before the
529  * common era
530  */
531 HanDate.prototype.getEra = function() {
532     return (this.year < 1) ? -1 : 1;
533 };
534 
535 /**
536  * Return the name of the calendar that governs this date.
537  *
538  * @return {string} a string giving the name of the calendar
539  */
540 HanDate.prototype.getCalendar = function() {
541     return "han";
542 };
543 
544 // register with the factory method
545 IDate._constructors["han"] = HanDate;
546 
547 module.exports = HanDate;