1 /** 2 * ae.utils.math.mixed_radix 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.math.mixed_radix; 15 16 import std.meta : staticIndexOf, staticMap; 17 18 static if (is(typeof({ import std.sumtype : SumType, match; }))) 19 import std.sumtype : SumType, match; 20 21 // TODO: Find what this thing is actually called. 22 /// A mixed-radix number coding system. 23 template MixedRadixCoder( 24 /// Numeric type for decoded items. 25 I, 26 /// Numeric type for encoded result. 27 E, 28 /// Use an encoding with an explicit end of items. 29 bool withEOF = false, 30 ) 31 { 32 /// This encoding system is LIFO, so the encoder buffers all items 33 /// until `.finish` is called. 34 struct Encoder( 35 /// Maximum number of encoded items. 36 size_t maxItems, 37 ) 38 { 39 private: 40 struct Item { I n, card; } 41 MaybeDynamicArray!(Item, maxItems) items; 42 43 public: 44 /// Encode one item. 45 /// `card` is the item's cardinality, i.e. one past its maximum. 46 void put(I n, I card) 47 in(0 <= n && n < card) 48 { 49 items ~= Item(n, card); 50 } 51 52 /// Finish encoding and return the encoded result. 53 E finish() 54 { 55 E result = withEOF ? 1 : 0; 56 foreach_reverse (ref item; items) 57 { 58 result *= item.card; 59 result += item.n; 60 } 61 return result; 62 } 63 } 64 65 /// As above. This will allocate the items dynamically. 66 alias VariableLengthEncoder = Encoder!(-1); 67 68 /// Like `Encoder`, but does not use a temporary buffer. 69 /// Instead, the user is expected to put the items in reverse order. 70 struct RetroEncoder 71 { 72 private: 73 E encoded = withEOF ? 1 : 0; 74 75 public: 76 void put(I n, I card) 77 in (0 <= n && n < card) 78 { 79 encoded *= card; 80 encoded += n; 81 } /// 82 83 E finish() 84 { 85 return encoded; 86 } /// 87 } 88 89 /// The decoder. 90 struct Decoder 91 { 92 private: 93 E encoded; 94 95 public: 96 this(E encoded) 97 { 98 this.encoded = encoded; 99 static if (withEOF) 100 assert(encoded > 0); 101 } /// 102 103 I get(I card) 104 in(card > 0) 105 { 106 I value = encoded % card; 107 encoded /= card; 108 static if (withEOF) 109 assert(encoded > 0, "Decoding error"); 110 return value; 111 } /// 112 113 static if (withEOF) 114 @property bool empty() const { return encoded == 1; } /// 115 } 116 } 117 118 /// 119 debug(ae_unittest) unittest 120 { 121 alias Coder = MixedRadixCoder!(uint, uint, true); 122 Coder.Encoder!2 encoder; 123 encoder.put(5, 8); 124 encoder.put(1, 2); 125 auto result = encoder.finish(); 126 127 auto decoder = Coder.Decoder(result); 128 assert(decoder.get(8) == 5); 129 assert(decoder.get(2) == 1); 130 assert(decoder.empty); 131 } 132 133 debug(ae_unittest) unittest 134 { 135 import std.meta : AliasSeq; 136 import std.traits : EnumMembers; 137 import std.exception : assertThrown; 138 import core.exception : AssertError; 139 140 alias I = uint; 141 alias E = uint; 142 143 enum Mode { dynamicSize, staticSize, retro } 144 foreach (mode; EnumMembers!Mode) 145 foreach (withEOF; AliasSeq!(false, true)) 146 { 147 void testImpl() 148 { 149 alias Coder = MixedRadixCoder!(I, E, withEOF); 150 151 static if (mode == Mode.retro) 152 { 153 Coder.RetroEncoder encoder; 154 encoder.put(1, 2); 155 encoder.put(5, 8); 156 } 157 else 158 { 159 static if (mode == Mode.dynamicSize) 160 Coder.VariableLengthEncoder encoder; 161 else 162 Coder.Encoder!2 encoder; 163 164 encoder.put(5, 8); 165 encoder.put(1, 2); 166 } 167 auto result = encoder.finish(); 168 169 auto decoder = Coder.Decoder(result); 170 static if (withEOF) assert(!decoder.empty); 171 assert(decoder.get(8) == 5); 172 static if (withEOF) assert(!decoder.empty); 173 assert(decoder.get(2) == 1); 174 static if (withEOF) assert(decoder.empty); 175 176 static if (withEOF) 177 { 178 debug assertThrown!AssertError(decoder.get(42)); 179 } 180 else 181 assert(decoder.get(42) == 0); 182 } 183 static if (mode == Mode.dynamicSize) 184 testImpl(); 185 else 186 { 187 @nogc void test() { testImpl(); } 188 test(); 189 } 190 } 191 } 192 193 /// Serializes structs and static arrays using a `MixedRadixCoder`. 194 /// Consults types' `.max` property to obtain cardinality. 195 template SerializationCoder(alias Coder, S) 196 { 197 private alias I = typeof(Coder.Decoder.init.get(0)); 198 private alias E = typeof(Coder.RetroEncoder.init.finish()); 199 200 private mixin template Visitor(bool serializing, bool deserializing, bool retro) 201 { 202 static assert(!(serializing && deserializing)); 203 204 void visit(T)(ref T value) 205 { 206 static if (is(T : I) && is(typeof(T.max) : I)) 207 { 208 I max = T.max; 209 I card = max; card++; 210 assert(card > max, "Overflow"); 211 handleLeaf(value, card); 212 } 213 else 214 static if (is(T == SumType!Types, Types...)) 215 { 216 alias SubCoder = MixedRadixCoder!(I, E, false); // No EOF 217 218 I totalCard = 0; 219 static foreach (Type; Types) 220 totalCard += SerializationCoder!(SubCoder, Type).cardinality; 221 222 I encoded; 223 static if (serializing) 224 { 225 I handler(FieldType)(ref const FieldType field) 226 { 227 I encoded; 228 enum typeIndex = staticIndexOf!(FieldType, Types); 229 static assert(typeIndex >= 0); 230 static foreach (Type; Types[0 .. typeIndex]) 231 {{ 232 E fieldCard = SerializationCoder!(SubCoder, Type).cardinality; 233 encoded += fieldCard; 234 }} 235 236 auto encodedField = SerializationCoder!(SubCoder, FieldType).serialize(field); 237 encoded += encodedField; 238 return encoded; 239 } 240 alias handlers = staticMap!(handler, Types); 241 encoded = value.match!(handlers); 242 } 243 handleLeaf(encoded, totalCard); 244 static if (deserializing) 245 { 246 I lowBound; 247 static foreach (Type; Types) 248 {{ 249 E fieldCard = SerializationCoder!(SubCoder, Type).cardinality; 250 I highBound = lowBound; highBound += fieldCard; 251 if (encoded < highBound) 252 { 253 auto encodedField = encoded - lowBound; 254 value = T(SerializationCoder!(SubCoder, Type).deserialize(encodedField)); 255 return; 256 } 257 lowBound = highBound; 258 }} 259 assert(false); 260 } 261 } 262 else 263 static if (is(T == struct)) 264 static if (retro) 265 foreach_reverse (ref field; value.tupleof) 266 visit(field); 267 else 268 foreach (ref field; value.tupleof) 269 visit(field); 270 else 271 static if (is(T == Q[N], Q, size_t N)) 272 static if (retro) 273 foreach_reverse (ref item; value) 274 visit(item); 275 else 276 foreach (ref item; value) 277 visit(item); 278 else 279 static assert(false, "Don't know what to do with " ~ T.stringof); 280 } 281 } 282 283 private struct Serializer 284 { 285 Coder.RetroEncoder encoder; 286 void handleLeaf(I value, I card) { encoder.put(value, card); } 287 mixin Visitor!(true, false, true); 288 } 289 290 E serialize()(auto ref const S s) 291 { 292 Serializer serializer; 293 serializer.visit(s); 294 return serializer.encoder.finish(); 295 } /// 296 297 private struct Deserializer 298 { 299 Coder.Decoder decoder; 300 void handleLeaf(T)(ref T value, I card) { value = cast(T)decoder.get(card); } 301 mixin Visitor!(false, true, false); 302 } 303 304 S deserialize(E encoded) nothrow @nogc 305 { 306 Deserializer deserializer; 307 deserializer.decoder = Coder.Decoder(encoded); 308 S result; 309 deserializer.visit(result); 310 static if (__traits(hasMember, deserializer.decoder, "empty")) 311 assert(deserializer.decoder.empty); 312 return result; 313 } /// 314 315 private struct MinWriter 316 { 317 Coder.RetroEncoder encoder; 318 void handleLeaf(I /*value*/, I card) { encoder.put(0, card); } 319 mixin Visitor!(false, false, true); 320 } 321 322 E minValue() nothrow @nogc 323 { 324 MinWriter writer; 325 S s = S.init; 326 writer.visit(s); 327 return writer.encoder.finish(); 328 } /// 329 330 private struct MaxWriter 331 { 332 Coder.RetroEncoder encoder; 333 void handleLeaf(I /*value*/, I card) { encoder.put(cast(I)(card - 1), card); } 334 mixin Visitor!(false, false, true); 335 } 336 337 E maxValue() nothrow @nogc 338 { 339 MaxWriter writer; 340 S s = S.init; 341 writer.visit(s); 342 return writer.encoder.finish(); 343 } /// 344 345 E cardinality() nothrow @nogc { return (maxValue - minValue) + 1; } 346 } 347 348 /// 349 debug(ae_unittest) unittest 350 { 351 static struct WithMax(T, T max_) 352 { 353 T value; 354 alias value this; 355 enum T max = max_; 356 } 357 358 enum E { a, b, c } 359 alias D6 = WithMax!(uint, 6); 360 361 static struct S 362 { 363 ubyte a; 364 bool b; 365 E[3] e; 366 D6 d; 367 } 368 369 alias Coder = SerializationCoder!(MixedRadixCoder!(uint, ulong, true), S); 370 auto s = S(1, true, [E.a, E.b, E.c], D6(4)); 371 auto encoded = Coder.serialize(s); 372 assert(Coder.deserialize(encoded) == s); 373 assert(Coder.minValue() <= encoded && encoded <= Coder.maxValue()); 374 } 375 376 static if (is(SumType!int)) 377 debug(ae_unittest) unittest 378 { 379 struct A {} 380 struct B { bool v; } 381 struct C {} 382 alias S = SumType!(A, B, C); 383 384 alias Coder = SerializationCoder!(MixedRadixCoder!(uint, ulong, false), S); 385 static assert(Coder.cardinality == 4); 386 387 foreach (s; [S(A()), S(B(false)), S(B(true)), S(C())]) 388 assert(Coder.deserialize(Coder.serialize(s)) == s); 389 } 390 391 private struct MaybeDynamicArray(T, size_t size = -1) 392 { 393 static if (size == -1) 394 { 395 T[] items; 396 alias items this; 397 } 398 else 399 { 400 T[size] items; 401 size_t length; 402 void opOpAssign(string op : "~")(T item) { items[length++] = item; } 403 T[] opSlice() { return items[0 .. length]; } 404 } 405 }