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 }