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 }