1 /** 2 Some additional string functions. 3 4 Copyright: © 2018 Arne Ludwig <arne.ludwig@posteo.de> 5 License: Subject to the terms of the MIT license, as written in the 6 included LICENSE file. 7 Authors: Arne Ludwig <arne.ludwig@posteo.de> 8 */ 9 module dentist.util..string; 10 11 import std.algorithm : 12 countUntil, 13 joiner, 14 min, 15 map; 16 import std.array : 17 appender, 18 array, 19 minimallyInitializedArray; 20 import std.conv : to; 21 import std.exception : basicExceptionCtors, enforce; 22 import std.functional : binaryFun; 23 import std.math : round, sqrt; 24 import std.range : 25 chain, 26 chunks, 27 cycle, 28 only, 29 take, 30 zip; 31 import std.range.primitives : hasSlicing; 32 import std..string : lineSplitter; 33 import std.traits : isSomeString; 34 import std.typecons : 35 Flag, 36 No, 37 tuple, 38 Yes; 39 40 /** 41 Adds one level of indentation for a multi-line string. Adds indentSize 42 spaces to each non-empty line. 43 44 Returns: indented string 45 */ 46 S indent(S)(S str, in size_t indentSize = 4) if (isSomeString!S) 47 { 48 enum lineSep = "\n"; 49 alias indentLine = (line) => chain(" ".cycle.take(line.length == 0 ? 0 : indentSize), line); 50 51 return str[] 52 .lineSplitter 53 .map!indentLine 54 .joiner(lineSep) 55 .chain(str[$ - lineSep.length .. $] == lineSep ? lineSep : "") 56 .array 57 .to!S; 58 } 59 60 /// 61 unittest 62 { 63 assert("a\nb".indent == " a\n b"); 64 assert("a\nb\n\n".indent == " a\n b\n\n"); 65 assert("a\nb\n".indent(2) == " a\n b\n"); 66 } 67 68 class AlignmentException : Exception 69 { 70 /// 71 mixin basicExceptionCtors; 72 } 73 74 /// One edit operation of the Needleman-Wunsch algorithm. 75 enum EditOp: byte 76 { 77 substitution, 78 deletetion, 79 insertion, 80 } 81 82 alias score_t = uint; 83 84 /// Represents an alignment of two sequences. 85 struct SequenceAlignment(S, alias scoreFun = "a == b ? 0 : 1") 86 { 87 alias getScore = binaryFun!scoreFun; 88 89 score_t score; 90 EditOp[] editPath; 91 S reference; 92 S query; 93 score_t indelPenalty; 94 Flag!"freeShift" freeShift; 95 96 /// Compute alignment score. 97 score_t computeScore() const pure 98 { 99 auto walkResult = walkEditOps(); 100 enforce!AlignmentException( 101 walkResult.i == reference.length && walkResult.j == query.length, 102 "invalid alignment", 103 ); 104 105 return walkResult.computedScore; 106 } 107 108 bool isValid() const pure nothrow 109 { 110 auto walkResult = walkEditOps(); 111 112 return isValid(walkResult); 113 } 114 115 private bool isValid(in WalkResult walkResult) const pure nothrow 116 { 117 return walkResult.i == reference.length && 118 walkResult.j == query.length && 119 walkResult.computedScore == score; 120 } 121 122 private static struct WalkResult 123 { 124 score_t computedScore; 125 size_t i, j; 126 } 127 128 private auto walkEditOps() const pure nothrow 129 { 130 WalkResult result; 131 foreach (k, editOp; editPath) 132 { 133 if (result.i > reference.length || result.j > query.length) 134 return result; 135 136 final switch (editOp) 137 { 138 case EditOp.substitution: 139 if (result.i == reference.length || result.j == query.length) 140 return result; 141 result.computedScore += getScore(reference[result.i], query[result.j]); 142 ++result.i; 143 ++result.j; 144 break; 145 case EditOp.deletetion: 146 if (result.i == reference.length) 147 return result; 148 result.computedScore += indelPenalty; 149 ++result.i; 150 break; 151 case EditOp.insertion: 152 if (result.j == query.length) 153 return result; 154 result.computedScore += indelPenalty; 155 ++result.j; 156 break; 157 } 158 } 159 if (freeShift) 160 { 161 auto firstSubstitution = editPath.countUntil(EditOp.substitution); 162 result.computedScore -= firstSubstitution > 0 163 ? firstSubstitution 164 : editPath.length; 165 } 166 167 return result; 168 } 169 170 /** 171 Get a partial alignment with respect to `reference`. 172 */ 173 auto partial(in size_t begin, in size_t end) inout pure nothrow 174 in 175 { 176 assert(this.isValid()); 177 } 178 out (partialAlignment) 179 { 180 assert(partialAlignment.isValid()); 181 } 182 do 183 { 184 assert(0 <= begin && begin <= end && end <= reference.length, "index out of bounds"); 185 186 if (end == begin) 187 return typeof(this)(0, [], reference[0 .. 0], query[0 .. 0], indelPenalty); 188 else if (0 == begin && end == reference.length) 189 return this; 190 191 score_t newScore; 192 size_t editBegin, editEnd; 193 size_t queryBegin, queryEnd; 194 size_t i, j; 195 foreach (k, editOp; editPath) 196 { 197 if (i == begin) 198 { 199 newScore = 0; 200 editBegin = k; 201 queryBegin = j; 202 } 203 204 final switch (editOp) 205 { 206 case EditOp.substitution: 207 newScore += getScore(reference[i], query[j]); 208 ++i; 209 ++j; 210 break; 211 case EditOp.deletetion: 212 newScore += indelPenalty; 213 ++i; 214 break; 215 case EditOp.insertion: 216 newScore += indelPenalty; 217 ++j; 218 break; 219 } 220 221 if (i >= end) 222 { 223 editEnd = k + 1; 224 queryEnd = j; 225 break; 226 } 227 } 228 229 auto partialAlignment = typeof(this)( 230 newScore, 231 editPath[editBegin .. editEnd], 232 reference[begin .. end], 233 query[queryBegin .. queryEnd], 234 indelPenalty, 235 freeShift, 236 ); 237 238 return partialAlignment; 239 } 240 241 auto opDollar() const pure nothrow 242 { 243 return reference.length; 244 } 245 246 /// ditto 247 auto opIndex(in size_t[2] slice) inout pure nothrow 248 { 249 return partial(slice[0], slice[1]); 250 } 251 252 /// 253 unittest 254 { 255 enum indelPenalty = 1; 256 auto alignment = findAlignment("GCATGCT", "GATTACA", indelPenalty); 257 258 assert(alignment.score == 4); 259 assert(alignment.toString == 260 "GCAT-GCT\n" ~ 261 "| || *|*\n" ~ 262 "G-ATTACA"); 263 264 auto partialAlignment = alignment[1 .. 5]; 265 266 assert(partialAlignment.score == 3); 267 assert(partialAlignment.toString == 268 "CAT-G\n" ~ 269 " || *\n" ~ 270 "-ATTA"); 271 } 272 273 size_t[2] opSlice(size_t dim)(in size_t begin, in size_t end) const pure nothrow 274 { 275 return [begin, end]; 276 } 277 278 /// Get a string representation of this alignment. Visual alignment breaks 279 /// unless elements of the sequences convert to single chars via `to!string`. 280 string toString( 281 alias matchSymbol = '|', 282 alias substitutionSymbol = '*', 283 alias indelSymbol = ' ', 284 alias gapSymbol = '-', 285 )(in size_t width = 0) const pure 286 { 287 auto referenceLine = appender!string; 288 referenceLine.reserve(editPath.length); 289 auto compareLine = appender!string; 290 compareLine.reserve(editPath.length); 291 auto queryLine = appender!string; 292 queryLine.reserve(editPath.length); 293 294 size_t i, j; 295 foreach (editOp; editPath) 296 { 297 final switch (editOp) 298 { 299 case EditOp.substitution: 300 referenceLine ~= reference[i].to!string; 301 compareLine ~= reference[i] == query[j] 302 ? matchSymbol 303 : substitutionSymbol; 304 queryLine ~= query[j].to!string; 305 ++i; 306 ++j; 307 break; 308 case EditOp.deletetion: 309 referenceLine ~= reference[i].to!string; 310 compareLine ~= indelSymbol; 311 queryLine ~= gapSymbol; 312 ++i; 313 break; 314 case EditOp.insertion: 315 referenceLine ~= gapSymbol; 316 compareLine ~= indelSymbol; 317 queryLine ~= query[j].to!string; 318 ++j; 319 break; 320 } 321 } 322 323 if (width == 0) 324 return only( 325 referenceLine.data.to!string, 326 compareLine.data.to!string, 327 queryLine.data.to!string, 328 ) 329 .joiner("\n") 330 .to!string; 331 else 332 return zip( 333 referenceLine.data.to!string.chunks(width), 334 compareLine.data.to!string.chunks(width), 335 queryLine.data.to!string.chunks(width), 336 ) 337 .map!(lineTriple => only(lineTriple.expand) 338 .joiner("\n")) 339 .joiner("\n\n") 340 .to!string; 341 } 342 343 /// 344 unittest 345 { 346 auto alignment = findAlignment("ACGTC", "AGTC", 1); 347 assert(alignment.toString == 348 "ACGTC\n" ~ 349 "| |||\n" ~ 350 "A-GTC"); 351 } 352 353 /// 354 unittest 355 { 356 auto alignment = findAlignment("GCATGCT", "GATTACA", 1); 357 358 assert(alignment.toString == 359 "GCAT-GCT\n" ~ 360 "| || *|*\n" ~ 361 "G-ATTACA"); 362 } 363 } 364 365 /** 366 Compute an alignment of `query` against `reference` using the 367 Needleman-Wunsch algorithm with non-negative scores and constant 368 indel penalty. Optionally, the `freeShift` mode may be activated 369 as to allow large indels at the beginning and end of the alignment. 370 371 **Implementation Notes:** The current implementation needs 372 `O(reference.length * query.length)` in time and memory. As the 373 memory requirement easily exceeds available memory it can be 374 limited for now. This may change in future and an implementation 375 using `O(max(reference.length, query.length))` memory will be 376 silently selected for large inputs. 377 378 Params: 379 scoreFun = calculate score for a 'substitution' at `i, j` using 380 `scoreFun(reference[i], reference[j])` 381 reference = Sequence to compare `query` against 382 query = Sequence to compare against `reference` 383 indelPenalty = Penalize each indel with this value 384 freeShift = Allow indels at the beginning and end of the alignment 385 memoryLimit = throw an error if the calculation would require more 386 than `memoryLimit` bytes. 387 Throws: AlignmentException if the calculation would require more than 388 `memoryLimit` bytes. 389 390 See_Also: http://en.wikipedia.org/wiki/Needleman-Wunsch_algorithm 391 */ 392 SequenceAlignment!(const(S), scoreFun) findAlignment( 393 alias scoreFun = "a == b ? 0 : 1", 394 S, 395 )( 396 in S reference, 397 in S query, 398 in score_t indelPenalty, 399 Flag!"freeShift" freeShift = No.freeShift, 400 size_t memoryLimit = 2^^20, 401 ) 402 { 403 alias score = binaryFun!scoreFun; 404 enforce!AlignmentException( 405 memoryRequired(reference, query) <= memoryLimit, 406 "memory limit exceeded; use short reference and/or query", 407 ); 408 409 auto F = DPMatrix!score_t(reference.length + 1, query.length + 1); 410 411 // Initialize scoring matrix 412 foreach (i; 0 .. reference.length + 1) 413 F[i, 0] = freeShift ? 0 : cast(score_t) i * indelPenalty; 414 foreach (j; 0 .. query.length + 1) 415 F[0, j] = freeShift ? 0 : cast(score_t) j * indelPenalty; 416 417 // Compute scoring matrix by rows in a cache friendly manner 418 foreach (i; 1 .. reference.length + 1) 419 foreach (j; 1 .. query.length + 1) 420 F[i, j] = min( 421 F[i - 1, j - 1] + score(reference[i - 1], query[j - 1]), 422 F[i - 1, j ] + indelPenalty, 423 F[i , j - 1] + indelPenalty, 424 ); 425 426 return typeof(return)( 427 F[$ - 1, $ - 1], 428 tracebackScoringMatrix(F), 429 reference, 430 query, 431 indelPenalty, 432 freeShift, 433 ); 434 } 435 436 /// 437 unittest 438 { 439 auto alignment = findAlignment("ACGTC", "AGTC", 1); 440 441 // - A G T C 442 // - 0 1 2 3 4 443 // A 1 0 1 2 3 444 // C 2 1 1 2 2 445 // G 3 2 1 2 3 446 // T 4 3 2 1 2 447 // C 5 4 3 2 1 448 // 449 // ACGTC 450 // | ||| 451 // A-GTC 452 453 assert(alignment.score == 1); 454 assert(alignment.editPath == [ 455 EditOp.substitution, 456 EditOp.deletetion, 457 EditOp.substitution, 458 EditOp.substitution, 459 EditOp.substitution, 460 ]); 461 } 462 463 /// 464 unittest 465 { 466 auto alignment = findAlignment("GCATGCT", "GATTACA", 1); 467 468 // - G A T T A C A 469 // - 0 1 2 3 4 5 6 7 470 // G 1 0 1 2 3 4 5 6 471 // C 2 1 2 3 4 5 4 5 472 // A 3 2 1 2 3 4 5 4 473 // T 4 3 2 1 2 3 6 5 474 // G 5 4 3 2 3 4 5 6 475 // C 6 5 4 3 4 5 4 5 476 // T 7 6 5 4 3 6 5 6 477 // 478 // GCAT-GCT 479 // | || *|* 480 // G-ATTACA 481 assert(alignment.score == 4); 482 assert(alignment.editPath == [ 483 EditOp.substitution, 484 EditOp.deletetion, 485 EditOp.substitution, 486 EditOp.substitution, 487 EditOp.insertion, 488 EditOp.substitution, 489 EditOp.substitution, 490 EditOp.substitution, 491 ]); 492 } 493 494 /// 495 unittest 496 { 497 auto alignment = findAlignment!"a == b ? 0 : 1"( 498 "tgaggacagaagggtcataggtttaattctggtcacaggcacattcctgg" ~ 499 "gttgcaggtttgatctccacctggtcggggcacatgca", 500 "tgaggacagaagggtcatggtttaattctggtcacaggcacattcctggg" ~ 501 "ttgtaggctcaattcccacccggtcggggccacatgca", 502 1, 503 ); 504 505 assert(alignment.toString(50) == 506 "tgaggacagaagggtcataggtttaattctggtcacaggcacattcctgg\n" ~ 507 "|||||||||||||||||| |||||||||||||||||||||||||||||||\n" ~ 508 "tgaggacagaagggtcat-ggtttaattctggtcacaggcacattcctgg\n" ~ 509 "\n" ~ 510 "gttgcaggtttgatctcc-acctggtcggggc-acatgca\n" ~ 511 "||||*|||*|**|| ||| |||*||||||||| |||||||\n" ~ 512 "gttgtaggctcaat-tcccacccggtcggggccacatgca"); 513 } 514 515 /// 516 unittest 517 { 518 auto alignment = findAlignment( 519 "tgaggacagaagggtcataggtttaattctggtcacaggcacattcctgg" ~ 520 "gttgcaggtttgatctccacctggtcggggcacatgca", 521 "aatgaacagctgaggacagaagggtcatggtttaattctggtcacaggca" ~ 522 "cattcctgggttgtaggctcaattcccacccggtcggggccacatgca", 523 1, 524 Yes.freeShift, 525 ); 526 527 assert(alignment.toString(50) == 528 "----------tgaggacagaagggtcataggtttaattctggtcacaggc\n" ~ 529 " |||||||||||||||||| |||||||||||||||||||||\n" ~ 530 "aatgaacagctgaggacagaagggtcat-ggtttaattctggtcacaggc\n" ~ 531 "\n" ~ 532 "acattcctgggttgcaggtttgatctcc-acctggtcggggc-acatgca\n" ~ 533 "||||||||||||||*|||*|**|| ||| |||*||||||||| |||||||\n" ~ 534 "acattcctgggttgtaggctcaat-tcccacccggtcggggccacatgca"); 535 } 536 537 /// 538 unittest 539 { 540 auto alignment = findAlignment( 541 "tatcctcaggtgaggactaacaacaaatatatatatatttatatctaaca" ~ 542 "acatatgatttctaaaatttcaaaaatcttaaggctgaattaat", 543 "tatcctcaggtgaggcttaacaacaaatatatatatactgtaatatctaa" ~ 544 "caacatatgattctaaaatttcaaaatgcttaaaggtctga", 545 1, 546 Yes.freeShift, 547 ); 548 549 assert(alignment.toString(50) == 550 "tatcctcaggtgaggact-aacaacaaatatatatata-ttta-tatcta\n" ~ 551 "||||||||||||||| || ||||||||||||||||||| |*|| ||||||\n" ~ 552 "tatcctcaggtgagg-cttaacaacaaatatatatatactgtaatatcta\n" ~ 553 "\n" ~ 554 "acaacatatgatttctaaaatttcaaaaatcttaa-gg-ctgaattaat\n" ~ 555 "||||||||||||| ||||||||||||||**||||| || |||| \n" ~ 556 "acaacatatgatt-ctaaaatttcaaaatgcttaaaggtctga------"); 557 } 558 559 unittest 560 { 561 auto alignment = findAlignment( 562 "tgaggacagaagggtcataggtttaattctggtcacaggcacattcctgg" ~ 563 "gttgcaggtttgatctccacctggtcggggcacatgcaggaggcaaccaa" ~ 564 "tcaatgtgtctctttctcagtgatgtttcttctctctgtctctctccccc" ~ 565 "ctctcttctactctctctgaaaaatagatggaaaaaatatcctcaggtga" ~ 566 "ggactaacaacaaatatatatatatttatatctaacaacatatgatttct" ~ 567 "aaaatttcaaaaatcttaaggctgaattaat", 568 "aatgaacagctgaggacagaagggtcatggtttaattctggtcacaggca" ~ 569 "cattcctgggttgtaggctcaattcccacccggtcggggccacatgcaga" ~ 570 "aggcaaccatcaatgtgtctctttcaagtgatgtttcttctctctgtcta" ~ 571 "ctccccctccctatctactctctgggaaacttatggaaaaaatatcctca" ~ 572 "ggtgaggcttaacaacaaatatatatatactgtaatatctaacaacatat" ~ 573 "gattctaaaatttcaaaatgcttaaaggtctga", 574 1, 575 Yes.freeShift, 576 ); 577 578 assert(alignment.toString(50) == 579 "----------tgaggacagaagggtcataggtttaattctggtcacaggc\n" ~ 580 " |||||||||||||||||| |||||||||||||||||||||\n" ~ 581 "aatgaacagctgaggacagaagggtcat-ggtttaattctggtcacaggc\n" ~ 582 "\n" ~ 583 "acattcctgggttgcaggtttgatctcc-acctggtcggggc-acatgca\n" ~ 584 "||||||||||||||*|||*|**|| ||| |||*||||||||| |||||||\n" ~ 585 "acattcctgggttgtaggctcaat-tcccacccggtcggggccacatgca\n" ~ 586 "\n" ~ 587 "ggaggcaaccaatcaatgtgtctctttctcagtgatgtttcttctctctg\n" ~ 588 "|*||||||||| |||||||||||||||| *||||||||||||||||||||\n" ~ 589 "gaaggcaacca-tcaatgtgtctctttc-aagtgatgtttcttctctctg\n" ~ 590 "\n" ~ 591 "tctctctcccccctctct-tctactctctctgaaaaatagatggaaaaaa\n" ~ 592 "||| *||||||| ||*|| ||||||||||**||||**|| |||||||||\n" ~ 593 "tct-actccccc-tccctatctactctctgggaaactta--tggaaaaaa\n" ~ 594 "\n" ~ 595 "tatcctcaggtgaggact-aacaacaaatatatatata-ttta-tatcta\n" ~ 596 "||||||||||||||| || ||||||||||||||||||| |*|| ||||||\n" ~ 597 "tatcctcaggtgagg-cttaacaacaaatatatatatactgtaatatcta\n" ~ 598 "\n" ~ 599 "acaacatatgatttctaaaatttcaaaaatcttaa-gg-ctgaattaat\n" ~ 600 "||||||||||||| ||||||||||||||**||||| || |||| \n" ~ 601 "acaacatatgatt-ctaaaatttcaaaatgcttaaaggtctga------"); 602 } 603 604 // Returns the amount of memory required to compute an alignment between reference and query. 605 size_t memoryRequired(S)(in S reference, in S query) 606 { 607 return score_t.sizeof * ( 608 // Space for the scoring matrix F 609 ((reference.length + 1) * (query.length + 1)) + 610 // Space for the edit path 611 (reference.length + query.length) 612 ); 613 } 614 615 /// Returns longest query and refernce length possible with memoryLimit. 616 size_t longestInputsLength(size_t memoryLimit) pure 617 { 618 return round(sqrt((memoryLimit / score_t.sizeof + 3).to!double) - 2).to!size_t; 619 } 620 621 // Find edit path of the best alignment 622 private EditOp[] tracebackScoringMatrix(in DPMatrix!score_t F) 623 { 624 auto editPath = minimallyInitializedArray!(EditOp[])(F.size[0] + F.size[1]); 625 size_t i = F.size[0] - 1; 626 size_t j = F.size[1] - 1; 627 size_t k = editPath.length; 628 629 while (0 < i && 0 < j) 630 { 631 auto matchScore = F[i - 1, j - 1]; 632 auto insertionScore = F[i , j - 1]; 633 auto deletionScore = F[i - 1, j ]; 634 auto nextScore = min( 635 matchScore, 636 deletionScore, 637 insertionScore, 638 ); 639 640 assert(k > 0); 641 switch (nextScore) 642 { 643 case matchScore: 644 editPath[--k] = EditOp.substitution; 645 --i; 646 --j; 647 break; 648 case insertionScore: 649 editPath[--k] = EditOp.insertion; 650 --j; 651 break; 652 case deletionScore: 653 editPath[--k] = EditOp.deletetion; 654 --i; 655 break; 656 default: 657 assert(0, "corrupted scoring matrix"); 658 } 659 } 660 661 while (0 < i) 662 { 663 assert(k > 0); 664 editPath[--k] = EditOp.deletetion; 665 --i; 666 } 667 668 while (0 < j) 669 { 670 assert(k > 0); 671 editPath[--k] = EditOp.insertion; 672 --j; 673 } 674 675 return editPath[k .. $]; 676 } 677 678 unittest 679 { 680 auto F = DPMatrix!score_t(6, 5); 681 F.elements = [ 682 // - A G T C 683 0, 1, 2, 3, 4, // - 684 1, 0, 1, 2, 3, // A 685 2, 1, 1, 2, 2, // C 686 3, 2, 1, 2, 3, // G 687 4, 3, 2, 1, 2, // T 688 5, 4, 3, 2, 1, // C 689 ]; 690 691 assert(F.tracebackScoringMatrix == [ 692 EditOp.substitution, 693 EditOp.deletetion, 694 EditOp.substitution, 695 EditOp.substitution, 696 EditOp.substitution, 697 ]); 698 } 699 700 private struct DPMatrix(T) 701 { 702 size_t[2] size; 703 T[] elements; 704 705 this(in size_t n, in size_t m) 706 { 707 this.size[0] = n; 708 this.size[1] = m; 709 this.elements = minimallyInitializedArray!(T[])(n * m); 710 } 711 712 this(in size_t n, in size_t m, ref T[] buffer) 713 { 714 this.size[0] = n; 715 this.size[1] = m; 716 auto numElements = n * m; 717 assert(buffer.length <= numElements); 718 this.elements = buffer; 719 } 720 721 unittest 722 { 723 auto M = DPMatrix!int(2, 3); 724 725 assert(M.size[0] == 2); 726 assert(M.size[1] == 3); 727 728 foreach (i; 0 .. M.size[0]) 729 foreach (j; 0 .. M.size[1]) 730 M[i, j] = cast(int) (i + j); 731 732 assert(M.elements == [ 733 0, 1, 2, 734 1, 2, 3, 735 ]); 736 } 737 738 size_t opDollar(size_t dim)() const pure nothrow if (dim < 2) 739 { 740 return size[dim]; 741 } 742 743 auto ref T opIndex(size_t i, size_t j) pure nothrow 744 { 745 assert(i < size[0] && j < size[1], "index out of bounds"); 746 747 return elements[i * size[1] + j]; 748 } 749 750 const(T) opIndex(size_t i, size_t j) const pure nothrow 751 { 752 assert(i < size[0] && j < size[1], "index out of bounds"); 753 754 return elements[i * size[1] + j]; 755 } 756 757 unittest 758 { 759 auto M = DPMatrix!int(2, 3); 760 M.elements = [ 761 0, 1, 2, 762 1, 2, 3, 763 ]; 764 765 assert(M[0, 0] == 0 + 0); 766 assert(M[0, 1] == 0 + 1); 767 assert(M[0, 2] == 0 + 2); 768 assert(M[1, 0] == 1 + 0); 769 assert(M[1, 1] == 1 + 1); 770 assert(M[1, 2] == 1 + 2); 771 } 772 773 int opApply(scope int delegate(size_t i, size_t j, ref T) yield) 774 { 775 mixin(opApplyImpl!(No.reverse)); 776 } 777 778 int opApplyReverse(scope int delegate(size_t i, size_t j, ref T) yield) 779 { 780 mixin(opApplyImpl!(Yes.reverse)); 781 } 782 783 unittest 784 { 785 auto M = DPMatrix!int(2, 3); 786 787 foreach (i, j, ref elem; M) 788 elem = cast(int) (i + j); 789 790 assert(M.elements == [ 791 0, 1, 2, 792 1, 2, 3, 793 ]); 794 } 795 796 int opApply(scope int delegate(size_t i, size_t j, in T) yield) const 797 { 798 mixin(opApplyImpl!(No.reverse)); 799 } 800 801 int opApplyReverse(scope int delegate(size_t i, size_t j, in T) yield) const 802 { 803 mixin(opApplyImpl!(Yes.reverse)); 804 } 805 806 unittest 807 { 808 auto M = DPMatrix!int(2, 3); 809 M.elements = [ 810 0, 1, 2, 811 1, 2, 3, 812 ]; 813 814 foreach (i, j, elem; cast(const(typeof(M))) M) 815 { 816 assert(M[i, j] == elem); 817 assert(elem == i + j); 818 } 819 } 820 821 private static enum opApplyImpl(Flag!"reverse" reverse) = ` 822 int result = 0; 823 824 foreach` ~ (reverse ? `_reverse` : ``) ~ ` (i, ref element; elements) 825 { 826 result = yield(i / size[1], i % size[1], element); 827 828 if (result) 829 break; 830 } 831 832 return result; 833 `; 834 }