1 module toml_foolery.encode.types.datetime;
2 
3 import std.datetime.systime : SysTime;
4 import std.datetime.date : DateTime, Date, TimeOfDay;
5 import std.datetime.timezone : LocalTime;
6 import datefmt : datefmt = format;
7 import toml_foolery.encode;
8 
9 
10 version(unittest)
11 {
12     import std.datetime.timezone : TimeZone, UTC, SimpleTimeZone;
13     import std.datetime : Duration, dur;
14 }
15 
16 
17 
18 package(toml_foolery.encode) enum bool makesTomlOffsetDateTime(T) = (
19     is(T == SysTime)
20 );
21 
22 package(toml_foolery.encode) enum bool makesTomlLocalDateTime(T) = (
23     is(T == SysTime)
24 );
25 
26 package(toml_foolery.encode) enum bool makesTomlLocalDate(T) = (
27     is(T == Date)
28 );
29 
30 package(toml_foolery.encode) enum bool makesTomlLocalTime(T) = (
31     is(T == TimeOfDay)
32 );
33 
34 
35 /// Serializes SysTime into:
36 /// TOML "Offset Date-Time" values.
37 /// OR
38 /// TOML "Local Date-Time" value, if timezone is LocalTime.
39 package(toml_foolery.encode) void tomlifyValueImpl(T)(
40     const T value,
41     ref Appender!string buffer,
42     immutable string[] parentTables
43 )
44 if (makesTomlOffsetDateTime!T || makesTomlLocalDateTime!T)
45 {
46     // This won't be true if value.timezone happens to be the same as the user's
47     // local timezone. It really has to be the LocalTime singleton instance.
48     if (value.timezone == LocalTime())
49     {
50         buffer.put(value.formatTime("%F %T.%g", false));
51     }
52     else
53     {
54         buffer.put(value.formatTime());
55     }
56 }
57 
58 /// Serializes Date into TOML "Local Date" values.
59 package(toml_foolery.encode) void tomlifyValueImpl(T)(
60     const T value,
61     ref Appender!string buffer,
62     immutable string[] parentTables
63 )
64 if (makesTomlLocalDate!T)
65 {
66     SysTime phonySysTime = SysTime(value);
67     buffer.put(formatTime(phonySysTime, "%F", false));
68 }
69 
70 /// Serializes TimeOfDay into TOML "Local Time" values.
71 package(toml_foolery.encode) void tomlifyValueImpl(T)(
72     const T value,
73     ref Appender!string buffer,
74     immutable string[] parentTables
75 )
76 if (makesTomlLocalTime!T)
77 {
78     SysTime phonySysTime = SysTime(DateTime(Date(), value));
79     buffer.put(formatTime(phonySysTime, "%T.%g", false));
80 }
81 
82 @("Encode `SysTime` values with non-LocalTime")
83 unittest
84 {
85     immutable TimeZone cet = new immutable SimpleTimeZone(dur!"hours"(1), "CET");
86     expect(_tomlifyValue(SysTime(DateTime(1996, 12, 11, 10, 20, 42), cet))).toEqual("1996-12-11 10:20:42.000 +01:00");
87 }
88 
89 @("Encode `SysTime` values with LocalTime")
90 unittest
91 {
92     expect(_tomlifyValue(SysTime(DateTime(2020, 1, 15, 15, 0, 33)))).toEqual("2020-01-15 15:00:33.000");
93 }
94 
95 @("Encode `Date` values")
96 unittest
97 {
98     expect(_tomlifyValue(Date(2020, 1, 15))).toEqual("2020-01-15");
99 }
100 
101 @("Encode `TimeOfDay` values")
102 unittest
103 {
104     expect(_tomlifyValue(TimeOfDay(15, 0, 33))).toEqual("15:00:33.000");
105 }
106 
107 
108 
109 /// Converts an instance of `SysTime` to a string with the following format:
110 /// YYYY-MM-DD HH:MM:SS TZ
111 /// where TZ is of the form ±HH:MM (UTC is Z).
112 ///
113 /// Params:
114 ///
115 ///     time = The instance of `SysTime` to format.
116 ///
117 ///     formatStr = Formatting string. See datefmt docs for details.
118 ///                  Default `%F %T.%g`.
119 ///
120 ///     appendTZ = If true, the resulting string will have the time zone
121 ///                 appended at the end. Default true.
122 ///
123 private string formatTime(SysTime time, string formatStr = "%F %T.%g", bool appendTZ = true)
124 {
125     string retVal = datefmt(time, formatStr);
126 
127     if (appendTZ)
128     {
129         // datefmt normally outputs timezones in the format +hhmm,
130         // but ISO says it should be +hh:mm, and I think that's more
131         // consistent considering the timestamp is also colon-separated.
132         string tz = datefmt(time, "%z");
133         string tzDirection = tz[0].to!string;
134         string tzHours = tz[1..3];
135         string tzMinutes = tz[3..$];
136         if (tzHours == "00" && tzMinutes == "00")
137         {
138             retVal ~= "Z";
139         }
140         else
141         {
142             retVal ~= " " ~ tzDirection ~ tzHours ~ ":" ~ tz[3..$];
143         }
144     }
145 
146     return retVal;
147 }
148 
149 @("formatTime")
150 unittest
151 {
152 
153     SysTime testUTC = SysTime(DateTime(2019, 11, 17, 20, 10, 35), dur!"msecs"(736), UTC());
154     string expectedUTC = "2019-11-17 20:10:35.736Z";
155     string actualUTC = formatTime(testUTC);
156     assert(actualUTC == expectedUTC, "Expected \"%s\", received \"%s\".".format(expectedUTC, actualUTC));
157 
158     immutable TimeZone est = new immutable SimpleTimeZone(dur!"hours"(-5), "EST");
159     SysTime testEST = SysTime(DateTime(2019, 11, 17, 20, 10, 35), dur!"msecs"(736), est);
160     string expectedEST = "2019-11-17 20:10:35.736 -05:00";
161     string actualEST = formatTime(testEST);
162     assert(actualEST == expectedEST, "Expected \"%s\", received \"%s\".".format(expectedEST, actualEST));
163 
164     immutable TimeZone cet = new immutable SimpleTimeZone(dur!"hours"(1), "CET");
165     SysTime testCET = SysTime(DateTime(2019, 11, 17, 20, 10, 35), dur!"msecs"(736), cet);
166     string expectCET = "2019-11-17 20:10:35.736 +01:00";
167     string actualCET = formatTime(testCET);
168     assert(actualCET == expectCET, "Expected \"%s\", received \"%s\".".format(expectCET, actualCET));
169 
170     immutable TimeZone npt = new immutable SimpleTimeZone(dur!"hours"(5) + dur!"minutes"(45), "NPT");
171     SysTime testNPT = SysTime(DateTime(2019, 11, 17, 20, 10, 35), dur!"msecs"(736), npt);
172     string expectNPT = "2019-11-17 20:10:35.736 +05:45";
173     string actualNPT = formatTime(testNPT);
174     assert(actualNPT == expectNPT, "Expected \"%s\", received \"%s\".".format(expectNPT, actualNPT));
175 }