1 /**
2     Some additional range 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.range;
10 
11 import std.algorithm : map;
12 import std.functional : unaryFun;
13 import std.meta : AliasSeq, staticMap;
14 import std.range :
15     chain,
16     ElementEncodingType,
17     ElementType,
18     hasLength,
19     hasSlicing,
20     iota,
21     isInputRange;
22 import std.range.primitives;
23 import std.traits : isSomeChar, rvalueOf;
24 import std.typecons : tuple, Tuple;
25 
26 /**
27     This range iterates over fixed-sized chunks of size chunkSize of a source
28     range. Source must be an input range. chunkSize must be greater than zero.
29 
30     See Also: `std.range.chunks`
31     Returns: Range of chunks, ie. `ElementType!Source[]`.
32 */
33 auto arrayChunks(Source)(Source range, in size_t chunkSize) if (isInputRange!Source)
34 {
35     alias Element = ElementType!Source;
36 
37     static struct ArrayChunks
38     {
39         private Source range;
40         private const size_t chunkSize;
41         private Element[] chunk;
42 
43         this(Source range, in size_t chunkSize)
44         {
45             this.range = range;
46             this.chunkSize = chunkSize;
47             this.popFront();
48         }
49 
50         void popFront()
51         {
52             if (range.empty)
53             {
54                 chunk = null;
55 
56                 return;
57             }
58 
59             chunk = new Element[chunkSize];
60 
61             foreach (i; 0 .. chunkSize)
62             {
63                 chunk[i] = range.front;
64 
65                 if (range.empty)
66                 {
67                     chunk = chunk[0 .. i + 1];
68                     break;
69                 }
70                 else
71                 {
72                     range.popFront();
73                 }
74             }
75         }
76 
77         @property Element[] front()
78         {
79             return chunk;
80         }
81 
82         @property bool empty()
83         {
84             return chunk is null;
85         }
86     }
87 
88     assert(chunkSize > 0, "chunkSize must be greater than zero");
89 
90     return ArrayChunks(range, chunkSize);
91 }
92 
93 ///
94 unittest
95 {
96     import std.array : array;
97     import std.range : iota;
98 
99     auto chunks = iota(10).arrayChunks(2);
100     assert(chunks.array == [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]);
101 }
102 
103 /**
104     Generate a range of `num` even-sized slices.
105 
106     Always returns a range of slices in contrast to `std.range.evenChunks`
107     which returns a range of `take`s.
108 
109     See Also: `std.range.evenChunks`
110     Returns: Range of even slices.
111 */
112 auto evenSlices(Source)(Source range, in size_t sliceCount)
113     if (isInputRange!Source && hasLength!Source && hasSlicing!Source)
114 {
115     assert(sliceCount > 0, "sliceCount must be positive");
116 
117     auto sliceSize = range.length / sliceCount;
118     auto numLargerSlices = range.length % sliceCount;
119 
120     return chain(
121         iota(numLargerSlices).map!(i => range[i*(sliceSize + 1) .. (i + 1)*(sliceSize + 1)]),
122         iota(numLargerSlices, sliceCount).map!(i => range[i*sliceSize + numLargerSlices .. (i + 1)*sliceSize + numLargerSlices]),
123     );
124 }
125 
126 ///
127 unittest
128 {
129     import std.algorithm : equal;
130     import std.range : iota;
131 
132     auto slices = iota(10).evenSlices(3);
133     assert(equal!equal(slices, [
134         [0, 1, 2, 3],
135         [4, 5, 6],
136         [7, 8, 9],
137     ]));
138 }
139 
140 /// Generate a tuple of tuples of chunkSize.
141 template chunks(size_t chunkSize)
142 {
143     auto chunks(T...)(T args) pure nothrow @safe if (args.length >= chunkSize)
144     {
145         return tuple(tuple(args[0 .. chunkSize]), chunks(args[chunkSize .. $]).expand);
146     }
147 
148     auto chunks(T...)(T args) pure nothrow @safe
149             if (0 < args.length && args.length < chunkSize)
150     {
151         return tuple(tuple(args[0 .. $]));
152     }
153 
154     auto chunks(T...)(T args) pure nothrow @safe if (args.length == 0)
155     {
156         return tuple();
157     }
158 }
159 
160 ///
161 unittest
162 {
163     auto c1 = chunks!2(0, 1, 2, 3, 4, 5);
164 
165     assert(c1 == tuple(tuple(0, 1), tuple(2, 3), tuple(4, 5)));
166 
167     auto c2 = chunks!3(false, "1", 2.0, 3, '4', 5);
168 
169     assert(c2 == tuple(tuple(false, "1", 2.0), tuple(3, '4', 5)));
170 
171     enum c4 = chunks!4(false, "1", 2.0, 3, '4', 5);
172 
173     static assert(c4 == tuple(tuple(false, "1", 2.0, 3), tuple('4', 5)));
174 }
175 
176 /// Split a list of aliases into chunks.
177 template Chunks(size_t chunkSize, T...)
178 {
179     static if (T.length >= chunkSize)
180     {
181         alias Chunks = AliasSeq!(Chunk!(T[0 .. chunkSize]), Chunks!(chunkSize, T[chunkSize .. $]));
182     }
183     else static if (0 < T.length && T.length < chunkSize)
184     {
185         alias Chunks = AliasSeq!(Chunk!(T[0 .. $]));
186     }
187     else static if (T.length == 0)
188     {
189         alias Chunks = AliasSeq!();
190     }
191     else
192     {
193         static assert(0);
194     }
195 }
196 
197 template Chunk(T...)
198 {
199     struct Chunk
200     {
201         alias chunks = T;
202     }
203 }
204 
205 ///
206 unittest
207 {
208     alias c1 = Chunks!(2, AliasSeq!(int, int, int, int, int, int));
209 
210     static assert(is(c1 == AliasSeq!(Chunk!(int, int), Chunk!(int, int), Chunk!(int, int))));
211     static foreach (pair; c1)
212     {
213         static foreach (type; pair.chunks)
214         {
215             static assert(is(type == int));
216         }
217     }
218 }
219 
220 /*
221     Build a comparator according to `pred`.
222 */
223 template Comparator(pred...) if (pred.length == 1)
224 {
225     /// Return comparison value akin to `opCmp`.
226     int compare(S, T = S)(in S a, in T b)
227     {
228         alias _pred = unaryFun!pred;
229         auto _a = _pred(a);
230         auto _b = _pred(b);
231 
232         if (_a < _b)
233             return -1;
234         else if (_a == _b)
235             return 0;
236         else
237             return 1;
238     }
239 
240     /// Return `true` iff `a < b`.
241     bool lt(S, T = S)(in S a, in T b)
242     {
243         return compare!(S, T)(a, b) < 0;
244     }
245 
246     /// Return `true` iff `a <= b`.
247     bool le(S, T = S)(in S a, in T b)
248     {
249         return compare!(S, T)(a, b) <= 0;
250     }
251 
252     /// Return `true` iff `a == b`.
253     bool eq(S, T = S)(in S a, in T b)
254     {
255         return compare!(S, T)(a, b) == 0;
256     }
257 
258     /// Return `true` iff `a >= b`.
259     bool ge(S, T = S)(in S a, in T b)
260     {
261         return compare!(S, T)(a, b) >= 0;
262     }
263 
264     /// Return `true` iff `a > b`.
265     bool gt(S, T = S)(in S a, in T b)
266     {
267         return compare!(S, T)(a, b) > 0;
268     }
269 }
270 
271 ///
272 unittest
273 {
274     alias compareSquares = Comparator!"a ^^ 2".compare;
275 
276     assert(compareSquares(1, 2) < 0);
277     assert(compareSquares(1, -2) < 0);
278     assert(compareSquares(-1, 1) == 0);
279     assert(compareSquares(-2.0, 1) > 0);
280 
281     alias compareByLength = Comparator!"a.length".compare;
282 
283     assert(compareByLength([], [1]) < 0);
284     assert(compareByLength([1, 2], [1]) > 0);
285     assert(compareByLength([1, 2], ["1", "2"]) == 0);
286 
287     alias compareAbsInts = Comparator!("a > 0 ? a : -a").compare!(int);
288 
289     assert(compareSquares(1, 2) < 0);
290     assert(compareSquares(1, -2) < 0);
291     assert(compareSquares(-1, 1) == 0);
292     assert(compareSquares(-2, 1) > 0);
293     static assert(!__traits(compiles, compareAbsInts(-2.0, 1.0)));
294 
295     alias ltSquared = Comparator!("a ^^ 2").lt;
296 
297     assert(ltSquared(1, 2));
298     assert(ltSquared(1, -2));
299     assert(!ltSquared(-2, -1));
300 
301     alias eqSquared = Comparator!("a ^^ 2").eq;
302 
303     assert(eqSquared(1, 1));
304     assert(eqSquared(1, -1));
305     assert(!eqSquared(1, 2));
306 }
307 
308 /// Take exactly `n` element from range. Throws an exception if range has not
309 /// enough  elements.
310 ///
311 /// Throws: Exception if range has less than `n` elements.
312 ElementType!R[n] takeExactly(size_t n, R)(R range) if (isInputRange!R)
313 {
314     import std.exception : enforce;
315 
316     ElementType!R[n] result;
317     size_t i = 0;
318 
319     while (!range.empty && i < n)
320     {
321         result[i++] = range.front;
322         range.popFront();
323     }
324 
325     enforce!Exception(i == n, "not enough elements");
326 
327     return result;
328 }
329 
330 ///
331 unittest
332 {
333     import std.exception : assertThrown;
334     import std.range : iota;
335 
336     static assert(is(typeof(iota(10).takeExactly!5) == int[5]));
337     assert(iota(10).takeExactly!5 == [0, 1, 2, 3, 4]);
338 
339     assertThrown!Exception(iota(2).takeExactly!5);
340 }
341 
342 class WrapLinesImpl(R)
343 {
344     R output;
345     size_t lineWidth;
346     size_t column;
347 
348     this(R output, size_t lineWidth)
349     {
350         this.output = output;
351         this.lineWidth = lineWidth;
352     }
353 
354     void put(inout(char) c)
355     {
356         import std.range.primitives;
357 
358         assert(c == '\n' || column < lineWidth);
359 
360         std.range.primitives.put(output, c);
361         ++column;
362 
363         if (c == '\n')
364         {
365             column = 0;
366         }
367 
368         if (column >= lineWidth)
369         {
370             put('\n');
371         }
372     }
373 
374     void put(string chunk)
375     {
376         foreach (c; chunk)
377         {
378             put(c);
379         }
380     }
381 }
382 
383 auto wrapLines(R)(R output, size_t lineWidth)
384 {
385     return new WrapLinesImpl!R(output, lineWidth);
386 }
387 
388 unittest
389 {
390     import std.range.primitives : put;
391 
392     auto outputBuffer = new dchar[12];
393     auto output = wrapLines(outputBuffer, 10);
394 
395     put(output, "hello world");
396 
397     assert(outputBuffer == "hello worl\nd");
398 }
399 
400 /// Return a tuple of `fun` applied to each value of `tuple`.
401 auto tupleMap(alias fun, Types...)(Types values)
402 {
403     import std.format : format;
404 
405     alias mapper = unaryFun!fun;
406 
407     enum makeTuple = format!`tuple(%-(mapper(values[%d])%|, %))`(iota(values.length));
408 
409     return mixin(makeTuple);
410 }
411 
412 ///
413 unittest
414 {
415     import std.conv : to;
416     import std.typecons : tuple;
417 
418     assert(
419         tupleMap!"2*a"(1, 2, 3.0) ==
420         tuple(2, 4, 6.0)
421     );
422     assert(
423         tupleMap!(x => to!string(x))(1, '2', 3.0) ==
424         tuple("1", "2", "3")
425     );
426 }
427 
428 
429 /// Wraps given input range and keeps track of the line number and column.
430 auto trackLineLocation(R)(R input, size_t line = 1, size_t column = 1) pure nothrow @safe @nogc
431     if (isInputRange!R && isSomeChar!(ElementEncodingType!R))
432 {
433     static struct LineLocationTracker
434     {
435         R input;
436         size_t line;
437         size_t column;
438 
439 
440         void popFront()
441         {
442             assert(!empty, "Attempting to popFront an empty " ~ typeof(this).stringof);
443 
444             if (front == '\n')
445             {
446                 ++line;
447                 column = 0;
448             }
449 
450             ++column;
451             input.popFront();
452         }
453 
454 
455         @property auto front()
456         {
457             assert(!empty, "Attempting to fetch the front of an empty " ~ typeof(this).stringof);
458 
459             return input.front;
460         }
461 
462 
463         @property bool empty()
464         {
465             return input.empty;
466         }
467 
468 
469         static if (hasLength!R)
470             @property auto length()
471             {
472                 return input.length;
473             }
474     }
475 
476     return LineLocationTracker(input, line, column);
477 }
478 
479 ///
480 unittest
481 {
482     import std.array;
483     import std.range;
484 
485     enum testDocLines = [
486         "line 1",
487         "line 2",
488         "line 3",
489         "line 4",
490     ];
491     auto testDoc = testDocLines.join('\n') ~ '\n';
492 
493     auto tracker = trackLineLocation(refRange(&testDoc));
494     // default tracker starts at line 1 column 1
495     assert(tracker.line == 1);
496     assert(tracker.column == 1);
497 
498     // read to end of first line
499     tracker.popFrontExactly(testDocLines[0].length);
500     // still on first line but column changed to last column in the line
501     // which is the newline itself
502     assert(tracker.line == 1);
503     assert(tracker.column == testDocLines[0].length + 1);
504 
505     // pop the newline
506     tracker.popFront();
507     // now we are on line 2 column 1
508     assert(tracker.line == 2);
509     assert(tracker.column == 1);
510 
511     // read until the end
512     tracker.popFrontN(testDoc.length);
513     assert(tracker.line == 5);
514     assert(tracker.column == 1);
515 }