1 module google.protobuf.duration;
2 
3 import core.time : StdDuration = Duration;
4 import std.exception : enforce;
5 import std.json : JSONValue;
6 import std.range : empty;
7 import google.protobuf;
8 
9 struct Duration
10 {
11     private struct _Message
12     {
13         @Proto(1) long seconds = protoDefaultValue!long;
14         @Proto(2) int nanos = protoDefaultValue!int;
15     }
16 
17     StdDuration duration;
18 
19     alias duration this;
20 
21     auto toProtobuf()
22     {
23         validateDuration;
24 
25         long seconds;
26         long nsecs;
27 
28         duration.split!("seconds", "nsecs")(seconds, nsecs);
29 
30         return _Message(seconds, cast(int) nsecs).toProtobuf;
31     }
32 
33     Duration fromProtobuf(R)(ref R inputRange)
34     {
35         import core.time : dur;
36 
37         auto message = inputRange.fromProtobuf!_Message;
38         duration = dur!"seconds"(message.seconds) + dur!"nsecs"(message.nanos);
39 
40         return this;
41     }
42 
43     JSONValue toJSONValue()()
44     {
45         import std.format : format;
46         import std.math : abs;
47         import google.protobuf.json_encoding : toJSONValue;
48 
49         validateDuration;
50 
51         long seconds;
52         long nsecs;
53 
54         duration.split!("seconds", "nsecs")(seconds, nsecs);
55         seconds = abs(seconds);
56         auto fractionalDigits = abs(nsecs);
57         auto fractionalLength = 9;
58 
59         foreach (i; 0 .. 3)
60         {
61             if (fractionalDigits % 1000 != 0)
62                 break;
63             fractionalDigits /= 1000;
64             fractionalLength -= 3;
65         }
66 
67         if (fractionalDigits)
68         {
69             return "%s%d.%0*ds"
70                 .format(duration.isNegative ? "-" : "", seconds, fractionalLength, fractionalDigits)
71                 .toJSONValue;
72         }
73         else
74         {
75             return "%s%ds".format(duration.isNegative ? "-" : "", seconds).toJSONValue;
76         }
77     }
78 
79     Duration fromJSONValue()(JSONValue value)
80     {
81         import core.time : dur;
82         import std.algorithm : skipOver;
83         import std.conv : ConvException, to;
84         import std.json : JSON_TYPE;
85         import std.regex : matchAll, regex;
86         import std..string : leftJustify;
87         import google.protobuf.json_decoding : fromJSONValue;
88 
89         if (value.type == JSON_TYPE.NULL)
90         {
91             duration = StdDuration.init;
92             return this;
93         }
94 
95         auto match = value.fromJSONValue!string.matchAll(`^(-)?(\d+)([.]\d*)?s$`);
96         enforce!ProtobufException(match, "Invalid duration JSON encoding");
97 
98         bool negative = !match.front[1].empty;
99         auto secondsPart = match.front[2];
100         auto nsecsPart = match.front[3];
101         nsecsPart.skipOver('.');
102 
103         try
104         {
105             duration = dur!"seconds"(secondsPart.to!ulong) + dur!"nsecs"(nsecsPart.leftJustify(9, '0').to!uint);
106             if (negative)
107                 duration = -duration;
108 
109             validateDuration;
110             return this;
111         }
112         catch (ConvException exception)
113         {
114             throw new ProtobufException(exception.msg);
115         }
116     }
117 
118     private void validateDuration()
119     {
120         import std.exception : enforce;
121 
122         auto seconds = duration.total!"seconds";
123         enforce!ProtobufException(-315_576_000_001L < seconds && seconds < 315_576_000_001,
124             "Duration is out of range approximately +-10_000 years.");
125     }
126 }
127 
128 unittest
129 {
130     import std.algorithm.comparison : equal;
131     import std.array : array;
132     import std.datetime : msecs, seconds;
133 
134     assert(equal(Duration(5.seconds + 5.msecs).toProtobuf, [0x08, 0x05, 0x10, 0xc0, 0x96, 0xb1, 0x02]));
135     assert(equal(Duration(5.msecs).toProtobuf, [0x10, 0xc0, 0x96, 0xb1, 0x02]));
136     assert(equal(Duration((-5).msecs).toProtobuf, [0x10, 0xc0, 0xe9, 0xce, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]));
137     assert(equal(Duration((-5).seconds + (-5).msecs).toProtobuf, [
138         0x08, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
139         0x10, 0xc0, 0xe9, 0xce, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]));
140 
141     assert(equal(Duration(5.msecs).toProtobuf, [0x10, 0xc0, 0x96, 0xb1, 0x02]));
142 
143     auto buffer = Duration(5.seconds + 5.msecs).toProtobuf.array;
144     assert(buffer.fromProtobuf!Duration == Duration(5.seconds + 5.msecs));
145     buffer = Duration(5.msecs).toProtobuf.array;
146     assert(buffer.fromProtobuf!Duration == Duration(5.msecs));
147     buffer = Duration((-5).msecs).toProtobuf.array;
148     assert(buffer.fromProtobuf!Duration == Duration((-5).msecs));
149     buffer = Duration((-5).seconds + (-5).msecs).toProtobuf.array;
150     assert(buffer.fromProtobuf!Duration == Duration((-5).seconds + (-5).msecs));
151 
152     buffer = Duration(StdDuration.zero).toProtobuf.array;
153     assert(buffer.empty);
154     assert(buffer.fromProtobuf!Duration == Duration.zero);
155 }
156 
157 unittest
158 {
159     import std.datetime : msecs, nsecs, seconds, weeks;
160     import std.exception : assertThrown;
161     import std.json : JSONValue;
162 
163     assert(Duration(0.seconds).toJSONValue == JSONValue("0s"));
164     assert(Duration(1.seconds).toJSONValue == JSONValue("1s"));
165     assert(Duration((-1).seconds).toJSONValue == JSONValue("-1s"));
166     assert(Duration(0.seconds + 50.msecs).toJSONValue == JSONValue("0.050s"));
167     assert(Duration(0.seconds - 50.msecs).toJSONValue == JSONValue("-0.050s"));
168     assert(Duration(0.seconds - 300.nsecs).toJSONValue == JSONValue("-0.000000300s"));
169     assert(Duration(-100.seconds - 300.nsecs).toJSONValue == JSONValue("-100.000000300s"));
170 
171     assertThrown!ProtobufException(Duration(530_000.weeks).toJSONValue);
172     assertThrown!ProtobufException(Duration(-530_000.weeks).toJSONValue);
173 
174     Duration foo;
175     assert(Duration(0.seconds) == foo.fromJSONValue(JSONValue("0s")));
176     assert(Duration(1.seconds) == foo.fromJSONValue(JSONValue("1s")));
177     assert(Duration((-1).seconds) == foo.fromJSONValue(JSONValue("-1s")));
178     assert(Duration(0.seconds + 50.msecs) == foo.fromJSONValue(JSONValue("0.050s")));
179     assert(Duration(0.seconds - 50.msecs) == foo.fromJSONValue(JSONValue("-0.050s")));
180     assert(Duration(0.seconds - 300.nsecs) == foo.fromJSONValue(JSONValue("-0.000000300s")));
181     assert(Duration(-100.seconds - 300.nsecs) == foo.fromJSONValue(JSONValue("-100.000000300s")));
182 
183     assertThrown!ProtobufException(foo.fromJSONValue(JSONValue("315576000001s")));
184     assertThrown!ProtobufException(foo.fromJSONValue(JSONValue("315576000001")));
185 }