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("./ilib.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;