1 /++ 2 String manipulation functions complementing the standard library. 3 4 Example: 5 --- 6 { 7 string line = "Lorem ipsum :sit amet"; 8 immutable lorem = line.advancePast(" :"); 9 assert(lorem == "Lorem ipsum", lorem); 10 assert(line == "sit amet", line); 11 } 12 { 13 string line = "Lorem ipsum :sit amet"; 14 immutable lorem = line.advancePast(':'); 15 assert(lorem == "Lorem ipsum ", lorem); 16 assert(line == "sit amet", line); 17 } 18 { 19 string line = "Lorem ipsum sit amet"; // mutable, will be modified by ref 20 string[] words; 21 22 while (line.length > 0) 23 { 24 immutable word = line.advancePast(" ", inherit: true); 25 words ~= word; 26 } 27 28 assert(words == [ "Lorem", "ipsum", "sit", "amet" ]); 29 } 30 --- 31 32 Copyright: [JR](https://github.com/zorael) 33 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 34 35 Authors: 36 [JR](https://github.com/zorael) 37 +/ 38 module lu..string; 39 40 private: 41 42 import std.traits : allSameType; 43 44 public: 45 46 @safe: 47 48 49 // advancePast 50 /++ 51 Given some string, finds the supplied needle token in it, returns the 52 string up to that point, and advances the passed string by ref to after the token. 53 54 The closest equivalent in Phobos is [std.algorithm.searching.findSplit], 55 which largely serves the same function but doesn't advance the input string. 56 57 Additionally takes an optional `inherit` bool argument, to toggle 58 whether the return value inherits the passed line (and clearing it) upon no 59 needle match. 60 61 Example: 62 --- 63 string foobar = "foo bar!"; 64 string foo = foobar.advancePast(" "); 65 string bar = foobar.advancePast("!"); 66 67 assert((foo == "foo"), foo); 68 assert((bar == "bar"), bar); 69 assert(!foobar.length); 70 71 enum line = "abc def ghi"; 72 string def = line[4..$].advancePast(" "); // now with auto ref 73 74 string foobar2 = "foo bar!"; 75 string foo2 = foobar2.advancePast(" "); 76 string bar2 = foobar2.advancePast("?", inherit: true); 77 78 assert((foo2 == "foo"), foo2); 79 assert((bar2 == "bar!"), bar2); 80 assert(!foobar2.length); 81 82 string slice2 = "snarfl"; 83 string verb2 = slice2.advancePast(" ", inherit: true); 84 85 assert((verb2 == "snarfl"), verb2); 86 assert(!slice2.length, slice2); 87 --- 88 89 Params: 90 haystack = Array to walk and advance. 91 needle = Token that delimits what should be returned and to where to advance. 92 May be another array or some individual character. 93 inherit = Optional flag of whether or not the whole string should be 94 returned and the haystack variable cleared on no needle match. 95 callingFile = Optional file name to attach to an exception. 96 callingLine = Optional line number to attach to an exception. 97 98 Returns: 99 The string `haystack` from the start up to the needle token. The original 100 variable is advanced to after the token. 101 102 Throws: 103 [AdvanceException] if the needle could not be found in the string. 104 +/ 105 auto advancePast(Haystack, Needle) 106 (auto ref return scope Haystack haystack, 107 const scope Needle needle, 108 const bool inherit = false, 109 const string callingFile = __FILE__, 110 const size_t callingLine = __LINE__) @safe 111 in 112 { 113 import std.traits : isArray; 114 115 static if (isArray!Needle) 116 { 117 if (!needle.length) 118 { 119 enum message = "Tried to `advancePast` with no `needle` given"; 120 throw new AdvanceExceptionImpl!(Haystack, Needle) 121 (message, 122 haystack.idup, 123 needle.idup, 124 callingFile, 125 callingLine); 126 } 127 } 128 } 129 do 130 { 131 import std.traits : isArray, isMutable, isSomeChar; 132 133 static if (!isMutable!Haystack) 134 { 135 enum message = "`advancePast` only works on mutable haystacks"; 136 static assert(0, message); 137 } 138 else static if (!isArray!Haystack) 139 { 140 enum message = "`advancePast` only works on array-type haystacks"; 141 static assert(0, message); 142 } 143 else static if ( 144 !isArray!Haystack && 145 !is(Needle : ElementType!Haystack) && 146 !is(Needle : ElementEncodingType!Haystack)) 147 { 148 enum message = "`advancePast` only works with array- or single-element-type needles"; 149 static assert(0, message); 150 } 151 152 static if (isArray!Needle || isSomeChar!Needle) 153 { 154 import std.string : indexOf; 155 immutable index = haystack.indexOf(needle); 156 } 157 else 158 { 159 import std.algorithm.searching : countUntil; 160 immutable index = haystack.countUntil(needle); 161 } 162 163 if (index == -1) 164 { 165 if (inherit) 166 { 167 // No needle match; inherit string and clear the original 168 static if (__traits(isRef, haystack)) scope(exit) haystack = null; 169 return haystack; 170 } 171 172 static if (isArray!Needle) 173 { 174 immutable needleIdup = needle.idup; 175 } 176 else 177 { 178 alias needleIdup = needle; 179 } 180 181 enum message = "Tried to advance a string past something that wasn't there"; 182 throw new AdvanceExceptionImpl!(Haystack, Needle) 183 (message, 184 haystack.idup, 185 needleIdup, 186 callingFile, 187 callingLine); 188 } 189 190 static if (isArray!Needle) 191 { 192 immutable separatorLength = needle.length; 193 } 194 else 195 { 196 enum separatorLength = 1; 197 } 198 199 static if (__traits(isRef, haystack)) scope(exit) haystack = haystack[(index+separatorLength)..$]; 200 return haystack[0..index]; 201 } 202 203 204 /// 205 unittest 206 { 207 import std.conv : to; 208 import std.string : indexOf; 209 210 { 211 string line = "Lorem ipsum :sit amet"; 212 immutable lorem = line.advancePast(" :"); 213 assert(lorem == "Lorem ipsum", lorem); 214 assert(line == "sit amet", line); 215 } 216 { 217 string line = "Lorem ipsum :sit amet"; 218 //immutable lorem = line.advancePast(" :"); 219 immutable lorem = line.advancePast(" :"); 220 assert(lorem == "Lorem ipsum", lorem); 221 assert(line == "sit amet", line); 222 } 223 { 224 string line = "Lorem ipsum :sit amet"; 225 immutable lorem = line.advancePast(':'); 226 assert(lorem == "Lorem ipsum ", lorem); 227 assert(line == "sit amet", line); 228 } 229 { 230 string line = "Lorem ipsum :sit amet"; 231 immutable lorem = line.advancePast(':'); 232 assert(lorem == "Lorem ipsum ", lorem); 233 assert(line == "sit amet", line); 234 } 235 { 236 string line = "Lorem ipsum :sit amet"; 237 immutable lorem = line.advancePast(' '); 238 assert(lorem == "Lorem", lorem); 239 assert(line == "ipsum :sit amet", line); 240 } 241 { 242 string line = "Lorem ipsum :sit amet"; 243 immutable lorem = line.advancePast(' '); 244 assert(lorem == "Lorem", lorem); 245 assert(line == "ipsum :sit amet", line); 246 } 247 /*{ 248 string line = "Lorem ipsum :sit amet"; 249 immutable lorem = line.advancePast(""); 250 assert(!lorem.length, lorem); 251 assert(line == "Lorem ipsum :sit amet", line); 252 }*/ 253 /*{ 254 string line = "Lorem ipsum :sit amet"; 255 immutable lorem = line.advancePast(""); 256 assert(!lorem.length, lorem); 257 assert(line == "Lorem ipsum :sit amet", line); 258 }*/ 259 { 260 string line = "Lorem ipsum :sit amet"; 261 immutable lorem = line.advancePast("Lorem ipsum"); 262 assert(!lorem.length, lorem); 263 assert(line == " :sit amet", line); 264 } 265 { 266 string line = "Lorem ipsum :sit amet"; 267 immutable lorem = line.advancePast("Lorem ipsum"); 268 assert(!lorem.length, lorem); 269 assert(line == " :sit amet", line); 270 } 271 { 272 string line = "Lorem ipsum :sit amet"; 273 immutable dchar dspace = ' '; 274 immutable lorem = line.advancePast(dspace); 275 assert(lorem == "Lorem", lorem); 276 assert(line == "ipsum :sit amet", line); 277 } 278 { 279 dstring dline = "Lorem ipsum :sit amet"d; 280 immutable dspace = " "d; 281 immutable lorem = dline.advancePast(dspace); 282 assert((lorem == "Lorem"d), lorem.to!string); 283 assert((dline == "ipsum :sit amet"d), dline.to!string); 284 } 285 { 286 dstring dline = "Lorem ipsum :sit amet"d; 287 immutable wchar wspace = ' '; 288 immutable lorem = dline.advancePast(wspace); 289 assert((lorem == "Lorem"d), lorem.to!string); 290 assert((dline == "ipsum :sit amet"d), dline.to!string); 291 } 292 { 293 wstring wline = "Lorem ipsum :sit amet"w; 294 immutable wchar wspace = ' '; 295 immutable lorem = wline.advancePast(wspace); 296 assert((lorem == "Lorem"w), lorem.to!string); 297 assert((wline == "ipsum :sit amet"w), wline.to!string); 298 } 299 { 300 wstring wline = "Lorem ipsum :sit amet"w; 301 immutable wspace = " "w; 302 immutable lorem = wline.advancePast(wspace); 303 assert((lorem == "Lorem"w), lorem.to!string); 304 assert((wline == "ipsum :sit amet"w), wline.to!string); 305 } 306 { 307 string user = "foo!bar@asdf.adsf.com"; 308 user = user.advancePast('!'); 309 assert((user == "foo"), user); 310 } 311 { 312 immutable def = "abc def ghi"[4..$].advancePast(" "); 313 assert((def == "def"), def); 314 } 315 { 316 import std.exception : assertThrown; 317 assertThrown!AdvanceException("abc def ghi"[4..$].advancePast("")); 318 } 319 { 320 string line = "Lorem ipsum"; 321 immutable head = line.advancePast(" "); 322 assert((head == "Lorem"), head); 323 assert((line == "ipsum"), line); 324 } 325 { 326 string line = "Lorem"; 327 immutable head = line.advancePast(" ", inherit: true); 328 assert((head == "Lorem"), head); 329 assert(!line.length, line); 330 } 331 { 332 string slice = "verb"; 333 string verb; 334 335 if (slice.indexOf(' ') != -1) 336 { 337 verb = slice.advancePast(' '); 338 } 339 else 340 { 341 verb = slice; 342 slice = string.init; 343 } 344 345 assert((verb == "verb"), verb); 346 assert(!slice.length, slice); 347 } 348 { 349 string slice = "verb"; 350 immutable verb = slice.advancePast(' ', inherit: true); 351 assert((verb == "verb"), verb); 352 assert(!slice.length, slice); 353 } 354 { 355 string url = "https://google.com/index.html#fragment-identifier"; 356 url = url.advancePast('#', inherit: true); 357 assert((url == "https://google.com/index.html"), url); 358 } 359 { 360 string url = "https://google.com/index.html"; 361 url = url.advancePast('#', inherit: true); 362 assert((url == "https://google.com/index.html"), url); 363 } 364 { 365 string line = "Lorem ipsum sit amet"; 366 string[] words; 367 368 while (line.length > 0) 369 { 370 immutable word = line.advancePast(" ", inherit: true); 371 words ~= word; 372 } 373 374 assert(words == [ "Lorem", "ipsum", "sit", "amet" ]); 375 } 376 { 377 import std.exception : assertThrown; 378 string url = "https://google.com/index.html#fragment-identifier"; 379 assertThrown!AdvanceException(url.advancePast("", inherit: true)); 380 } 381 } 382 383 384 // AdvanceException 385 /++ 386 Exception, to be thrown when a call to [advancePast] went wrong. 387 388 It is a normal [object.Exception|Exception] but with an attached needle and haystack. 389 +/ 390 abstract class AdvanceException : Exception 391 { 392 /++ 393 Returns a string of the original haystack the call to [advancePast] was operating on. 394 +/ 395 string haystack() pure @safe; 396 397 /++ 398 Returns a string of the original needle the call to [advancePast] was operating on. 399 +/ 400 string needle() pure @safe; 401 402 /++ 403 Create a new [AdvanceExceptionImpl], without attaching anything. 404 +/ 405 this( 406 const string message, 407 const string file = __FILE__, 408 const size_t line = __LINE__, 409 Throwable nextInChain = null) pure nothrow @nogc @safe 410 { 411 super(message, file, line, nextInChain); 412 } 413 } 414 415 416 // AdvanceExceptionImpl 417 /++ 418 Exception, to be thrown when a call to [advancePast] went wrong. 419 420 This is the templated implementation, so that we can support more than one 421 kind of needle and haystack combination. 422 423 It is a normal [object.Exception|Exception] but with an attached needle and haystack. 424 425 Params: 426 Haystack = Haystack array type. 427 Needle = Needle array or char-like type. 428 +/ 429 final class AdvanceExceptionImpl(Haystack, Needle) : AdvanceException 430 { 431 private: 432 import std.conv : to; 433 434 /++ 435 Raw haystack that `haystack` converts to string and returns. 436 +/ 437 string _haystack; 438 439 /++ 440 Raw needle that `needle` converts to string and returns. 441 +/ 442 string _needle; 443 444 public: 445 /++ 446 Returns a string of the original needle the call to `advancePast` was operating on. 447 448 Returns: 449 The raw haystack (be it any kind of string), converted to a `string`. 450 +/ 451 override string haystack() pure @safe 452 { 453 return _haystack; 454 } 455 456 /++ 457 Returns a string of the original needle the call to `advancePast` was operating on. 458 459 Returns: 460 The raw needle (be it any kind of string or character), converted to a `string`. 461 +/ 462 override string needle() pure @safe 463 { 464 return _needle; 465 } 466 467 /++ 468 Create a new `AdvanceExceptionImpl`, without attaching anything. 469 +/ 470 this( 471 const string message, 472 const string file = __FILE__, 473 const size_t line = __LINE__, 474 Throwable nextInChain = null) pure @safe nothrow @nogc 475 { 476 super(message, file, line, nextInChain); 477 } 478 479 /++ 480 Create a new `AdvanceExceptionImpl`, attaching a command. 481 +/ 482 this( 483 const string message, 484 const Haystack haystack, 485 const Needle needle, 486 const string file = __FILE__, 487 const size_t line = __LINE__, 488 Throwable nextInChain = null) pure @safe 489 { 490 this._haystack = haystack.to!string; 491 this._needle = needle.to!string; 492 super(message, file, line, nextInChain); 493 } 494 } 495 496 497 // plurality 498 /++ 499 Selects the correct singular or plural form of a word depending on the 500 numerical count of it. 501 502 Technically works with any type provided the number is some comparable integral. 503 504 Example: 505 --- 506 string one = 1.plurality("one", "two"); 507 string two = 2.plurality("one", "two"); 508 string many = (-2).plurality("one", "many"); 509 string many0 = 0.plurality("one", "many"); 510 511 assert((one == "one"), one); 512 assert((two == "two"), two); 513 assert((many == "many"), many); 514 assert((many0 == "many"), many0); 515 --- 516 517 Params: 518 num = Numerical count. 519 singular = The singular form. 520 plural = The plural form. 521 522 Returns: 523 The singular if num is `1` or `-1`, otherwise the plural. 524 +/ 525 pragma(inline, true) 526 T plurality(Num, T)( 527 const Num num, 528 const return scope T singular, 529 const return scope T plural) pure nothrow @nogc 530 { 531 import std.traits : isIntegral; 532 533 static if (!isIntegral!Num) 534 { 535 enum message = "`plurality` only works with integral types"; 536 static assert(0, message); 537 } 538 539 return ((num == 1) || (num == -1)) ? singular : plural; 540 } 541 542 /// 543 unittest 544 { 545 static assert(10.plurality("one","many") == "many"); 546 static assert(1.plurality("one", "many") == "one"); 547 static assert((-1).plurality("one", "many") == "one"); 548 static assert(0.plurality("one", "many") == "many"); 549 } 550 551 552 // unenclosed 553 /++ 554 Removes paired preceding and trailing tokens around a string line. 555 Assumes ASCII. 556 557 You should not need to use this directly; rather see [unquoted] and 558 [unsinglequoted]. 559 560 Params: 561 token = Token character to strip away. 562 line = String line to remove any enclosing tokens from. 563 564 Returns: 565 A slice of the passed string line without enclosing tokens. 566 +/ 567 private auto unenclosed(char token = '"')(/*const*/ return scope string line) pure nothrow @nogc 568 { 569 enum escaped = "\\" ~ token; 570 571 if (line.length < 2) 572 { 573 return line; 574 } 575 else if ((line[0] == token) && (line[$-1] == token)) 576 { 577 if ((line.length >= 3) && (line[$-2..$] == escaped)) 578 { 579 // End quote is escaped 580 return line; 581 } 582 583 return line[1..$-1].unenclosed!token; 584 } 585 else 586 { 587 return line; 588 } 589 } 590 591 592 // unquoted 593 /++ 594 Removes paired preceding and trailing double quotes, unquoting a word. 595 Assumes ASCII. 596 597 Does not decode the string and may thus give weird results on weird inputs. 598 599 Example: 600 --- 601 string quoted = `"This is a quote"`; 602 string unquotedLine = quoted.unquoted; 603 assert((unquotedLine == "This is a quote"), unquotedLine); 604 --- 605 606 Params: 607 line = The (potentially) quoted string. 608 609 Returns: 610 A slice of the `line` argument that excludes the quotes. 611 +/ 612 pragma(inline, true) 613 auto unquoted(/*const*/ return scope string line) pure nothrow @nogc 614 { 615 return unenclosed!'"'(line); 616 } 617 618 /// 619 unittest 620 { 621 assert(`"Lorem ipsum sit amet"`.unquoted == "Lorem ipsum sit amet"); 622 assert(`"""""Lorem ipsum sit amet"""""`.unquoted == "Lorem ipsum sit amet"); 623 // Unbalanced quotes are left untouched 624 assert(`"Lorem ipsum sit amet`.unquoted == `"Lorem ipsum sit amet`); 625 assert(`"Lorem \"`.unquoted == `"Lorem \"`); 626 assert("\"Lorem \\\"".unquoted == "\"Lorem \\\""); 627 assert(`"\"`.unquoted == `"\"`); 628 } 629 630 631 // unsinglequoted 632 /++ 633 Removes paired preceding and trailing single quotes around a line. 634 Assumes ASCII. 635 636 Does not decode the string and may thus give weird results on weird inputs. 637 638 Example: 639 --- 640 string quoted = `'This is single-quoted'`; 641 string unquotedLine = quoted.unsinglequoted; 642 assert((unquotedLine == "This is single-quoted"), unquotedLine); 643 --- 644 645 Params: 646 line = The (potentially) single-quoted string. 647 648 Returns: 649 A slice of the `line` argument that excludes the single-quotes. 650 +/ 651 pragma(inline, true) 652 auto unsinglequoted(/*const*/ return scope string line) pure nothrow @nogc 653 { 654 return unenclosed!'\''(line); 655 } 656 657 /// 658 unittest 659 { 660 assert(`'Lorem ipsum sit amet'`.unsinglequoted == "Lorem ipsum sit amet"); 661 assert(`''''Lorem ipsum sit amet''''`.unsinglequoted == "Lorem ipsum sit amet"); 662 // Unbalanced quotes are left untouched 663 assert(`'Lorem ipsum sit amet`.unsinglequoted == `'Lorem ipsum sit amet`); 664 assert(`'Lorem \'`.unsinglequoted == `'Lorem \'`); 665 assert("'Lorem \\'".unsinglequoted == "'Lorem \\'"); 666 assert(`'`.unsinglequoted == `'`); 667 } 668 669 670 // stripSuffix 671 /++ 672 Strips the supplied string from the end of a string. 673 674 Example: 675 --- 676 string suffixed = "Kameloso"; 677 string stripped = suffixed.stripSuffix("oso"); 678 assert((stripped == "Kamel"), stripped); 679 --- 680 681 Params: 682 line = Original line to strip the suffix from. 683 suffix = Suffix string to strip. 684 685 Returns: 686 `line` with `suffix` sliced off the end. 687 +/ 688 auto stripSuffix( 689 /*const*/ return scope string line, 690 const scope string suffix) pure nothrow @nogc 691 { 692 if (line.length < suffix.length) return line; 693 return (line[($-suffix.length)..$] == suffix) ? line[0..($-suffix.length)] : line; 694 } 695 696 /// 697 unittest 698 { 699 immutable line = "harblsnarbl"; 700 assert(line.stripSuffix("snarbl") == "harbl"); 701 assert(line.stripSuffix("") == "harblsnarbl"); 702 assert(line.stripSuffix("INVALID") == "harblsnarbl"); 703 assert(!line.stripSuffix("harblsnarbl").length); 704 } 705 706 707 // tabs 708 /++ 709 Returns a range of *spaces* equal to that of `num` tabs (\t). 710 711 Use [std.conv.to] or [std.conv.text] or similar to flatten to a string. 712 713 Example: 714 --- 715 string indentation = 2.tabs.text; 716 assert((indentation == " "), `"` ~ indentation ~ `"`); 717 string smallIndent = 1.tabs!2.text; 718 assert((smallIndent == " "), `"` ~ smallIndent ~ `"`); 719 --- 720 721 Params: 722 spaces = How many spaces make up a tab. 723 num = How many tabs we want. 724 725 Returns: 726 A range of whitespace equalling (`num` * `spaces`) spaces. 727 +/ 728 auto tabs(uint spaces = 4)(const int num) pure nothrow @nogc 729 in ((num >= 0), "Negative number of tabs passed to `tabs`") 730 { 731 import std.range : repeat, takeExactly; 732 import std.algorithm.iteration : joiner; 733 import std.array : array; 734 735 enum char[spaces] tab = ' '.repeat.takeExactly(spaces).array; 736 return tab[].repeat.takeExactly(num).joiner; 737 } 738 739 /// 740 @system 741 unittest 742 { 743 import std.array : Appender; 744 import std.conv : to; 745 import std.exception : assertThrown; 746 import std.format : formattedWrite; 747 import std.algorithm.comparison : equal; 748 import core.exception : AssertError; 749 750 auto one = 1.tabs!4; 751 auto two = 2.tabs!3; 752 auto three = 3.tabs!2; 753 auto zero = 0.tabs; 754 755 assert(one.equal(" "), one.to!string); 756 assert(two.equal(" "), two.to!string); 757 assert(three.equal(" "), three.to!string); 758 assert(zero.equal(string.init), zero.to!string); 759 760 assertThrown!AssertError((-1).tabs); 761 762 Appender!(char[]) sink; 763 sink.formattedWrite("%sHello world", 2.tabs!2); 764 assert((sink.data == " Hello world"), sink.data); 765 } 766 767 768 // indentInto 769 /++ 770 Indents lines in a string into an output range sink with the supplied number of tabs. 771 772 Params: 773 spaces = How many spaces in an indenting tab. 774 wallOfText = String to indent the individual lines of. 775 sink = Output range to fill with the indented lines. 776 numTabs = Optional amount of tabs to indent with, default 1. 777 skip = How many lines to skip indenting. 778 +/ 779 void indentInto(uint spaces = 4, Sink) 780 (const string wallOfText, 781 auto ref Sink sink, 782 const uint numTabs = 1, 783 const uint skip = 0) 784 { 785 import std.algorithm.iteration : splitter; 786 import std.range : enumerate; 787 import std.range : isOutputRange; 788 789 static if (!isOutputRange!(Sink, char[])) 790 { 791 enum message = "`indentInto` only works with output ranges of `char[]`"; 792 static assert(0, message); 793 } 794 795 if (numTabs == 0) 796 { 797 sink.put(wallOfText); 798 return; 799 } 800 801 // Must be mutable to work with formattedWrite. That or .to!string 802 auto indent = numTabs.tabs!spaces; 803 804 foreach (immutable i, immutable line; wallOfText.splitter("\n").enumerate) 805 { 806 if (i > 0) sink.put("\n"); 807 808 if (!line.length) 809 { 810 sink.put("\n"); 811 continue; 812 } 813 814 if (skip > i) 815 { 816 sink.put(line); 817 } 818 else 819 { 820 // Cannot just put(indent), put(line) because indent is a joiner Result 821 import std.format : formattedWrite; 822 sink.formattedWrite("%s%s", indent, line); 823 } 824 } 825 } 826 827 /// 828 unittest 829 { 830 import std.array : Appender; 831 832 Appender!(char[]) sink; 833 834 immutable string_ = 835 "Lorem ipsum 836 sit amet 837 I don't remember 838 any more offhand 839 so shrug"; 840 841 string_.indentInto(sink); 842 assert((sink.data == 843 " Lorem ipsum 844 sit amet 845 I don't remember 846 any more offhand 847 so shrug"), '\n' ~ sink.data); 848 849 sink.clear(); 850 string_.indentInto!3(sink, 2); 851 assert((sink.data == 852 " Lorem ipsum 853 sit amet 854 I don't remember 855 any more offhand 856 so shrug"), '\n' ~ sink.data); 857 858 sink.clear(); 859 string_.indentInto(sink, 0); 860 assert((sink.data == 861 "Lorem ipsum 862 sit amet 863 I don't remember 864 any more offhand 865 so shrug"), '\n' ~ sink.data); 866 } 867 868 869 // indent 870 /++ 871 Indents lines in a string with the supplied number of tabs. Returns a newly 872 allocated string. 873 874 Params: 875 spaces = How many spaces make up a tab. 876 wallOfText = String to indent the lines of. 877 numTabs = Amount of tabs to indent with, default 1. 878 skip = How many lines to skip indenting. 879 880 Returns: 881 A string with all the lines of the original string indented. 882 +/ 883 string indent(uint spaces = 4) 884 (const string wallOfText, 885 const uint numTabs = 1, 886 const uint skip = 0) pure 887 { 888 import std.array : Appender; 889 890 Appender!(char[]) sink; 891 sink.reserve(wallOfText.length + 10*spaces*numTabs); // Extra room for 10 indents 892 893 wallOfText.indentInto!spaces(sink, numTabs, skip); 894 return sink.data; 895 } 896 897 /// 898 unittest 899 { 900 immutable string_ = 901 "Lorem ipsum 902 sit amet 903 I don't remember 904 any more offhand 905 so shrug"; 906 907 immutable indentedOne = string_.indent; 908 assert((indentedOne == 909 " Lorem ipsum 910 sit amet 911 I don't remember 912 any more offhand 913 so shrug"), '\n' ~ indentedOne); 914 915 immutable indentedTwo = string_.indent(2); 916 assert((indentedTwo == 917 " Lorem ipsum 918 sit amet 919 I don't remember 920 any more offhand 921 so shrug"), '\n' ~ indentedTwo); 922 923 immutable indentedZero = string_.indent(0); 924 assert((indentedZero == 925 "Lorem ipsum 926 sit amet 927 I don't remember 928 any more offhand 929 so shrug"), '\n' ~ indentedZero); 930 931 immutable indentedSkipTwo = string_.indent(1, 2); 932 assert((indentedSkipTwo == 933 "Lorem ipsum 934 sit amet 935 I don't remember 936 any more offhand 937 so shrug"), '\n' ~ indentedSkipTwo); 938 } 939 940 941 // strippedRight 942 /++ 943 Returns a slice of the passed string with any trailing whitespace and/or 944 linebreaks sliced off. Overload that implicitly strips `" \n\r\t"`. 945 946 Duplicates [std.string.stripRight], which we can no longer trust not to 947 assert on unexpected input. 948 949 Params: 950 line = Line to strip the right side of. 951 952 Returns: 953 The passed line without any trailing whitespace or linebreaks. 954 +/ 955 auto strippedRight(/*const*/ return scope string line) pure nothrow @nogc 956 { 957 if (!line.length) return line; 958 return strippedRight(line, " \n\r\t"); 959 } 960 961 /// 962 unittest 963 { 964 static if (!is(typeof("blah".strippedRight) == string)) 965 { 966 enum message = "`lu.string.strippedRight` should return a mutable string"; 967 static assert(0, message); 968 } 969 970 { 971 immutable trailing = "abc "; 972 immutable stripped = trailing.strippedRight; 973 assert((stripped == "abc"), stripped); 974 } 975 { 976 immutable trailing = " "; 977 immutable stripped = trailing.strippedRight; 978 assert((stripped == ""), stripped); 979 } 980 { 981 immutable empty = ""; 982 immutable stripped = empty.strippedRight; 983 assert((stripped == ""), stripped); 984 } 985 { 986 immutable noTrailing = "abc"; 987 immutable stripped = noTrailing.strippedRight; 988 assert((stripped == "abc"), stripped); 989 } 990 { 991 immutable linebreak = "abc\r\n \r\n"; 992 immutable stripped = linebreak.strippedRight; 993 assert((stripped == "abc"), stripped); 994 } 995 } 996 997 998 // strippedRight 999 /++ 1000 Returns a slice of the passed string with any trailing passed characters. 1001 Implementation template capable of handling both individual characters and 1002 string of tokens to strip. 1003 1004 Duplicates [std.string.stripRight], which we can no longer trust not to 1005 assert on unexpected input. 1006 1007 Params: 1008 line = Line to strip the right side of. 1009 chaff = Character or string of characters to strip away. 1010 1011 Returns: 1012 The passed line without any trailing passed characters. 1013 +/ 1014 auto strippedRight(Line, Chaff) 1015 (/*const*/ return scope Line line, 1016 const scope Chaff chaff) pure nothrow @nogc 1017 { 1018 import std.traits : isArray; 1019 import std.range : ElementEncodingType, ElementType; 1020 1021 static if (!isArray!Line) 1022 { 1023 enum message = "`strippedRight` only works on strings and arrays"; 1024 static assert(0, message); 1025 } 1026 else static if ( 1027 !is(Chaff : Line) && 1028 !is(Chaff : ElementType!Line) && 1029 !is(Chaff : ElementEncodingType!Line)) 1030 { 1031 enum message = "`strippedRight` only works with array- or single-element-type chaff"; 1032 static assert(0, message); 1033 } 1034 1035 if (!line.length) return line; 1036 1037 static if (isArray!Chaff) 1038 { 1039 if (!chaff.length) return line; 1040 } 1041 1042 size_t pos = line.length; 1043 1044 loop: 1045 while (pos > 0) 1046 { 1047 static if (isArray!Chaff) 1048 { 1049 import std.string : representation; 1050 1051 immutable currentChar = line[pos-1]; 1052 1053 foreach (immutable c; chaff.representation) 1054 { 1055 if (currentChar == c) 1056 { 1057 // Found a char we should strip 1058 --pos; 1059 continue loop; 1060 } 1061 } 1062 } 1063 else 1064 { 1065 if (line[pos-1] == chaff) 1066 { 1067 --pos; 1068 continue loop; 1069 } 1070 } 1071 1072 break loop; 1073 } 1074 1075 return line[0..pos]; 1076 } 1077 1078 /// 1079 unittest 1080 { 1081 { 1082 immutable trailing = "abc,"; 1083 immutable stripped = trailing.strippedRight(','); 1084 assert((stripped == "abc"), stripped); 1085 } 1086 { 1087 immutable trailing = "abc!!!"; 1088 immutable stripped = trailing.strippedRight('!'); 1089 assert((stripped == "abc"), stripped); 1090 } 1091 { 1092 immutable trailing = "abc"; 1093 immutable stripped = trailing.strippedRight(' '); 1094 assert((stripped == "abc"), stripped); 1095 } 1096 { 1097 immutable trailing = ""; 1098 immutable stripped = trailing.strippedRight(' '); 1099 assert(!stripped.length, stripped); 1100 } 1101 { 1102 immutable trailing = "abc,!.-"; 1103 immutable stripped = trailing.strippedRight("-.!,"); 1104 assert((stripped == "abc"), stripped); 1105 } 1106 { 1107 immutable trailing = "abc!!!"; 1108 immutable stripped = trailing.strippedRight("!"); 1109 assert((stripped == "abc"), stripped); 1110 } 1111 { 1112 immutable trailing = "abc"; 1113 immutable stripped = trailing.strippedRight(" ABC"); 1114 assert((stripped == "abc"), stripped); 1115 } 1116 { 1117 immutable trailing = ""; 1118 immutable stripped = trailing.strippedRight(" "); 1119 assert(!stripped.length, stripped); 1120 } 1121 } 1122 1123 1124 // strippedLeft 1125 /++ 1126 Returns a slice of the passed string with any preceding whitespace and/or 1127 linebreaks sliced off. Overload that implicitly strips `" \n\r\t"`. 1128 1129 Duplicates [std.string.stripLeft], which we can no longer trust not to 1130 assert on unexpected input. 1131 1132 Params: 1133 line = Line to strip the left side of. 1134 1135 Returns: 1136 The passed line without any preceding whitespace or linebreaks. 1137 +/ 1138 auto strippedLeft(/*const*/ return scope string line) pure nothrow @nogc 1139 { 1140 if (!line.length) return line; 1141 return strippedLeft(line, " \n\r\t"); 1142 } 1143 1144 /// 1145 unittest 1146 { 1147 static if (!is(typeof("blah".strippedLeft) == string)) 1148 { 1149 enum message = "`lu.string.strippedLeft` should return a mutable string"; 1150 static assert(0, message); 1151 } 1152 1153 { 1154 immutable preceded = " abc"; 1155 immutable stripped = preceded.strippedLeft; 1156 assert((stripped == "abc"), stripped); 1157 } 1158 { 1159 immutable preceded = " "; 1160 immutable stripped = preceded.strippedLeft; 1161 assert((stripped == ""), stripped); 1162 } 1163 { 1164 immutable empty = ""; 1165 immutable stripped = empty.strippedLeft; 1166 assert((stripped == ""), stripped); 1167 } 1168 { 1169 immutable noPreceded = "abc"; 1170 immutable stripped = noPreceded.strippedLeft; 1171 assert((stripped == noPreceded), stripped); 1172 } 1173 { 1174 immutable linebreak = "\r\n\r\n abc"; 1175 immutable stripped = linebreak.strippedLeft; 1176 assert((stripped == "abc"), stripped); 1177 } 1178 } 1179 1180 1181 // strippedLeft 1182 /++ 1183 Returns a slice of the passed string with any preceding passed characters 1184 sliced off. Implementation capable of handling both individual characters 1185 and strings of tokens to strip. 1186 1187 Duplicates [std.string.stripLeft], which we can no longer trust not to 1188 assert on unexpected input. 1189 1190 Params: 1191 line = Line to strip the left side of. 1192 chaff = Character or string of characters to strip away. 1193 1194 Returns: 1195 The passed line without any preceding passed characters. 1196 +/ 1197 auto strippedLeft(Line, Chaff) 1198 (/*const*/ return scope Line line, 1199 const scope Chaff chaff) pure nothrow @nogc 1200 { 1201 import std.traits : isArray; 1202 import std.range : ElementEncodingType, ElementType; 1203 1204 static if (!isArray!Line) 1205 { 1206 enum message = "`strippedLeft` only works on strings and arrays"; 1207 static assert(0, message); 1208 } 1209 else static if ( 1210 !is(Chaff : Line) && 1211 !is(Chaff : ElementType!Line) && 1212 !is(Chaff : ElementEncodingType!Line)) 1213 { 1214 enum message = "`strippedLeft` only works with array- or single-element-type chaff"; 1215 static assert(0, message); 1216 } 1217 1218 if (!line.length) return line; 1219 1220 static if (isArray!Chaff) 1221 { 1222 if (!chaff.length) return line; 1223 } 1224 1225 size_t pos; 1226 1227 loop: 1228 while (pos < line.length) 1229 { 1230 static if (isArray!Chaff) 1231 { 1232 import std.string : representation; 1233 1234 immutable currentChar = line[pos]; 1235 1236 foreach (immutable c; chaff.representation) 1237 { 1238 if (currentChar == c) 1239 { 1240 // Found a char we should strip 1241 ++pos; 1242 continue loop; 1243 } 1244 } 1245 } 1246 else 1247 { 1248 if (line[pos] == chaff) 1249 { 1250 ++pos; 1251 continue loop; 1252 } 1253 } 1254 1255 break loop; 1256 } 1257 1258 return line[pos..$]; 1259 } 1260 1261 /// 1262 unittest 1263 { 1264 { 1265 immutable trailing = ",abc"; 1266 immutable stripped = trailing.strippedLeft(','); 1267 assert((stripped == "abc"), stripped); 1268 } 1269 { 1270 immutable trailing = "!!!abc"; 1271 immutable stripped = trailing.strippedLeft('!'); 1272 assert((stripped == "abc"), stripped); 1273 } 1274 { 1275 immutable trailing = "abc"; 1276 immutable stripped = trailing.strippedLeft(' '); 1277 assert((stripped == "abc"), stripped); 1278 } 1279 { 1280 immutable trailing = ""; 1281 immutable stripped = trailing.strippedLeft(' '); 1282 assert(!stripped.length, stripped); 1283 } 1284 { 1285 immutable trailing = ",abc"; 1286 immutable stripped = trailing.strippedLeft(","); 1287 assert((stripped == "abc"), stripped); 1288 } 1289 { 1290 immutable trailing = "!!!abc"; 1291 immutable stripped = trailing.strippedLeft(",1!"); 1292 assert((stripped == "abc"), stripped); 1293 } 1294 { 1295 immutable trailing = "abc"; 1296 immutable stripped = trailing.strippedLeft(" "); 1297 assert((stripped == "abc"), stripped); 1298 } 1299 { 1300 immutable trailing = ""; 1301 immutable stripped = trailing.strippedLeft(" "); 1302 assert(!stripped.length, stripped); 1303 } 1304 } 1305 1306 1307 // stripped 1308 /++ 1309 Returns a slice of the passed string with any preceding or trailing 1310 whitespace or linebreaks sliced off both ends. Overload that implicitly 1311 strips `" \n\r\t"`. 1312 1313 It merely calls both [strippedLeft] and [strippedRight]. As such it 1314 duplicates [std.string.strip], which we can no longer trust not to assert 1315 on unexpected input. 1316 1317 Params: 1318 line = Line to strip both the right and left side of. 1319 1320 Returns: 1321 The passed line, stripped of surrounding whitespace. 1322 +/ 1323 auto stripped(/*const*/ return scope string line) pure nothrow @nogc 1324 { 1325 return line.strippedLeft.strippedRight; 1326 } 1327 1328 /// 1329 unittest 1330 { 1331 static if (!is(typeof("blah".stripped) == string)) 1332 { 1333 enum message = "`lu.string.stripped` should return a mutable string"; 1334 static assert(0, message); 1335 } 1336 1337 { 1338 immutable line = " abc "; 1339 immutable stripped_ = line.stripped; 1340 assert((stripped_ == "abc"), stripped_); 1341 } 1342 { 1343 immutable line = " "; 1344 immutable stripped_ = line.stripped; 1345 assert((stripped_ == ""), stripped_); 1346 } 1347 { 1348 immutable line = ""; 1349 immutable stripped_ = line.stripped; 1350 assert((stripped_ == ""), stripped_); 1351 } 1352 { 1353 immutable line = "abc"; 1354 immutable stripped_ = line.stripped; 1355 assert((stripped_ == "abc"), stripped_); 1356 } 1357 { 1358 immutable line = " \r\n abc\r\n\r\n"; 1359 immutable stripped_ = line.stripped; 1360 assert((stripped_ == "abc"), stripped_); 1361 } 1362 } 1363 1364 1365 // stripped 1366 /++ 1367 Returns a slice of the passed string with any preceding or trailing 1368 passed characters sliced off. Implementation template capable of handling both 1369 individual characters and strings of tokens to strip. 1370 1371 It merely calls both [strippedLeft] and [strippedRight]. As such it 1372 duplicates [std.string.strip], which we can no longer trust not to assert 1373 on unexpected input. 1374 1375 Params: 1376 line = Line to strip both the right and left side of. 1377 chaff = Character or string of characters to strip away. 1378 1379 Returns: 1380 The passed line, stripped of surrounding passed characters. 1381 +/ 1382 auto stripped(Line, Chaff) 1383 (/*const*/ return scope Line line, 1384 const scope Chaff chaff) pure nothrow @nogc 1385 { 1386 import std.traits : isArray; 1387 import std.range : ElementEncodingType, ElementType; 1388 1389 static if (!isArray!Line) 1390 { 1391 enum message = "`stripped` only works on strings and arrays"; 1392 static assert(0, message); 1393 } 1394 else static if ( 1395 !is(Chaff : Line) && 1396 !is(Chaff : ElementType!Line) && 1397 !is(Chaff : ElementEncodingType!Line)) 1398 { 1399 enum message = "`stripped` only works with array- or single-element-type chaff"; 1400 static assert(0, message); 1401 } 1402 1403 return line.strippedLeft(chaff).strippedRight(chaff); 1404 } 1405 1406 /// 1407 unittest 1408 { 1409 { 1410 immutable line = " abc "; 1411 immutable stripped_ = line.stripped(' '); 1412 assert((stripped_ == "abc"), stripped_); 1413 } 1414 { 1415 immutable line = "!!!"; 1416 immutable stripped_ = line.stripped('!'); 1417 assert((stripped_ == ""), stripped_); 1418 } 1419 { 1420 immutable line = ""; 1421 immutable stripped_ = line.stripped('_'); 1422 assert((stripped_ == ""), stripped_); 1423 } 1424 { 1425 immutable line = "abc"; 1426 immutable stripped_ = line.stripped('\t'); 1427 assert((stripped_ == "abc"), stripped_); 1428 } 1429 { 1430 immutable line = " \r\n abc\r\n\r\n "; 1431 immutable stripped_ = line.stripped(' '); 1432 assert((stripped_ == "\r\n abc\r\n\r\n"), stripped_); 1433 } 1434 { 1435 immutable line = " abc "; 1436 immutable stripped_ = line.stripped(" \t"); 1437 assert((stripped_ == "abc"), stripped_); 1438 } 1439 { 1440 immutable line = "!,!!"; 1441 immutable stripped_ = line.stripped("!,"); 1442 assert((stripped_ == ""), stripped_); 1443 } 1444 { 1445 immutable line = ""; 1446 immutable stripped_ = line.stripped("_"); 1447 assert((stripped_ == ""), stripped_); 1448 } 1449 { 1450 immutable line = "abc"; 1451 immutable stripped_ = line.stripped("\t\r\n"); 1452 assert((stripped_ == "abc"), stripped_); 1453 } 1454 { 1455 immutable line = " \r\n abc\r\n\r\n "; 1456 immutable stripped_ = line.stripped(" _"); 1457 assert((stripped_ == "\r\n abc\r\n\r\n"), stripped_); 1458 } 1459 } 1460 1461 1462 // encode64 1463 /++ 1464 Base64-encodes a string. 1465 1466 Merely wraps [std.base64.Base64.encode|Base64.encode] and 1467 [std.string.representation] into one function that will work with strings. 1468 1469 Params: 1470 line = String line to encode. 1471 1472 Returns: 1473 An encoded Base64 string. 1474 1475 See_Also: 1476 - https://en.wikipedia.org/wiki/Base64 1477 +/ 1478 string encode64(const string line) pure nothrow 1479 { 1480 import std.base64 : Base64; 1481 import std.string : representation; 1482 1483 return Base64.encode(line.representation); 1484 } 1485 1486 /// 1487 unittest 1488 { 1489 { 1490 immutable password = "harbl snarbl 12345"; 1491 immutable encoded = encode64(password); 1492 assert((encoded == "aGFyYmwgc25hcmJsIDEyMzQ1"), encoded); 1493 } 1494 { 1495 immutable string password; 1496 immutable encoded = encode64(password); 1497 assert(!encoded.length, encoded); 1498 } 1499 } 1500 1501 1502 // decode64 1503 /++ 1504 Base64-decodes a string. 1505 1506 Merely wraps [std.base64.Base64.decode|Base64.decode] and 1507 [std.string.representation] into one function that will work with strings. 1508 1509 Params: 1510 encoded = Encoded string to decode. 1511 1512 Returns: 1513 A decoded normal string. 1514 1515 See_Also: 1516 - https://en.wikipedia.org/wiki/Base64 1517 +/ 1518 string decode64(const string encoded) pure 1519 { 1520 import std.base64 : Base64; 1521 return (cast(char[])Base64.decode(encoded)).idup; 1522 } 1523 1524 /// 1525 unittest 1526 { 1527 { 1528 immutable password = "base64:aGFyYmwgc25hcmJsIDEyMzQ1"; 1529 immutable decoded = decode64(password[7..$]); 1530 assert((decoded == "harbl snarbl 12345"), decoded); 1531 } 1532 { 1533 immutable password = "base64:"; 1534 immutable decoded = decode64(password[7..$]); 1535 assert(!decoded.length, decoded); 1536 } 1537 } 1538 1539 1540 // splitLineAtPosition 1541 /++ 1542 Splits a string with on boundary as delimited by a supplied separator, into 1543 one or more more lines not longer than the passed maximum length. 1544 1545 If a line cannot be split due to the line being too short or the separator 1546 not occurring in the text, it is added to the returned array as-is and no 1547 more splitting is done. 1548 1549 Example: 1550 --- 1551 string line = "I am a fish in a sort of long sentence~"; 1552 enum maxLineLength = 20; 1553 auto splitLines = line.splitLineAtPosition(' ', maxLineLength); 1554 1555 assert(splitLines[0] == "I am a fish in a"); 1556 assert(splitLines[1] == "sort of a long"); 1557 assert(splitLines[2] == "sentence~"); 1558 --- 1559 1560 Params: 1561 line = String line to split. 1562 separator = Separator character with which to split the `line`. 1563 maxLength = Maximum length of the separated lines. 1564 1565 Returns: 1566 A `T[]` array with lines split out of the passed `line`. 1567 +/ 1568 auto splitLineAtPosition(Line, Separator) 1569 (const Line line, 1570 const Separator separator, 1571 const size_t maxLength) pure //nothrow 1572 in 1573 { 1574 static if (is(Separator : Line)) 1575 { 1576 enum message = "Tried to `splitLineAtPosition` but no " ~ 1577 "`separator` was supplied"; 1578 assert(separator.length, message); 1579 } 1580 } 1581 do 1582 { 1583 import std.traits : isArray; 1584 import std.range : ElementEncodingType, ElementType; 1585 1586 static if (!isArray!Line) 1587 { 1588 enum message = "`splitLineAtPosition` only works on strings and arrays"; 1589 static assert(0, message); 1590 } 1591 else static if ( 1592 !is(Separator : Line) && 1593 !is(Separator : ElementType!Line) && 1594 !is(Separator : ElementEncodingType!Line)) 1595 { 1596 enum message = "`splitLineAtPosition` only works on strings and arrays of characters"; 1597 static assert(0, message); 1598 } 1599 1600 string[] lines; 1601 if (!line.length) return lines; 1602 1603 string slice = line; // mutable 1604 lines.reserve(cast(int)(line.length / maxLength) + 1); 1605 1606 whileloop: 1607 while(true) 1608 { 1609 import std.algorithm.comparison : min; 1610 1611 for (size_t i = min(maxLength, slice.length); i > 0; --i) 1612 { 1613 if (slice[i-1] == separator) 1614 { 1615 lines ~= slice[0..i-1]; 1616 slice = slice[i..$]; 1617 continue whileloop; 1618 } 1619 } 1620 break; 1621 } 1622 1623 if (slice.length) 1624 { 1625 // Remnant 1626 1627 if (lines.length) 1628 { 1629 lines[$-1] ~= separator ~ slice; 1630 } 1631 else 1632 { 1633 // Max line was too short to fit anything. Returning whole line 1634 lines ~= slice; 1635 } 1636 } 1637 1638 return lines; 1639 } 1640 1641 /// 1642 unittest 1643 { 1644 import std.conv : text; 1645 1646 { 1647 immutable prelude = "PRIVMSG #garderoben :"; 1648 immutable maxLength = 250 - prelude.length; 1649 1650 immutable rawLine = "Lorem ipsum dolor sit amet, ea has velit noluisse, " ~ 1651 "eos eius appetere constituto no, ad quas natum eos. Perpetua " ~ 1652 "electram mnesarchum usu ne, mei vero dolorem no. Ea quando scripta " ~ 1653 "quo, minim legendos ut vel. Ut usu graece equidem posidonium. Ius " ~ 1654 "denique ponderum verterem no, quo te mentitum officiis referrentur. " ~ 1655 "Sed an dolor iriure vocibus. " ~ 1656 "Lorem ipsum dolor sit amet, ea has velit noluisse, " ~ 1657 "eos eius appetere constituto no, ad quas natum eos. Perpetua " ~ 1658 "electram mnesarchum usu ne, mei vero dolorem no. Ea quando scripta " ~ 1659 "quo, minim legendos ut vel. Ut usu graece equidem posidonium. Ius " ~ 1660 "denique ponderum verterem no, quo te mentitum officiis referrentur. " ~ 1661 "Sed an dolor iriure vocibus. ssssssssssssssssssssssssssssssssssss" ~ 1662 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1663 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1664 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1665 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1666 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1667 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1668 "ssssssssssssssssssssssssssssssssssssssssssssssssssssssss"; 1669 const splitLines = rawLine.splitLineAtPosition(' ', maxLength); 1670 assert((splitLines.length == 4), splitLines.length.text); 1671 } 1672 { 1673 immutable prelude = "PRIVMSG #garderoben :"; 1674 immutable maxLength = 250 - prelude.length; 1675 1676 immutable rawLine = "ssssssssssssssssssssssssssssssssssss" ~ 1677 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1678 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1679 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1680 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1681 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1682 "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~ 1683 "ssssssssssssssssssssssssssssssssssssssssssssssssssssssss"; 1684 const splitLines = rawLine.splitLineAtPosition(' ', maxLength); 1685 assert((splitLines.length == 1), splitLines.length.text); 1686 assert(splitLines[0] == rawLine); 1687 } 1688 } 1689 1690 1691 // escapeControlCharacters 1692 /++ 1693 Replaces the control characters '\n', '\t', '\r' and '\0' with the escaped 1694 "\\n", "\\t", "\\r" and "\\0". Does not allocate a new string if there 1695 was nothing to escape. 1696 1697 Params: 1698 line = String line to escape characters in. 1699 1700 Returns: 1701 A new string with control characters escaped, or the original one unchanged. 1702 +/ 1703 string escapeControlCharacters(/*const*/ return scope string line) pure nothrow 1704 { 1705 import std.array : Appender; 1706 import std.string : representation; 1707 1708 if (!line.length) return line; 1709 1710 Appender!(char[]) sink; 1711 size_t lastEnd; 1712 bool reserved; 1713 1714 immutable asBytes = line.representation; 1715 1716 void commitUpTo(const size_t i) 1717 { 1718 if (!reserved) 1719 { 1720 sink.reserve(asBytes.length + 16); // guesstimate 1721 reserved = true; 1722 } 1723 sink.put(asBytes[lastEnd..i]); 1724 } 1725 1726 for (size_t i; i<asBytes.length; ++i) 1727 { 1728 import std.algorithm.comparison : among; 1729 1730 if (asBytes[i].among!('\n', '\t', '\r', '\0')) 1731 { 1732 commitUpTo(i); 1733 lastEnd = i+1; 1734 1735 switch (asBytes[i]) 1736 { 1737 case '\n': sink.put(`\n`); break; 1738 case '\t': sink.put(`\t`); break; 1739 case '\r': sink.put(`\r`); break; 1740 case '\0': sink.put(`\0`); break; 1741 default: break; 1742 } 1743 } 1744 } 1745 1746 if (!sink.data.length) return line; 1747 1748 sink.put(asBytes[lastEnd..$]); 1749 return sink.data; 1750 } 1751 1752 /// 1753 unittest 1754 { 1755 { 1756 immutable line = "abc\ndef"; 1757 immutable expected = "abc\\ndef"; 1758 immutable actual = escapeControlCharacters(line); 1759 assert((actual == expected), actual); 1760 } 1761 { 1762 immutable line = "\n\t\r\0"; 1763 immutable expected = "\\n\\t\\r\\0"; 1764 immutable actual = escapeControlCharacters(line); 1765 assert((actual == expected), actual); 1766 } 1767 { 1768 immutable line = ""; 1769 immutable expected = ""; 1770 immutable actual = escapeControlCharacters(line); 1771 assert((actual == expected), actual); 1772 assert(actual is line); // No string allocated 1773 } 1774 { 1775 immutable line = "nothing to escape"; 1776 immutable expected = "nothing to escape"; 1777 immutable actual = escapeControlCharacters(line); 1778 assert((actual == expected), actual); 1779 assert(actual is line); // No string allocated 1780 } 1781 } 1782 1783 1784 // removeControlCharacters 1785 /++ 1786 Removes the control characters `'\n'`, `'\t'`, `'\r'` and `'\0'` from a string. 1787 Does not allocate a new string if there was nothing to remove. 1788 1789 Params: 1790 line = String line to "remove" characters from. 1791 1792 Returns: 1793 A new string with control characters removed, or the original one unchanged. 1794 +/ 1795 string removeControlCharacters(/*const*/ return scope string line) pure nothrow 1796 { 1797 import std.array : Appender; 1798 import std.string : representation; 1799 1800 if (!line.length) return line; 1801 1802 Appender!(char[]) sink; 1803 size_t lastEnd; 1804 bool reserved; 1805 1806 immutable asBytes = line.representation; 1807 1808 void commitUpTo(const size_t i) 1809 { 1810 if (!reserved) 1811 { 1812 sink.reserve(asBytes.length); 1813 reserved = true; 1814 } 1815 sink.put(asBytes[lastEnd..i]); 1816 } 1817 1818 for (size_t i; i<asBytes.length; ++i) 1819 { 1820 import std.algorithm.comparison : among; 1821 1822 if (asBytes[i].among!('\n', '\t', '\r', '\0')) 1823 { 1824 commitUpTo(i); 1825 lastEnd = i+1; 1826 } 1827 } 1828 1829 if (lastEnd == 0) return line; 1830 1831 sink.put(asBytes[lastEnd..$]); 1832 return sink.data; 1833 } 1834 1835 /// 1836 unittest 1837 { 1838 { 1839 immutable line = "abc\ndef"; 1840 immutable expected = "abcdef"; 1841 immutable actual = removeControlCharacters(line); 1842 assert((actual == expected), actual); 1843 } 1844 { 1845 immutable line = "\n\t\r\0"; 1846 immutable expected = ""; 1847 immutable actual = removeControlCharacters(line); 1848 assert((actual == expected), actual); 1849 } 1850 { 1851 immutable line = ""; 1852 immutable expected = ""; 1853 immutable actual = removeControlCharacters(line); 1854 assert((actual == expected), actual); 1855 } 1856 { 1857 immutable line = "nothing to escape"; 1858 immutable expected = "nothing to escape"; 1859 immutable actual = removeControlCharacters(line); 1860 assert((actual == expected), actual); 1861 assert(line is actual); // No new string was allocated 1862 } 1863 } 1864 1865 1866 // SplitResults 1867 /++ 1868 The result of a call to [splitInto]. 1869 +/ 1870 enum SplitResults 1871 { 1872 /++ 1873 The number of arguments passed the number of separated words in the input string. 1874 +/ 1875 match, 1876 1877 /++ 1878 The input string did not have enough words to match the passed arguments. 1879 +/ 1880 underrun, 1881 1882 /++ 1883 The input string had too many words and could not fit into the passed arguments. 1884 +/ 1885 overrun, 1886 } 1887 1888 1889 // splitInto 1890 /++ 1891 Splits a string by a passed separator and assign the delimited words to the 1892 passed strings by ref. 1893 1894 Note: Does *not* take quoted substrings into consideration. 1895 1896 Params: 1897 separator = What token to separate the input string into words with. 1898 slice = Input string of words separated by `separator`. 1899 strings = Variadic list of strings to assign the split words in `slice`. 1900 1901 Returns: 1902 A [SplitResults] with the results of the split attempt. 1903 +/ 1904 auto splitInto(string separator = " ", Strings...) 1905 (auto ref string slice, 1906 scope ref Strings strings) 1907 if (Strings.length && is(Strings[0] == string) && allSameType!Strings) 1908 { 1909 if (!slice.length) 1910 { 1911 return Strings.length ? SplitResults.underrun : SplitResults.match; 1912 } 1913 1914 foreach (immutable i, ref thisString; strings) 1915 { 1916 import std.string : indexOf; 1917 1918 ptrdiff_t pos = slice.indexOf(separator); // mutable 1919 1920 if ((pos == 0) && (separator.length < slice.length)) 1921 { 1922 while (slice[0..separator.length] == separator) 1923 { 1924 slice = slice[separator.length..$]; 1925 } 1926 1927 pos = slice.indexOf(separator); 1928 } 1929 1930 if (pos == -1) 1931 { 1932 thisString = slice; 1933 static if (__traits(isRef, slice)) slice = string.init; 1934 return (i+1 == Strings.length) ? SplitResults.match : SplitResults.underrun; 1935 } 1936 1937 thisString = slice[0..pos]; 1938 slice = slice[pos+separator.length..$]; 1939 } 1940 1941 return SplitResults.overrun; 1942 } 1943 1944 /// 1945 unittest 1946 { 1947 import lu.conv : Enum; 1948 1949 { 1950 string line = "abc def ghi"; 1951 string abc, def, ghi; 1952 immutable results = line.splitInto(abc, def, ghi); 1953 1954 assert((abc == "abc"), abc); 1955 assert((def == "def"), def); 1956 assert((ghi == "ghi"), ghi); 1957 assert(!line.length, line); 1958 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 1959 } 1960 { 1961 string line = "abc def ghi"; 1962 string abc, def, ghi; 1963 immutable results = line.splitInto(abc, def, ghi); 1964 1965 assert((abc == "abc"), abc); 1966 assert((def == "def"), def); 1967 assert((ghi == "ghi"), ghi); 1968 assert(!line.length, line); 1969 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 1970 } 1971 { 1972 string line = "abc_def ghi"; 1973 string abc, def, ghi; 1974 immutable results = line.splitInto!"_"(abc, def, ghi); 1975 1976 assert((abc == "abc"), abc); 1977 assert((def == "def ghi"), def); 1978 assert(!ghi.length, ghi); 1979 assert(!line.length, line); 1980 assert((results == SplitResults.underrun), Enum!SplitResults.toString(results)); 1981 } 1982 { 1983 string line = "abc def ghi"; 1984 string abc, def; 1985 immutable results = line.splitInto(abc, def); 1986 1987 assert((abc == "abc"), abc); 1988 assert((def == "def"), def); 1989 assert((line == "ghi"), line); 1990 assert((results == SplitResults.overrun), Enum!SplitResults.toString(results)); 1991 } 1992 { 1993 string line = "abc///def"; 1994 string abc, def; 1995 immutable results = line.splitInto!"//"(abc, def); 1996 1997 assert((abc == "abc"), abc); 1998 assert((def == "/def"), def); 1999 assert(!line.length, line); 2000 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2001 } 2002 { 2003 string line = "abc 123 def I am a fish"; 2004 string abc, a123, def; 2005 immutable results = line.splitInto(abc, a123, def); 2006 2007 assert((abc == "abc"), abc); 2008 assert((a123 == "123"), a123); 2009 assert((def == "def"), def); 2010 assert((line == "I am a fish"), line); 2011 assert((results == SplitResults.overrun), Enum!SplitResults.toString(results)); 2012 } 2013 { 2014 string line; 2015 string abc, def; 2016 immutable results = line.splitInto(abc, def); 2017 assert((results == SplitResults.underrun), Enum!SplitResults.toString(results)); 2018 } 2019 } 2020 2021 2022 // splitInto 2023 /++ 2024 Splits a string by a passed separator and assign the delimited words to the 2025 passed strings by ref. Overload that stores overflow strings into a passed array. 2026 2027 Note: *Does* take quoted substrings into consideration. 2028 2029 Params: 2030 separator = What token to separate the input string into words with. 2031 slice = Input string of words separated by `separator`. 2032 strings = Variadic list of strings to assign the split words in `slice`. 2033 overflow = Overflow array. 2034 2035 Returns: 2036 A [SplitResults] with the results of the split attempt. 2037 +/ 2038 auto splitInto(string separator = " ", Strings...) 2039 (const string slice, 2040 ref Strings strings, 2041 out string[] overflow) 2042 if (!Strings.length || (is(Strings[0] == string) && allSameType!Strings)) 2043 { 2044 if (!slice.length) 2045 { 2046 return Strings.length ? SplitResults.underrun : SplitResults.match; 2047 } 2048 2049 auto chunks = splitWithQuotes!separator(slice); 2050 2051 foreach (immutable i, ref thisString; strings) 2052 { 2053 if (chunks.length > i) 2054 { 2055 thisString = chunks[i]; 2056 } 2057 } 2058 2059 if (strings.length < chunks.length) 2060 { 2061 overflow = chunks[strings.length..$]; 2062 return SplitResults.overrun; 2063 } 2064 else if (strings.length == chunks.length) 2065 { 2066 return SplitResults.match; 2067 } 2068 else /*if (strings.length > chunks.length)*/ 2069 { 2070 return SplitResults.underrun; 2071 } 2072 } 2073 2074 /// 2075 unittest 2076 { 2077 import lu.conv : Enum; 2078 import std.conv : text; 2079 2080 { 2081 string line = "abc def ghi"; 2082 string abc, def, ghi; 2083 string[] overflow; 2084 immutable results = line.splitInto(abc, def, ghi, overflow); 2085 2086 assert((abc == "abc"), abc); 2087 assert((def == "def"), def); 2088 assert((ghi == "ghi"), ghi); 2089 assert(!overflow.length, overflow.text); 2090 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2091 } 2092 { 2093 string line = "abc##def##ghi"; 2094 string abc, def, ghi; 2095 string[] overflow; 2096 immutable results = line.splitInto!"##"(abc, def, ghi, overflow); 2097 2098 assert((abc == "abc"), abc); 2099 assert((def == "def"), def); 2100 assert((ghi == "ghi"), ghi); 2101 assert(!overflow.length, overflow.text); 2102 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2103 } 2104 { 2105 string line = "abc def ghi"; 2106 string abc, def, ghi; 2107 string[] overflow; 2108 immutable results = line.splitInto(abc, def, ghi, overflow); 2109 2110 assert((abc == "abc"), abc); 2111 assert((def == "def"), def); 2112 assert((ghi == "ghi"), ghi); 2113 assert(!overflow.length, overflow.text); 2114 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2115 } 2116 { 2117 string line = "abc_def ghi"; 2118 string abc, def, ghi; 2119 string[] overflow; 2120 immutable results = line.splitInto!"_"(abc, def, ghi, overflow); 2121 2122 assert((abc == "abc"), abc); 2123 assert((def == "def ghi"), def); 2124 assert(!ghi.length, ghi); 2125 assert(!overflow.length, overflow.text); 2126 assert((results == SplitResults.underrun), Enum!SplitResults.toString(results)); 2127 } 2128 { 2129 string line = "abc def ghi"; 2130 string abc, def; 2131 string[] overflow; 2132 immutable results = line.splitInto(abc, def, overflow); 2133 2134 assert((abc == "abc"), abc); 2135 assert((def == "def"), def); 2136 assert((overflow == [ "ghi" ]), overflow.text); 2137 assert((results == SplitResults.overrun), Enum!SplitResults.toString(results)); 2138 } 2139 { 2140 string line = "abc///def"; 2141 string abc, def; 2142 string[] overflow; 2143 immutable results = line.splitInto!"//"(abc, def, overflow); 2144 2145 assert((abc == "abc"), abc); 2146 assert((def == "/def"), def); 2147 assert(!overflow.length, overflow.text); 2148 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2149 } 2150 { 2151 string line = "abc 123 def I am a fish"; 2152 string abc, a123, def; 2153 string[] overflow; 2154 immutable results = line.splitInto(abc, a123, def, overflow); 2155 2156 assert((abc == "abc"), abc); 2157 assert((a123 == "123"), a123); 2158 assert((def == "def"), def); 2159 assert((overflow == [ "I", "am", "a", "fish" ]), overflow.text); 2160 assert((results == SplitResults.overrun), Enum!SplitResults.toString(results)); 2161 } 2162 { 2163 string line = `abc 123 def "I am a fish"`; 2164 string abc, a123, def; 2165 string[] overflow; 2166 immutable results = line.splitInto(abc, a123, def, overflow); 2167 2168 assert((abc == "abc"), abc); 2169 assert((a123 == "123"), a123); 2170 assert((def == "def"), def); 2171 assert((overflow == [ "I am a fish" ]), overflow.text); 2172 assert((results == SplitResults.overrun), Enum!SplitResults.toString(results)); 2173 } 2174 { 2175 string line; 2176 string abc, def; 2177 string[] overflow; 2178 immutable results = line.splitInto(abc, def, overflow); 2179 assert((results == SplitResults.underrun), Enum!SplitResults.toString(results)); 2180 } 2181 { 2182 string line = "abchonkelonkhonkelodef"; 2183 string abc, def; 2184 string[] overflow; 2185 immutable results = line.splitInto!"honkelonk"(abc, def, overflow); 2186 2187 assert((abc == "abc"), abc); 2188 assert((def == "honkelodef"), def); 2189 assert(!overflow.length, overflow.text); 2190 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2191 } 2192 { 2193 string line = "honkelonkhonkelodef"; 2194 string abc, def; 2195 string[] overflow; 2196 immutable results = line.splitInto!"honkelonk"(abc, def, overflow); 2197 2198 assert((abc == "honkelodef"), abc); 2199 assert((def == string.init), def); 2200 assert(!overflow.length, overflow.text); 2201 assert((results == SplitResults.underrun), Enum!SplitResults.toString(results)); 2202 } 2203 { 2204 string line = "###########hirrsteff#snabel"; 2205 string abc, def; 2206 string[] overflow; 2207 immutable results = line.splitInto!"#"(abc, def, overflow); 2208 2209 assert((abc == "hirrsteff"), abc); 2210 assert((def == "snabel"), def); 2211 assert(!overflow.length, overflow.text); 2212 assert((results == SplitResults.match), Enum!SplitResults.toString(results)); 2213 } 2214 { 2215 string line = "abc def ghi"; 2216 string[] overflow; 2217 immutable results = line.splitInto(overflow); 2218 immutable expectedOverflow = [ "abc", "def", "ghi" ]; 2219 2220 assert((overflow == expectedOverflow), overflow.text); 2221 assert((results == SplitResults.overrun), Enum!SplitResults.toString(results)); 2222 } 2223 } 2224 2225 2226 // splitWithQuotes 2227 /++ 2228 Splits a string into an array of strings by whitespace, but honours quotes. 2229 2230 Intended to be used with ASCII strings; may or may not work with more 2231 elaborate UTF-8 strings. 2232 2233 Example: 2234 --- 2235 string s = `title "this is my title" author "john doe"`; 2236 immutable splitUp = splitWithQuotes(s); 2237 assert(splitUp == [ "title", "this is my title", "author", "john doe" ]); 2238 --- 2239 2240 Params: 2241 separator = Separator string. May be more than one character. 2242 line = Input string. 2243 2244 Returns: 2245 A `string[]` composed of the input string split up into substrings, 2246 delimited by whitespace. Quoted sections are treated as one substring. 2247 +/ 2248 auto splitWithQuotes(string separator = " ")(const string line) 2249 { 2250 import std.array : Appender; 2251 import std.string : representation; 2252 2253 static if (!separator.length) 2254 { 2255 enum message = "`splitWithQuotes` only works with non-empty separators"; 2256 static assert(0, message); 2257 } 2258 2259 if (!line.length) return null; 2260 2261 Appender!(string[]) sink; 2262 sink.reserve(8); // guesstimate 2263 2264 size_t start; 2265 bool betweenQuotes; 2266 bool escaping; 2267 bool escapedAQuote; 2268 bool escapedABackslash; 2269 2270 string replaceEscaped(const string line) 2271 { 2272 import std.array : replace; 2273 2274 string slice = line; // mutable 2275 if (escapedABackslash) slice = slice.replace(`\\`, "\1\1"); 2276 if (escapedAQuote) slice = slice.replace(`\"`, `"`); 2277 if (escapedABackslash) slice = slice.replace("\1\1", `\`); 2278 return slice; 2279 } 2280 2281 immutable asUbytes = line.representation; 2282 size_t separatorStep; 2283 2284 for (size_t i; i < asUbytes.length; ++i) 2285 { 2286 immutable c = asUbytes[i]; 2287 2288 if (escaping) 2289 { 2290 if (c == '\\') 2291 { 2292 escapedABackslash = true; 2293 } 2294 else if (c == '"') 2295 { 2296 escapedAQuote = true; 2297 } 2298 2299 escaping = false; 2300 } 2301 else if (separatorStep >= separator.length) 2302 { 2303 separatorStep = 0; 2304 } 2305 else if (!betweenQuotes && (c == separator[separatorStep])) 2306 { 2307 static if (separator.length > 1) 2308 { 2309 if (i == 0) 2310 { 2311 ++separatorStep; 2312 continue; 2313 } 2314 else if (++separatorStep >= separator.length) 2315 { 2316 // Full separator 2317 immutable end = i-separator.length+1; 2318 if (start != end) sink.put(line[start..end]); 2319 start = i+1; 2320 } 2321 } 2322 else 2323 { 2324 // Full separator 2325 if (start != i) sink.put(line[start..i]); 2326 start = i+1; 2327 } 2328 } 2329 else if (c == '\\') 2330 { 2331 escaping = true; 2332 } 2333 else if (c == '"') 2334 { 2335 if (betweenQuotes) 2336 { 2337 if (escapedAQuote || escapedABackslash) 2338 { 2339 sink.put(replaceEscaped(line[start+1..i])); 2340 escapedAQuote = false; 2341 escapedABackslash = false; 2342 } 2343 else if (i > start+1) 2344 { 2345 sink.put(line[start+1..i]); 2346 } 2347 2348 betweenQuotes = false; 2349 start = i+1; 2350 } 2351 else if (i > start+1) 2352 { 2353 sink.put(line[start+1..i]); 2354 betweenQuotes = true; 2355 start = i+1; 2356 } 2357 else 2358 { 2359 betweenQuotes = true; 2360 } 2361 } 2362 } 2363 2364 if (line.length > start+1) 2365 { 2366 if (betweenQuotes) 2367 { 2368 if (escapedAQuote || escapedABackslash) 2369 { 2370 sink.put(replaceEscaped(line[start+1..$])); 2371 } 2372 else 2373 { 2374 sink.put(line[start+1..$]); 2375 } 2376 } 2377 else 2378 { 2379 sink.put(line[start..$]); 2380 } 2381 } 2382 2383 return sink.data; 2384 } 2385 2386 /// 2387 unittest 2388 { 2389 import std.conv : text; 2390 2391 { 2392 enum input = `title "this is my title" author "john doe"`; 2393 immutable splitUp = splitWithQuotes(input); 2394 immutable expected = 2395 [ 2396 "title", 2397 "this is my title", 2398 "author", 2399 "john doe" 2400 ]; 2401 assert(splitUp == expected, splitUp.text); 2402 } 2403 { 2404 enum input = `string without quotes`; 2405 immutable splitUp = splitWithQuotes(input); 2406 immutable expected = 2407 [ 2408 "string", 2409 "without", 2410 "quotes", 2411 ]; 2412 assert(splitUp == expected, splitUp.text); 2413 } 2414 { 2415 enum input = string.init; 2416 immutable splitUp = splitWithQuotes(input); 2417 immutable expected = (string[]).init; 2418 assert(splitUp == expected, splitUp.text); 2419 } 2420 { 2421 enum input = `title "this is \"my\" title" author "john\\" doe`; 2422 immutable splitUp = splitWithQuotes(input); 2423 immutable expected = 2424 [ 2425 "title", 2426 `this is "my" title`, 2427 "author", 2428 `john\`, 2429 "doe" 2430 ]; 2431 assert(splitUp == expected, splitUp.text); 2432 } 2433 { 2434 enum input = `title "this is \"my\" title" author "john\\\" doe`; 2435 immutable splitUp = splitWithQuotes(input); 2436 immutable expected = 2437 [ 2438 "title", 2439 `this is "my" title`, 2440 "author", 2441 `john\" doe` 2442 ]; 2443 assert(splitUp == expected, splitUp.text); 2444 } 2445 { 2446 enum input = `this has "unbalanced quotes`; 2447 immutable splitUp = splitWithQuotes(input); 2448 immutable expected = 2449 [ 2450 "this", 2451 "has", 2452 "unbalanced quotes" 2453 ]; 2454 assert(splitUp == expected, splitUp.text); 2455 } 2456 { 2457 enum input = `""`; 2458 immutable splitUp = splitWithQuotes(input); 2459 immutable expected = (string[]).init; 2460 assert(splitUp == expected, splitUp.text); 2461 } 2462 { 2463 enum input = `"`; 2464 immutable splitUp = splitWithQuotes(input); 2465 immutable expected = (string[]).init; 2466 assert(splitUp == expected, splitUp.text); 2467 } 2468 { 2469 enum input = `"""""""""""`; 2470 immutable splitUp = splitWithQuotes(input); 2471 immutable expected = (string[]).init; 2472 assert(splitUp == expected, splitUp.text); 2473 } 2474 } 2475 2476 2477 // replaceFromAA 2478 /++ 2479 Replaces space-separated tokens (that begin with a token character) in a 2480 string with values from a supplied associative array. 2481 2482 The AA values are of some type of function or delegate returning strings. 2483 2484 Example: 2485 --- 2486 const @safe string delegate()[string] aa = 2487 [ 2488 "$foo" : () => "bar", 2489 "$baz" : () => "quux" 2490 "$now" : () => Clock.currTime.toISOExtString(), 2491 "$rng" : () => uniform(0, 100).to!string, 2492 "$hirr" : () => 10.to!string, 2493 ]; 2494 2495 immutable line = "first $foo second $baz third $hirr end"; 2496 enum expected = "first bar second quux third 10 end"; 2497 immutable actual = line.replaceFromAA(aa); 2498 assert((actual == expected), actual); 2499 --- 2500 2501 Params: 2502 tokenCharacter = What character to use to denote tokens, defaults to '`$`' 2503 but may be any `char`. 2504 line = String to replace tokens in. 2505 aa = Associative array of token keys and replacement callable values. 2506 2507 Returns: 2508 A new string with occurrences of any passed tokens replaced, or the 2509 original string as-is if there were no changes made. 2510 +/ 2511 auto replaceFromAA(char tokenCharacter = '$', Fn) 2512 (const string line, 2513 const Fn[string] aa) 2514 { 2515 import std.array : Appender; 2516 import std.string : indexOf; 2517 import std.traits : isSomeFunction; 2518 2519 static if (!isSomeFunction!Fn) 2520 { 2521 enum message = "`replaceFromAA` only works with functions and delegates"; 2522 static assert(0, message); 2523 } 2524 2525 Appender!(char[]) sink; 2526 sink.reserve(line.length + 32); // guesstimate 2527 size_t previousEnd; 2528 2529 for (size_t i = 0; i < line.length; ++i) 2530 { 2531 if (line[i] == tokenCharacter) 2532 { 2533 immutable spacePos = line.indexOf(' ', i); 2534 immutable end = (spacePos == -1) ? line.length : spacePos; 2535 immutable key = line[i..end]; 2536 2537 if (const replacement = key in aa) 2538 { 2539 sink.put(line[previousEnd..i]); 2540 sink.put((*replacement)()); 2541 i += key.length; 2542 previousEnd = i; 2543 } 2544 } 2545 } 2546 2547 if (previousEnd == 0) return line; 2548 2549 sink.put(line[previousEnd..$]); 2550 return sink.data.idup; 2551 } 2552 2553 /// 2554 unittest 2555 { 2556 static auto echo(const string what) { return what; } 2557 2558 immutable hello = "hello"; 2559 2560 @safe string delegate()[string] aa = 2561 [ 2562 "$foo" : () => hello, 2563 "$bar" : () => echo("I was one"), 2564 "$baz" : () => "BAZ", 2565 ]; 2566 2567 enum line = "I thought what I'd $foo was, I'd pretend $bar of those deaf-$baz"; 2568 enum expected = "I thought what I'd hello was, I'd pretend I was one of those deaf-BAZ"; 2569 immutable actual = line.replaceFromAA(aa); 2570 assert((actual == expected), actual); 2571 }