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