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