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 }