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