1 /++ 2 This module contains functions that in some way or another manipulates 3 struct and class instances, as well as (associative) arrays. 4 5 Example: 6 --- 7 struct Foo 8 { 9 string nickname; 10 string address; 11 } 12 13 Foo foo; 14 15 foo.setMemberByName("nickname", "foobar"); 16 foo.setMemberByName("address", "subdomain.address.tld"); 17 18 assert(foo.nickname == "foobar"); 19 assert(foo.address == "subdomain.address.tld"); 20 21 foo.replaceMembers("subdomain.address.tld", "foobar"); 22 assert(foo.address == "foobar"); 23 24 foo.replaceMembers("foobar", string.init); 25 assert(foo.nickname.length == 0); 26 assert(foo.address.length == 0); 27 --- 28 29 Copyright: [JR](https://github.com/zorael) 30 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 31 32 Authors: 33 [JR](https://github.com/zorael) 34 +/ 35 module lu.objmanip; 36 37 private: 38 39 import std.traits : isAggregateType, isAssociativeArray, isEqualityComparable, isMutable; 40 import std.typecons : Flag, No, Yes; 41 42 public: 43 44 45 // Separator 46 /++ 47 Public import of `lu.uda.Separator`. 48 +/ 49 import lu.uda : Separator; 50 51 52 // setMemberByName 53 /++ 54 Given a struct/class object, sets one of its members by its string name to a 55 specified value. Overload that takes the value as a string and tries to 56 convert it into the target type. 57 58 It does not currently recurse into other struct/class members. 59 60 Example: 61 --- 62 struct Foo 63 { 64 string name; 65 int number; 66 bool alive; 67 } 68 69 Foo foo; 70 71 foo.setMemberByName("name", "James Bond"); 72 foo.setMemberByName("number", "007"); 73 foo.setMemberByName("alive", "false"); 74 75 assert(foo.name == "James Bond"); 76 assert(foo.number == 7); 77 assert(!foo.alive); 78 --- 79 80 Params: 81 thing = Reference object whose members to set. 82 memberToSet = String name of the thing's member to set. 83 valueToSet = String contents of the value to set the member to; string 84 even if the member is of a different type. 85 86 Returns: 87 `true` if a member was found and set, `false` if nothing was done. 88 89 Throws: [std.conv.ConvException|ConvException] if a string could not be 90 converted into an array, if a passed string could not be converted into 91 a bool, or if [std.conv.to] failed to convert a string into wanted type `T`. 92 [SetMemberException] if an unexpected exception was thrown. 93 +/ 94 auto setMemberByName(Thing) 95 (ref Thing thing, 96 const string memberToSet, 97 const string valueToSet) 98 if (isAggregateType!Thing && isMutable!Thing) 99 in (memberToSet.length, "Tried to set member by name but no member string was given") 100 { 101 import lu.string : stripSuffix, stripped, unquoted; 102 import std.conv : ConvException, to; 103 104 bool success; 105 106 top: 107 switch (memberToSet) 108 { 109 static foreach (immutable i; 0..thing.tupleof.length) 110 {{ 111 alias QualT = typeof(thing.tupleof[i]); 112 113 static if (!isMutable!QualT) 114 { 115 // Can't set const or immutable, so just ignore and break 116 enum memberstring = __traits(identifier, thing.tupleof[i]); 117 118 case memberstring: 119 break top; 120 } 121 else 122 { 123 import lu.traits : isSerialisable; 124 import std.traits : Unqual; 125 126 alias T = Unqual!QualT; 127 128 static if (isSerialisable!(thing.tupleof[i])) 129 { 130 enum memberstring = __traits(identifier, thing.tupleof[i]); 131 132 case memberstring: 133 { 134 import std.traits : isAggregateType, isArray, 135 isAssociativeArray, isPointer, isSomeString; 136 137 static if (isAggregateType!T) 138 { 139 static if (__traits(compiles, { thing.tupleof[i] = string.init; })) 140 { 141 thing.tupleof[i] = valueToSet.stripped.unquoted; 142 success = true; 143 } 144 145 // Else do nothing 146 } 147 else static if (!isSomeString!T && isArray!T) 148 { 149 import lu.traits : udaIndexOf; 150 import lu.uda : Separator; 151 import std.algorithm.iteration : splitter; 152 import std.array : replace; 153 import std.traits : getUDAs; 154 155 thing.tupleof[i].length = 0; 156 157 static if (udaIndexOf!(thing.tupleof[i], Separator) != -1) 158 { 159 alias separators = getUDAs!(thing.tupleof[i], Separator); 160 } 161 else static if ((__VERSION__ >= 2087L) && (udaIndexOf!(thing.tupleof[i], string) != -1)) 162 { 163 alias separators = getUDAs!(thing.tupleof[i], string); 164 } 165 else 166 { 167 import std.format : format; 168 static assert(0, "`%s.%s` is missing a `Separator` annotation" 169 .format(Thing.stringof, memberstring)); 170 } 171 172 enum escapedPlaceholder = "\0\0"; // anything really 173 enum ephemeralSeparator = "\1\1"; // ditto 174 enum doubleEphemeral = ephemeralSeparator ~ ephemeralSeparator; 175 enum doubleEscapePlaceholder = "\2\2"; 176 177 string values = valueToSet.replace("\\\\", doubleEscapePlaceholder); 178 179 foreach (immutable thisSeparator; separators) 180 { 181 static if (is(Unqual!(typeof(thisSeparator)) == Separator)) 182 { 183 enum escapedSeparator = '\\' ~ thisSeparator.token; 184 enum separator = thisSeparator.token; 185 } 186 else 187 { 188 enum escapedSeparator = '\\' ~ thisSeparator; 189 alias separator = thisSeparator; 190 } 191 192 values = values 193 .replace(escapedSeparator, escapedPlaceholder) 194 .replace(separator, ephemeralSeparator) 195 .replace(escapedPlaceholder, separator); 196 } 197 198 values = values 199 .replace(doubleEphemeral, ephemeralSeparator) 200 .replace(doubleEscapePlaceholder, "\\"); 201 202 auto range = values.splitter(ephemeralSeparator); 203 204 foreach (immutable entry; range) 205 { 206 if (!entry.length) continue; 207 208 try 209 { 210 import std.range : ElementEncodingType; 211 212 thing.tupleof[i] ~= entry 213 .stripped 214 .unquoted 215 .to!(ElementEncodingType!T); 216 217 success = true; 218 } 219 catch (ConvException e) 220 { 221 import std.format : format; 222 223 enum pattern = "Could not convert `%s.%s` array " ~ 224 "entry \"%s\" into `%s` (%s)"; 225 immutable message = pattern.format( 226 Thing.stringof.stripSuffix("Settings"), 227 memberToSet, 228 entry, 229 T.stringof, 230 e.msg); 231 232 throw new ConvException(message); 233 } 234 catch (Exception e) 235 { 236 import std.format : format; 237 238 enum pattern = "A set-member action failed: %s"; 239 immutable message = pattern.format(e.msg); 240 241 throw new SetMemberException(message, Thing.stringof, 242 memberToSet, values); 243 } 244 } 245 } 246 else static if (is(T : string)) 247 { 248 thing.tupleof[i] = valueToSet.stripped.unquoted; 249 success = true; 250 } 251 else static if (isAssociativeArray!T) 252 { 253 static if (__traits(compiles, valueToSet.to!T)) 254 { 255 try 256 { 257 thing.tupleof[i] = valueToSet.stripped.unquoted.to!T; 258 success = true; 259 } 260 catch (ConvException e) 261 { 262 import std.format : format; 263 264 enum pattern = "Could not convert `%s.%s` text \"%s\" " ~ 265 "to a `%s` associative array (%s)"; 266 immutable message = pattern.format( 267 Thing.stringof.stripSuffix("Settings"), 268 memberToSet, 269 valueToSet.stripped.unquoted, 270 T.stringof, 271 e.msg); 272 273 throw new ConvException(message); 274 } 275 catch (Exception e) 276 { 277 import std.format : format; 278 279 enum pattern = "A set-member action failed (AA): %s"; 280 immutable message = pattern.format(e.msg); 281 282 throw new SetMemberException(message, Thing.stringof, 283 memberToSet, valueToSet.stripped.unquoted); 284 } 285 } 286 else 287 { 288 // Inconvertible AA, silently ignore 289 } 290 } 291 else static if (isPointer!T) 292 { 293 // Ditto for pointers 294 } 295 else static if (is(T == bool)) 296 { 297 import std.uni : toLower; 298 299 switch (valueToSet.stripped.unquoted.toLower) 300 { 301 case "true": 302 case "yes": 303 case "on": 304 case "1": 305 thing.tupleof[i] = true; 306 break; 307 308 case "false": 309 case "no": 310 case "off": 311 case "0": 312 thing.tupleof[i] = false; 313 break; 314 315 default: 316 import std.format : format; 317 318 enum pattern = "Invalid value for setting `%s.%s`: " ~ 319 `could not convert "%s" to a boolean value`; 320 immutable message = pattern.format( 321 Thing.stringof.stripSuffix("Settings"), 322 memberToSet, 323 valueToSet); 324 325 throw new ConvException(message); 326 } 327 328 success = true; 329 } 330 else 331 { 332 try 333 { 334 static if (is(T == enum)) 335 { 336 import lu.conv : Enum; 337 338 immutable asString = valueToSet 339 .stripped 340 .unquoted; 341 thing.tupleof[i] = Enum!T.fromString(asString); 342 } 343 else 344 { 345 /*writefln("%s.%s = %s.to!%s", Thing.stringof, 346 memberstring, valueToSet, T.stringof);*/ 347 thing.tupleof[i] = valueToSet 348 .stripped 349 .unquoted 350 .to!T; 351 } 352 353 success = true; 354 } 355 catch (ConvException e) 356 { 357 import std.format : format; 358 359 enum pattern = "Invalid value for setting `%s.%s`: " ~ 360 "could not convert \"%s\" to `%s` (%s)"; 361 immutable message = pattern.format( 362 Thing.stringof.stripSuffix("Settings"), 363 memberToSet, 364 valueToSet, 365 T.stringof, 366 e.msg); 367 368 throw new ConvException(message); 369 } 370 catch (Exception e) 371 { 372 import std.format : format; 373 374 enum pattern = "A set-member action failed: %s"; 375 immutable message = pattern.format(e.msg); 376 377 throw new SetMemberException(message, Thing.stringof, 378 memberToSet, valueToSet); 379 } 380 } 381 break top; 382 } 383 } 384 } 385 }} 386 387 default: 388 break; 389 } 390 391 return success; 392 } 393 394 /// 395 unittest 396 { 397 import lu.uda : Separator; 398 import std.conv : to; 399 400 struct Foo 401 { 402 string bar; 403 int baz; 404 float* f; 405 string[string] aa; 406 407 @Separator("|") 408 @Separator(" ") 409 { 410 string[] arr; 411 string[] matey; 412 } 413 414 @Separator(";;") 415 { 416 string[] parrots; 417 string[] withSpaces; 418 } 419 420 @Separator(`\o/`) 421 { 422 string[] blurgh; 423 } 424 425 static if (__VERSION__ >= 2087L) 426 { 427 @(`\o/`) 428 { 429 int[] blargh; 430 } 431 } 432 } 433 434 Foo foo; 435 bool success; 436 437 success = foo.setMemberByName("bar", "asdf fdsa adf"); 438 assert(success); 439 assert((foo.bar == "asdf fdsa adf"), foo.bar); 440 441 success = foo.setMemberByName("baz", "42"); 442 assert(success); 443 assert((foo.baz == 42), foo.baz.to!string); 444 445 success = foo.setMemberByName("aa", `["abc":"def", "ghi":"jkl"]`); 446 assert(success); 447 assert((foo.aa == [ "abc":"def", "ghi":"jkl" ]), foo.aa.to!string); 448 449 success = foo.setMemberByName("arr", "herp|derp|dirp|darp"); 450 assert(success); 451 assert((foo.arr == [ "herp", "derp", "dirp", "darp"]), foo.arr.to!string); 452 453 success = foo.setMemberByName("arr", "herp derp dirp|darp"); 454 assert(success); 455 assert((foo.arr == [ "herp", "derp", "dirp", "darp"]), foo.arr.to!string); 456 457 success = foo.setMemberByName("matey", "this,should,not,be,separated"); 458 assert(success); 459 assert((foo.matey == [ "this,should,not,be,separated" ]), foo.matey.to!string); 460 461 success = foo.setMemberByName("parrots", "squaawk;;parrot sounds;;repeating"); 462 assert(success); 463 assert((foo.parrots == [ "squaawk", "parrot sounds", "repeating"]), 464 foo.parrots.to!string); 465 466 success = foo.setMemberByName("withSpaces", ` squoonk ;;" spaced ";;" "`); 467 assert(success); 468 assert((foo.withSpaces == [ "squoonk", ` spaced `, " "]), 469 foo.withSpaces.to!string); 470 471 success = foo.setMemberByName("invalid", "oekwpo"); 472 assert(!success); 473 474 /*success = foo.setMemberByName("", "true"); 475 assert(!success);*/ 476 477 success = foo.setMemberByName("matey", "hirr steff\\ stuff staff\\|stirf hooo"); 478 assert(success); 479 assert((foo.matey == [ "hirr", "steff stuff", "staff|stirf", "hooo" ]), foo.matey.to!string); 480 481 success = foo.setMemberByName("matey", "hirr steff\\\\ stuff staff\\\\|stirf hooo"); 482 assert(success); 483 assert((foo.matey == [ "hirr", "steff\\", "stuff", "staff\\", "stirf", "hooo" ]), foo.matey.to!string); 484 485 success = foo.setMemberByName("matey", "asdf\\ fdsa\\\\ hirr steff"); 486 assert(success); 487 assert((foo.matey == [ "asdf fdsa\\", "hirr", "steff" ]), foo.matey.to!string); 488 489 success = foo.setMemberByName("blurgh", "asdf\\\\o/fdsa\\\\\\o/hirr\\o/\\o/\\o/\\o/\\o/\\o/\\o/\\o/steff"); 490 assert(success); 491 assert((foo.blurgh == [ "asdf\\o/fdsa\\", "hirr", "steff" ]), foo.blurgh.to!string); 492 493 static if (__VERSION__ >= 2087L) 494 { 495 success = foo.setMemberByName("blargh", `1\o/2\o/3\o/4\o/5`); 496 assert(success); 497 assert((foo.blargh == [ 1, 2, 3, 4, 5 ]), foo.blargh.to!string); 498 } 499 500 class C 501 { 502 string abc; 503 int def; 504 } 505 506 C c = new C; 507 508 success = c.setMemberByName("abc", "this is abc"); 509 assert(success); 510 assert((c.abc == "this is abc"), c.abc); 511 512 success = c.setMemberByName("def", "42"); 513 assert(success); 514 assert((c.def == 42), c.def.to!string); 515 516 import lu.conv : Enum; 517 518 enum E { abc, def, ghi } 519 520 struct S 521 { 522 E e = E.ghi; 523 } 524 525 S s; 526 527 assert(s.e == E.ghi); 528 success = s.setMemberByName("e", "def"); 529 assert(success); 530 assert((s.e == E.def), Enum!E.toString(s.e)); 531 532 struct StructWithOpAssign 533 { 534 string thing = "init"; 535 536 void opAssign(const string thing) 537 { 538 this.thing = thing; 539 } 540 } 541 542 StructWithOpAssign assignable; 543 assert((assignable.thing == "init"), assignable.thing); 544 assignable = "new thing"; 545 assert((assignable.thing == "new thing"), assignable.thing); 546 547 struct StructWithAssignableMember 548 { 549 StructWithOpAssign child; 550 } 551 552 StructWithAssignableMember parent; 553 success = parent.setMemberByName("child", "flerp"); 554 assert(success); 555 assert((parent.child.thing == "flerp"), parent.child.thing); 556 557 class ClassWithOpAssign 558 { 559 string thing = "init"; 560 561 void opAssign(const string thing) //@safe pure nothrow @nogc 562 { 563 this.thing = thing; 564 } 565 } 566 567 class ClassWithAssignableMember 568 { 569 ClassWithOpAssign child; 570 571 this() 572 { 573 child = new ClassWithOpAssign; 574 } 575 } 576 577 ClassWithAssignableMember parent2 = new ClassWithAssignableMember; 578 success = parent2.setMemberByName("child", "flerp"); 579 assert(success); 580 assert((parent2.child.thing == "flerp"), parent2.child.thing); 581 } 582 583 584 // setMemberByName 585 /++ 586 Given a struct/class object, sets one of its members by its string name to a 587 specified value. Overload that takes a value of the same type as the target 588 member, rather than a string to convert. Integer promotion applies. 589 590 It does not currently recurse into other struct/class members. 591 592 Example: 593 --- 594 struct Foo 595 { 596 int i; 597 double d; 598 } 599 600 Foo foo; 601 602 foo.setMemberByName("i", 42); 603 foo.setMemberByName("d", 3.14); 604 605 assert(foo.i == 42); 606 assert(foo.d = 3.14); 607 --- 608 609 Params: 610 thing = Reference object whose members to set. 611 memberToSet = String name of the thing's member to set. 612 valueToSet = Value, of the same type as the target member. 613 614 Returns: 615 `true` if a member was found and set, `false` if not. 616 617 Throws: [SetMemberException] if the passed `valueToSet` was not the same type 618 (or implicitly convertible to) the member to set. 619 +/ 620 auto setMemberByName(Thing, Val) 621 (ref Thing thing, 622 const string memberToSet, 623 /*const*/ Val valueToSet) 624 if (isAggregateType!Thing && isMutable!Thing && !is(Val : string)) 625 in (memberToSet.length, "Tried to set member by name but no member string was given") 626 { 627 bool success; 628 629 top: 630 switch (memberToSet) 631 { 632 static foreach (immutable i; 0..thing.tupleof.length) 633 {{ 634 alias QualT = typeof(thing.tupleof[i]); 635 enum memberstring = __traits(identifier, thing.tupleof[i]); 636 637 static if (!isMutable!QualT) 638 { 639 // Can't set const or immutable, so just ignore and break 640 case memberstring: 641 break top; 642 } 643 else 644 { 645 import lu.traits : isSerialisable; 646 import std.traits : Unqual; 647 648 alias T = Unqual!QualT; 649 650 static if (isSerialisable!(thing.tupleof[i])) 651 { 652 case memberstring: 653 { 654 static if (is(Val : T)) 655 { 656 thing.tupleof[i] = valueToSet; 657 success = true; 658 break top; 659 } 660 else 661 { 662 import std.conv : to; 663 enum message = "A set-member action failed due to type mismatch"; 664 throw new SetMemberException(message, Thing.stringof, 665 memberToSet, valueToSet.to!string); 666 } 667 } 668 } 669 } 670 }} 671 672 default: 673 break; 674 } 675 676 return success; 677 } 678 679 /// 680 unittest 681 { 682 import std.conv : to; 683 import std.exception : assertThrown; 684 685 struct Foo 686 { 687 string s; 688 int i; 689 bool b; 690 const double d; 691 } 692 693 Foo foo; 694 695 bool success; 696 697 success = foo.setMemberByName("s", "harbl"); 698 assert(success); 699 assert((foo.s == "harbl"), foo.s); 700 701 success = foo.setMemberByName("i", 42); 702 assert(success); 703 assert((foo.i == 42), foo.i.to!string); 704 705 success = foo.setMemberByName("b", true); 706 assert(success); 707 assert(foo.b); 708 709 success = foo.setMemberByName("d", 3.14); 710 assert(!success); 711 712 assertThrown!SetMemberException(foo.setMemberByName("b", 3.14)); 713 } 714 715 716 @safe: 717 718 719 // SetMemberException 720 /++ 721 Exception, to be thrown when [setMemberByName] fails for some given reason. 722 723 It is a normal [object.Exception|Exception] but with attached strings of 724 the type name, name of member and the value that was attempted to set. 725 +/ 726 final class SetMemberException : Exception 727 { 728 /++ 729 Name of type that was attempted to set the member of. 730 +/ 731 string typeName; 732 733 /++ 734 Name of the member that was attempted to set. 735 +/ 736 string memberToSet; 737 738 /++ 739 String representation of the value that was attempted to assign. 740 +/ 741 string valueToSet; 742 743 /++ 744 Create a new [SetMemberException], without attaching anything. 745 +/ 746 this(const string message, 747 const string file = __FILE__, 748 const size_t line = __LINE__, 749 Throwable nextInChain = null) pure nothrow @nogc @safe 750 { 751 super(message, file, line, nextInChain); 752 } 753 754 /++ 755 Create a new [SetMemberException], attaching extra set-member information. 756 +/ 757 this(const string message, 758 const string typeName, 759 const string memberToSet, 760 const string valueToSet, 761 const string file = __FILE__, 762 const size_t line = __LINE__, 763 Throwable nextInChain = null) pure nothrow @nogc @safe 764 { 765 super(message, file, line, nextInChain); 766 767 this.typeName = typeName; 768 this.memberToSet = memberToSet; 769 this.valueToSet = valueToSet; 770 } 771 } 772 773 774 // replaceMembers 775 /++ 776 Inspects a passed struct or class for members whose values match that of the 777 passed `token`. Matches members are set to a replacement value, which is 778 an optional parameter that defaults to the `.init` value of the token's type. 779 780 Params: 781 recurse = Whether or not to recurse into aggregate members. 782 thing = Reference to a struct or class whose members to iterate over. 783 token = What value to look for in members, be it a string or an integer 784 or whatever; anything that can be compared to. 785 replacement = What to assign matched values. Defaults to the `.init` 786 of the matched type. 787 +/ 788 void replaceMembers(Flag!"recurse" recurse = No.recurse, Thing, Token) 789 (ref Thing thing, 790 Token token, 791 Token replacement = Token.init) pure nothrow @nogc 792 if (isAggregateType!Thing && isMutable!Thing && isEqualityComparable!Token) 793 { 794 import std.range : ElementEncodingType, ElementType; 795 import std.traits : isAggregateType, isArray, isSomeString; 796 797 foreach (immutable i, ref member; thing.tupleof) 798 { 799 alias T = typeof(member); 800 801 static if (isAggregateType!T) 802 { 803 static if (recurse) 804 { 805 // Recurse 806 member.replaceMembers!recurse(token, replacement); 807 } 808 } 809 else static if (is(T : Token)) 810 { 811 if (member == token) 812 { 813 member = replacement; 814 } 815 } 816 else static if ( 817 isArray!T && 818 (is(ElementEncodingType!T : Token) || is(ElementType!T : Token))) 819 { 820 if ((member.length == 1) && (member[0] == token)) 821 { 822 if (replacement == typeof(replacement).init) 823 { 824 member = typeof(member).init; 825 } 826 else 827 { 828 member[0] = replacement; 829 } 830 } 831 } 832 } 833 } 834 835 /// 836 unittest 837 { 838 struct Bar 839 { 840 string s = "content"; 841 } 842 843 struct Foo 844 { 845 Bar b; 846 string s = "more content"; 847 } 848 849 Foo foo1, foo2; 850 foo1.replaceMembers("-"); 851 assert(foo1 == foo2); 852 853 foo2.s = "-"; 854 foo2.replaceMembers("-"); 855 assert(!foo2.s.length); 856 foo2.b.s = "-"; 857 foo2.replaceMembers!(Yes.recurse)("-", "herblp"); 858 assert((foo2.b.s == "herblp"), foo2.b.s); 859 860 Foo foo3; 861 foo3.s = "---"; 862 foo3.b.s = "---"; 863 foo3.replaceMembers!(No.recurse)("---"); 864 assert(!foo3.s.length); 865 assert((foo3.b.s == "---"), foo3.b.s); 866 foo3.replaceMembers!(Yes.recurse)("---"); 867 assert(!foo3.b.s.length); 868 869 class Baz 870 { 871 string barS = "init"; 872 string barT = "*"; 873 Foo f; 874 } 875 876 Baz b1 = new Baz; 877 Baz b2 = new Baz; 878 879 b1.replaceMembers("-"); 880 assert((b1.barS == b2.barS), b1.barS); 881 assert((b1.barT == b2.barT), b1.barT); 882 883 b1.replaceMembers("*"); 884 assert(b1.barS.length, b1.barS); 885 assert(!b1.barT.length, b1.barT); 886 assert(b1.f.s.length, b1.f.s); 887 888 b1.replaceMembers!(Yes.recurse)("more content"); 889 assert(!b1.f.s.length, b1.f.s); 890 891 import std.conv : to; 892 893 struct Qux 894 { 895 int i = 42; 896 } 897 898 Qux q; 899 900 q.replaceMembers("*"); 901 assert(q.i == 42); 902 903 q.replaceMembers(43); 904 assert(q.i == 42); 905 906 q.replaceMembers(42, 99); 907 assert((q.i == 99), q.i.to!string); 908 909 struct Flerp 910 { 911 string[] arr; 912 } 913 914 Flerp flerp; 915 flerp.arr = [ "-" ]; 916 assert(flerp.arr.length == 1); 917 flerp.replaceMembers("-"); 918 assert(!flerp.arr.length); 919 } 920 921 922 // pruneAA 923 /++ 924 Iterates an associative array and deletes invalid entries, either if the value 925 is in a default `.init` state or as per the optionally passed predicate. 926 927 It is supposedly undefined behaviour to remove an associative array's fields 928 when foreaching through it. So far we have been doing a simple mark-sweep 929 garbage collection whenever we encounter this use-case in the code, so why 930 not just make a generic solution instead and deduplicate code? 931 932 Example: 933 --- 934 auto aa = 935 [ 936 "abc" : "def", 937 "ghi" : string.init; 938 "mno" : "123", 939 "pqr" : string.init, 940 ]; 941 942 pruneAA(aa); 943 944 assert("ghi" !in aa); 945 assert("pqr" !in aa); 946 947 pruneAA!((entry) => entry.length > 0)(aa); 948 949 assert("abc" !in aa); 950 assert("mno" !in aa); 951 --- 952 953 Params: 954 pred = Optional predicate if special logic is needed to determine whether 955 an entry is to be removed or not. 956 aa = The associative array to modify. 957 +/ 958 void pruneAA(alias pred = null, AA)(ref AA aa) 959 if (isAssociativeArray!AA && isMutable!AA) 960 { 961 if (!aa.length) return; 962 963 string[] toRemove; 964 965 // Mark 966 foreach (/*immutable*/ key, value; aa) 967 { 968 static if (!is(typeof(pred) == typeof(null))) 969 { 970 import std.functional : binaryFun, unaryFun; 971 972 alias unaryPred = unaryFun!pred; 973 alias binaryPred = binaryFun!pred; 974 975 static if (__traits(compiles, unaryPred(value))) 976 { 977 if (unaryPred(value)) toRemove ~= key; 978 } 979 else static if (__traits(compiles, binaryPred(key, value))) 980 { 981 if (unaryPred(key, value)) toRemove ~= key; 982 } 983 else 984 { 985 enum message = "Unknown predicate type passed to `pruneAA`"; 986 static assert(0, message); 987 } 988 } 989 else 990 { 991 if (value == typeof(value).init) 992 { 993 toRemove ~= key; 994 } 995 } 996 } 997 998 // Sweep 999 foreach (immutable key; toRemove) 1000 { 1001 aa.remove(key); 1002 } 1003 } 1004 1005 /// 1006 unittest 1007 { 1008 import std.conv : text; 1009 1010 { 1011 auto aa = 1012 [ 1013 "abc" : "def", 1014 "ghi" : "jkl", 1015 "mno" : "123", 1016 "pqr" : string.init, 1017 ]; 1018 1019 pruneAA!((a) => a == "def")(aa); 1020 assert("abc" !in aa); 1021 1022 pruneAA!((a,b) => a == "pqr")(aa); 1023 assert("pqr" !in aa); 1024 1025 pruneAA!`a == "123"`(aa); 1026 assert("mno" !in aa); 1027 } 1028 { 1029 struct Record 1030 { 1031 string name; 1032 int id; 1033 } 1034 1035 auto aa = 1036 [ 1037 "rhubarb" : Record("rhubarb", 100), 1038 "raspberry" : Record("raspberry", 80), 1039 "blueberry" : Record("blueberry", 0), 1040 "apples" : Record("green apples", 60), 1041 "yakisoba" : Record("yakisoba", 78), 1042 "cabbage" : Record.init, 1043 ]; 1044 1045 pruneAA(aa); 1046 assert("cabbage" !in aa); 1047 1048 pruneAA!((entry) => entry.id < 80)(aa); 1049 assert("blueberry" !in aa); 1050 assert("apples" !in aa); 1051 assert("yakisoba" !in aa); 1052 assert((aa.length == 2), aa.length.text); 1053 } 1054 { 1055 import std.algorithm.searching : canFind; 1056 1057 string[][string] aa = 1058 [ 1059 "abc" : [ "a", "b", "c" ], 1060 "def" : [ "d", "e", "f" ], 1061 "ghi" : [ "g", "h", "i" ], 1062 "jkl" : [ "j", "k", "l" ], 1063 ]; 1064 1065 pruneAA(aa); 1066 assert((aa.length == 4), aa.length.text); 1067 1068 pruneAA!((entry) => entry.canFind("a"))(aa); 1069 assert("abc" !in aa); 1070 } 1071 }