1 module aermicioi.aedi_property_reader.convertor.chaining_convertor; 2 3 import std.algorithm; 4 import std.experimental.allocator; 5 import std.experimental.logger; 6 import std.conv; 7 import std.array; 8 import std.range : drop; 9 import std.range.primitives : isInputRange, ElementType; 10 import std.typecons : No; 11 import aermicioi.aedi_property_reader.convertor.convertor; 12 import aermicioi.aedi_property_reader.convertor.exception; 13 import aermicioi.aedi_property_reader.convertor.placeholder; 14 import aermicioi.aedi_property_reader.convertor.traits : n; 15 16 @safe interface PricingStrategy { 17 18 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow; 19 } 20 21 private bool passable(size_t score) @safe nothrow { 22 return score !is size_t.max; 23 } 24 25 @safe class NumericPricingStrategy : PricingStrategy { 26 27 private const TypeInfo[] identifications = [typeid(ulong), typeid(uint), typeid(ushort), typeid(ubyte), typeid(long), typeid(int), typeid(short), typeid(byte), typeid(real), typeid(double), typeid(float)]; 28 private const size_t[][] prices = [ 29 [0, 1, 2, 3, 1, 2, 3, 4, 5, 6, 7], // typeid(ulong), 30 [0, 0, 1, 2, 0, 1, 2, 3, 5, 6, 7], // typeid(uint), 31 [0, 0, 0, 1, 0, 0, 1, 2, 5, 6, 7], // typeid(ushort), 32 [0, 0, 0, 0, 0, 0, 0, 1, 5, 6, 7], // typeid(ubyte), 33 [1, 2, 3, 4, 0, 1, 2, 3, 5, 6, 7], // typeid(long), 34 [1, 1, 2, 3, 0, 0, 1, 2, 5, 6, 7], // typeid(int), 35 [1, 1, 1, 2, 0, 0, 0, 1, 5, 6, 7], // typeid(short), 36 [1, 1, 1, 1, 0, 0, 0, 0, 5, 6, 7], // typeid(byte), 37 [5, 6, 7, 8, 4, 5, 6, 7, 0, 1, 2], // typeid(real), 38 [5, 6, 7, 8, 4, 5, 6, 7, 0, 0, 1], // typeid(double), 39 [5, 6, 7, 8, 4, 5, 6, 7, 0, 0, 0], // typeid(float) 40 ]; 41 42 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 43 import std.math; 44 import std.algorithm : canFind; 45 46 if (!identifications.canFind!(tested => tested is from) || !identifications.canFind!(tested => tested is to)) { 47 return size_t.max; 48 } 49 50 size_t fromIndex = identifications.countUntil!(tested => tested is from); 51 size_t toIndex = identifications.countUntil!(tested => tested is to); 52 53 return prices[fromIndex][toIndex]; 54 } 55 } 56 57 @safe class DefaultPricingStrategy : PricingStrategy { 58 private size_t price = 1000; 59 60 this() { 61 62 } 63 64 this(size_t price) { 65 this.price = price; 66 } 67 68 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 69 return price; 70 } 71 } 72 73 @safe class MinimalBidPricingStrategy : PricingStrategy { 74 75 private const(PricingStrategy)[] strategies; 76 77 this(PricingStrategy[] strategies...) 78 in (!strategies.empty, "Cannot select minimal bidding strategy when none provided.") { 79 this.strategies = strategies.dup; 80 } 81 82 typeof(this) add(const PricingStrategy strategy) nothrow { 83 import std.algorithm : canFind; 84 if (!this.strategies.canFind!(candidate => candidate is strategy)) { 85 86 this.strategies ~= strategy; 87 } 88 89 return this; 90 } 91 92 typeof(this) remove(const PricingStrategy strategy) @trusted nothrow { 93 import std.algorithm : filter; 94 this.strategies = this.strategies.filter!(candidate => candidate !is strategy).array; 95 96 return this; 97 } 98 99 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 100 return strategies.map!(strategy => strategy.rate(from, to)).minElement(size_t.max); 101 } 102 } 103 104 @safe class IdenticalTypePriceStrategy : PricingStrategy { 105 106 private size_t price; 107 108 this(size_t price = 0) { 109 this.price = price; 110 } 111 112 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 113 if (from is to) { 114 return price; 115 } 116 117 return size_t.max; 118 } 119 } 120 121 @safe class OffsettingPriceStrategy : PricingStrategy { 122 123 private size_t offset; 124 private const PricingStrategy strategy; 125 126 this(const PricingStrategy strategy, size_t price = 1) 127 in (strategy !is null, "Cannot offset a price when decorated pricing is missing.") { 128 this.offset = price; 129 this.strategy = strategy; 130 } 131 132 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 133 size_t result = strategy.rate(from, to); 134 135 if (result.passable) { 136 result += this.offset; 137 } 138 139 return result; 140 } 141 } 142 143 @safe class OverridablePricingStrategy : PricingStrategy { 144 private { 145 const PricingStrategy strategy; 146 147 @safe static struct Relation { 148 const TypeInfo from; 149 const TypeInfo to; 150 size_t price; 151 152 ptrdiff_t opEquals(in ref Relation relation) const nothrow { 153 return (this.from is relation.from) && (this.to is relation.to); 154 } 155 156 ref typeof(this) opAssign(in ref Relation relation) nothrow 157 in (this == relation, "Cannot assign to a relation with different identity") { 158 this.price = relation.price; 159 160 return this; 161 } 162 } 163 164 Relation[] relations; 165 } 166 167 this(const PricingStrategy strategy) nothrow 168 in (strategy !is null, "Cannot override a price when no underlying strategy is provided") { 169 this.strategy = strategy; 170 171 } 172 173 typeof(this) modify(const TypeInfo from, const TypeInfo to, size_t price) nothrow { 174 auto fresh = Relation(from, to, price); 175 176 foreach (ref relation; relations) { 177 178 if (relation == fresh) { 179 relation = fresh; 180 return this; 181 } 182 } 183 184 relations ~= fresh; 185 186 return this; 187 } 188 189 typeof(this) modify(const TypeInfo from, const TypeInfo to) nothrow { 190 this.modify(from, to, size_t.max); 191 192 return this; 193 } 194 195 typeof(this) clear() { 196 this.relations = null; 197 198 return this; 199 } 200 201 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 202 auto modified = this.relations[].filter!(relation => (relation.from is from) && (relation.to is to)); 203 204 if (!modified.empty) { 205 return modified.front.price; 206 } 207 208 return this.strategy.rate(from, to); 209 } 210 } 211 212 class ByTypePricingStrategy : PricingStrategy { 213 struct Entry { 214 const TypeInfo type; 215 size_t price; 216 } 217 218 private const(Entry)[] entries; 219 private const(size_t delegate(size_t from, size_t to) @safe scope nothrow) adjuster; 220 221 this() { 222 import std.functional : toDelegate; 223 this((size_t from, size_t to) scope nothrow => cast(size_t) ((from + to) / 2)); 224 } 225 226 this(size_t delegate(size_t from, size_t to) @safe scope nothrow adjuster) { 227 this.adjuster = adjuster; 228 } 229 230 /** 231 Set entry 232 233 Params: 234 entries = an overriding entry; 235 236 Returns: 237 typeof(this) 238 **/ 239 typeof(this) add(Entry[] entries...) @safe nothrow pure { 240 foreach (entry; entries) { 241 if (!this.entries.canFind!(candidate => candidate.type is entry.type)) { 242 this.entries ~= entry; 243 } 244 } 245 246 return this; 247 } 248 249 /** 250 ditto 251 **/ 252 typeof(this) add(const TypeInfo type, size_t price) @safe nothrow pure { 253 return this.add(Entry(type, price)); 254 } 255 256 size_t rate(const TypeInfo from, const TypeInfo to) const nothrow { 257 auto fromCandidate = this.entries.filter!(entry => entry.type is from); 258 auto toCandidate = this.entries.filter!(entry => entry.type is to); 259 260 261 if (fromCandidate.empty || toCandidate.empty) { 262 return size_t.max; 263 } 264 265 return adjuster(fromCandidate.front.price, toCandidate.front.price); 266 } 267 } 268 269 /** 270 A convertor tries to build a chain of convertors from source to destination. 271 **/ 272 class ChainingConvertor : CombinedConvertorImpl { 273 import std.algorithm : canFind, find; 274 275 private { 276 @safe struct Node { 277 import std.typecons : Rebindable; 278 size_t price = size_t.max; 279 bool visited; 280 Rebindable!(const(TypeInfo)) type; 281 Rebindable!(const(TypeInfo)) next; 282 283 this(const(TypeInfo) type, size_t price, bool visited, const(TypeInfo) next = null) @trusted { 284 this(type); 285 this.price = price; 286 this.visited = visited; 287 this.next = next; 288 } 289 290 this(const(TypeInfo) type) @trusted 291 in (type !is null, "Cannot create node for a null type") { 292 this.type = type; 293 } 294 295 bool opEquals(in ref Node node) const { 296 return this.type.get is node.type.get; 297 } 298 299 size_t toHash() const nothrow { 300 try { 301 302 return this.type.toHash; 303 } catch (Exception e) { 304 305 assert(false, "Something completely wrong went during hashing"); 306 } 307 } 308 } 309 310 @safe static struct Path { 311 import std.typecons : Rebindable; 312 private Node[const TypeInfo] graph; 313 private Rebindable!(const TypeInfo) current; 314 private Rebindable!(const TypeInfo) next; 315 316 this(Node[const TypeInfo] graph, const TypeInfo from) nothrow { 317 this.graph = graph; 318 this.current = this.graph[from].type; 319 this.next = this.graph[from].next; 320 } 321 322 const(TypeInfo) front() inout nothrow { 323 return current; 324 } 325 326 bool empty() nothrow const { 327 return current.get is null; 328 } 329 330 void popFront() nothrow { 331 current = next; 332 333 if (next !is null) { 334 next = graph[next].next; 335 } 336 } 337 338 Path save() nothrow { 339 return Path(graph, current); 340 } 341 } 342 343 Convertor[] convertors_; 344 PricingStrategy appraiser; 345 } 346 347 public { 348 349 /** 350 Default constructor for AggregateConvertor 351 **/ 352 this(PricingStrategy appraiser, Convertor[] convertors...) { 353 this.convertors = convertors.dup; 354 this.appraiser = appraiser; 355 } 356 357 /** 358 Check whether convertor is able to convert from type to type. 359 360 Check whether convertor is able to convert from type to type. 361 This set of methods should be the most precise way of determining 362 whether convertor is able to convert from type to type, since it 363 provides both components to the decision logic implemented by convertor 364 compared to the case with $(D_INLINECODE convertsTo) and $(D_INLINECODE convertsFrom). 365 Note that those methods are still useful when categorization or other 366 logic should be applied per original or destination type. 367 368 Implementation: 369 This is default implementation of converts methods which delegate 370 the decision to $(D_INLINECODE convertsTo) and $(D_INLINECODE convertsFrom). 371 372 Params: 373 from = the original component or it's type to convert from 374 to = the destination component or it's type to convert to 375 376 Returns: 377 true if it is able to convert from component to destination component 378 **/ 379 override bool converts(const TypeInfo from, const TypeInfo to) @safe const nothrow { 380 debug(trace) trace("Checking if ", from, " is convertable to ", to).n; 381 return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to).empty; 382 } 383 384 /** 385 ditto 386 **/ 387 override bool converts(const TypeInfo from, in Object to) @safe const nothrow { 388 debug(trace) trace("Checking if ", from, " is convertable to ", to.identify).n; 389 return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to.identify).empty; 390 } 391 392 /** 393 ditto 394 **/ 395 override bool converts(in Object from, const TypeInfo to) @safe const nothrow { 396 debug(trace) trace("Checking if ", from.identify, " is convertable to ", to).n; 397 return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to).empty; 398 } 399 400 /** 401 ditto 402 **/ 403 override bool converts(in Object from, in Object to) @safe const nothrow { 404 debug(trace) trace("Checking if ", from.identify, " is convertable to ", to.identify).n; 405 return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to.identify).empty; 406 } 407 408 /** 409 Convert from component to component. 410 411 Finds a right convertor from component to component and uses it 412 to execute conversion from component to component. 413 414 Params: 415 from = typeal component that is to be converted. 416 to = destination object that will be constructed out for typeal one. 417 allocator = optional allocator that could be used to construct to component. 418 Throws: 419 ConvertorException when convertor is not able to convert from, or to component. 420 Returns: 421 Resulting converted component. 422 **/ 423 override Object convert(in Object from, const TypeInfo to, RCIAllocator allocator = theAllocator) const 424 { 425 static union Context { 426 const Object constant; 427 Object mutable; 428 } 429 430 import std.range : slide, take, drop; 431 432 if (super.converts(from, to)) { 433 return super.convert(from, to, allocator); 434 } 435 436 Path path = this.search(from, to); 437 438 if (path.empty) { 439 throw new ConvertorException(text("Could not find a way to convert from ", from.identify, " to ", to, " type")); 440 } 441 442 debug(trace) trace("Found best conversion path commencing conversion ", path, "."); 443 Context value = Context(from); 444 445 foreach (conversion; path.slide(2).take(2)) { 446 value.mutable = this.convertors.filter!(c => c.converts(conversion.front, conversion.drop(1).front)).front 447 .convert(value.mutable, conversion.drop(1).front, allocator); 448 } 449 450 path = path.drop(1); 451 452 foreach (conversion; path.slide!(No.withPartial)(3)) { 453 Object temporary = this.convertors.filter!(c => c.converts(conversion.drop(1).front, conversion.drop(2).front)).front 454 .convert(value.mutable, conversion.drop(2).front, allocator); 455 this.convertors.filter!(c => c.destroys(conversion.front, conversion.drop(1).front)).front 456 .destruct(conversion.front, value.mutable, allocator); 457 458 value.mutable = temporary; 459 } 460 461 return value.mutable; 462 } 463 464 mixin ToStringMixin!(); 465 mixin OpCmpMixin!(); 466 467 override size_t toHash() @trusted { 468 import std.range : only; 469 size_t result = super.toHash(); 470 471 foreach (convertor; convertors) { 472 Object hashable = cast(Object) convertor; 473 474 size_t hash = (hashable !is null) ? hashable.toHash() : typeid(convertor).getHash(cast(void*) convertor); 475 476 result = result * 31 + hash; 477 } 478 479 return result; 480 } 481 482 override bool opEquals(Object o) { 483 return super.opEquals(o) || ((this.classinfo is o.classinfo) && this.opEquals(cast(ChainingConvertor) o)); 484 } 485 486 bool opEquals(ChainingConvertor convertor) { 487 import std.algorithm : equal; 488 return (convertor !is null) && (this.convertors.equal(convertor.convertors)); 489 } 490 } 491 492 private Path search(T)(T from, const TypeInfo to) @safe const nothrow 493 if (is(T : const Object) || is(T : const TypeInfo)) { 494 import std.range : slide, take, generate, chain; 495 import aermicioi.aedi_property_reader.convertor.traits : n; 496 497 debug(trace) trace("Searching for a way to convert ", from.identify, " to ", to).n; 498 499 OverridablePricingStrategy appraiser = new OverridablePricingStrategy(this.appraiser); 500 501 foreach (path; this.range(from.identify, to, appraiser)) { 502 503 debug(trace) trace("Found a way to convert ", from.identify, " to ", to, " using a chain of ", path, " convertors.").n; 504 505 bool found = false; 506 auto step = path.take(2); 507 if (!this.convertors.filter!(c => c.converts(from, step.drop(1).front)).empty) { 508 auto remainder = path.drop(1).slide(2).find!( 509 (conversion) => this.convertors.filter!(c => c.converts(conversion.front, conversion.drop(1).front)).empty 510 ); 511 512 found = remainder.empty; 513 if (!remainder.empty) { 514 step = remainder.front; 515 } 516 } 517 518 if (found) { 519 520 return path; 521 } 522 523 debug(trace) trace("Found chain of convertors broke at ", step, " marking it to avoid in next chain.").n; 524 appraiser.modify(step.front, step.drop(1).front); 525 } 526 527 debug(trace) trace("Could not find a way to convert ", from.identify, " to ", to).n; 528 return Path.init; 529 } 530 531 private Path pave(const TypeInfo from, const TypeInfo to, PricingStrategy appraiser) @safe const nothrow { 532 import std.algorithm; 533 import std.range; 534 535 try { 536 537 Node[const TypeInfo] nodes; 538 foreach (type; this.convertors.map!((c) @safe => chain(c.to, c.from)).joiner.filter!((type) @safe => type !is typeid(void))) { 539 nodes[type] = Node(type, size_t.max, false); 540 } 541 542 Node current = Node(to, 0, true); 543 nodes[to] = current; 544 Node[] unvisited; 545 546 do { 547 debug(ChainingConvertorTraceGraph) trace("Processing neighbors of ", current.type).n; 548 549 auto candidates = this.convertors.filter!(c => c.convertsTo(current.type)); 550 551 foreach (convertor; candidates) { 552 foreach (fromType; convertor.from) { 553 size_t score = appraiser.rate(fromType, current.type); 554 555 debug(ChainingConvertorTraceGraph) trace("Checking convertion price to ", current.type, " from ", fromType, " of ", score).n; 556 if (score.passable && ((current.price + score) < nodes[fromType].price)) { 557 558 debug(ChainingConvertorTraceGraph) trace(score, " less than one of ", nodes[fromType].price, " setting new one.").n; 559 nodes[fromType].price = current.price + score; 560 nodes[fromType].next = current.type; 561 } 562 } 563 } 564 565 unvisited = unvisited 566 .filter!(candidate => !nodes[candidate.type].visited) 567 .chain(candidates.map!(convertor => convertor.from).joiner.map!(from => nodes[from]).filter!(node => !node.visited)) 568 .array; 569 unvisited.sort!((f, s) => f.price < s.price); 570 571 nodes[current.type] = current; 572 573 if (!unvisited.empty) { 574 debug(ChainingConvertorTraceGraph) trace("Selecting new unvisited node of ", unvisited.front.type).n; 575 current = unvisited.front; 576 current.visited = true; 577 578 unvisited.popFront; 579 } 580 } while (!unvisited.empty && (current.type !is from)); 581 582 if (current.type !is from) { 583 return Path.init; 584 } 585 586 return Path(nodes, current.type); 587 } catch (Exception e) { 588 589 throw new Error("Encountered unexpected exception during paving", e); 590 } 591 } 592 593 auto range(const TypeInfo from, const TypeInfo to, PricingStrategy appraiser) const nothrow @safe { 594 import std.range : generate, chain, only; 595 import std.algorithm : until; 596 auto searcher = generate!(() => this.pave(from, to, appraiser)).until!(path => path.empty || path.drop(1).empty); 597 598 return searcher; 599 } 600 } 601 602 /** 603 A convertor that will run a chain of conversions. 604 **/ 605 class ChainedConvertor : Convertor { 606 import std.algorithm : canFind, find; 607 608 private { 609 Convertor[] convertors; 610 } 611 612 public { 613 614 /** 615 Default constructor for AggregateConvertor 616 **/ 617 this(Convertor[] convertors...) 618 in (convertors.length > 0, "Chained convertor expects at least one convertor in chain to operate properly.") 619 in (ChainedConvertor.valid(convertors), "Chained convertor expects a unbroken chain of convertors, one provided is broken at some point.") { 620 this.convertors = convertors.dup; 621 } 622 623 @property { 624 625 /** 626 Get the type info of component that convertor can convert from. 627 628 Get the type info of component that convertor can convert from. 629 The method is returning the default type that it is able to convert, 630 though it is not necessarily limited to this type only. More generalistic 631 checks should be done by convertsFrom method. 632 633 Returns: 634 type info of component that convertor is able to convert. 635 **/ 636 const(TypeInfo)[] from() @safe const nothrow pure { 637 return this.convertors[0].from; 638 } 639 640 /** 641 Get the type info of component that convertor is able to convert to. 642 643 Get the type info of component that convertor is able to convert to. 644 The method is returning the default type that is able to convert, 645 though it is not necessarily limited to this type only. More generalistic 646 checks should be done by convertsTo method. 647 648 Returns: 649 type info of component that can be converted to. 650 **/ 651 const(TypeInfo)[] to() @safe const nothrow pure { 652 return this.convertors[$ - 1].to; 653 } 654 } 655 656 mixin ConvertsFromToMixin DefaultImplementation; 657 658 /** 659 Check whether this convertor is able to destroy to component. 660 661 The destroys family of methods are designed purposely for identification 662 whether convertor was able to convert from type to destination to, and 663 is eligible for destruction of converted components. 664 665 Params: 666 from = original component which was converted. 667 to = converted component that should be destroyed by convertor. 668 669 Returns: 670 true if convertor is eligible for destroying to, or false otherwise. 671 **/ 672 bool destroys(const TypeInfo from, const TypeInfo to) @safe const nothrow { 673 return this.convertors.back.converts(from, to); 674 } 675 676 /** 677 ditto 678 **/ 679 bool destroys(in Object from, const TypeInfo to) @safe const nothrow { 680 return this.convertors.back.converts(from, to); 681 } 682 683 /** 684 ditto 685 **/ 686 bool destroys(const TypeInfo from, in Object to) @safe const nothrow { 687 return this.convertors.back.converts(from, to); 688 } 689 690 /** 691 ditto 692 **/ 693 bool destroys(in Object from, in Object to) @safe const nothrow { 694 return this.convertors.back.converts(from, to); 695 } 696 697 /** 698 Convert from component to component. 699 700 Finds a right convertor from component to component and uses it 701 to execute conversion from component to component. 702 703 Params: 704 from = typeal component that is to be converted. 705 to = destination object that will be constructed out for typeal one. 706 allocator = optional allocator that could be used to construct to component. 707 Throws: 708 ConvertorException when convertor is not able to convert from, or to component. 709 Returns: 710 Resulting converted component. 711 **/ 712 override Object convert(in Object from, const TypeInfo to, RCIAllocator allocator = theAllocator) const 713 { 714 import std.math; 715 716 union Context { 717 const Object constant; 718 Object mutable; 719 } 720 721 Context value = Context(from); 722 723 foreach (convertor; convertors[0 .. $.min(2)]) { 724 value.mutable = convertor.convert(value.mutable, convertor.to[0], allocator); 725 } 726 727 if (convertors.length > 2) 728 foreach (index, convertor; convertors[2 .. $]) { 729 Object temporary = convertor.convert(value.mutable, convertor.to[0], allocator); 730 convertors[index - 1].destruct(convertors[index - 1].from[0], value.mutable, allocator); 731 732 value.mutable = temporary; 733 } 734 735 return value.mutable; 736 } 737 738 /** 739 Destroy component created using this convertor. 740 741 Find a suitable convertor for destruction and use it to execute destruction. 742 743 Params: 744 converted = component that should be destroyed. 745 allocator = allocator used to allocate converted component. 746 **/ 747 void destruct(const TypeInfo from, ref Object converted, RCIAllocator allocator = theAllocator) const { 748 import std.exception : enforce; 749 enforce!ConvertorException(this.destroys(from, converted), text( 750 "Cannot destroy ", converted.identify, " which was not converted from ", from, ".", 751 " Expected destroyable type of ", this.to, " from origin of ", this.from 752 )); 753 754 this.convertors.back.destruct(from, converted, allocator); 755 } 756 757 mixin ToStringMixin!(); 758 mixin OpCmpMixin!(); 759 760 override size_t toHash() @trusted { 761 import std.range : only; 762 size_t result = super.toHash(); 763 764 foreach (convertor; convertors) { 765 Object hashable = cast(Object) convertor; 766 767 size_t hash = (hashable !is null) ? hashable.toHash() : typeid(convertor).getHash(cast(void*) convertor); 768 769 result = result * 31 + hash; 770 } 771 772 return result; 773 } 774 775 override bool opEquals(Object o) { 776 return super.opEquals(o) || ((this.classinfo is o.classinfo) && this.opEquals(cast(ChainedConvertor) o)); 777 } 778 779 bool opEquals(ChainedConvertor convertor) { 780 import std.algorithm : equal; 781 return (convertor !is null) && (this.convertors.equal(convertor.convertors)); 782 } 783 } 784 785 private static valid(Convertor[] convertors) { 786 import std.range : slide; 787 import std.algorithm : any; 788 789 if (convertors.length == 1) { 790 return true; 791 } 792 793 foreach (window; convertors.slide(2)) { 794 if (!window[0].to.any!(to => window[1].from.any!(from => from is to))) { 795 return false; 796 } 797 } 798 799 return true; 800 } 801 }