1 /** Extensions to skipOver.
2  *
3  * See_Also: https://forum.dlang.org/post/tswdobtabsjarszfkmbt@forum.dlang.org
4  * See_Also: https://forum.dlang.org/post/ybamybeakxwxwleebnwb@forum.dlang.org
5  */
6 module nxt.skip_ex;
7 
8 import std.functional : binaryFun;
9 import std.range.primitives : front, back, save, empty, popBack, hasSlicing, isBidirectionalRange, ElementType;
10 
11 version(unittest)
12 {
13     import nxt.array_help : s;
14 }
15 
16 /** Skip over the ending portion of `haystack` that matches `needle`, or nothing upon no match.
17  *
18  * See_Also: std.algorithm.searching.skipOver.
19  */
20 bool skipOverBack(Haystack, Needle)(scope ref Haystack haystack,
21                                     scope Needle needle) // non`const` because may be range with mutable range primitives
22 if (isBidirectionalRange!Haystack &&
23     isBidirectionalRange!Needle &&
24     is(typeof(haystack.back == needle.back)))
25 {
26     static if (is(typeof(haystack[] == needle) : bool) &&
27                is(typeof(needle.length > haystack.length) : bool) &&
28                is(typeof(haystack = haystack[])))
29     {
30         if (haystack.length >= needle.length &&
31             haystack[$ - needle.length .. $] == needle)
32         {
33             haystack = haystack[0 .. haystack.length - needle.length];
34             return true;
35         }
36         return false;
37     }
38     else
39     {
40         return skipOverBack!((a, b) => a == b)(haystack, needle);
41     }
42 }
43 
44 ///
45 bool skipOverBack(alias pred, Haystack, Needle)(scope ref Haystack haystack,
46                                                 scope Needle needle) // non`const` because may be range with mutable range primitives
47 if (isBidirectionalRange!Haystack &&
48     isBidirectionalRange!Needle &&
49     is(typeof(binaryFun!pred(haystack.back, needle.back)))) // TODO: Needle doesn't have to bi-directional if Haystack is RandomAccess and Needle.hasLength
50 {
51     import std.range.primitives : hasLength;
52     static if (hasLength!Haystack && hasLength!Needle)
53     {
54         if (haystack.length < needle.length) { return false; } // fast discardal
55     }
56     auto r = haystack.save;
57     while (!needle.empty &&
58            !r.empty &&
59            binaryFun!pred(r.back, needle.back))
60     {
61         r.popBack();
62         needle.popBack();
63     }
64     if (needle.empty)
65     {
66         haystack = r;
67     }
68     return needle.empty;
69 }
70 
71 ///
72 @safe pure nothrow @nogc unittest
73 {
74     auto s1_ = [1, 2, 3].s;
75     auto s1 = s1_[];
76     const s2_ = [2, 3].s;
77     const s2 = s2_[];
78     s1.skipOverBack(s2);
79     assert(s1 == [1].s);
80     s1.skipOverBack(s2);        // no effect
81     assert(s1 == [1].s);
82 }
83 
84 @safe pure nothrow @nogc unittest
85 {
86     import std.algorithm : equal;
87     auto s1 = "Hello world";
88     assert(!skipOverBack(s1, "Ha"));
89     assert(s1 == "Hello world");
90     assert(skipOverBack(s1, "world") && s1 == "Hello ");
91 }
92 
93 /** Variadic version of $(D skipOver).
94  *
95  * Returns: index + 1 into matching $(D needles), 0 otherwise.
96  *
97  * TODO: Reuse `skipOver with many needles` or write own array-version of `skipOver` that's faster.
98  */
99 size_t skipOverEither(alias pred = "a == b", Range, Ranges...)(scope ref Range haystack,
100                                                                scope Ranges needles)
101 if (Ranges.length >= 2)
102 {
103     import nxt.array_traits : isSameSlices;
104     foreach (const index, const ref needle; needles)
105     {
106         static if (pred == "a == b" &&
107                    isSameSlices!(Range, Ranges)) // fast
108         {
109             // `nothrow` char[] fast path
110             if (haystack.length >= needle.length &&
111                 haystack[0 .. needle.length] == needle) // TODO: `haystack.ptr`
112             {
113                 haystack = haystack[needle.length .. haystack.length]; // TODO: `haystack.ptr`
114                 return index + 1;
115             }
116         }
117         else
118         {
119             import std.algorithm.searching : skipOver;
120             if (haystack.skipOver(needle)) // TODO: nothrow
121                 return index + 1;
122         }
123     }
124     return 0;
125 }
126 
127 @safe pure nothrow /* TODO: nothrow @nogc */ unittest
128 {
129     import std.algorithm.searching : startsWith;
130     auto x = "beta version";
131     assert(x.startsWith("beta"));
132 }
133 
134 @safe pure /* TODO: nothrow @nogc */ unittest
135 {
136     import std.algorithm.searching : skipOver;
137     auto x = "beta version";
138     assert(x.skipOver("beta"));
139     assert(x == " version");
140 }
141 
142 @safe pure nothrow @nogc unittest
143 {
144     auto x = "beta version";
145     assert(x.skipOverEither("beta", "be") == 1);
146     assert(x == " version");
147 }
148 
149 @safe pure nothrow @nogc unittest
150 {
151     auto x = "beta version";
152     assert(x.skipOverEither("be", "_") == 1);
153 }
154 
155 @safe pure nothrow @nogc unittest
156 {
157     auto x = "beta version";
158     assert(x.skipOverEither("x", "y") == 0);
159 }
160 
161 @safe pure nothrow @nogc unittest
162 {
163 
164     auto x = "beta version";
165     assert(x.skipOverEither("x", "y") == 0);
166 }
167 
168 /** Skip Over Shortest Matching prefix in $(D needles) that prefixes $(D haystack).
169  *
170  * TODO: Make return value a specific type that has bool conversion so we can
171  * call it as
172  * if (auto hit = r.skipOverShortestOf(...)) { ... }
173  */
174 size_t skipOverShortestOf(alias pred = "a == b",
175                           Range,
176                           Ranges...)(scope ref Range haystack,
177                                      scope Ranges needles)
178 if (Ranges.length >= 2)
179 {
180     import std.algorithm.searching : startsWith;
181     const hit = startsWith!pred(haystack, needles);
182     if (hit)
183     {
184         // get needle lengths
185         size_t[needles.length] lengths;
186         foreach (const index, ref needle; needles)
187         {
188             import std.traits : isSomeString, isSomeChar;
189             import std.range.primitives : ElementType;
190             import core.internal.traits : Unqual;
191 
192             alias Needle = Unqual!(typeof(needle));
193 
194             static if (is(Unqual!Range ==
195                           Needle))
196             {
197                 lengths[index] = needle.length;
198             }
199             else static if (is(Unqual!(ElementType!Range) ==
200                                Unqual!(ElementType!Needle)))
201             {
202                 lengths[index] = needle.length;
203             }
204             else static if (isSomeString!Range &&
205                             isSomeString!Needle)
206             {
207                 lengths[index] = needle.length;
208             }
209             else static if (isSomeChar!(ElementType!Range) &&
210                             isSomeChar!Needle)
211             {
212                 lengths[index] = 1;
213             }
214             else static if (is(Unqual!(ElementType!Range) ==
215                                Needle))
216             {
217                 lengths[index] = 1;
218             }
219             else
220             {
221                 static assert(0,
222                               "Cannot handle needle of type " ~ Needle.stringof ~
223                               " when haystack has ElementType " ~ (ElementType!Range).stringof);
224             }
225         }
226 
227         import std.range: popFrontN;
228         haystack.popFrontN(lengths[hit - 1]);
229     }
230 
231     return hit;
232 
233 }
234 
235 @safe pure unittest
236 {
237     auto x = "beta version";
238     assert(x.skipOverShortestOf("beta", "be") == 2);
239     assert(x == "ta version");
240 }
241 
242 @safe pure unittest
243 {
244     auto x = "beta version";
245     assert(x.skipOverShortestOf("be", "beta") == 1);
246     assert(x == "ta version");
247 }
248 
249 @safe pure unittest
250 {
251     auto x = "beta version";
252     assert(x.skipOverShortestOf('b', "be", "beta") == 1);
253     assert(x == "eta version");
254 }
255 
256 /** Skip Over Longest Matching prefix in $(D needles) that prefixes $(D haystack).
257  */
258 SkipOverLongest skipOverLongestOf(alias pred = "a == b", Range, Ranges...)(scope ref Range haystack,
259                                                                            scope Ranges needles)
260 {
261     // TODO: figure out which needles that are prefixes of other needles by first
262     // sorting them and then use some adjacent filtering algorithm
263     static assert(0, "TODO: implement");
264 }
265 
266 /** Skip Over Back Shortest Match of `needles` in `haystack`. */
267 size_t skipOverBackShortestOf(alias pred = "a == b", Range, Ranges...)(scope ref Range haystack,
268                                                                        scope Ranges needles) @trusted
269 // TODO: We cannot prove that cast(ubyte[]) of a type that have no directions is safe
270 if (Ranges.length >= 2)
271 {
272     import std.range: retro, ElementType;
273     import std.traits: hasIndirections;
274     import core.internal.traits : Unqual;
275     import std.meta: staticMap, AliasSeq;
276     // import nxt.traits_ex: allSame;
277 
278     static if ((!hasIndirections!(ElementType!Range))/*  && */
279                /* allSame!(Unqual!Range, staticMap!(Unqual, Ranges)) */)
280     {
281         auto retroHaystack = (cast(ubyte[])haystack).retro;
282 
283         alias Retro(Range) = typeof((ubyte[]).init.retro);
284         AliasSeq!(staticMap!(Retro, Ranges)) retroNeedles;
285         foreach (const index, const ref needle; needles)
286         {
287             retroNeedles[index] = (cast(ubyte[])needle).retro;
288         }
289 
290         const retroHit = retroHaystack.skipOverShortestOf(retroNeedles);
291         haystack = haystack[0.. $ - (haystack.length - retroHaystack.length)];
292 
293         return retroHit;
294     }
295     else
296     {
297         static assert(0, "Unsupported combination of haystack type " ~ Range.stringof ~
298                       " with needle types " ~ Ranges.stringof);
299     }
300 }
301 
302 @safe pure nothrow @nogc unittest
303 {
304     auto x = "alpha_beta";
305     assert(x.skipOverBackShortestOf("x", "beta") == 2);
306     assert(x == "alpha_");
307 }
308 
309 @safe pure nothrow @nogc unittest
310 {
311     auto x = "alpha_beta";
312     assert(x.skipOverBackShortestOf("a", "beta") == 1);
313     assert(x == "alpha_bet");
314 }
315 
316 /** Drop $(D prefixes) in $(D s).
317  *
318  * TODO: Use multi-argument skipOver when it becomes available
319  * http://forum.dlang.org/thread/bug-12335-3@https.d.puremagic.com%2Fissues%2F
320 */
321 void skipOverPrefixes(R, A)(scope ref R s,
322                             const scope A prefixes)
323 {
324     import std.algorithm.searching : skipOver;
325     foreach (prefix; prefixes)
326     {
327         if (s.length > prefix.length &&
328             s.skipOver(prefix))
329         {
330             break;
331         }
332     }
333 }
334 
335 /** Drop $(D suffixes) in $(D s).
336  */
337 void skipOverSuffixes(R, A)(scope ref R s,
338                             const scope A suffixes)
339 {
340     foreach (suffix; suffixes)
341     {
342         if (s.length > suffix.length &&
343             s.endsWith(suffix))
344         {
345             s = s[0 .. $ - suffix.length]; // TODO: .ptr
346             break;
347         }
348     }
349 }
350 
351 /** Drop either both prefix `frontPrefix` and suffix `backSuffix` or do nothing.
352  *
353  * Returns: `true` upon drop, `false` otherwise.
354  */
355 bool skipOverFrontAndBack(alias pred = "a == b", R, E, F)(scope ref R r,
356                                                           scope E frontPrefix,
357                                                           scope F backSuffix)
358 if (isBidirectionalRange!R &&
359     is(typeof(binaryFun!pred(ElementType!R.init, E.init))) &&
360     is(typeof(binaryFun!pred(ElementType!R.init, F.init))))
361 {
362     import core.internal.traits : Unqual;
363     import std.traits : isArray;
364     static if (isArray!R &&
365                is(Unqual!(typeof(R.init[0])) == E)) // for instance if `R` is `string` and `E` is `char`
366     {
367         if (r.length >= 2 &&
368             r[0] == frontPrefix &&
369             r[$ - 1] == backSuffix)
370         {
371             r = r[1 .. $ - 1];
372             return true;
373         }
374     }
375     else
376     {
377         if (r.length >= 2 && // TODO: express this requirement in `r` as `hasLength`
378             binaryFun!pred(r.front, frontPrefix) &&
379             binaryFun!pred(r.back, backSuffix))
380         {
381             import std.range.primitives : popBack, popFront;
382             r.popFront();
383             r.popBack();
384             return true;
385         }
386     }
387     return false;
388 }
389 
390 @safe pure nothrow @nogc unittest
391 {
392     auto expr = `"alpha"`;
393     assert(expr.skipOverFrontAndBack('"', '"'));
394     assert(expr == `alpha`);
395 }
396 
397 @safe pure nothrow @nogc unittest
398 {
399     auto expr_ = `"alpha"`;
400     auto expr = expr_;
401     assert(!expr.skipOverFrontAndBack(',', '"'));
402     assert(expr == expr_);
403 }
404 
405 @safe pure nothrow @nogc unittest
406 {
407     auto expr_ = `"alpha`;
408     auto expr = expr_;
409     assert(!expr.skipOverFrontAndBack('"', '"'));
410     assert(expr == expr_);
411 }
412 
413 @safe pure nothrow @nogc unittest
414 {
415     auto expr_ = `alpha"`;
416     auto expr = expr_;
417     assert(!expr.skipOverFrontAndBack('"', '"'));
418     assert(expr == expr_);
419 }