1 /**
2     Create a new range concatenating input range/value with any additional ranges and/or values.
3 */
4 module ddash.algorithm.concat;
5 
6 ///
7 unittest {
8     import std.range: iota;
9 
10     // Concat stuff
11     assert([1, 2, 3].concat(4, [5], [6, 7], 8).equal(1.iota(9)));
12 
13     // Concat single element
14     assert([1].concat(2).equal([1, 2]));
15 
16     // Implicitly convertible doubles with ints
17     assert([1.0].concat([2, 3]).equal([1.0, 2.0, 3.0]));
18 
19     // Concat nothing to single value
20     assert(1.concat().equal([1]));
21 
22     // Concat nothing to range
23     assert([1].concat().equal([1]));
24 
25     // Concat values to another value
26     assert(1.concat(2, 3).equal([1, 2, 3]));
27 
28     // Concat ranges or values to another value
29     assert(1.concat(2, [3, 4]).equal([1, 2, 3, 4]));
30 
31     // Concat strings
32     assert("yo".concat("dles").equal("yodles"));
33 
34     // Concat stuff to string
35     assert("abc".concat(1, 2, 3).equal("abc123"));
36 }
37 
38 
39 import ddash.common;
40 
41 /**
42     Concats everything together using best effort.
43 
44     It will concat as long as there is a common type between all sets of inputs. When an input
45     is a range it will use `ElementType` as it's type.
46 
47     If the first input is a string, all subsequent inputs are converted to a string as well
48 
49     Params:
50         values = set of values that share a common type.
51 
52     Returns:
53         A range of common type or a string if the first value is a string
54 
55     Benchmarks:
56         Bottom line, concat (which uses a static foreach) is faster than eager and
57         recursive approach both with and without .array.
58 
59         If you are dealing only with pure strings, however, then appender is your
60         friend
61 
62         ---
63         Benchmarking concat against eager, recursive, and standard lib
64         the (array) versions involve a call to .array
65         concat single arguments:
66           concat:                1 hnsec
67           concatRecurse:         5 ms, 921 μs, and 6 hnsecs
68           concatEager:           5 ms, 417 μs, and 5 hnsecs
69           concat        (array): 1 ms and 372 μs
70           concatRecurse (array): 7 ms, 587 μs, and 2 hnsecs
71           concatEager   (array): 5 ms, 968 μs, and 2 hnsecs
72         concat multiple ranges together
73           concat:                1 ms, 491 μs, and 3 hnsecs
74           concatRecurse:         2 ms, 195 μs, and 3 hnsecs
75           concatEager:           7 ms, 787 μs, and 2 hnsecs
76           concat        (array): 3 ms, 174 μs, and 2 hnsecs
77           concatRecurse (array): 2 ms, 816 μs, and 7 hnsecs
78           concatEager   (array): 6 ms, 838 μs, and 1 hnsec
79         concat single args and multiple ranges:
80           concat:                1 ms, 858 μs, and 1 hnsec
81           concatRecurse:         3 ms, 479 μs, and 2 hnsecs
82           concatEager:           7 ms, 374 μs, and 9 hnsecs
83           concat        (array): 3 ms, 358 μs, and 5 hnsecs
84           concatRecurse (array): 5 ms, 558 μs, and 1 hnsec
85           concatEager   (array): 6 ms, 997 μs, and 3 hnsecs
86         concat strings and chars:
87           concat:                 0 hnsecs
88           concatRecurse:          1 ms, 368 μs, and 8 hnsecs
89           concat         (array): 4 ms, 238 μs, and 3 hnsecs
90           concatRecurse  (array): 5 ms, 77 μs, and 9 hnsecs
91         concat strings vs appender and join
92           concat:                 0 hnsecs
93           join:                   6 ms, 930 μs, and 4 hnsecs
94           appender:               3 ms, 186 μs, and 1 hnsec
95           concat    (array):      4 ms, 917 μs, and 6 hnsecs
96           join      (array):      9 ms, 243 μs, and 7 hnsecs
97           appender  (array):      3 ms, 57 μs, and 8 hnsecs
98         ---
99 
100     Since:
101         0.0.1
102 */
103 auto concat(Values...)(Values values) if (from!"bolts.traits".areCombinable!Values) {
104     import std.range: isInputRange, chain, only;
105     import std.traits: isNarrowString, isSomeChar;
106     import std.conv: to;
107 
108     static if (!Values.length) { // If none we're done
109         return;
110     } else static if (Values.length == 1) { // If one we return it as a range
111         static if (isInputRange!(Values[0])) {
112             return values[0];
113         } else {
114             return only(values[0]);
115         }
116     } else {
117         alias Head = Values[0];
118         alias Rest = Values[1..$];
119         // If head is a string start with a string, if it's a range start a chain, if it's a value create a range
120         static if (isNarrowString!(Head)) {
121             auto link0 = values[0];
122         } else {
123             static if (isInputRange!(Head)) {
124                 auto link0 = chain(values[0]);
125             } else {
126                 auto link0 = only(values[0]);
127             }
128         }
129 
130         // Declare a variable called `linkX` equal to the previous link chained with an input range
131         static string link(int i, string range) {
132             return "auto link" ~ i.to!string ~ " = link" ~ (i - 1).to!string
133                  ~ ".chain(" ~ range ~ ");";
134         }
135 
136         static foreach (I; 1 .. Values.length) {
137             static if (isNarrowString!Head) {
138                 static if (isNarrowString!(Values[I])) {
139                     mixin(link(I, q{ values[I] }));
140                 } else static if (isInputRange!(Values[I])) {
141                     // If you don't rename you get a conflict if std.array is included
142                     // becuase that has .join as well (And we include it in the unittest version)
143                     //
144                     // Interestingly, "import ddash.algorithm: myJoin = join" does not work either
145                     import ddash.algorithm: stringify;
146                     mixin(link(I, q{ values[I].stringify }));
147                 } else static if (isSomeChar!(Values[I])) {
148                     mixin(link(I, q{ only(values[I]) }));
149                 } else {
150                     mixin(link(I, q{ values[I].to!string }));
151                 }
152             } else {
153                 static if (isInputRange!(Values[I])) {
154                     mixin(link(I, q{ values[I] }));
155                 } else {
156                     mixin(link(I, q{ only(values[I]) }));
157                 }
158             }
159         }
160         mixin("return link" ~ (Values.length - 1).to!string ~ ";");
161     }
162 }
163 
164 unittest {
165     import std.array;
166 
167     int i = 1;
168     double d = 2.2;
169     char c = 'c';
170     string s = "oo";
171 
172     auto a = concat(i, d, c, [i, i]).array;
173     assert(a == [i, d, c, i, i]);
174     static assert(is(typeof(a) == double[]));
175 
176     import std.algorithm: filter;
177 
178     assert(concat(i, i, i).equal([i, i, i]));
179     assert([i, i].concat([i, i]).equal([i, i, i, i]));
180     assert([i, i].filter!"true".concat([i, i], i, i).equal([i, i, i, i, i, i]));
181     assert([i, i].concat(d, c, i).equal([i, i, d, c, i]));
182     assert(d.concat(i, c, i).equal([d, i, c, i]));
183     assert(c.concat(d, i).equal([c, d, i]));
184     assert([c, c].concat([d, d], 2).equal("cc2.22.22"));
185     assert(s.concat([c, c], c).equal("ooccc"));
186     assert(s.concat(s, [s]).equal("oooooo"));
187 }
188 
189 unittest {
190     import std.algorithm: filter;
191     import std.range: only;
192     // Make sure if it's a single range the same type is returned
193     auto a = [1, 2];
194     auto b = [1].filter!"true";
195     static assert(is(typeof(concat(a)) == typeof(a)));
196     static assert(is(typeof(concat(b)) == typeof(b)));
197     static assert(is(typeof(concat(1)) == typeof(only(1))));
198 }