1 module marty.hash;
2 
3 import std.typecons : Nullable;
4 import std.algorithm : sort;
5 import std.algorithm.iteration : map, filter, uniq;
6 import std.array : array, assocArray;
7 import std.range : zip;
8 
9 /**
10  * A Hash object that is immutable with time travelling features.
11  */
12 immutable class Hash(K, V) {
13 public:
14     alias Value = Nullable!V;
15 
16     /**
17      * Initialize a hash with data from a standard hash
18      */
19     this(in V[K] data) pure {
20         Value[K] hash;
21         foreach(K key, V value; data) {
22             hash[key] = Value(value);
23         }
24         _data = cast(immutable)hash.dup;
25         _previousVersion = null;
26     }
27 
28     ///
29     unittest {
30         immutable data = ["foo": 1];
31         immutable subject = new immutable(Hash!(string, int))(data);
32     }
33 
34     /**
35      * Create a new hash with the passed in value set.
36      * Returns: New updated hash object with value inserted.
37      */
38     auto insert(in K key, in V value) pure nothrow {
39         immutable(Value[K]) newHash = [key: Value(value)];
40         return new immutable(Hash!(K, V))(newHash, this);
41     }
42 
43     ///
44     unittest {
45         immutable data = ["foo": V(1)];
46         immutable subject = new immutable(Hash!(string, int))(data);
47         auto result = subject.insert("bar", 2);
48     }
49 
50     /**
51      * Fetch a value from the hash. Raise an error if the key does not exist
52      * in the hash.
53      */
54     Value opIndex(in K key) @nogc pure nothrow  {
55         auto ptr = key in _data;
56         return ptr ? *ptr : _previousVersion[key];
57     }
58 
59     ///
60     unittest {
61         immutable data = ["foo": V(1)];
62         immutable subject = new immutable(Hash!(string, int))(data);
63         auto result = subject.insert("bar", 2);
64         assert(result["bar"] == 2);
65         assert(result["foo"] == 1);
66     }
67 
68     /**
69      * Return the previous state of the hash.
70      */
71     auto rollBack() @nogc pure nothrow {
72         return _previousVersion;
73     }
74 
75     ///
76     unittest {
77         immutable data = ["foo": 1];
78         immutable subject = new immutable(Hash!(K, V))(data);
79         auto result = subject.insert("foo", 2).rollBack;
80         assert(result["foo"] == 1);
81     }
82 
83     /**
84      * Returns an updated hash with a key value removed from the hash.
85      */
86     auto remove(in K key) pure nothrow {
87         Value value;
88         immutable(Value[K]) newHash = [key: value];
89         return new immutable(Hash!(K, V))(newHash, this);
90     }
91 
92     ///
93     unittest {
94         immutable data = ["foo": 1];
95         immutable subject = new immutable(Hash!(string, int))(data);
96         auto result = subject.remove("foo");
97         assert(result["foo"].isNull);
98     }
99 
100     /**
101      * Returns all the keys in the hash which have a non null value.
102      */
103     K[] keys() pure nothrow {
104         return allKeys.filter!(k => !this[k].isNull).array;
105     }
106 
107     ///
108     unittest {
109         {
110             immutable data = ["foo": 1];
111             immutable subject = new immutable(Hash!(string, int))(data);
112             assert(subject.keys[0] == "foo");
113             assert(subject.keys.length == 1);
114         }
115 
116         {
117             immutable data = ["foo": 1];
118             immutable subject = new immutable(Hash!(string, int))(data)
119               .insert("bar", 2);
120             assert(subject.keys[0] == "bar");
121             assert(subject.keys[1] == "foo");
122         }
123 
124         {
125             immutable data = ["foo": 1];
126             immutable subject = new immutable(Hash!(string, int))(data)
127               .insert("bar", 2)
128               .remove("bar");
129             assert(subject.keys[0] == "foo");
130             assert(subject.keys.length == 1);
131         }
132 
133         {
134             immutable data = ["foo": 1];
135             immutable subject = new immutable(Hash!(string, int))(data)
136               .insert("bar", 2)
137               .remove("foo");
138             assert(subject.keys[0] == "bar");
139             assert(subject.keys.length == 1);
140         }
141 
142         {
143             immutable data = ["foo": 1];
144             immutable subject = new immutable(Hash!(string, int))(data)
145               .insert("foo", 2);
146             assert(subject.keys[0] == "foo");
147             assert(subject.keys.length == 1);
148         }
149     }
150 
151     /**
152      * Returns an array of all the values in the hash.
153      */
154     V[] values() pure nothrow {
155         return keys
156             .map!(k => this[k].get)
157             .array;
158     }
159 
160     ///
161     unittest {
162         {
163             immutable data = ["foo": 1];
164             immutable subject = new immutable(Hash!(string, int))(data)
165               .insert("bar", 2);
166             assert(subject.values[0] == 2);
167             assert(subject.values[1] == 1);
168         }
169     }
170 
171     /**
172      * Makes foreach work on this class.
173      */
174     int opApply(int delegate(K key, V value) dg) {
175         auto hash = zip(keys, values).assocArray;
176         foreach(K key, V value; hash) {
177             dg(key, value);
178         }
179         return 0;
180     }
181 
182     ///
183     unittest {
184         immutable data = ["foo": 1];
185         immutable base = new immutable(Hash!(string, int))(data);
186         auto subject = base.insert("bar", 2);
187 
188         V[] values;
189         K[] keys;
190 
191         foreach(K key, V value; subject) {
192             values[values.length++] = value;
193             keys[keys.length++] = key;
194         }
195 
196         assert(values.length == 2);
197         assert(keys.length == 2);
198     }
199 
200     ///
201     unittest {
202         immutable data = ["foo": 1];
203         immutable base = new immutable(Hash!(string, int))(data);
204         auto subject = base.insert("bar", 2).insert("foo", 3);
205 
206         V[] values;
207         K[] keys;
208 
209         foreach(K key, V value; subject) {
210             values[values.length++] = value;
211             keys[keys.length++] = key;
212         }
213 
214         assert(values.length == 2);
215         assert(keys.length == 2);
216     }
217 
218     /**
219      * Compacts the storage hash to speed up lookups.
220      */
221     auto compact() pure {
222         Value[K] hash = zip(keys, values.map!(v => Value(v))).assocArray;
223         immutable Value[K] newHash = cast(immutable)hash.dup;
224         return new immutable(Hash!(K, V))(newHash, this);
225     }
226 
227     ///
228     unittest {
229         immutable data = ["foo": 1];
230         immutable subject = new immutable(Hash!(string, int))(data);
231         auto result = subject.insert("bar", 2).compact;
232         assert(result._data["foo"] == 1);
233         assert(result["foo"] == 1);
234         assert(result._data["bar"] == 2);
235         assert(result["bar"] == 2);
236     }
237 
238     /**
239      * Purge the previous version from history.
240      */
241     auto purge() pure {
242         V[K] hash = zip(keys, values).assocArray;
243         immutable V[K] newHash = cast(immutable)hash.dup;
244         return new immutable(Hash!(K, V))(newHash);
245     }
246 
247     ///
248     unittest {
249         immutable data = ["foo": 1];
250         immutable subject = new immutable(Hash!(string, int))(data);
251         auto result = subject.insert("bar", 2).purge;
252         assert(result._data["foo"] == 1);
253         assert(result["foo"] == 1);
254         assert(result._data["bar"] == 2);
255         assert(result["bar"] == 2);
256 
257         assert(result._previousVersion is null);
258     }
259 
260 private:
261 
262     Value[K] _data;
263     Hash!(K, V) _previousVersion;
264 
265     this(immutable(Value[K]) data, immutable(Hash!(K, V)) previousVersion) pure nothrow {
266         _data = data;
267         _previousVersion = cast(immutable)previousVersion;
268     }
269 
270     /**
271      * Returns an array of all the keys that have ever been set in the hash.
272      */
273     K[] allKeys() pure nothrow {
274         if (_previousVersion) {
275             return sort(_data.keys ~ _previousVersion.allKeys).uniq.array;
276         }
277         else {
278             return _data.keys;
279         }
280     }
281 }
282 
283 unittest {
284     alias Foo = immutable(Hash!(string, int));
285 }