1 /**
2  * ae.utils.math.mixed_radix_coding
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 // TODO: Find what this thing is actually called.
17 /// A mixed-radix number coding system.
18 template MixedRadixCoder(
19 	/// Numeric type for decoded items.
20 	I,
21 	/// Numeric type for encoded result.
22 	E,
23 	/// Use an encoding with an explicit end of items.
24 	bool withEOF = false,
25 )
26 {
27 	/// This encoding system is LIFO, so the encoder buffers all items
28 	/// until `.finish` is called.
29 	struct Encoder(
30 		/// Maximum number of encoded items.
31 		size_t maxItems,
32 	)
33 	{
34 	private:
35 		struct Item { I n, card; }
36 		MaybeDynamicArray!(Item, maxItems) items;
37 
38 	public:
39 		/// Encode one item.
40 		/// `card` is the item's cardinality, i.e. one past its maximum.
41 		void put(I n, I card)
42 		in(0 <= n && n < card)
43 		{
44 			items ~= Item(n, card);
45 		}
46 
47 		/// Finish encoding and return the encoded result.
48 		E finish()
49 		{
50 			E result = withEOF ? 1 : 0;
51 			foreach_reverse (ref item; items)
52 			{
53 				result *= item.card;
54 				result += item.n;
55 			}
56 			return result;
57 		}
58 	}
59 
60 	/// As above. This will allocate the items dynamically.
61 	alias VariableLengthEncoder = Encoder!(-1);
62 
63 	/// Like `Encoder`, but does not use a temporary buffer.
64 	/// Instead, the user is expected to put the items in reverse order.
65 	struct RetroEncoder
66 	{
67 	private:
68 		E encoded = withEOF ? 1 : 0;
69 
70 	public:
71 		void put(I n, I card)
72 		{
73 			assert(0 <= n && n < card);
74 			encoded *= card;
75 			encoded += n;
76 		} ///
77 
78 		E finish()
79 		{
80 			return encoded;
81 		} ///
82 	}
83 
84 	/// The decoder.
85 	struct Decoder
86 	{
87 	private:
88 		E encoded;
89 
90 	public:
91 		this(E encoded)
92 		{
93 			this.encoded = encoded;
94 			static if (withEOF)
95 				assert(encoded > 0);
96 		} ///
97 
98 		I get(I card)
99 		in(card > 0)
100 		{
101 			I value = encoded % card;
102 			encoded /= card;
103 			static if (withEOF)
104 				assert(encoded > 0, "Decoding error");
105 			return value;
106 		} ///
107 
108 		static if (withEOF)
109 		@property bool empty() const { return encoded == 1; } ///
110 	}
111 }
112 
113 ///
114 unittest
115 {
116 	alias Coder = MixedRadixCoder!(uint, uint, true);
117 	Coder.Encoder!2 encoder;
118 	encoder.put(5, 8);
119 	encoder.put(1, 2);
120 	auto result = encoder.finish();
121 
122 	auto decoder = Coder.Decoder(result);
123 	assert(decoder.get(8) == 5);
124 	assert(decoder.get(2) == 1);
125 	assert(decoder.empty);
126 }
127 
128 unittest
129 {
130 	import std.meta : AliasSeq;
131 	import std.traits : EnumMembers;
132 	import std.exception : assertThrown;
133 	import core.exception : AssertError;
134 
135 	alias I = uint;
136 	alias E = uint;
137 
138 	enum Mode { dynamicSize, staticSize, retro }
139 	foreach (mode; EnumMembers!Mode)
140 		foreach (withEOF; AliasSeq!(false, true))
141 		{
142 			void testImpl()
143 			{
144 				alias Coder = MixedRadixCoder!(I, E, withEOF);
145 
146 				static if (mode == Mode.retro)
147 				{
148 					Coder.RetroEncoder encoder;
149 					encoder.put(1, 2);
150 					encoder.put(5, 8);
151 				}
152 				else
153 				{
154 					static if (mode == Mode.dynamicSize)
155 						Coder.VariableLengthEncoder encoder;
156 					else
157 						Coder.Encoder!2 encoder;
158 
159 					encoder.put(5, 8);
160 					encoder.put(1, 2);
161 				}
162 				auto result = encoder.finish();
163 
164 				auto decoder = Coder.Decoder(result);
165 				static if (withEOF) assert(!decoder.empty);
166 				assert(decoder.get(8) == 5);
167 				static if (withEOF) assert(!decoder.empty);
168 				assert(decoder.get(2) == 1);
169 				static if (withEOF) assert(decoder.empty);
170 
171 				static if (withEOF)
172 				{
173 					debug assertThrown!AssertError(decoder.get(42));
174 				}
175 				else
176 					assert(decoder.get(42) == 0);
177 			}
178 			static if (mode == Mode.dynamicSize)
179 				testImpl();
180 			else
181 			{
182 				@nogc void test() { testImpl(); }
183 				test();
184 			}
185 		}
186 }
187 
188 /// Serializes structs and static arrays using a `MixedRadixCoder`.
189 /// Consults types' `.max' property to obtain cardinality.
190 template SerializationCoder(alias Coder, S)
191 {
192 	private alias I = typeof(Coder.Decoder.init.get(0));
193 	private alias E = typeof(Coder.RetroEncoder.init.finish());
194 
195 	private struct Serializer
196 	{
197 		Coder.RetroEncoder encoder;
198 
199 		void put(T)(auto ref const T value)
200 		{
201 			static if (is(T : I) && is(typeof(T.max) : I))
202 			{
203 				I max = T.max;
204 				I card = max; card++;
205 				assert(card > max, "Overflow");
206 				encoder.put(value, card);
207 			}
208 			else
209 			static if (is(T == struct))
210 				foreach_reverse (const ref field; value.tupleof)
211 					put(field);
212 			else
213 			static if (is(T == Q[N], Q, size_t N))
214 				foreach_reverse (ref item; value)
215 					put(item);
216 			else
217 				static assert(false, "Don't know how to serialize " ~ T.stringof);
218 		}
219 	}
220 
221 	E serialize()(auto ref const S s)
222 	{
223 		Serializer serializer;
224 		serializer.put(s);
225 		return serializer.encoder.finish();
226 	} ///
227 
228 	private struct Deserializer
229 	{
230 		Coder.Decoder decoder;
231 
232 		void get(T)(ref T value)
233 		{
234 			static if (is(T : I) && is(typeof(T.max) : I))
235 			{
236 				I max = T.max;
237 				I card = max; card++;
238 				assert(card > max, "Overflow");
239 				value = cast(T)decoder.get(card);
240 			}
241 			else
242 			static if (is(T == struct))
243 				foreach (ref field; value.tupleof)
244 					get(field);
245 			else
246 			static if (is(T == Q[N], Q, size_t N))
247 				foreach (ref item; value)
248 					get(item);
249 			else
250 				static assert(false, "Don't know how to deserialize " ~ T.stringof);
251 		}
252 	}
253 
254 	S deserialize(E encoded)
255 	{
256 		Deserializer deserializer;
257 		deserializer.decoder = Coder.Decoder(encoded);
258 		S result;
259 		deserializer.get(result);
260 		static if (__traits(hasMember, deserializer.decoder, "empty"))
261 			assert(deserializer.decoder.empty);
262 		return result;
263 	} ///
264 }
265 
266 ///
267 unittest
268 {
269 	static struct WithMax(T, T max_)
270 	{
271 		T value;
272 		alias value this;
273 		enum T max = max_;
274 	}
275 
276 	enum E { a, b, c }
277 	alias D6 = WithMax!(uint, 6);
278 
279 	static struct S
280 	{
281 		ubyte a;
282 		bool b;
283 		E[3] e;
284 		D6 d;
285 	}
286 
287 	alias Coder = SerializationCoder!(MixedRadixCoder!(uint, ulong, true), S);
288 	auto s = S(1, true, [E.a, E.b, E.c], D6(4));
289 	assert(Coder.deserialize(Coder.serialize(s)) == s);
290 }
291 
292 private struct MaybeDynamicArray(T, size_t size = -1)
293 {
294 	static if (size == -1)
295 	{
296 		T[] items;
297 		alias items this;
298 	}
299 	else
300 	{
301 		T[size] items;
302 		size_t length;
303 		void opOpAssign(string op : "~")(T item) { items[length++] = item; }
304 		T[] opSlice() { return items[0 .. length]; }
305 	}
306 }