1 /**
2 Bindings for the script Date class. This module contains both the D class definition and the script bindings.
3 See https://pillager86.github.io/dmildew/Date.html for more details.
4 
5 ────────────────────────────────────────────────────────────────────────────────
6 
7 Copyright (C) 2021 pillager86.rf.gd
8 
9 This program is free software: you can redistribute it and/or modify it under 
10 the terms of the GNU General Public License as published by the Free Software 
11 Foundation, either version 3 of the License, or (at your option) any later 
12 version.
13 
14 This program is distributed in the hope that it will be useful, but WITHOUT ANY
15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
16 PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17 
18 You should have received a copy of the GNU General Public License along with 
19 this program.  If not, see <https://www.gnu.org/licenses/>.
20 */
21 module mildew.stdlib.date;
22 
23 import core.time: TimeException;
24 import std.datetime.systime;
25 import std.datetime.date;
26 import std.datetime.timezone;
27 
28 import mildew.binder;
29 import mildew.environment;
30 import mildew.interpreter;
31 import mildew.types;
32 
33 /** 
34  * Initializes the Date library, which includes the Date() constructor. Documentation for the
35  * library can be found at https://pillager86.github.io/dmildew/Date.html
36  * Params:
37  *  interpreter = The Interpreter object for which to load the library.
38  */
39 void initializeDateLibrary(Interpreter interpreter)
40 {
41     auto Date_ctor = new ScriptFunction("Date", &native_Date_ctor, true);
42     Date_ctor["now"] = new ScriptFunction("Date.now", &native_Date_s_now);
43     // TODO separate the prototype so that Dates can be constructed with Date(). This will become the
44     // sole use case as to why thisObj is passed by pointer rather than value.
45     Date_ctor["prototype"]["getDate"] = new ScriptFunction("Date.prototype.getDate", &native_Date_getDate);
46     Date_ctor["prototype"]["getDay"] = new ScriptFunction("Date.prototype.getDay", &native_Date_getDay);
47     Date_ctor["prototype"]["getFullYear"] = new ScriptFunction("Date.prototype.getFullYear", 
48             &native_Date_getFullYear);
49     Date_ctor["prototype"]["getHours"] = new ScriptFunction("Date.prototype.getHours", 
50             &native_Date_getHours);
51     Date_ctor["prototype"]["getMilliseconds"] = new ScriptFunction("Date.prototype.getMilliseconds", 
52             &native_Date_getMilliseconds);
53     Date_ctor["prototype"]["getMinutes"] = new ScriptFunction("Date.prototype.getMinutes", 
54             &native_Date_getMinutes);
55     Date_ctor["prototype"]["getMonth"] = new ScriptFunction("Date.prototype.getMonth", 
56             &native_Date_getMonth);
57     Date_ctor["prototype"]["getSeconds"] = new ScriptFunction("Date.prototype.getSeconds", 
58             &native_Date_getSeconds);
59     Date_ctor["prototype"]["getTime"] = new ScriptFunction("Date.prototype.getTime", 
60             &native_Date_getTime);
61     Date_ctor["prototype"]["getTimezone"] = new ScriptFunction("Date.prototype.getTimezone", 
62             &native_Date_getTimezone);
63     Date_ctor["prototype"]["setDate"] = new ScriptFunction("Date.prototype.setDate", 
64             &native_Date_setDate);
65     Date_ctor["prototype"]["setFullYear"] = new ScriptFunction("Date.prototype.setFullYear", 
66             &native_Date_setFullYear);
67     Date_ctor["prototype"]["setHours"] = new ScriptFunction("Date.prototype.setHours", 
68             &native_Date_setHours);
69     Date_ctor["prototype"]["setMilliseconds"] = new ScriptFunction("Date.prototype.setMillseconds", 
70             &native_Date_setMilliseconds);
71     Date_ctor["prototype"]["setMinutes"] = new ScriptFunction("Date.prototype.setMinutes", 
72             &native_Date_setMinutes);
73     Date_ctor["prototype"]["setMonth"] = new ScriptFunction("Date.prototype.setMonth", 
74             &native_Date_setMonth);
75     Date_ctor["prototype"]["setSeconds"] = new ScriptFunction("Date.prototype.setSeconds", 
76             &native_Date_setSeconds);
77     Date_ctor["prototype"]["setTime"] = new ScriptFunction("Date.prototype.setTime", 
78             &native_Date_setTime);
79     Date_ctor["prototype"]["toDateString"] = new ScriptFunction("Date.prototype.toDateString", 
80             &native_Date_toDateString);
81     Date_ctor["prototype"]["toISOString"] = new ScriptFunction("Date.prototype.toISOString", 
82             &native_Date_toISOString);
83     Date_ctor["prototype"]["toUTC"] = new ScriptFunction("Date.prototype.toUTC", 
84             &native_Date_toUTC);
85     interpreter.forceSetGlobal("Date", Date_ctor, false);
86 }
87 
88 private:
89 
90 /**
91  * The Date class
92  */
93 class ScriptDate
94 {
95 public:
96     /**
97      * Creates a Date representing the time and date of object creation
98      */ 
99     this()
100     {
101         _sysTime = Clock.currTime();
102     }
103 
104     this(in long num)
105     {
106         _sysTime = SysTime.fromUnixTime(num);
107     }
108 
109     /// takes month 0-11 like JavaScript
110     this(in int year, in int monthIndex, in int day=1, in int hours=0, in int minutes=0, 
111          in int seconds=0, in int milliseconds=0)
112     {
113         import core.time: msecs;
114         auto dt = DateTime(year, monthIndex+1, day, hours, minutes, seconds);
115         _sysTime = SysTime(dt, msecs(milliseconds), UTC());
116     }
117 
118     /// This string has to be formatted as "2020-Jan-01 00:00:00" for example. Anything different throws an exception
119     this(in string str)
120     {
121         try 
122         {
123             auto dt = DateTime.fromSimpleString(str);
124             _sysTime = SysTime(dt, UTC());
125         }
126         catch(DateTimeException)
127         {
128             auto dt = DateTime.fromISOString(str);
129             _sysTime = SysTime(dt, UTC());
130         }
131     }
132 
133     static long now() 
134     {
135         immutable start = SysTime.fromUnixTime(0);
136         return (Clock.currTime - start).total!"msecs";
137     }
138 
139     /// returns day of month
140     int getDate() const
141     {
142         auto dt = cast(DateTime)_sysTime;
143         return dt.day;
144     }
145 
146     /// returns day of week
147     int getDay() const
148     {
149         auto dt = cast(DateTime)_sysTime;
150         return dt.dayOfWeek;
151     }
152 
153     int getFullYear() const
154     {
155         auto dt = cast(DateTime)_sysTime;
156         return dt.year;
157     }
158 
159     /// get the hour of the date
160     int getHours() const
161     {
162         return _sysTime.hour;
163     }
164 
165     long getMilliseconds() const
166     {
167         return _sysTime.fracSecs.total!"msecs";
168     }
169 
170     int getMinutes() const
171     {
172         return _sysTime.minute;
173     }
174 
175     /// returns month from 0-11
176     int getMonth() const
177     {
178         return cast(int)(_sysTime.month) - 1;
179     }
180 
181     int getSeconds() const
182     {
183         return _sysTime.second;
184     }
185 
186     long getTime() const
187     {
188         return _sysTime.toUnixTime * 1000; // TODO fix
189     }
190 
191     long getTimezone() const
192     {
193         return _sysTime.timezone.utcOffsetAt(_sysTime.stdTime).total!"minutes";
194     }
195 
196     // TODO UTC stuff
197 
198     void setDate(in int d)
199     {
200         _sysTime.day = d;
201     }
202 
203     void setFullYear(in int year)
204     {
205         _sysTime.year = year;
206     }
207 
208     void setHours(in int hours, in int minutes=0, in int seconds=0)
209     {
210         _sysTime.hour = hours;
211         _sysTime.minute = minutes;
212         _sysTime.second = seconds;
213     }
214 
215     void setMilliseconds(in uint ms)
216     {
217         import core.time: msecs, Duration;
218         _sysTime.fracSecs = msecs(ms);
219     }
220 
221     void setMinutes(in uint minutes)
222     {
223         _sysTime.minute = minutes;
224     }
225 
226     void setMonth(in uint month)
227     {
228         _sysTime.month = cast(Month)(month % 12 + 1);
229     }
230 
231     void setSeconds(in uint s)
232     {
233         _sysTime.second = cast(ubyte)(s%60);
234     }
235 
236     void setTime(in long unixTimeMs)
237     {
238         _sysTime = _sysTime.fromUnixTime(unixTimeMs / 1000); // TODO fix
239     }
240 
241     string toISOString() const
242     {
243         auto dt = cast(DateTime)_sysTime;
244         return dt.toISOString();
245     }
246 
247     ScriptDate toUTC() const
248     {
249         auto newSD = new ScriptDate(0);
250         newSD._sysTime = _sysTime.toUTC();
251         return newSD;
252     }
253 
254     override string toString() const
255     {
256         auto tz = _sysTime.timezone.dstName;
257         return (cast(DateTime)_sysTime).toString() ~ " " ~ tz;
258     }
259 
260 private:
261     SysTime _sysTime;
262 }
263 
264 ScriptAny native_Date_ctor(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
265 {
266     if(!thisObj.isObject)
267     {
268         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
269         return ScriptAny.UNDEFINED;
270     }
271     auto obj = thisObj.toValue!ScriptObject;
272     try 
273     {
274         if(args.length == 0)
275             obj.nativeObject = new ScriptDate();
276         else if(args.length == 1)
277         {
278             if(args[0].isNumber)
279                 obj.nativeObject = new ScriptDate(args[0].toValue!long);
280             else
281                 obj.nativeObject = new ScriptDate(args[0].toString());
282         }
283         else if(args.length >= 2)
284         {
285             immutable year = args[0].toValue!int;
286             immutable month = args[1].toValue!int;
287             immutable day = args.length > 2? args[2].toValue!int : 1;
288             immutable hours = args.length > 3 ? args[3].toValue!int : 0;
289             immutable minutes = args.length > 4 ? args[4].toValue!int : 0; 
290             immutable seconds = args.length > 5 ? args[5].toValue!int : 0;
291             immutable mseconds = args.length > 6 ? args[6].toValue!int : 0;
292             obj.nativeObject = new ScriptDate(year, month, day, hours, minutes, seconds, mseconds);
293         }
294         else
295             nfe = NativeFunctionError.WRONG_NUMBER_OF_ARGS;
296     }
297     catch(TimeException tex)
298     {
299         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
300         return ScriptAny(tex.msg);
301     }
302     return ScriptAny.UNDEFINED;
303 }
304 
305 ScriptAny native_Date_s_now(Environment environment,
306                             ScriptAny* thisObj,
307                             ScriptAny[] args,
308                             ref NativeFunctionError nfe)
309 {
310     return ScriptAny(ScriptDate.now());
311 }
312 
313 ScriptAny native_Date_getDate(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
314 {
315     auto date = thisObj.toNativeObject!ScriptDate;
316     if(date is null)
317     {
318         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
319         return ScriptAny.UNDEFINED;
320     }
321     return ScriptAny(date.getDate());
322 }
323 
324 ScriptAny native_Date_getDay(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
325 {
326     auto date = thisObj.toNativeObject!ScriptDate;
327     if(date is null)
328     {
329         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
330         return ScriptAny.UNDEFINED;
331     }
332     return ScriptAny(date.getDay());
333 }
334 
335 ScriptAny native_Date_getFullYear(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
336 {
337     auto date = thisObj.toNativeObject!ScriptDate;
338     if(date is null)
339     {
340         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
341         return ScriptAny.UNDEFINED;
342     }
343     return ScriptAny(date.getFullYear());
344 }
345 
346 ScriptAny native_Date_getHours(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
347 {
348     auto date = thisObj.toNativeObject!ScriptDate;
349     if(date is null)
350     {
351         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
352         return ScriptAny.UNDEFINED;
353     }
354     return ScriptAny(date.getHours());
355 }
356 
357 ScriptAny native_Date_getMilliseconds(Environment env, ScriptAny* thisObj, 
358                                       ScriptAny[] args, ref NativeFunctionError nfe)
359 {
360     auto date = thisObj.toNativeObject!ScriptDate;
361     if(date is null)
362     {
363         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
364         return ScriptAny.UNDEFINED;
365     }
366     return ScriptAny(date.getMilliseconds());
367 }
368 
369 ScriptAny native_Date_getMinutes(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
370 {
371     auto date = thisObj.toNativeObject!ScriptDate;
372     if(date is null)
373     {
374         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
375         return ScriptAny.UNDEFINED;
376     }
377     return ScriptAny(date.getMinutes());
378 }
379 
380 ScriptAny native_Date_getMonth(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
381 {
382     if(!thisObj.isObject)
383     {
384         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
385         return ScriptAny.UNDEFINED;
386     }
387     auto dateObj = (*thisObj).toValue!ScriptDate;
388     if(dateObj is null)
389     {
390         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
391         return ScriptAny.UNDEFINED;
392     }
393     return ScriptAny(dateObj.getMonth);
394 }
395 
396 ScriptAny native_Date_getSeconds(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
397 {
398     auto date = thisObj.toNativeObject!ScriptDate;
399     if(date is null)
400     {
401         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
402         return ScriptAny.UNDEFINED;
403     }
404     return ScriptAny(date.getSeconds());
405 }
406 
407 ScriptAny native_Date_getTime(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
408 {
409     auto date = thisObj.toNativeObject!ScriptDate;
410     if(date is null)
411     {
412         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
413         return ScriptAny.UNDEFINED;
414     }
415     return ScriptAny(date.getTime());
416 }
417 
418 ScriptAny native_Date_getTimezone(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
419 {
420     auto date = thisObj.toNativeObject!ScriptDate;
421     if(date is null)
422     {
423         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
424         return ScriptAny.UNDEFINED;
425     }
426     return ScriptAny(date.getTimezone());
427 }
428 
429 ScriptAny native_Date_setDate(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
430 {
431     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
432     mixin(TO_ARG_CHECK_INDEX!("d", 0, int));
433     try 
434     {
435         date.setDate(d);
436     }
437     catch(TimeException ex)
438     {
439         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
440         return ScriptAny(ex.msg);
441     }
442     return ScriptAny.UNDEFINED;
443 }
444 
445 ScriptAny native_Date_setFullYear(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
446 {
447     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
448     mixin(TO_ARG_CHECK_INDEX!("year", 0, int));
449     try // this might not be needed here
450     {
451         date.setFullYear(year);
452     }
453     catch(TimeException ex)
454     {
455         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
456         return ScriptAny(ex.msg);
457     }
458     return ScriptAny.UNDEFINED;
459 }
460 
461 ScriptAny native_Date_setHours(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
462 {
463     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
464     mixin(TO_ARG_CHECK_INDEX!("hour", 0, int));
465     mixin(TO_ARG_OPT!("minute", 1, 0, int));
466     mixin(TO_ARG_OPT!("second", 2, 0, int));
467     date.setHours(hour%24, minute%60, second%60);
468     return ScriptAny.UNDEFINED;
469 }
470 
471 ScriptAny native_Date_setMilliseconds(Environment env, ScriptAny* thisObj, 
472                                       ScriptAny[] args, ref NativeFunctionError nfe)
473 {
474     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
475     mixin(TO_ARG_CHECK_INDEX!("ms", 0, uint));
476     date.setMilliseconds(ms % 1000);
477     return ScriptAny.UNDEFINED;
478 }
479 
480 ScriptAny native_Date_setMinutes(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
481 {
482     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
483     mixin(TO_ARG_CHECK_INDEX!("minutes", 0, uint));
484     date.setMinutes(minutes % 60);
485     return ScriptAny.UNDEFINED;
486 }
487 
488 ScriptAny native_Date_setMonth(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
489 {
490     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
491     mixin(TO_ARG_CHECK_INDEX!("m", 0, uint));
492     try 
493     {
494         date.setMonth(m);
495     }
496     catch(TimeException ex)
497     {
498         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
499         return ScriptAny(ex.msg);
500     }
501     return ScriptAny.UNDEFINED;
502 }
503 
504 ScriptAny native_Date_setSeconds(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
505 {
506     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
507     mixin(TO_ARG_CHECK_INDEX!("s", 0, uint));
508     date.setSeconds(s);
509     return ScriptAny.UNDEFINED;
510 }
511 
512 ScriptAny native_Date_setTime(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
513 {
514     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
515     mixin(TO_ARG_CHECK_INDEX!("t", 0, long));
516     date.setTime(t);
517     return ScriptAny.UNDEFINED;
518 }
519 
520 ScriptAny native_Date_toDateString(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
521 {
522     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
523     return ScriptAny(date.toString());
524 }
525 
526 ScriptAny native_Date_toISOString(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
527 {
528     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
529     return ScriptAny(date.toISOString());
530 }
531 
532 ScriptAny native_Date_toUTC(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
533 {
534     mixin(CHECK_THIS_NATIVE_OBJECT!("date", ScriptDate));
535     auto newDate = date.toUTC();
536     auto newSD = new ScriptObject("Date", thisObj.toValue!ScriptObject.prototype, newDate);
537     return ScriptAny(newSD);
538 }