1 /*
  2  * EthiopicDate.js - Represent a date in the Ethiopic calendar
  3  *
  4  * Copyright © 2015, 2018, 2021 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 MathUtils = require("./MathUtils.js");
 22 
 23 var EthiopicRataDie = require("./EthiopicRataDie.js");
 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 var EthiopicCal = require("./EthiopicCal.js");
 29 
 30 /**
 31  * @class
 32  * Construct a new date object for the Ethiopic Calendar. The constructor can be called
 33  * with a parameter object that contains any of the following properties:
 34  *
 35  * <ul>
 36  * <li><i>unixtime<i> - sets the time of this instance according to the given
 37  * unix time. Unix time is the number of milliseconds since midnight on Jan 1, 1970 (Gregorian).
 38  * <li><i>julianday</i> - the Julian Day to set into this date
 39  * <li><i>year</i> - any integer
 40  * <li><i>month</i> - 1 to 13, where 1 means Maskaram, 2 means Teqemt, etc., and 13 means Paguemen
 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 ethiopic 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 Ethiopic 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>unixtime</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 Ethiopic date
 77  */
 78 var EthiopicDate = function(params) {
 79     this.cal = new EthiopicCal();
 80 
 81     params = params || {};
 82 
 83     if (typeof(params.noinstance) === 'boolean' && params.noinstance) {
 84         // for doing inheritance, so don't need to fill in the data. The inheriting class only wants the methods.
 85         return;
 86     }
 87     if (params.timezone) {
 88         this.timezone = params.timezone;
 89     }
 90     if (params.locale) {
 91         this.locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale;
 92     }
 93 
 94     if (!this.timezone) {
 95         if (this.locale) {
 96             new LocaleInfo(this.locale, {
 97                 sync: params.sync,
 98                 loadParams: params.loadParams,
 99                 onLoad: ilib.bind(this, function(li) {
100                     this.li = li;
101                     this.timezone = li.getTimeZone();
102                     this._init(params);
103                 })
104             });
105         } else {
106             this.timezone = "local";
107             this._init(params);
108         }
109     } else {
110         this._init(params);
111     }
112 };
113 
114 EthiopicDate.prototype = new IDate({ noinstance: true });
115 EthiopicDate.prototype.parent = IDate;
116 EthiopicDate.prototype.constructor = EthiopicDate;
117 
118 /**
119  * Initialize this instance
120  * @private
121  */
122 EthiopicDate.prototype._init = function (params) {
123     new TimeZone({
124         id: this.timezone,
125         sync: params.sync,
126         loadParams: params.loadParams,
127         onLoad: ilib.bind(this, function(tz) {
128             this.tz = tz;
129 
130             if (params.year || params.month || params.day || params.hour ||
131                 params.minute || params.second || params.millisecond ) {
132                 /**
133                  * Year in the Ethiopic calendar.
134                  * @type number
135                  */
136                 this.year = parseInt(params.year, 10) || 0;
137                 /**
138                  * The month number, ranging from 1 (Maskaram) to 13 (Paguemen).
139                  * @type number
140                  */
141                 this.month = parseInt(params.month, 10) || 1;
142                 /**
143                  * The day of the month. This ranges from 1 to 30.
144                  * @type number
145                  */
146                 this.day = parseInt(params.day, 10) || 1;
147                 /**
148                  * The hour of the day. This can be a number from 0 to 23, as times are
149                  * stored unambiguously in the 24-hour clock.
150                  * @type number
151                  */
152                 this.hour = parseInt(params.hour, 10) || 0;
153                 /**
154                  * The minute of the hours. Ranges from 0 to 59.
155                  * @type number
156                  */
157                 this.minute = parseInt(params.minute, 10) || 0;
158                 /**
159                  * The second of the minute. Ranges from 0 to 59.
160                  * @type number
161                  */
162                 this.second = parseInt(params.second, 10) || 0;
163                 /**
164                  * The millisecond of the second. Ranges from 0 to 999.
165                  * @type number
166                  */
167                 this.millisecond = parseInt(params.millisecond, 10) || 0;
168 
169                 /**
170                  * The day of the year. Ranges from 1 to 366.
171                  * @type number
172                  */
173                 this.dayOfYear = parseInt(params.dayOfYear, 10);
174 
175                 if (typeof(params.dst) === 'boolean') {
176                     this.dst = params.dst;
177                 }
178 
179                 this.rd = this.newRd(this);
180 
181                 // add the time zone offset to the rd to convert to UTC
182                 // getOffsetMillis requires that this.year, this.rd, and this.dst
183                 // are set in order to figure out which time zone rules apply and
184                 // what the offset is at that point in the year
185                 this.offset = this.tz._getOffsetMillisWallTime(this) / 86400000;
186                 if (this.offset !== 0) {
187                     this.rd = this.newRd({
188                         rd: this.rd.getRataDie() - this.offset
189                     });
190                 }
191             }
192 
193             if (!this.rd) {
194                 this.rd = this.newRd(params);
195                 this._calcDateComponents();
196             }
197 
198             if (typeof(params.onLoad) === "function") {
199                 params.onLoad(this);
200             }
201         })
202     });
203 };
204 
205 /**
206  * Return a new RD for this date type using the given params.
207  * @protected
208  * @param {Object=} params the parameters used to create this rata die instance
209  * @returns {RataDie} the new RD instance for the given params
210  */
211 EthiopicDate.prototype.newRd = function (params) {
212     return new EthiopicRataDie(params);
213 };
214 
215 /**
216  * Return the year for the given RD
217  * @protected
218  * @param {number} rd RD to calculate from
219  * @returns {number} the year for the RD
220  */
221 EthiopicDate.prototype._calcYear = function(rd) {
222     var year = Math.floor((4*(Math.floor(rd)-1) + 1463)/1461);
223 
224     return year;
225 };
226 
227 /**
228  * Calculate date components for the given RD date.
229  * @protected
230  */
231 EthiopicDate.prototype._calcDateComponents = function () {
232     var remainder,
233         rd = this.rd.getRataDie();
234 
235     this.year = this._calcYear(rd);
236 
237     if (typeof(this.offset) === "undefined") {
238         this.year = this._calcYear(rd);
239 
240         // now offset the RD by the time zone, then recalculate in case we were
241         // near the year boundary
242         if (!this.tz) {
243             this.tz = new TimeZone({id: this.timezone});
244         }
245         this.offset = this.tz.getOffsetMillis(this) / 86400000;
246     }
247 
248     if (this.offset !== 0) {
249         rd += this.offset;
250         this.year = this._calcYear(rd);
251     }
252 
253     var jan1 = this.newRd({
254         year: this.year,
255         month: 1,
256         day: 1,
257         hour: 0,
258         minute: 0,
259         second: 0,
260         millisecond: 0
261     });
262     remainder = rd + 1 - jan1.getRataDie();
263 
264     this.month = Math.floor((remainder-1)/30) + 1;
265     remainder = remainder - (this.month-1) * 30;
266 
267     this.day = Math.floor(remainder);
268     remainder -= this.day;
269 
270     // now convert to milliseconds for the rest of the calculation
271     remainder = Math.round(remainder * 86400000);
272 
273     this.hour = Math.floor(remainder/3600000);
274     remainder -= this.hour * 3600000;
275 
276     this.minute = Math.floor(remainder/60000);
277     remainder -= this.minute * 60000;
278 
279     this.second = Math.floor(remainder/1000);
280     remainder -= this.second * 1000;
281 
282     this.millisecond = remainder;
283 };
284 
285 /**
286  * Return the day of the week of this date. The day of the week is encoded
287  * as number from 0 to 6, with 0=Sunday, 1=Monday, etc., until 6=Saturday.
288  *
289  * @return {number} the day of the week
290  */
291 EthiopicDate.prototype.getDayOfWeek = function() {
292     var rd = Math.floor(this.rd.getRataDie() + (this.offset || 0));
293     return MathUtils.mod(rd-5, 7);
294 };
295 
296 /**
297  * Return the name of the calendar that governs this date.
298  *
299  * @return {string} a string giving the name of the calendar
300  */
301 EthiopicDate.prototype.getCalendar = function() {
302     return "ethiopic";
303 };
304 
305 //register with the factory method
306 IDate._constructors["ethiopic"] = EthiopicDate;
307 
308 module.exports = EthiopicDate;
309