1 /*
  2  * IDate.js - Represent a date in any calendar. This class is subclassed for each
  3  * calendar and includes some shared functionality.
  4  *
  5  * Copyright © 2012-2015, 2018, JEDLSoft
  6  *
  7  * Licensed under the Apache License, Version 2.0 (the "License");
  8  * you may not use this file except in compliance with the License.
  9  * You may obtain a copy of the License at
 10  *
 11  *     http://www.apache.org/licenses/LICENSE-2.0
 12  *
 13  * Unless required by applicable law or agreed to in writing, software
 14  * distributed under the License is distributed on an "AS IS" BASIS,
 15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 16  *
 17  * See the License for the specific language governing permissions and
 18  * limitations under the License.
 19  */
 20 
 21 var LocaleInfo = require("./LocaleInfo.js");
 22 
 23 /**
 24  * @class
 25  * Superclass for all the calendar date classes that contains shared
 26  * functionality. This class is never instantiated on its own. Instead,
 27  * you should use the {@link DateFactory} function to manufacture a new
 28  * instance of a subclass of IDate. This class is called IDate for "ilib
 29  * date" so that it does not conflict with the built-in Javascript Date
 30  * class.
 31  *
 32  * @private
 33  * @constructor
 34  * @param {Object=} options The date components to initialize this date with
 35  */
 36 var IDate = function(options) {
 37 };
 38 
 39 /* place for the subclasses to put their constructors so that the factory method
 40  * can find them. Do this to add your date after it's defined:
 41  * IDate._constructors["mytype"] = IDate.MyTypeConstructor;
 42  */
 43 IDate._constructors = {};
 44 
 45 IDate.prototype = {
 46     getType: function() {
 47         return "date";
 48     },
 49 
 50     /**
 51      * Return the unix time equivalent to this date instance. Unix time is
 52      * the number of milliseconds since midnight on Jan 1, 1970 UTC (Gregorian). This
 53      * method only returns a valid number for dates between midnight,
 54      * Jan 1, 1970 UTC (Gregorian) and Jan 19, 2038 at 3:14:07am UTC (Gregorian) when
 55      * the unix time runs out. If this instance encodes a date outside of that range,
 56      * this method will return -1. For date types that are not Gregorian, the point
 57      * in time represented by this date object will only give a return value if it
 58      * is in the correct range in the Gregorian calendar as given previously.
 59      *
 60      * @return {number} a number giving the unix time, or -1 if the date is outside the
 61      * valid unix time range
 62      */
 63     getTime: function() {
 64         return this.rd.getTime();
 65     },
 66 
 67     /**
 68      * Return the extended unix time equivalent to this Gregorian date instance. Unix time is
 69      * the number of milliseconds since midnight on Jan 1, 1970 UTC. Traditionally unix time
 70      * (or the type "time_t" in C/C++) is only encoded with an unsigned 32 bit integer, and thus
 71      * runs out on Jan 19, 2038. However, most Javascript engines encode numbers well above
 72      * 32 bits and the Date object allows you to encode up to 100 million days worth of time
 73      * after Jan 1, 1970, and even more interestingly, 100 million days worth of time before
 74      * Jan 1, 1970 as well. This method returns the number of milliseconds in that extended
 75      * range. If this instance encodes a date outside of that range, this method will return
 76      * NaN.
 77      *
 78      * @return {number} a number giving the extended unix time, or Nan if the date is outside
 79      * the valid extended unix time range
 80      */
 81     getTimeExtended: function() {
 82         return this.rd.getTimeExtended();
 83     },
 84 
 85     /**
 86      * Set the time of this instance according to the given unix time. Unix time is
 87      * the number of milliseconds since midnight on Jan 1, 1970.
 88      *
 89      * @param {number} millis the unix time to set this date to in milliseconds
 90      */
 91     setTime: function(millis) {
 92         this.rd = this.newRd({
 93             unixtime: millis,
 94             cal: this.cal
 95         });
 96         this._calcDateComponents();
 97     },
 98 
 99     getDays: function() {
100         return this.day;
101     },
102     getMonths: function() {
103         return this.month;
104     },
105     getYears: function() {
106         return this.year;
107     },
108     getHours: function() {
109         return this.hour;
110     },
111     getMinutes: function() {
112         return this.minute;
113     },
114     getSeconds: function() {
115         return this.second;
116     },
117     getMilliseconds: function() {
118         return this.millisecond;
119     },
120     getEra: function() {
121         return (this.year < 1) ? -1 : 1;
122     },
123 
124     setDays: function(day) {
125         this.day = parseInt(day, 10) || 1;
126         this.rd._setDateComponents(this);
127     },
128     setMonths: function(month) {
129         this.month = parseInt(month, 10) || 1;
130         this.rd._setDateComponents(this);
131     },
132     setYears: function(year) {
133         this.year = parseInt(year, 10) || 0;
134         this.rd._setDateComponents(this);
135     },
136 
137     setHours: function(hour) {
138         this.hour = parseInt(hour, 10) || 0;
139         this.rd._setDateComponents(this);
140     },
141     setMinutes: function(minute) {
142         this.minute = parseInt(minute, 10) || 0;
143         this.rd._setDateComponents(this);
144     },
145     setSeconds: function(second) {
146         this.second = parseInt(second, 10) || 0;
147         this.rd._setDateComponents(this);
148     },
149     setMilliseconds: function(milli) {
150         this.millisecond = parseInt(milli, 10) || 0;
151         this.rd._setDateComponents(this);
152     },
153 
154     /**
155      * Return a new date instance in the current calendar that represents the first instance
156      * of the given day of the week before the current date. The day of the week is encoded
157      * as a number where 0 = Sunday, 1 = Monday, etc.
158      *
159      * @param {number} dow the day of the week before the current date that is being sought
160      * @return {IDate} the date being sought
161      */
162     before: function (dow) {
163         return new this.constructor({
164             rd: this.rd.before(dow, this.offset),
165             timezone: this.timezone
166         });
167     },
168 
169     /**
170      * Return a new date instance in the current calendar that represents the first instance
171      * of the given day of the week after the current date. The day of the week is encoded
172      * as a number where 0 = Sunday, 1 = Monday, etc.
173      *
174      * @param {number} dow the day of the week after the current date that is being sought
175      * @return {IDate} the date being sought
176      */
177     after: function (dow) {
178         return new this.constructor({
179             rd: this.rd.after(dow, this.offset),
180             timezone: this.timezone
181         });
182     },
183 
184     /**
185      * Return a new Gregorian date instance that represents the first instance of the
186      * given day of the week on or before the current date. The day of the week is encoded
187      * as a number where 0 = Sunday, 1 = Monday, etc.
188      *
189      * @param {number} dow the day of the week on or before the current date that is being sought
190      * @return {IDate} the date being sought
191      */
192     onOrBefore: function (dow) {
193         return new this.constructor({
194             rd: this.rd.onOrBefore(dow, this.offset),
195             timezone: this.timezone
196         });
197     },
198 
199     /**
200      * Return a new Gregorian date instance that represents the first instance of the
201      * given day of the week on or after the current date. The day of the week is encoded
202      * as a number where 0 = Sunday, 1 = Monday, etc.
203      *
204      * @param {number} dow the day of the week on or after the current date that is being sought
205      * @return {IDate} the date being sought
206      */
207     onOrAfter: function (dow) {
208         return new this.constructor({
209             rd: this.rd.onOrAfter(dow, this.offset),
210             timezone: this.timezone
211         });
212     },
213 
214     /**
215      * Return a Javascript Date object that is equivalent to this date
216      * object.
217      *
218      * @return {Date|undefined} a javascript Date object
219      */
220     getJSDate: function() {
221         var unix = this.rd.getTimeExtended();
222         return isNaN(unix) ? undefined : new Date(unix);
223     },
224 
225     /**
226      * Return the Rata Die (fixed day) number of this date.
227      *
228      * @protected
229      * @return {number} the rd date as a number
230      */
231     getRataDie: function() {
232         return this.rd.getRataDie();
233     },
234 
235     /**
236      * Set the date components of this instance based on the given rd.
237      * @protected
238      * @param {number} rd the rata die date to set
239      */
240     setRd: function (rd) {
241         this.rd = this.newRd({
242             rd: rd,
243             cal: this.cal
244         });
245         this._calcDateComponents();
246     },
247 
248     /**
249      * Return the Julian Day equivalent to this calendar date as a number.
250      *
251      * @return {number} the julian date equivalent of this date
252      */
253     getJulianDay: function() {
254         return this.rd.getJulianDay();
255     },
256 
257     /**
258      * Set the date of this instance using a Julian Day.
259      * @param {number|JulianDay} date the Julian Day to use to set this date
260      */
261     setJulianDay: function (date) {
262         this.rd = this.newRd({
263             julianday: (typeof(date) === 'object') ? date.getDate() : date,
264             cal: this.cal
265         });
266         this._calcDateComponents();
267     },
268 
269     /**
270      * Return the time zone associated with this date, or
271      * undefined if none was specified in the constructor.
272      *
273      * @return {string|undefined} the name of the time zone for this date instance
274      */
275     getTimeZone: function() {
276         return this.timezone || "local";
277     },
278 
279     /**
280      * Set the time zone associated with this date.
281      * @param {string=} tzName the name of the time zone to set into this date instance,
282      * or "undefined" to unset the time zone
283      */
284     setTimeZone: function (tzName) {
285         if (!tzName || tzName === "") {
286             // same as undefining it
287             this.timezone = undefined;
288             this.tz = undefined;
289         } else if (typeof(tzName) === 'string') {
290             this.timezone = tzName;
291             this.tz = undefined;
292             // assuming the same UTC time, but a new time zone, now we have to
293             // recalculate what the date components are
294             this._calcDateComponents();
295         }
296     },
297 
298     /**
299      * Return the rd number of the first Sunday of the given ISO year.
300      * @protected
301      * @param {number} year the year for which the first Sunday is being sought
302      * @return {number} the rd of the first Sunday of the ISO year
303      */
304     firstSunday: function (year) {
305         var firstDay = this.newRd({
306             year: year,
307             month: 1,
308             day: 1,
309             hour: 0,
310             minute: 0,
311             second: 0,
312             millisecond: 0,
313             cal: this.cal
314         });
315         var firstThu = this.newRd({
316             rd: firstDay.onOrAfter(4),
317             cal: this.cal
318         });
319         return firstThu.before(0);
320     },
321 
322     /**
323      * Return the ISO 8601 week number in the current year for the current date. The week
324      * number ranges from 0 to 55, as some years have 55 weeks assigned to them in some
325      * calendars.
326      *
327      * @return {number} the week number for the current date
328      */
329     getWeekOfYear: function() {
330         var rd = Math.floor(this.rd.getRataDie());
331         var year = this._calcYear(rd + this.offset);
332         var yearStart = this.firstSunday(year);
333         var nextYear;
334 
335         // if we have a January date, it may be in this ISO year or the previous year
336         if (rd < yearStart) {
337             yearStart = this.firstSunday(year-1);
338         } else {
339             // if we have a late December date, it may be in this ISO year, or the next year
340             nextYear = this.firstSunday(year+1);
341             if (rd >= nextYear) {
342                 yearStart = nextYear;
343             }
344         }
345 
346         return Math.floor((rd-yearStart)/7) + 1;
347     },
348 
349     /**
350      * Return the ordinal number of the week within the month. The first week of a month is
351      * the first one that contains 4 or more days in that month. If any days precede this
352      * first week, they are marked as being in week 0. This function returns values from 0
353      * through 6.<p>
354      *
355      * The locale is a required parameter because different locales that use the same
356      * Gregorian calendar consider different days of the week to be the beginning of
357      * the week. This can affect the week of the month in which some days are located.
358      *
359      * @param {Locale|string} locale the locale or locale spec to use when figuring out
360      * the first day of the week
361      * @return {number} the ordinal number of the week within the current month
362      */
363     getWeekOfMonth: function(locale) {
364         var li = new LocaleInfo(locale);
365 
366         var first = this.newRd({
367             year: this._calcYear(this.rd.getRataDie()+this.offset),
368             month: this.getMonths(),
369             day: 1,
370             hour: 0,
371             minute: 0,
372             second: 0,
373             millisecond: 0,
374             cal: this.cal
375         });
376         var weekStart = first.onOrAfter(li.getFirstDayOfWeek());
377 
378         if (weekStart - first.getRataDie() > 3) {
379             // if the first week has 4 or more days in it of the current month, then consider
380             // that week 1. Otherwise, it is week 0. To make it week 1, move the week start
381             // one week earlier.
382             weekStart -= 7;
383         }
384         return Math.floor((this.rd.getRataDie() - weekStart) / 7) + 1;
385     }
386 };
387 
388 module.exports = IDate;