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