1 module toml_foolery.decode.types.datetime;
2 
3 import std.conv : to;
4 import std.datetime;
5 import std.regex : ctRegex, matchFirst, Captures;
6 import std.variant : Algebraic;
7 
8 import toml_foolery.decode : TomlDecodingException;
9 
10 version(unittest) import exceeds_expectations;
11 
12 
13 package(toml_foolery.decode) alias DateAndOrTime = Algebraic!(SysTime, Date, TimeOfDay);
14 
15 package(toml_foolery.decode) DateAndOrTime parseTomlGenericDateTime(string value)
16 {
17     enum auto offsetDateTimeRegEx =
18         ctRegex!(`^(\d\d\d\d)-(\d\d)-(\d\d)[Tt ](\d\d):(\d\d):(\d\d)(?:\.(\d+))?(?:[Zz]|([+-])(\d\d):(\d\d))$`);
19     //  0 full     1year      2mon   3day       4hr    5min   6sec       7frac          8tzd  9tzh   10tzm
20 
21     enum auto localDateTimeRegEx =
22         ctRegex!(`^(\d\d\d\d)-(\d\d)-(\d\d)[Tt ](\d\d):(\d\d):(\d\d)(?:\.(\d+))?$`);
23     //  0 full     1year      2mon   3day       4hr    5min   6sec       7frac
24 
25     enum auto dateRegex =
26         ctRegex!(`^(\d\d\d\d)-(\d\d)-(\d\d)$`);
27     //  0 full     1year      2mon   3day
28 
29     enum auto timeRegex =
30         ctRegex!(`^(\d\d):(\d\d):(\d\d)(?:\.(\d+))?$`);
31     //  0 full     1hr    2min   3sec       4frac
32 
33     Captures!string captures;
34 
35     captures = value.matchFirst(offsetDateTimeRegEx);
36     if (!captures.empty) return DateAndOrTime(parseRFC3339(captures));
37 
38     captures = value.matchFirst(localDateTimeRegEx);
39     if (!captures.empty) return DateAndOrTime(parseRFC3339NoOffset(captures));
40 
41     captures = value.matchFirst(dateRegex);
42     if (!captures.empty) return DateAndOrTime(parseRFC3339DateOnly(captures));
43 
44     captures = value.matchFirst(timeRegex);
45     assert (!captures.empty,
46         `Input "` ~ value ~ `" matches none of the following regexes:` ~
47         "\n\t" ~ offsetDateTimeRegEx.to!string ~
48         "\n\t" ~ localDateTimeRegEx.to!string ~
49         "\n\t" ~ dateRegex.to!string ~
50         "\n\t" ~ timeRegex.to!string
51     );
52     return DateAndOrTime(parseRFC3339TimeOnly(captures));
53 }
54 
55 /// Up to nanosecond precision is supported.
56 /// Additional precision is truncated, obeying the TOML spec.
57 package(toml_foolery.decode) SysTime parseTomlOffsetDateTime(string value)
58 {
59     DateAndOrTime dt = parseTomlGenericDateTime(value);
60     assert(dt.peek!SysTime !is null, "Expected SysTime, but got: " ~ dt.type.to!string);
61     assert(dt.get!SysTime.timezone != LocalTime(), "Expected SysTime with an offset, but got LocalTime.");
62     return dt.get!SysTime;
63 }
64 
65 package(toml_foolery.decode) SysTime parseTomlLocalDateTime(string value)
66 {
67     DateAndOrTime dt = parseTomlGenericDateTime(value);
68 
69     assert(
70         dt.peek!SysTime !is null,
71         "Expected SysTime, but got: " ~ dt.type.to!string
72     );
73 
74     assert(
75         dt.get!SysTime.timezone == LocalTime(),
76         "Expected SysTime with LocalTime, but got time zone: " ~ dt.get!SysTime.timezone.to!string
77     );
78 
79     return dt.get!SysTime;
80 }
81 
82 package(toml_foolery.decode) Date parseTomlLocalDate(string value)
83 {
84     DateAndOrTime dt = parseTomlGenericDateTime(value);
85     assert(dt.peek!Date !is null, "Expected Date, but got: " ~ dt.type.to!string);
86     return dt.get!Date;
87 }
88 
89 package(toml_foolery.decode) TimeOfDay parseTomlLocalTime(string value)
90 {
91     DateAndOrTime dt = parseTomlGenericDateTime(value);
92     assert(dt.peek!TimeOfDay !is null, "Expected TimeOfDay, but got: " ~ dt.type.to!string);
93     return dt.get!TimeOfDay;
94 }
95 
96 /// Parses any [RFC 3339](https://tools.ietf.org/html/rfc3339) string.
97 ///
98 /// The grammar in §5.6 of the above document is reproduced below for convenience.
99 ///
100 /// ```abnf
101 /// date-fullyear   = 4DIGIT
102 /// date-month      = 2DIGIT  ; 01-12
103 /// date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
104 ///                           ; month/year
105 /// time-hour       = 2DIGIT  ; 00-23
106 /// time-minute     = 2DIGIT  ; 00-59
107 /// time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
108 ///                           ; rules
109 /// time-secfrac    = "." 1*DIGIT
110 /// time-numoffset  = ("+" / "-") time-hour ":" time-minute
111 /// time-offset     = "Z" / time-numoffset
112 ///
113 /// partial-time    = time-hour ":" time-minute ":" time-second
114 ///                   [time-secfrac]
115 /// full-date       = date-fullyear "-" date-month "-" date-mday
116 /// full-time       = partial-time time-offset
117 ///
118 /// date-time       = full-date "T" full-time
119 /// ```
120 ///
121 /// Throws:
122 ///     DateTimeException if the given string represents an invalid date.
123 ///
124 private SysTime parseRFC3339(Captures!string captures)
125 {
126     import std.range : padRight;
127 
128     string yearStr         = captures[1];
129     string monthStr        = captures[2];
130     string dayStr          = captures[3];
131     string hourStr         = captures[4];
132     string minuteStr       = captures[5];
133     string secondStr       = captures[6];
134     string fracStr         = captures[7]  != "" ? captures[7]  :  "0";
135     string offsetDirStr    = captures[8]  != "" ? captures[8]  :  "+";
136     string hourOffsetStr   = captures[9]  != "" ? captures[9]  : "00";
137     string minuteOffsetStr = captures[10] != "" ? captures[10] : "00";
138 
139     return SysTime(
140         DateTime(
141             yearStr.to!int,
142             monthStr.to!int,
143             dayStr.to!int,
144             hourStr.to!int,
145             minuteStr.to!int,
146             secondStr.to!int
147         ),
148         nsecs(fracStr.padRight('0', 9).to!string[0..9].to!long),
149         new immutable SimpleTimeZone(hours((offsetDirStr ~ hourOffsetStr).to!long) + minutes(minuteOffsetStr.to!long))
150     );
151 }
152 
153 private SysTime parseRFC3339NoOffset(Captures!string captures)
154 out (retVal; retVal.timezone == LocalTime())
155 {
156     import std.range : padRight;
157 
158     string yearStr         = captures[1];
159     string monthStr        = captures[2];
160     string dayStr          = captures[3];
161     string hourStr         = captures[4];
162     string minuteStr       = captures[5];
163     string secondStr       = captures[6];
164     string fracStr         = captures[7]  != "" ? captures[7]  :  "0";
165 
166     return SysTime(
167         DateTime(
168             yearStr.to!int,
169             monthStr.to!int,
170             dayStr.to!int,
171             hourStr.to!int,
172             minuteStr.to!int,
173             secondStr.to!int
174         ),
175         nsecs(fracStr.padRight('0', 9).to!string[0..9].to!long)
176     );
177 }
178 
179 private Date parseRFC3339DateOnly(Captures!string captures)
180 {
181     import std.range : padRight;
182 
183     string yearStr         = captures[1];
184     string monthStr        = captures[2];
185     string dayStr          = captures[3];
186 
187     return Date(
188         yearStr.to!int,
189         monthStr.to!int,
190         dayStr.to!int
191     );
192 }
193 
194 private TimeOfDay parseRFC3339TimeOnly(Captures!string captures)
195 {
196     import std.range : padRight;
197 
198     string hourStr         = captures[1];
199     string minuteStr       = captures[2];
200     string secondStr       = captures[3];
201     // string fracStr         = captures[4]  != "" ? captures[7]  :  "0";
202 
203     return TimeOfDay(
204         hourStr.to!int,
205         minuteStr.to!int,
206         secondStr.to!int
207     );
208 }
209 
210 @("Offset Date-Time (UTC) -> SysTime")
211 unittest
212 {
213     SysTime expected = SysTime(DateTime(2020, 1, 20, 21, 54, 56), UTC());
214 
215     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56.000z")).toEqual(expected);
216     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56.000Z")).toEqual(expected);
217     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56.000+00:00")).toEqual(expected);
218     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56.000-00:00")).toEqual(expected);
219 
220     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56.000z")).toEqual(expected);
221     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56.000Z")).toEqual(expected);
222     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56.000+00:00")).toEqual(expected);
223     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56.000-00:00")).toEqual(expected);
224 
225     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56.000z")).toEqual(expected);
226     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56.000Z")).toEqual(expected);
227     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56.000+00:00")).toEqual(expected);
228     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56.000-00:00")).toEqual(expected);
229 
230 
231     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56z")).toEqual(expected);
232     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56Z")).toEqual(expected);
233     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56+00:00")).toEqual(expected);
234     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56-00:00")).toEqual(expected);
235 
236     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56z")).toEqual(expected);
237     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56Z")).toEqual(expected);
238     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56+00:00")).toEqual(expected);
239     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56-00:00")).toEqual(expected);
240 
241     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56z")).toEqual(expected);
242     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56Z")).toEqual(expected);
243     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56+00:00")).toEqual(expected);
244     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56-00:00")).toEqual(expected);
245 }
246 
247 @("Offset Date-Time (NPT) -> SysTime")
248 unittest
249 {
250     immutable TimeZone npt = new immutable SimpleTimeZone(dur!"hours"(5) + dur!"minutes"(45), "NPT");
251     SysTime expected = SysTime(DateTime(2020, 1, 20, 21, 54, 56), npt);
252     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56.000+05:45")).toEqual(expected);
253     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56.000+05:45")).toEqual(expected);
254     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56.000+05:45")).toEqual(expected);
255 
256     expect(parseTomlOffsetDateTime("2020-01-20 21:54:56+05:45")).toEqual(expected);
257     expect(parseTomlOffsetDateTime("2020-01-20t21:54:56+05:45")).toEqual(expected);
258     expect(parseTomlOffsetDateTime("2020-01-20T21:54:56+05:45")).toEqual(expected);
259 }
260 
261 @("Offset Date-Time — truncate fracsecs")
262 unittest
263 {
264     SysTime expected = SysTime(DateTime(2020, 1, 26, 16, 55, 23), nsecs(999_999_999), UTC());
265     expect(parseTomlOffsetDateTime("2020-01-26 16:55:23.999999999Z")).toEqual(expected);
266     expect(parseTomlOffsetDateTime("2020-01-26 16:55:23.999999999999Z")).toEqual(expected);
267 }
268 
269 @("Local Date-Time -> SysTime")
270 unittest
271 {
272     expect(parseTomlLocalDateTime("2020-01-26 17:13:11")).toEqual(
273         SysTime(
274             DateTime(2020, 1, 26, 17, 13, 11),
275             0.nsecs,
276             LocalTime()
277         )
278     );
279 }
280 
281 @("Local Date-Time -> SysTime (with fractional seconds)")
282 unittest
283 {
284     expect(parseTomlLocalDateTime("2020-01-26 17:13:11.999999999")).toEqual(
285         SysTime(
286             DateTime(2020, 1, 26, 17, 13, 11),
287             999_999_999.nsecs,
288             LocalTime()
289         )
290     );
291 
292     expect(parseTomlLocalDateTime("2020-01-26 17:13:11.999999999999")).toEqual(
293         SysTime(
294             DateTime(2020, 1, 26, 17, 13, 11),
295             999_999_999.nsecs,
296             LocalTime()
297         )
298     );
299 }
300 
301 @("Local Date -> Date")
302 unittest
303 {
304     expect(parseTomlLocalDate("2020-01-26")).toEqual(Date(2020, 1, 26));
305 }
306 
307 @("Local Time -> Time")
308 unittest
309 {
310     expect(parseTomlLocalTime("13:51:15")).toEqual(TimeOfDay(13, 51, 15));
311 }