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 }