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 }