1 /**
2     Some additional alogorithm functions.
3 
4     Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de>
5     License: Subject to the terms of the MIT license, as written in the
6              included LICENSE file.
7     Authors: Arne Ludwig <arne.ludwig@posteo.de>
8 */
9 module dalicious.algorithm.iteration;
10 
11 import std.algorithm :
12     copy,
13     countUntil,
14     min,
15     OpenRight,
16     uniq;
17 import std.conv : to;
18 import std.functional : binaryFun, unaryFun;
19 import std.traits : isDynamicArray;
20 import std.typecons : Yes;
21 import std.range.primitives;
22 
23 
24 /**
25     Slices an input array into slices of equivalent adjacent elements.
26     In other languages this is often called `partitionBy`, `groupBy`
27     or `sliceWhen`.
28 
29     Equivalence is defined by the predicate `pred`, which can be binary,
30     which is passed to `std.functional.binaryFun`. Two range elements
31     `a` and `b` are considered equivalent if `pred(a,b)` is true.
32 
33     This predicate must be an equivalence relation, that is, it must be
34     reflexive (`pred(x,x)` is always true), symmetric
35     (`pred(x,y) == pred(y,x)`), and transitive (`pred(x,y) && pred(y,z)`
36     implies `pred(x,z)`). If this is not the case, the range returned by
37     sliceBy may assert at runtime or behave erratically.
38 
39     Params:
40      pred  = Predicate for determining equivalence.
41      array = An array to be sliced.
42 
43     Returns: With a binary predicate, a range of slices is returned in which
44     all elements in a given slice are equivalent under the given predicate.
45 
46     Notes:
47 
48     Equivalent elements separated by an intervening non-equivalent element will
49     appear in separate subranges; this function only considers adjacent
50     equivalence. Elements in the subranges will always appear in the same order
51     they appear in the original range.
52 */
53 auto sliceBy(alias pred, Array)(Array array) pure nothrow
54         if (isDynamicArray!Array)
55 {
56     return SliceByImpl!(pred, Array)(array);
57 }
58 
59 ///
60 unittest
61 {
62     import std.algorithm.comparison : equal;
63 
64     // Grouping by particular attribute of each element:
65     auto data = [
66         [1, 1],
67         [1, 2],
68         [2, 2],
69         [2, 3]
70     ];
71 
72     auto r1 = data.sliceBy!((a,b) => a[0] == b[0]);
73     assert(r1.equal([
74         data[0 .. 2],
75         data[2 .. 4]
76     ]));
77 
78     auto r2 = data.sliceBy!((a,b) => a[1] == b[1]);
79     assert(r2.equal([
80         data[0 .. 1],
81         data[1 .. 3],
82         data[3 .. 4],
83     ]));
84 }
85 
86 private struct SliceByImpl(alias pred, Array)
87         if (isDynamicArray!Array)
88 {
89     private alias equivalent = binaryFun!pred;
90 
91     private Array _array;
92     private size_t sliceStart;
93     private size_t sliceEnd;
94 
95     this(Array array)
96     {
97         this._array = array;
98 
99         if (!empty)
100         {
101             popFront();
102         }
103     }
104 
105     void popFront()
106     {
107         assert(!empty, "Attempting to popFront an empty SliceByImpl");
108 
109         sliceStart = sliceEnd++;
110 
111         if (empty)
112         {
113             return;
114         }
115 
116         auto refElement = _array[sliceStart];
117         while (sliceEnd < _array.length && equivalent(refElement, _array[sliceEnd]))
118         {
119             ++sliceEnd;
120         }
121     }
122 
123     @property bool empty() const pure nothrow
124     {
125         return sliceStart >= _array.length;
126     }
127 
128     @property auto front()
129     {
130         assert(!empty, "Attempting to fetch the front of an empty SliceByImpl");
131 
132         return _array[sliceStart .. sliceEnd];
133     }
134 
135     @property SliceByImpl!(pred, Array) save() const pure nothrow
136     {
137         return cast(typeof(return)) this;
138     }
139 }
140 
141 /// Return the prefix of `haystack` where `pred` is not satisfied.
142 Array sliceUntil(alias pred = "a == b", Array, Needle)(
143     Array haystack,
144     Needle needle,
145     OpenRight openRight = Yes.openRight,
146 )
147         if (isDynamicArray!Array)
148 {
149     alias predFun = binaryFun!pred;
150 
151     foreach (i, ref e; haystack)
152         if (predFun(e, needle))
153             return haystack[0 .. (openRight ? i : i + 1)];
154 
155     return haystack[0 .. $];
156 }
157 
158 /// ditto
159 Array sliceUntil(alias pred = "a", Array)(
160     Array haystack,
161     OpenRight openRight = Yes.openRight,
162 )
163         if (isDynamicArray!Array)
164 {
165     alias predFun = unaryFun!pred;
166 
167     foreach (i, ref e; haystack)
168         if (predFun(e))
169             return haystack[0 .. (openRight ? i : i + 1)];
170 
171     return haystack[0 .. $];
172 }
173 
174 ///
175 unittest
176 {
177     import std.typecons : No;
178 
179     int[] a = [ 1, 2, 4, 7, 7, 2, 4, 7, 3, 5];
180     assert(a.sliceUntil(7) == [1, 2, 4]);
181     assert(a.sliceUntil!"a == 7" == [1, 2, 4]);
182     assert(a.sliceUntil(7, No.openRight) == [1, 2, 4, 7]);
183 }
184 
185 /// Returns array filtered in-place.
186 auto ref Array filterInPlace(alias pred = "a", Array)(auto ref Array array) if (isDynamicArray!Array)
187 {
188     import std.algorithm : filter;
189 
190     auto bufferRest = array.filter!pred.copy(array);
191 
192     array.length -= bufferRest.length;
193 
194     return array;
195 }
196 
197 ///
198 unittest
199 {
200     alias isEven = n => n % 2 == 0;
201     auto arr = [1, 2, 2, 2, 3, 3, 4];
202 
203     assert(filterInPlace!isEven(arr) == [2, 2, 2, 4]);
204     // The input array gets modified.
205     assert(arr == [2, 2, 2, 4]);
206 
207     // Can be called with non-lvalues
208     assert(filterInPlace!isEven([1, 2, 2, 2, 3, 3, 4]) == [2, 2, 2, 4]);
209 }
210 
211 /// Returns array `uniq`ified in-place.
212 auto ref Array uniqInPlace(alias pred = "a == b", Array)(auto ref Array array) if (isDynamicArray!Array)
213 {
214     auto bufferRest = array.uniq.copy(array);
215 
216     array.length -= bufferRest.length;
217 
218     return array;
219 }
220 
221 ///
222 unittest
223 {
224     auto arr = [1, 2, 2, 2, 3, 3, 4];
225 
226     assert(uniqInPlace(arr) == [1, 2, 3, 4]);
227     // The input array gets modified.
228     assert(arr == [1, 2, 3, 4]);
229 
230     // Can be called with non-lvalues
231     assert(uniqInPlace([1, 2, 2, 2, 3, 3, 4]) == [1, 2, 3, 4]);
232 }
233 
234 /// Replaces the first occurrence of `needle` by `replacement` in `array` if
235 /// present. Modifies array.
236 Array replaceInPlace(alias pred = "a == b", Array, E)(auto ref Array array, E needle, E replacement)
237         if (isDynamicArray!Array)
238 {
239     auto i = array.countUntil!pred(needle);
240 
241     if (i < 0)
242         return array;
243 
244     array[i] = replacement;
245 
246     return array;
247 }
248 
249 ///
250 unittest
251 {
252     auto arr = [1, 2, 3, 4, 2, 3];
253 
254     assert(arr.replaceInPlace(2, 7) == [1, 7, 3, 4, 2, 3]);
255     // The input array gets modified.
256     assert(arr == [1, 7, 3, 4, 2, 3]);
257     // Replaces only the first occurrence
258     assert(arr.replaceInPlace(2, 7) == [1, 7, 3, 4, 7, 3]);
259 
260     // Can be called with non-lvalues
261     assert([1, 2, 3].replaceInPlace(2, 7) == [1, 7, 3]);
262 }
263 
264 /// Get the first element in range assuming it to be non-empty.
265 ElementType!Range first(Range)(Range range) if (isInputRange!Range)
266 {
267     assert(!range.empty, "must not call first on an empty range");
268 
269     return range.front;
270 }
271 
272 ///
273 unittest
274 {
275     assert(first([1, 2, 3]) == 1);
276     assert(first("abcd") == 'a');
277 }
278 
279 /// Get the last element in range assuming it to be non-empty.
280 ElementType!Range last(Range)(Range range) if (isInputRange!Range)
281 {
282     assert(!range.empty, "must not call last on an empty range");
283 
284     static if (isBidirectionalRange!Range)
285     {
286         return range.back;
287     }
288     else static if (hasLength!Range)
289     {
290         foreach (i; 0 .. range.length - 1)
291             range.popFront();
292 
293         return range.front;
294     }
295     else static if (isForwardRange!Range)
296     {
297         auto checkpoint = range;
298 
299         while (!range.empty)
300         {
301             checkpoint = range.save;
302             range.popFront();
303         }
304 
305         return checkpoint.front;
306     }
307     else
308     {
309         typeof(return) lastElement;
310 
311         while (!range.empty)
312         {
313             lastElement = range.front;
314             range.popFront();
315         }
316 
317         return lastElement;
318     }
319 
320 }
321 
322 ///
323 unittest
324 {
325     import std.algorithm : filter;
326     import std.range : take, takeExactly;
327 
328     struct PowersOfTwo(bool shouldSave)
329     {
330         size_t i = 1;
331 
332         void popFront() pure nothrow
333         {
334             i *= 2;
335         }
336 
337         @property size_t front() const pure nothrow
338         {
339             return i + 0;
340         }
341 
342         @property bool empty() const pure nothrow
343         {
344             return false;
345         }
346 
347         static if (shouldSave)
348         {
349             @property PowersOfTwo save() const pure nothrow
350             {
351                 return cast(typeof(return)) this;
352             }
353         }
354     }
355 
356     assert(last([1, 2, 3]) == 3);
357     assert(last(PowersOfTwo!true(1).takeExactly(5)) == 16);
358     assert(last(PowersOfTwo!true(1).take(5)) == 16);
359     assert(last(PowersOfTwo!false(1).take(5)) == 16);
360 }
361 
362 /**
363     Find an optimal solution using backtracking.
364 */
365 T[] backtracking(alias isFeasible, alias score, T)(
366     T[] candidates,
367     T[] solution = [],
368 )
369 {
370     auto optimalSolution = solution;
371     auto optimalScore = score(optimalSolution);
372 
373     foreach (i, candidate; candidates)
374     {
375         if (isFeasible(cast(const(T[])) solution ~ candidate))
376         {
377             auto newSolution = backtracking!(isFeasible, score)(
378                 candidates[0 .. i] ~ candidates[i + 1 .. $],
379                 solution ~ candidate,
380             );
381             auto newScore = score(cast(const(T[])) newSolution);
382 
383             if (newScore > optimalScore)
384                 optimalSolution = newSolution;
385         }
386     }
387 
388     return optimalSolution;
389 }
390 
391 
392 import std.algorithm : map;
393 
394 /// Cast elements to `const(char)`.
395 alias charRange = map!"cast(const char) a";