1 /*
  2  * DateFactory.js - Factory class to create the right subclasses of a date for any
  3  * calendar or locale.
  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 ilib = require("./ilib.js");
 22 var JSUtils = require("./JSUtils.js");
 23 
 24 var Locale = require("./Locale.js");
 25 var LocaleInfo = require("./LocaleInfo.js");
 26 var JulianDay = require("./JulianDay.js");
 27 var CalendarFactory = require("./CalendarFactory.js");
 28 
 29 // Statically depend on these even though we don't use them
 30 // to guarantee they are loaded into the cache already.
 31 var IDate = require("./IDate.js");
 32 
 33 /**
 34  * Factory method to create a new instance of a date subclass.<p>
 35  *
 36  * The options parameter can be an object that contains the following
 37  * properties:
 38  *
 39  * <ul>
 40  * <li><i>type</i> - specify the type/calendar of the date desired. The
 41  * list of valid values changes depending on which calendars are
 42  * defined. When assembling your iliball.js, include those date type
 43  * you wish to use in your program or web page, and they will register
 44  * themselves with this factory method. The "gregorian",
 45  * and "julian" calendars are all included by default, as they are the
 46  * standard calendars for much of the world. If not specified, the type
 47  * of the date returned is the one that is appropriate for the locale.
 48  * This property may also be given as "calendar" instead of "type".
 49  *
 50  * <li><i>onLoad</i> - a callback function to call when the date object is fully
 51  * loaded. When the onLoad option is given, the date factory will attempt to
 52  * load any missing locale data using the ilib loader callback.
 53  * When the constructor is done (even if the data is already preassembled), the
 54  * onLoad function is called with the current instance as a parameter, so this
 55  * callback can be used with preassembled or dynamic loading or a mix of the two.
 56  *
 57  * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
 58  * asynchronously. If this option is given as "false", then the "onLoad"
 59  * callback must be given, as the instance returned from this constructor will
 60  * not be usable for a while.
 61  *
 62  * <li><i>loadParams</i> - an object containing parameters to pass to the
 63  * loader callback function when locale data is missing. The parameters are not
 64  * interpretted or modified in any way. They are simply passed along. The object
 65  * may contain any property/value pairs as long as the calling code is in
 66  * agreement with the loader callback function as to what those parameters mean.
 67  * </ul>
 68  *
 69  * The options object is also passed down to the date constructor, and
 70  * thus can contain the the properties as the date object being instantiated.
 71  * See the documentation for {@link GregorianDate}, and other
 72  * subclasses for more details on other parameter that may be passed in.<p>
 73  *
 74  * Please note that if you do not give the type parameter, this factory
 75  * method will create a date object that is appropriate for the calendar
 76  * that is most commonly used in the specified or current ilib locale.
 77  * For example, in Thailand, the most common calendar is the Thai solar
 78  * calendar. If the current locale is "th-TH" (Thai for Thailand) and you
 79  * use this factory method to construct a new date without specifying the
 80  * type, it will automatically give you back an instance of
 81  * {@link ThaiSolarDate}. This is convenient because you do not
 82  * need to know which locales use which types of dates. In fact, you
 83  * should always use this factory method to make new date instances unless
 84  * you know that you specifically need a date in a particular calendar.<p>
 85  *
 86  * Also note that when you pass in the date components such as year, month,
 87  * day, etc., these components should be appropriate for the given date
 88  * being instantiated. That is, in our Thai example in the previous
 89  * paragraph, the year and such should be given as a Thai solar year, not
 90  * the Gregorian year that you get from the Javascript Date class. In
 91  * order to initialize a date instance when you don't know what subclass
 92  * will be instantiated for the locale, use a parameter such as "unixtime"
 93  * or "julianday" which are unambiguous and based on UTC time, instead of
 94  * the year/month/date date components. The date components for that UTC
 95  * time will be calculated and the time zone offset will be automatically
 96  * factored in.
 97  *
 98  * @static
 99  * @param {Object=} options options controlling the construction of this instance, or
100  * undefined to use the default options
101  * @return {IDate} an instance of a calendar object of the appropriate type
102  */
103 var DateFactory = function(options) {
104     var locale,
105         type,
106         sync = true,
107         obj;
108 
109     if (options) {
110         if (options.locale) {
111             locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
112         }
113 
114         type = options.type || options.calendar;
115 
116         if (typeof(options.sync) === 'boolean') {
117             sync = options.sync;
118         }
119     }
120 
121     if (!locale) {
122         locale = new Locale();    // default locale
123     }
124 
125     if (!type) {
126         new LocaleInfo(locale, {
127             sync: sync,
128             loadParams: options && options.loadParams,
129             onLoad: function(info) {
130                 type = info.getCalendar();
131 
132                 obj = DateFactory._init(type, options);
133             }
134         });
135     } else {
136         obj = DateFactory._init(type, options);
137     }
138 
139     return obj
140 };
141 
142 /**
143  * Map calendar names to classes to initialize in the dynamic code model.
144  * TODO: Need to figure out some way that this doesn't have to be updated by hand.
145  * @private
146  */
147 DateFactory._dynMap = {
148     "coptic":       "Coptic",
149     "ethiopic":     "Ethiopic",
150     "gregorian":    "Gregorian",
151     "han":          "Han",
152     "hebrew":       "Hebrew",
153     "islamic":      "Islamic",
154     "julian":       "Julian",
155     "persian":      "Persian",
156     "persian-algo": "PersianAlgo",
157     "thaisolar":    "ThaiSolar"
158 };
159 
160 function circumventWebPackDate(x) {
161     return "./" + x + "Date.js";
162 }
163 
164 function circumventWebPackCal(x) {
165     return "./" + x + "Cal.js";
166 }
167 
168 /**
169  * Dynamically load the code for a calendar and calendar class if necessary.
170  * @protected
171  */
172 DateFactory._dynLoadDate = function (name, fnc) {
173     if (!IDate._constructors[name]) {
174         var entry = DateFactory._dynMap[name];
175         if (entry) {
176             IDate._constructors[name] = require(fnc(entry));
177         }
178     }
179     return IDate._constructors[name];
180 };
181 
182 /**
183  * @protected
184  * @static
185  */
186 DateFactory._init = function(type, options) {
187     var cons;
188 
189     if (ilib.isDynCode()) {
190         DateFactory._dynLoadDate(type, circumventWebPackDate);
191         CalendarFactory._dynLoadCalendar(type, circumventWebPackCal);
192     }
193 
194     cons = IDate._constructors[type];
195 
196     // pass the same options through to the constructor so the subclass
197     // has the ability to do something with if it needs to
198     if (!cons && typeof(options.onLoad) === "function") {
199         options.onLoad(undefined);
200     }
201     return cons && new cons(options);
202 };
203 
204 /**
205  * Convert JavaScript Date objects and other types into native Dates. This accepts any
206  * string or number that can be translated by the JavaScript Date class,
207  * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse)
208  * any JavaScript Date classed object, any IDate subclass, an JulianDay object, an object
209  * containing the normal options to initialize an IDate instance, or null (will
210  * return null or undefined if input is null or undefined). Normal output is
211  * a standard native subclass of the IDate object as appropriate for the locale.
212  *
213  * @static
214  * @protected
215  * @param {IDate|Object|JulianDay|Date|string|number=} inDate The input date object, string or Number.
216  * @param {IString|string=} timezone timezone to use if a new date object is created
217  * @param {Locale|string=} locale locale to use when constructing an IDate
218  * @return {IDate|null|undefined} an IDate subclass equivalent to the given inDate
219  */
220 DateFactory._dateToIlib = function(inDate, timezone, locale) {
221     if (typeof(inDate) === 'undefined' || inDate === null) {
222         return inDate;
223     }
224     if (inDate instanceof IDate) {
225         return inDate;
226     }
227     if (typeof(inDate) === 'number') {
228         return DateFactory({
229             unixtime: inDate,
230             timezone: timezone,
231             locale: locale
232         });
233     }
234     if (typeof(inDate) === 'string') {
235         inDate = new Date(inDate);
236     }
237     if (JSUtils.isDate(inDate)) {
238         return DateFactory({
239             unixtime: inDate.getTime(),
240             timezone: timezone,
241             locale: locale
242         });
243     }
244     if (inDate instanceof JulianDay) {
245         return DateFactory({
246             jd: inDate,
247             timezone: timezone,
248             locale: locale
249         });
250     }
251     if (typeof(inDate) === 'object') {
252         return DateFactory(inDate);
253     }
254     return DateFactory({
255         unixtime: inDate.getTime(),
256         timezone: timezone,
257         locale: locale
258     });
259 };
260 
261 module.exports = DateFactory;
262