1 /*
2  * Geario - A cross-platform abstraction library with asynchronous I/O.
3  *
4  * Copyright (C) 2021-2022 Kerisy.com
5  *
6  * Website: https://www.kerisy.com
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module geario.util.DateTime;
13 
14 import core.atomic;
15 import core.stdc.time;
16 import core.thread : Thread;
17 import std.datetime;
18 import std.format : formattedWrite;
19 import std.string;
20 
21 
22 int MonthByName(string month) {
23     if (month == "Jan")
24         return 0;
25     if (month == "Feb")
26         return 1;
27     if (month == "Mar")
28         return 2;
29     if (month == "Apr")
30         return 3;
31     if (month == "May")
32         return 4;
33     if (month == "Jun")
34         return 5;
35     if (month == "Jul")
36         return 6;
37     if (month == "Aug")
38         return 7;
39     if (month == "Sep")
40         return 8;
41     if (month == "Oct")
42         return 9;
43     if (month == "Nov")
44         return 10;
45     if (month == "Dec")
46         return 11;
47 
48     return -1;
49 }
50 
51 int WeekDayByName(string wday) {
52     if (wday == "Sun")
53         return 0;
54     if (wday == "Mon")
55         return 1;
56     if (wday == "Tue")
57         return 2;
58     if (wday == "Wed")
59         return 3;
60     if (wday == "Thu")
61         return 4;
62     if (wday == "Fri")
63         return 5;
64     if (wday == "Sat")
65         return 6;
66 
67     return -1;
68 }
69 
70 
71 short MonthToShort(Month month) {
72     short resultMonth;
73     switch (month) {
74     case Month.jan:
75         resultMonth = 1;
76         break;
77     case Month.feb:
78         resultMonth = 2;
79         break;
80     case Month.mar:
81         resultMonth = 3;
82         break;
83     case Month.apr:
84         resultMonth = 4;
85         break;
86     case Month.may:
87         resultMonth = 5;
88         break;
89     case Month.jun:
90         resultMonth = 6;
91         break;
92     case Month.jul:
93         resultMonth = 7;
94         break;
95     case Month.aug:
96         resultMonth = 8;
97         break;
98     case Month.sep:
99         resultMonth = 9;
100         break;
101     case Month.oct:
102         resultMonth = 10;
103         break;
104     case Month.nov:
105         resultMonth = 11;
106         break;
107     case Month.dec:
108         resultMonth = 12;
109         break;
110     default:
111         resultMonth = 0;
112         break;
113     }
114 
115     return resultMonth;
116 }
117 
118 string DayAsString(DayOfWeek day) {
119     final switch (day) with (DayOfWeek) {
120     case mon:
121         return "Mon";
122     case tue:
123         return "Tue";
124     case wed:
125         return "Wed";
126     case thu:
127         return "Thu";
128     case fri:
129         return "Fri";
130     case sat:
131         return "Sat";
132     case sun:
133         return "Sun";
134     }
135 }
136 
137 string MonthAsString(Month month) {
138     final switch (month) with (Month) {
139     case jan:
140         return "Jan";
141     case feb:
142         return "Feb";
143     case mar:
144         return "Mar";
145     case apr:
146         return "Apr";
147     case may:
148         return "May";
149     case jun:
150         return "Jun";
151     case jul:
152         return "Jul";
153     case aug:
154         return "Aug";
155     case sep:
156         return "Sep";
157     case oct:
158         return "Oct";
159     case nov:
160         return "Nov";
161     case dec:
162         return "Dec";
163     }
164 }
165 
166 enum TimeUnit : string {
167     Year = "years",
168     Month = "months",
169     Week = "weeks",
170     Day = "days",
171     Hour = "hours",
172     Second = "seconds",
173     Millisecond = "msecs",
174     Microsecond = "usecs",
175     HectoNanosecond = "hnsecs",
176     Nanosecond = "nsecs"
177 }
178 
179 // return current unix timestamp
180 long time() {
181     return Clock.currTime.toUnixTime();
182 }
183 
184 // return formated time string from timestamp
185 string date(string format, long timestamp = 0) {
186     import std.datetime : SysTime;
187     import std.conv : to;
188 
189     long newTimestamp = timestamp > 0 ? timestamp : time();
190 
191     string timeString;
192 
193     SysTime st = SysTime.fromUnixTime(newTimestamp);
194 
195     // format to ubyte
196     foreach (c; format) {
197         switch (c) {
198         case 'Y':
199             timeString ~= st.year.to!string;
200             break;
201         case 'y':
202             timeString ~= (st.year.to!string)[2 .. $];
203             break;
204         case 'm':
205             short month = MonthToShort(st.month);
206             timeString ~= month < 10 ? "0" ~ month.to!string : month.to!string;
207             break;
208         case 'd':
209             timeString ~= st.day < 10 ? "0" ~ st.day.to!string : st.day.to!string;
210             break;
211         case 'H':
212             timeString ~= st.hour < 10 ? "0" ~ st.hour.to!string : st.hour.to!string;
213             break;
214         case 'i':
215             timeString ~= st.minute < 10 ? "0" ~ st.minute.to!string : st.minute.to!string;
216             break;
217         case 's':
218             timeString ~= st.second < 10 ? "0" ~ st.second.to!string : st.second.to!string;
219             break;
220         default:
221             timeString ~= c;
222             break;
223         }
224     }
225 
226     return timeString;
227 }
228 
229 /**
230  * 
231  */
232 class DateTime {
233     /**
234      * Returns the current time in milliseconds.  Note that
235      * while the unit of time of the return value is a millisecond,
236      * the granularity of the value depends on the underlying
237      * operating system and may be larger.  For example, many
238      * operating systems measure time in units of tens of
239      * milliseconds.
240      *
241      * <p> See the description of the class {@code Date} for
242      * a discussion of slight discrepancies that may arise between
243      * "computer time" and coordinated universal time (UTC).
244      *
245      * @return  the difference, measured in milliseconds, between
246      *          the current time and midnight, January 1, 1970 UTC.
247      */
248     static long CurrentTimeMillis() @trusted @property {
249         return CurrentTime!(TimeUnit.Millisecond)();
250     }
251 
252     static long CurrentTimeNsecs() @trusted @property {
253         return CurrentTime!(TimeUnit.Nanosecond)();
254     }
255 
256     static long CurrentUnixTime() @trusted @property {
257         return CurrentTime!(TimeUnit.Second)();
258     }
259 
260     alias CurrentTimeSecond = CurrentUnixTime;
261 
262     /**
263     */
264     static long CurrentTime(TimeUnit targetUnit)() @trusted @property {
265         version (Windows) {
266             import core.sys.windows.winbase;
267             import core.sys.windows.winnt;
268 
269             /**
270                 http://www.frenk.com/2009/12/convert-filetime-to-unix-timestamp/
271                 https://stackoverflow.com/questions/10849717/what-is-the-significance-of-january-1-1601
272                 https://stackoverflow.com/questions/1090869/why-is-1-1-1970-the-epoch-time
273                 https://www.unixtimestamp.com/
274             */
275             FILETIME fileTime;
276             GetSystemTimeAsFileTime(&fileTime);
277             ULARGE_INTEGER date, adjust;
278             date.HighPart = fileTime.dwHighDateTime;
279             date.LowPart = fileTime.dwLowDateTime;
280 
281             // 100-nanoseconds = milliseconds * 10000
282             adjust.QuadPart = 11644473600000 * 10000;
283 
284             // removes the diff between 1970 and 1601
285             date.QuadPart -= adjust.QuadPart;
286 
287             // converts back from 100-nanoseconds to milliseconds
288             return convert!(TimeUnit.HectoNanosecond, targetUnit)(date.QuadPart);
289 
290         } else version (Posix) {
291                 import core.sys.posix.signal : timespec;
292             version (OSX) {
293                 import core.sys.posix.sys.time : gettimeofday, timeval;
294 
295                 timeval tv = void;
296                 // Posix gettimeofday called with a valid timeval address
297                 // and a null second parameter doesn't fail.
298                 gettimeofday(&tv, null);
299                 return convert!(TimeUnit.Second, targetUnit)(tv.tv_sec) + 
300                     convert!(TimeUnit.Microsecond, targetUnit)(tv.tv_usec);
301 
302             } else version (linux) {
303                     import core.sys.linux.time : CLOCK_REALTIME_COARSE;
304                     import core.sys.posix.time : clock_gettime, CLOCK_REALTIME;
305 
306                     timespec ts = void;
307                     immutable Error = clock_gettime(CLOCK_REALTIME, &ts);
308                     // Posix clock_gettime called with a valid address and valid clock_id is only
309                     // permitted to fail if the number of seconds does not fit in time_t. If tv_sec
310                     // is long or larger overflow won't happen before 292 billion years A.D.
311                     static if (ts.tv_sec.max < long.max) {
312                         if (Error)
313                             throw new TimeException("Call to clock_gettime() failed");
314                     }
315                     return convert!(TimeUnit.Second, targetUnit)(ts.tv_sec) + 
316                         convert!(TimeUnit.Nanosecond, targetUnit)(ts.tv_nsec);
317 
318             } else version (FreeBSD) {
319                 import core.sys.freebsd.time : clock_gettime, CLOCK_REALTIME;
320 
321                 timespec ts = void;
322                 immutable Error = clock_gettime(CLOCK_REALTIME, &ts);
323                 // Posix clock_gettime called with a valid address and valid clock_id is only
324                 // permitted to fail if the number of seconds does not fit in time_t. If tv_sec
325                 // is long or larger overflow won't happen before 292 billion years A.D.
326                 static if (ts.tv_sec.max < long.max) {
327                     if (Error)
328                         throw new TimeException("Call to clock_gettime() failed");
329                 }
330                 return convert!(TimeUnit.Second, targetUnit)(ts.tv_sec) + 
331                         convert!(TimeUnit.Nanosecond, targetUnit)(ts.tv_nsec);
332             } else version (NetBSD) {
333                 import core.sys.netbsd.time : clock_gettime, CLOCK_REALTIME;
334 
335                 timespec ts = void;
336                 immutable Error = clock_gettime(CLOCK_REALTIME, &ts);
337                 // Posix clock_gettime called with a valid address and valid clock_id is only
338                 // permitted to fail if the number of seconds does not fit in time_t. If tv_sec
339                 // is long or larger overflow won't happen before 292 billion years A.D.
340                 static if (ts.tv_sec.max < long.max) {
341                     if (Error)
342                         throw new TimeException("Call to clock_gettime() failed");
343                 }
344                 return convert!(TimeUnit.Second, targetUnit)(ts.tv_sec) + 
345                     convert!(TimeUnit.Nanosecond, targetUnit)(ts.tv_nsec);
346             } else version (DragonFlyBSD) {
347                 import core.sys.dragonflybsd.time : clock_gettime, CLOCK_REALTIME;
348 
349                 timespec ts = void;
350                 immutable Error = clock_gettime(CLOCK_REALTIME, &ts);
351                 // Posix clock_gettime called with a valid address and valid clock_id is only
352                 // permitted to fail if the number of seconds does not fit in time_t. If tv_sec
353                 // is long or larger overflow won't happen before 292 billion years A.D.
354                 static if (ts.tv_sec.max < long.max) {
355                     if (Error)
356                         throw new TimeException("Call to clock_gettime() failed");
357                 }
358                 return convert!(TimeUnit.Second, targetUnit)(ts.tv_sec) + 
359                     convert!(TimeUnit.Nanosecond, targetUnit)(ts.tv_nsec);
360             } else version (Solaris) {
361                 import core.sys.solaris.time : clock_gettime, CLOCK_REALTIME;
362 
363                 timespec ts = void;
364                 immutable Error = clock_gettime(CLOCK_REALTIME, &ts);
365                 // Posix clock_gettime called with a valid address and valid clock_id is only
366                 // permitted to fail if the number of seconds does not fit in time_t. If tv_sec
367                 // is long or larger overflow won't happen before 292 billion years A.D.
368                 static if (ts.tv_sec.max < long.max) {
369                     if (Error)
370                         throw new TimeException("Call to clock_gettime() failed");
371                 }
372                 return convert!(TimeUnit.Second, targetUnit)(ts.tv_sec) + 
373                     convert!(TimeUnit.Nanosecond, targetUnit)(ts.tv_nsec);
374             } else
375                 static assert(0, "Unsupported OS");
376         } else
377             static assert(0, "Unsupported OS");
378     }
379 
380     static string GetTimeAsGMT() {
381         return cast(string)*timingValue;
382     }
383 
384     static shared long timestamp;
385 
386     static void StartClock() {
387         if (cas(&_isClockRunning, false, true)) {
388             dateThread.start();
389         }
390     }
391 
392     static void StopClock() @nogc {
393         atomicStore(_isClockRunning, false);
394     }
395 
396     private static shared const(char)[]* timingValue;
397     private __gshared Thread dateThread;
398     private static shared bool _isClockRunning = false;
399 
400     shared static this() {
401         import std.array;
402 
403         Appender!(char[])[2] bufs;
404         const(char)[][2] targets;
405 
406         void tick(size_t index) {
407             bufs[index].clear();
408             timestamp = core.stdc.time.time(null);
409             auto date = Clock.currTime!(ClockType.coarse)(UTC());
410             size_t sz = UpdateDate(bufs[index], date);
411             targets[index] = bufs[index].data;
412             atomicStore(timingValue, cast(shared)&targets[index]);
413         }
414 
415         tick(0);
416 
417         dateThread = new Thread({
418             size_t cur = 1;
419             while (_isClockRunning) {
420                 tick(cur);
421                 cur = 1 - cur;
422                 Thread.sleep(1.seconds);
423             }
424         });
425 
426         dateThread.isDaemon = true;
427         // FIXME: Needing refactor or cleanup -@zxp at 12/30/2018, 10:10:09 AM
428         // 
429         // It's not a good idea to launch another thread in shared static this().
430         // https://issues.dlang.org/show_bug.cgi?id=19492
431         // StartClock();
432     }
433 
434     shared static ~this() @nogc {
435         if (cas(&_isClockRunning, true, false)) {
436             // dateThread.join();
437         }
438     }
439 
440     private static size_t UpdateDate(Output, D)(ref Output sink, D date) {
441         return formattedWrite(sink, "%s, %02s %s %04s %02s:%02s:%02s GMT", DayAsString(date.dayOfWeek),
442                 date.day, MonthAsString(date.month), date.year, date.hour,
443                 date.minute, date.second);
444     }
445 
446 }