1 module google.protobuf.timestamp; 2 3 import std.datetime : DateTime, SysTime, unixTimeToStdTime, UTC; 4 import std.exception : enforce; 5 import std.json : JSONValue; 6 import google.protobuf; 7 8 struct Timestamp 9 { 10 private struct _Message 11 { 12 @Proto(1) long seconds = google.protobuf.common.protoDefaultValue!long; 13 @Proto(2) int nanos = google.protobuf.common.protoDefaultValue!int; 14 } 15 16 // workaround LDC bug #2990 17 static immutable protoDefaultValue = Timestamp(SysTime(DateTime(1970, 1, 1, 0, 0, 0), UTC())); 18 19 private static immutable defaultTimestampValue = SysTime(DateTime(1970, 1, 1, 0, 0, 0), UTC()); 20 SysTime timestamp = defaultTimestampValue; 21 22 alias timestamp this; 23 24 auto toProtobuf() 25 { 26 long epochDelta = timestamp.stdTime - unixTimeToStdTime(0); 27 28 return _Message(epochDelta / 1_000_000_0, epochDelta % 1_000_000_0 * 100).toProtobuf; 29 } 30 31 Timestamp fromProtobuf(R)(ref R inputRange) 32 { 33 auto message = inputRange.fromProtobuf!_Message; 34 long epochDelta = message.seconds * 1_000_000_0 + message.nanos / 100; 35 timestamp.stdTime = epochDelta + unixTimeToStdTime(0); 36 37 return this; 38 } 39 40 JSONValue toJSONValue()() 41 { 42 import std.format : format; 43 import google.protobuf.json_encoding; 44 45 validateTimestamp; 46 47 auto utc = timestamp.toUTC; 48 auto fractionalDigits = utc.fracSecs.total!"nsecs"; 49 auto fractionalLength = 9; 50 51 foreach (i; 0 .. 3) 52 { 53 if (fractionalDigits % 1000 != 0) 54 break; 55 fractionalDigits /= 1000; 56 fractionalLength -= 3; 57 } 58 59 if (fractionalDigits) 60 return "%04d-%02d-%02dT%02d:%02d:%02d.%0*dZ".format(utc.year, utc.month, utc.day, utc.hour, utc.minute, 61 utc.second, fractionalLength, fractionalDigits).toJSONValue; 62 else 63 return "%04d-%02d-%02dT%02d:%02d:%02dZ".format(utc.year, utc.month, utc.day, utc.hour, utc.minute, 64 utc.second).toJSONValue; 65 } 66 67 Timestamp fromJSONValue()(JSONValue value) 68 { 69 import core.time : dur; 70 import std.algorithm : skipOver; 71 import std.conv : ConvException, to; 72 import std.datetime : DateTime, DateTimeException, Month, SimpleTimeZone, UTC; 73 import std.json : JSON_TYPE; 74 import std.regex : matchAll, regex; 75 import std..string : leftJustify; 76 import google.protobuf.json_decoding : fromJSONValue; 77 78 if (value.type == JSON_TYPE.NULL) 79 { 80 timestamp = defaultTimestampValue; 81 return this; 82 } 83 84 auto match = value.fromJSONValue!string.matchAll( 85 `^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(.\d*)?((Z)|([+-])(\d{2}):(\d{2}))$`); 86 enforce!ProtobufException(match, "Invalid timestamp JSON encoding"); 87 88 auto yearPart = match.front[1]; 89 auto monthPart = match.front[2]; 90 auto dayPart = match.front[3]; 91 auto hourPart = match.front[4]; 92 auto minutePart = match.front[5]; 93 auto secondPart = match.front[6]; 94 auto fracSecsPart = match.front[7]; 95 fracSecsPart.skipOver('.'); 96 97 try 98 { 99 import std.file: append; 100 if (match.front[8] == "Z") 101 { 102 timestamp = SysTime( 103 DateTime(yearPart.to!short, monthPart.to!ubyte.to!Month, dayPart.to!ubyte, 104 hourPart.to!ubyte, minutePart.to!ubyte, secondPart.to!ubyte), 105 dur!"nsecs"(fracSecsPart.leftJustify(9, '0').to!uint), UTC()); 106 } 107 else 108 { 109 auto tz_offset = dur!"hours"(match.front[11].to!uint) + dur!"minutes"(match.front[12].to!uint); 110 if (match.front[10] == "-") 111 tz_offset = -tz_offset; 112 timestamp = SysTime( 113 DateTime(yearPart.to!short, monthPart.to!ubyte.to!Month, dayPart.to!ubyte, 114 hourPart.to!ubyte, minutePart.to!ubyte, secondPart.to!ubyte), 115 dur!"nsecs"(fracSecsPart.leftJustify(9, '0').to!uint), 116 new immutable SimpleTimeZone(tz_offset)); 117 } 118 119 validateTimestamp; 120 return this; 121 } 122 catch (ConvException exception) 123 { 124 throw new ProtobufException(exception.msg); 125 } 126 catch (DateTimeException exception) 127 { 128 throw new ProtobufException(exception.msg); 129 } 130 } 131 132 void validateTimestamp() 133 { 134 auto year = timestamp.toUTC.year; 135 enforce!ProtobufException(0 < year && year < 10_000, 136 "Timestamp is out of range [0001-01-01T00:00:00Z 9999-12-31T23:59:59.999999999Z]"); 137 } 138 } 139 140 unittest 141 { 142 import std.algorithm.comparison : equal; 143 import std.array : array, empty; 144 import std.datetime : DateTime, msecs, seconds, UTC; 145 146 static const epoch = SysTime(DateTime(1970, 1, 1), UTC()); 147 148 assert(equal(Timestamp(epoch + 5.seconds + 5.msecs).toProtobuf, [ 149 0x08, 0x05, 0x10, 0xc0, 0x96, 0xb1, 0x02])); 150 assert(equal(Timestamp(epoch + 5.msecs).toProtobuf, [ 151 0x10, 0xc0, 0x96, 0xb1, 0x02])); 152 assert(equal(Timestamp(epoch + (-5).msecs).toProtobuf, [ 153 0x10, 0xc0, 0xe9, 0xce, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01])); 154 assert(equal(Timestamp(epoch + (-5).seconds + (-5).msecs).toProtobuf, [ 155 0x08, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 156 0x10, 0xc0, 0xe9, 0xce, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01])); 157 158 auto buffer = Timestamp(epoch + 5.seconds + 5.msecs).toProtobuf.array; 159 assert(buffer.fromProtobuf!Timestamp == Timestamp(epoch + 5.seconds + 5.msecs)); 160 buffer = Timestamp(epoch + 5.msecs).toProtobuf.array; 161 assert(buffer.fromProtobuf!Timestamp == Timestamp(epoch + 5.msecs)); 162 buffer = Timestamp(epoch + (-5).msecs).toProtobuf.array; 163 assert(buffer.fromProtobuf!Timestamp == Timestamp(epoch + (-5).msecs)); 164 buffer = Timestamp(epoch + (-5).seconds + (-5).msecs).toProtobuf.array; 165 assert(buffer.fromProtobuf!Timestamp == Timestamp(epoch + (-5).seconds + (-5).msecs)); 166 167 buffer = Timestamp(epoch).toProtobuf.array; 168 assert(buffer.empty); 169 assert(buffer.fromProtobuf!Timestamp == epoch); 170 } 171 172 unittest 173 { 174 import std.datetime : DateTime, hours, minutes, msecs, nsecs, seconds, SimpleTimeZone, UTC; 175 import std.exception : assertThrown; 176 import std.json : JSONValue; 177 178 static const epoch = SysTime(DateTime(1970, 1, 1), UTC()); 179 180 assert(protoDefaultValue!Timestamp == epoch); 181 182 assert(Timestamp(epoch).toJSONValue == JSONValue("1970-01-01T00:00:00Z")); 183 assert(Timestamp(epoch + 5.seconds).toJSONValue == JSONValue("1970-01-01T00:00:05Z")); 184 assert(Timestamp(epoch + 5.seconds + 50.msecs).toJSONValue == JSONValue("1970-01-01T00:00:05.050Z")); 185 assert(Timestamp(epoch + 5.seconds + 300.nsecs).toJSONValue == JSONValue("1970-01-01T00:00:05.000000300Z")); 186 187 immutable nonUTCTimeZone = new SimpleTimeZone(-3600.seconds); 188 static const nonUTCTimestamp = SysTime(DateTime(1970, 1, 1), nonUTCTimeZone); 189 assert(Timestamp(nonUTCTimestamp).toJSONValue == JSONValue("1970-01-01T01:00:00Z")); 190 191 static const tooSmall = SysTime(DateTime(0, 12, 31), UTC()); 192 assertThrown!ProtobufException(Timestamp(tooSmall).toJSONValue); 193 static const tooLarge = SysTime(DateTime(10_000, 1, 1), UTC()); 194 assertThrown!ProtobufException(Timestamp(tooLarge).toJSONValue); 195 196 assert(epoch == JSONValue("1970-01-01T00:00:00Z").fromJSONValue!Timestamp); 197 assert(epoch + 5.seconds == JSONValue("1970-01-01T00:00:05Z").fromJSONValue!Timestamp); 198 assert(epoch + 5.seconds + 50.msecs == JSONValue("1970-01-01T00:00:05.050Z").fromJSONValue!Timestamp); 199 assert(epoch + 5.seconds + 300.nsecs == JSONValue("1970-01-01T00:00:05.000000300Z").fromJSONValue!Timestamp); 200 201 assert(epoch + 2.hours == JSONValue("1970-01-01T00:00:00-02:00").fromJSONValue!Timestamp); 202 assert(epoch - 2.hours - 30.minutes == JSONValue("1970-01-01T00:00:00+02:30").fromJSONValue!Timestamp); 203 assert(epoch + 5.seconds + 50.msecs + 2.hours == 204 JSONValue("1970-01-01T00:00:05.050-02:00").fromJSONValue!Timestamp); 205 assert(epoch + 5.seconds + 50.msecs - 2.hours - 30.minutes == 206 JSONValue("1970-01-01T00:00:05.050+02:30").fromJSONValue!Timestamp); 207 assert(epoch + 5.seconds + 300.nsecs + 2.hours == 208 JSONValue("1970-01-01T00:00:05.000000300-02:00").fromJSONValue!Timestamp); 209 assert(epoch + 5.seconds + 300.nsecs - 2.hours - 30.minutes == 210 JSONValue("1970-01-01T00:00:05.000000300+02:30").fromJSONValue!Timestamp); 211 }