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 }