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 }