1 /**
2  * Utility code related to string and text processing.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.text;
15 
16 import std.algorithm;
17 import std.ascii;
18 import std.exception;
19 import std.conv;
20 import std.format;
21 import std.string;
22 import std.traits;
23 import std.typetuple;
24 
25 import core.stdc.string;
26 
27 import ae.utils.array;
28 import ae.utils.meta;
29 import ae.utils.textout;
30 
31 public import ae.utils.regex;
32 
33 alias indexOf = std..string.indexOf;
34 
35 public import ae.utils.text.ascii : ascii, DecimalSize, toDec, toDecFixed;
36 
37 // ************************************************************************
38 
39 /// Convenience helper
40 bool contains(T, U)(T[] str, U[] what)
41 	if (is(Unqual!T == Unqual!U))
42 {
43 	return str.indexOf(what)>=0;
44 }
45 
46 /// CTFE helper
47 string formatAs(T)(auto ref T obj, string fmt)
48 {
49 	return format(fmt, obj);
50 }
51 
52 /// Consume a LF or CRLF terminated line from s.
53 /// Sets s to null and returns the remainder
54 /// if there is no line terminator in s.
55 T[] eatLine(T)(ref T[] s, bool eatIncompleteLines = true)
56 {
57 	return s.skipUntil([T('\n')], eatIncompleteLines).chomp();
58 }
59 
60 deprecated template eatLine(OnEof onEof)
61 {
62 	T[] eatLine(T)(ref T[] s)
63 	{
64 		return s.eatUntil!onEof([T('\n')]).chomp();
65 	}
66 }
67 
68 unittest
69 {
70 	string s = "Hello\nworld";
71 	assert(s.eatLine() == "Hello");
72 	assert(s.eatLine() == "world");
73 	assert(s is null);
74 	assert(s.eatLine() is null);
75 }
76 
77 // Uses memchr (not Boyer-Moore), best for short strings.
78 T[] fastReplace(T)(T[] what, T[] from, T[] to)
79 	if (T.sizeof == 1) // TODO (uses memchr)
80 {
81 	alias Unqual!T U;
82 
83 //	debug scope(failure) std.stdio.writeln("fastReplace crashed: ", [what, from, to]);
84 	enum RAM = cast(U*)null;
85 
86 	if (what.length < from.length || from.length==0)
87 		return what;
88 
89 	if (from.length==1)
90 	{
91 		auto fromc = from[0];
92 		if (to.length==1)
93 		{
94 			auto p = cast(T*)memchr(what.ptr, fromc, what.length);
95 			if (!p)
96 				return what;
97 
98 			auto result = what.dup;
99 			auto delta = result.ptr - what.ptr;
100 			auto toChar = to[0];
101 			auto end = what.ptr + what.length;
102 			do
103 			{
104 				(cast(U*)p)[delta] = toChar; // zomg hax lol
105 				p++;
106 				p = cast(T*)memchr(p, fromc, end - p);
107 			} while (p);
108 			return result;
109 		}
110 		else
111 		{
112 			auto p = cast(immutable(T)*)memchr(what.ptr, fromc, what.length);
113 			if (!p)
114 				return what;
115 
116 			auto sb = StringBuilder(what.length);
117 			do
118 			{
119 				sb.put(what[0..p-what.ptr], to);
120 				what = what[p-what.ptr+1..$];
121 				p = cast(immutable(T)*)memchr(what.ptr, fromc, what.length);
122 			}
123 			while (p);
124 
125 			sb.put(what);
126 			return sb.get();
127 		}
128 	}
129 
130 	auto head = from[0];
131 	auto tail = from[1..$];
132 
133 	auto p = cast(T*)what.ptr;
134 	auto end = p + what.length - tail.length;
135 	p = cast(T*)memchr(p, head, end-p);
136 	while (p)
137 	{
138 		p++;
139 		if (p[0..tail.length] == tail)
140 		{
141 			if (from.length == to.length)
142 			{
143 				auto result = what.dup;
144 				auto deltaMinusOne = (result.ptr - what.ptr) - 1;
145 
146 				goto replaceA;
147 			dummyA: // compiler complains
148 
149 				do
150 				{
151 					p++;
152 					if (p[0..tail.length] == tail)
153 					{
154 					replaceA:
155 						(cast(U*)p+deltaMinusOne)[0..to.length] = to[];
156 					}
157 					p = cast(T*)memchr(p, head, end-p);
158 				}
159 				while (p);
160 
161 				return result;
162 			}
163 			else
164 			{
165 				auto start = cast(T*)what.ptr;
166 				auto sb = StringBuilder(what.length);
167 				goto replaceB;
168 			dummyB: // compiler complains
169 
170 				do
171 				{
172 					p++;
173 					if (p[0..tail.length] == tail)
174 					{
175 					replaceB:
176 						sb.put(RAM[cast(size_t)start .. cast(size_t)p-1], to);
177 						start = p + tail.length;
178 						what = what[start-what.ptr..$];
179 					}
180 					else
181 					{
182 						what = what[p-what.ptr..$];
183 					}
184 					p = cast(T*)memchr(what.ptr, head, what.length);
185 				}
186 				while (p);
187 
188 				//sb.put(what);
189 				sb.put(RAM[cast(size_t)start..cast(size_t)(what.ptr+what.length)]);
190 				return sb.get();
191 			}
192 
193 			assert(0);
194 		}
195 		p = cast(T*)memchr(p, head, end-p);
196 	}
197 
198 	return what;
199 }
200 
201 unittest
202 {
203 	import std.array;
204 	void test(string haystack, string from, string to)
205 	{
206 		auto description = `("` ~ haystack ~ `", "` ~ from ~ `", "` ~ to ~ `")`;
207 
208 		auto r1 = fastReplace(haystack, from, to);
209 		auto r2 =     replace(haystack, from, to);
210 		assert(r1 == r2, `Bad replace: ` ~ description ~ ` == "` ~ r1 ~ `"`);
211 
212 		if (r1 == haystack)
213 			assert(r1 is haystack, `Pointless reallocation: ` ~ description);
214 	}
215 
216 	test("Mary had a little lamb", "a", "b");
217 	test("Mary had a little lamb", "a", "aaa");
218 	test("Mary had a little lamb", "Mary", "Lucy");
219 	test("Mary had a little lamb", "Mary", "Jimmy");
220 	test("Mary had a little lamb", "lamb", "goat");
221 	test("Mary had a little lamb", "lamb", "sheep");
222 	test("Mary had a little lamb", " l", " x");
223 	test("Mary had a little lamb", " l", " xx");
224 
225 	test("Mary had a little lamb", "X" , "Y" );
226 	test("Mary had a little lamb", "XX", "Y" );
227 	test("Mary had a little lamb", "X" , "YY");
228 	test("Mary had a little lamb", "XX", "YY");
229 	test("Mary had a little lamb", "aX", "Y" );
230 	test("Mary had a little lamb", "aX", "YY");
231 
232 	test("foo", "foobar", "bar");
233 }
234 
235 T[][] fastSplit(T, U)(T[] s, U d)
236 	if (is(Unqual!T == Unqual!U))
237 {
238 	if (!s.length)
239 		return null;
240 
241 	auto p = cast(T*)memchr(s.ptr, d, s.length);
242 	if (!p)
243 		return [s];
244 
245 	size_t n;
246 	auto end = s.ptr + s.length;
247 	do
248 	{
249 		n++;
250 		p++;
251 		p = cast(T*) memchr(p, d, end-p);
252 	}
253 	while (p);
254 
255 	auto result = new T[][n+1];
256 	n = 0;
257 	auto start = s.ptr;
258 	p = cast(T*) memchr(start, d, s.length);
259 	do
260 	{
261 		result[n++] = start[0..p-start];
262 		start = ++p;
263 		p = cast(T*) memchr(p, d, end-p);
264 	}
265 	while (p);
266 	result[n] = start[0..end-start];
267 
268 	return result;
269 }
270 
271 T[][] splitAsciiLines(T)(T[] text)
272 	if (is(Unqual!T == char))
273 {
274 	auto lines = text.fastSplit('\n');
275 	foreach (ref line; lines)
276 		if (line.length && line[$-1]=='\r')
277 			line = line[0..$-1];
278 	return lines;
279 }
280 
281 unittest
282 {
283 	assert(splitAsciiLines("a\nb\r\nc\r\rd\n\re\r\n\nf") == ["a", "b", "c\r\rd", "\re", "", "f"]);
284 	assert(splitAsciiLines(string.init) == splitLines(string.init));
285 }
286 
287 T[] asciiStrip(T)(T[] s)
288 	if (is(Unqual!T == char))
289 {
290 	while (s.length && isWhite(s[0]))
291 		s = s[1..$];
292 	while (s.length && isWhite(s[$-1]))
293 		s = s[0..$-1];
294 	return s;
295 }
296 
297 unittest
298 {
299 	string s = "Hello, world!";
300 	assert(asciiStrip(s) is s);
301 	assert(asciiStrip("\r\n\tHello ".dup) == "Hello");
302 }
303 
304 /// Covering slice-list of s with interleaved whitespace.
305 T[][] segmentByWhitespace(T)(T[] s)
306 	if (is(Unqual!T == char))
307 {
308 	if (!s.length)
309 		return null;
310 
311 	T[][] segments;
312 	bool wasWhite = isWhite(s[0]);
313 	size_t start = 0;
314 	foreach (p, char c; s)
315 	{
316 		bool isWhite = isWhite(c);
317 		if (isWhite != wasWhite)
318 			segments ~= s[start..p],
319 			start = p;
320 		wasWhite = isWhite;
321 	}
322 	segments ~= s[start..$];
323 
324 	return segments;
325 }
326 
327 T[] newlinesToSpaces(T)(T[] s)
328 	if (is(Unqual!T == char))
329 {
330 	auto slices = segmentByWhitespace(s);
331 	foreach (ref slice; slices)
332 		if (slice.contains("\n"))
333 			slice = " ";
334 	return slices.join();
335 }
336 
337 ascii normalizeWhitespace(ascii s)
338 {
339 	auto slices = segmentByWhitespace(strip(s));
340 	foreach (i, ref slice; slices)
341 		if (i & 1) // odd
342 			slice = " ";
343 	return slices.join();
344 }
345 
346 unittest
347 {
348 	assert(normalizeWhitespace(" Mary  had\ta\nlittle\r\n\tlamb") == "Mary had a little lamb");
349 }
350 
351 string[] splitByCamelCase(string s)
352 {
353 	string[] result;
354 	size_t start = 0;
355 	foreach (i; 1..s.length+1)
356 		if (i == s.length
357 		 || (isLower(s[i-1]) && isUpper(s[i]))
358 		 || (i+1 < s.length && isUpper(s[i-1]) && isUpper(s[i]) && isLower(s[i+1]))
359 		)
360 		{
361 			result ~= s[start..i];
362 			start = i;
363 		}
364 	return result;
365 }
366 
367 unittest
368 {
369 	assert(splitByCamelCase("parseIPString") == ["parse", "IP", "String"]);
370 	assert(splitByCamelCase("IPString") == ["IP", "String"]);
371 }
372 
373 string camelCaseJoin(string[] arr)
374 {
375 	if (!arr.length)
376 		return null;
377 	string result = arr[0];
378 	foreach (s; arr[1..$])
379 		result ~= std.ascii.toUpper(s[0]) ~ s[1..$];
380 	return result;
381 }
382 
383 unittest
384 {
385 	assert("parse-IP-string".split('-').camelCaseJoin() == "parseIPString");
386 }
387 
388 // ************************************************************************
389 
390 private __gshared char[256] asciiLower, asciiUpper;
391 
392 shared static this()
393 {
394 	foreach (c; 0..256)
395 	{
396 		asciiLower[c] = cast(char)std.ascii.toLower(c);
397 		asciiUpper[c] = cast(char)std.ascii.toUpper(c);
398 	}
399 }
400 
401 void xlat(alias TABLE, T)(T[] buf)
402 {
403 	foreach (ref c; buf)
404 		c = TABLE[c];
405 }
406 
407 alias xlat!(asciiLower, char) asciiToLower;
408 alias xlat!(asciiUpper, char) asciiToUpper;
409 
410 // ************************************************************************
411 
412 /// Case-insensitive ASCII string.
413 alias CIAsciiString = NormalizedArray!(immutable(char), s => s.byCodeUnit.map!(std.ascii.toLower));
414 
415 ///
416 unittest
417 {
418 	CIAsciiString s = "test";
419 	assert(s == "TEST");
420 	assert(s >= "Test" && s <= "Test");
421 	assert(CIAsciiString("a") == CIAsciiString("A"));
422 	assert(CIAsciiString("a") != CIAsciiString("B"));
423 	assert(CIAsciiString("a") <  CIAsciiString("B"));
424 	assert(CIAsciiString("A") <  CIAsciiString("b"));
425 	assert(CIAsciiString("я") != CIAsciiString("Я"));
426 }
427 
428 /// Case-insensitive Unicode string.
429 alias CIUniString = NormalizedArray!(immutable(char), s => s.map!(std.uni.toLower));
430 
431 ///
432 unittest
433 {
434 	CIUniString s = "привет";
435 	assert(s == "ПРИВЕТ");
436 	assert(s >= "Привет" && s <= "Привет");
437 	assert(CIUniString("я") == CIUniString("Я"));
438 	assert(CIUniString("а") != CIUniString("Б"));
439 	assert(CIUniString("а") <  CIUniString("Б"));
440 	assert(CIUniString("А") <  CIUniString("б"));
441 }
442 
443 // ************************************************************************
444 
445 import std.utf;
446 
447 /// Convert any data to a valid UTF-8 bytestream, so D's string functions can
448 /// properly work on it.
449 string rawToUTF8(in char[] s)
450 {
451 	auto d = new dchar[s.length];
452 	foreach (i, char c; s)
453 		d[i] = c;
454 	return toUTF8(d);
455 }
456 
457 /// Undo rawToUTF8.
458 ascii UTF8ToRaw(in char[] r) pure
459 {
460 	auto s = new char[r.length];
461 	size_t i = 0;
462 	foreach (dchar c; r)
463 	{
464 		assert(c < '\u0100');
465 		s[i++] = cast(char)c;
466 	}
467 	return s[0..i];
468 }
469 
470 unittest
471 {
472 	char[1] c;
473 	for (int i=0; i<256; i++)
474 	{
475 		c[0] = cast(char)i;
476 		assert(UTF8ToRaw(rawToUTF8(c[])) == c[], format("%s -> %s -> %s", cast(ubyte[])c[], cast(ubyte[])rawToUTF8(c[]), cast(ubyte[])UTF8ToRaw(rawToUTF8(c[]))));
477 	}
478 }
479 
480 /// Where a delegate with this signature is required.
481 string nullStringTransform(in char[] s) { return to!string(s); }
482 
483 string forceValidUTF8(string s)
484 {
485 	try
486 	{
487 		validate(s);
488 		return s;
489 	}
490 	catch (UTFException)
491 		return rawToUTF8(s);
492 }
493 
494 // ************************************************************************
495 
496 /// Return the slice up to the first NUL character,
497 /// or of the whole array if none is found.
498 C[] fromZArray(C, n)(ref C[n] arr)
499 {
500 	auto p = arr.representation.countUntil(0);
501 	return arr[0 .. p<0 ? $ : p];
502 }
503 
504 /// ditto
505 C[] fromZArray(C)(C[] arr)
506 {
507 	auto p = arr.representation.countUntil(0);
508 	return arr[0 .. p<0 ? $ : p];
509 }
510 
511 unittest
512 {
513 	char[4] arr = "ab\0d";
514 	assert(arr.fromZArray == "ab");
515 	arr[] = "abcd";
516 	assert(arr.fromZArray == "abcd");
517 }
518 
519 unittest
520 {
521 	string arr = "ab\0d";
522 	assert(arr.fromZArray == "ab");
523 	arr = "abcd";
524 	assert(arr.fromZArray == "abcd");
525 }
526 
527 // ************************************************************************
528 
529 /// Formats binary data as a hex dump (three-column layout consisting of hex
530 /// offset, byte values in hex, and printable low-ASCII characters).
531 string hexDump(const(void)[] b)
532 {
533 	auto data = cast(const(ubyte)[]) b;
534 	assert(data.length);
535 	size_t i=0;
536 	string s;
537 	while (i<data.length)
538 	{
539 		s ~= format("%08X:  ", i);
540 		foreach (x; 0..16)
541 		{
542 			if (i+x<data.length)
543 				s ~= format("%02X ", data[i+x]);
544 			else
545 				s ~= "   ";
546 			if (x==7)
547 				s ~= "| ";
548 		}
549 		s ~= "  ";
550 		foreach (x; 0..16)
551 		{
552 			if (i+x<data.length)
553 				if (data[i+x]==0)
554 					s ~= ' ';
555 				else
556 				if (data[i+x]<32 || data[i+x]>=128)
557 					s ~= '.';
558 				else
559 					s ~= cast(char)data[i+x];
560 			else
561 				s ~= ' ';
562 		}
563 		s ~= "\n";
564 		i += 16;
565 	}
566 	return s;
567 }
568 
569 import std.conv;
570 
571 T fromHex(T : ulong = uint, C)(const(C)[] s)
572 {
573 	T result = parse!T(s, 16);
574 	enforce(s.length==0, new ConvException("Could not parse entire string"));
575 	return result;
576 }
577 
578 ubyte[] arrayFromHex(in char[] hex, ubyte[] buf = null)
579 {
580 	if (buf is null)
581 		buf = new ubyte[hex.length/2];
582 	else
583 		assert(buf.length == hex.length/2);
584 	for (int i=0; i<hex.length; i+=2)
585 		buf[i/2] = cast(ubyte)(
586 			hexDigits.indexOf(hex[i  ], CaseSensitive.no)*16 +
587 			hexDigits.indexOf(hex[i+1], CaseSensitive.no)
588 		);
589 	return buf;
590 }
591 
592 template toHex(alias digits = hexDigits)
593 {
594 	char[] toHex(in ubyte[] data, char[] buf) pure
595 	{
596 		assert(buf.length == data.length*2);
597 		foreach (i, b; data)
598 		{
599 			buf[i*2  ] = digits[b>>4];
600 			buf[i*2+1] = digits[b&15];
601 		}
602 		return buf;
603 	}
604 
605 	string toHex(in ubyte[] data) pure
606 	{
607 		auto buf = new char[data.length*2];
608 		foreach (i, b; data)
609 		{
610 			buf[i*2  ] = digits[b>>4];
611 			buf[i*2+1] = digits[b&15];
612 		}
613 		return buf;
614 	}
615 }
616 
617 alias toLowerHex = toHex!lowerHexDigits;
618 
619 void toHex(T : ulong, size_t U = T.sizeof*2)(T n, ref char[U] buf)
620 {
621 	foreach (i; Reverse!(RangeTuple!(T.sizeof*2)))
622 	{
623 		buf[i] = hexDigits[n & 0xF];
624 		n >>= 4;
625 	}
626 }
627 
628 unittest
629 {
630 	ubyte[] bytes = [0x12, 0x34];
631 	assert(toHex(bytes) == "1234");
632 }
633 
634 unittest
635 {
636 	ubyte[] bytes = [0x12, 0x34];
637 	char[] buf = new char[4];
638 	toHex(bytes, buf);
639 	assert(buf == "1234");
640 }
641 
642 unittest
643 {
644 	char[8] buf;
645 	toHex(0x01234567, buf);
646 	assert(buf == "01234567");
647 }
648 
649 /// How many significant decimal digits does a FP type have
650 /// (determined empirically)
651 enum significantDigits(T : real) = 2 + 2 * T.sizeof;
652 
653 /// Format string for a FP type which includes all necessary
654 /// significant digits
655 enum fpFormatString(T) = "%." ~ text(significantDigits!T) ~ "g";
656 
657 /// Get shortest string representation of a FP type that still converts to exactly the same number.
658 template fpToString(F)
659 {
660 	string fpToString(F v)
661 	{
662 		/// Bypass FPU register, which may contain a different precision
663 		static F forceType(F d) { static F n; n = d; return n; }
664 
665 		StaticBuf!(char, 64) buf;
666 		formattedWrite(&buf, fpFormatString!F, forceType(v));
667 		char[] s = buf.data();
668 
669 		if (s != "nan" && s != "-nan" && s != "inf" && s != "-inf")
670 		{
671 			if (forceType(to!F(s)) != v)
672 			{
673 				static if (is(F == real))
674 				{
675 					// Something funny with DM libc real parsing... e.g. 0.6885036635121051783
676 					return s.idup;
677 				}
678 				else
679 					assert(false, "Initial conversion fails: " ~ format(fpFormatString!F, to!F(s)));
680 			}
681 
682 			foreach_reverse (i; 1..s.length)
683 				if (s[i]>='0' && s[i]<='8')
684 				{
685 					s[i]++;
686 					if (forceType(to!F(s[0..i+1]))==v)
687 						s = s[0..i+1];
688 					else
689 						s[i]--;
690 				}
691 			while (s.length>2 && s[$-1]!='.' && forceType(to!F(s[0..$-1]))==v)
692 				s = s[0..$-1];
693 		}
694 		return s.idup;
695 	}
696 
697 	static if (!is(F == real))
698 	unittest
699 	{
700 		union U
701 		{
702 			ubyte[F.sizeof] bytes;
703 			F d;
704 			string toString() { return (fpFormatString!F ~ " %a [%(%02X %)]").format(d, d, bytes[]); }
705 		}
706 		import std.random : Xorshift, uniform;
707 		import std.stdio : stderr;
708 		Xorshift rng;
709 		foreach (n; 0..10000)
710 		{
711 			U u;
712 			foreach (ref b; u.bytes[])
713 				b = uniform!ubyte(rng);
714 			static if (is(F == real))
715 				u.bytes[7] |= 0x80; // require normalized value
716 			scope(failure) stderr.writeln("Input:\t", u);
717 			auto s = fpToString(u.d);
718 			scope(failure) stderr.writeln("Result:\t", s);
719 			if (s == "nan" || s == "-nan")
720 				continue; // there are many NaNs...
721 			U r;
722 			r.d = to!F(s);
723 			assert(r.bytes == u.bytes,
724 				"fpToString mismatch:\nOutput:\t%s".format(r));
725 		}
726 	}
727 }
728 
729 alias doubleToString = fpToString!double;
730 
731 unittest
732 {
733 	alias floatToString = fpToString!float;
734 	alias realToString = fpToString!real;
735 }
736 
737 string numberToString(T)(T v)
738 	if (isNumeric!T)
739 {
740 	static if (is(T : real))
741 		return fpToString(v);
742 	else
743 		return toDec(v);
744 }
745 
746 // ************************************************************************
747 
748 /// Simpler implementation of Levenshtein string distance
749 int stringDistance(string s, string t)
750 {
751 	int n = cast(int)s.length;
752 	int m = cast(int)t.length;
753 	if (n == 0) return m;
754 	if (m == 0) return n;
755 	int[][] distance = new int[][](n+1, m+1); // matrix
756 	int cost=0;
757 	//init1
758 	foreach (i; 0..n+1) distance[i][0]=i;
759 	foreach (j; 0..m+1) distance[0][j]=j;
760 	//find min distance
761 	foreach (i; 1..n+1)
762 		foreach (j; 1..m+1)
763 		{
764 			cost = t[j-1] == s[i-1] ? 0 : 1;
765 			distance[i][j] = min(
766 				distance[i-1][j  ] + 1,
767 				distance[i  ][j-1] + 1,
768 				distance[i-1][j-1] + cost
769 			);
770 		}
771 	return distance[n][m];
772 }
773 
774 /// Return a number between 0.0 and 1.0 indicating how similar two strings are
775 /// (1.0 if identical)
776 float stringSimilarity(string string1, string string2)
777 {
778 	float dis = stringDistance(string1, string2);
779 	float maxLen = string1.length;
780 	if (maxLen < string2.length)
781 		maxLen = string2.length;
782 	if (maxLen == 0)
783 		return 1;
784 	else
785 		return 1f - dis/maxLen;
786 }
787 
788 /// Select best match from a list of items.
789 /// Returns -1 if none are above the threshold.
790 sizediff_t findBestMatch(in string[] items, string target, float threshold = 0.7)
791 {
792 	sizediff_t found = -1;
793 	float best = 0;
794 
795 	foreach (i, item; items)
796 	{
797 		float match = stringSimilarity(toLower(item),toLower(target));
798 		if (match>threshold && match>=best)
799 		{
800 			best = match;
801 			found = i;
802 		}
803 	}
804 
805 	return found;
806 }
807 
808 /// Select best match from a list of items.
809 /// Returns null if none are above the threshold.
810 string selectBestFrom(in string[] items, string target, float threshold = 0.7)
811 {
812 	auto index = findBestMatch(items, target, threshold);
813 	return index < 0 ? null : items[index];
814 }
815 
816 // ************************************************************************
817 
818 
819 string randomString(int length=20, string chars="abcdefghijklmnopqrstuvwxyz")
820 {
821 	import std.random;
822 	import std.range;
823 
824 	return length.iota.map!(n => chars[uniform(0, $)]).array;
825 }