1use std::collections::HashMap;
4use std::ops::Range;
5
6pub const SIMILARITY_THRESHOLD: f64 = 0.45;
8pub const ABSORB_EQUAL_MAX: usize = 2;
10pub const CHAR_DIFF_MAX_CHARS: usize = 4_000;
12pub const PAIRING_MAX_PRODUCT: usize = 10_000;
14pub const DOC_MAX_BLOCKS: usize = 20_000;
16pub const MYERS_MAX_EDITS: usize = 2_000;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Op {
22 Equal { a: Range<usize>, b: Range<usize> },
23 Delete { a: Range<usize> },
24 Insert { b: Range<usize> },
25}
26
27pub fn diff_ops<T: Eq>(a: &[T], b: &[T], max_edits: usize) -> Option<Vec<Op>> {
29 let (n, m) = (a.len(), b.len());
30 let offset = max_edits as isize + 1;
32 let mut v = vec![0usize; 2 * max_edits + 3];
33 let mut trace: Vec<Vec<usize>> = Vec::new();
35 let idx = |k: isize| (k + offset) as usize;
36 let mut found = false;
37 for d in 0..=max_edits as isize {
38 let mut k = -d;
39 while k <= d {
40 let mut x = if k == -d || (k != d && v[idx(k - 1)] < v[idx(k + 1)]) {
41 v[idx(k + 1)]
42 } else {
43 v[idx(k - 1)] + 1
44 };
45 let mut y = (x as isize - k) as usize;
46 while x < n && y < m && a[x] == b[y] {
48 x += 1;
49 y += 1;
50 }
51 v[idx(k)] = x;
52 if x >= n && y >= m {
53 found = true;
54 }
55 k += 2;
56 }
57 trace.push((-d..=d).step_by(2).map(|k| v[idx(k)]).collect());
58 if found {
59 break;
60 }
61 }
62 if !found {
63 return None;
64 }
65
66 let mut rev: Vec<Op> = Vec::new();
68 let (mut x, mut y) = (n, m);
69 for d in (0..trace.len() as isize).rev() {
70 let k = x as isize - y as isize;
71 if d == 0 {
72 if x > 0 {
73 rev.push(Op::Equal { a: 0..x, b: 0..y });
74 }
75 break;
76 }
77 let prev = &trace[(d - 1) as usize];
78 let at = |kk: isize| prev[((kk + d - 1) / 2) as usize];
79 let prev_k = if k == -d || (k != d && at(k - 1) < at(k + 1)) {
80 k + 1
81 } else {
82 k - 1
83 };
84 let px = at(prev_k);
85 let py = (px as isize - prev_k) as usize;
86 let (mx, my) = if prev_k == k + 1 {
88 (px, py + 1)
89 } else {
90 (px + 1, py)
91 };
92 if x > mx {
93 rev.push(Op::Equal { a: mx..x, b: my..y });
94 }
95 if prev_k == k + 1 {
96 rev.push(Op::Insert { b: py..my });
97 } else {
98 rev.push(Op::Delete { a: px..mx });
99 }
100 x = px;
101 y = py;
102 }
103
104 let mut ops: Vec<Op> = Vec::new();
106 for op in rev.into_iter().rev() {
107 match (ops.last_mut(), &op) {
108 (Some(Op::Equal { a, b }), Op::Equal { a: na, b: nb }) => {
109 a.end = na.end;
110 b.end = nb.end;
111 }
112 (Some(Op::Delete { a }), Op::Delete { a: na }) => a.end = na.end,
113 (Some(Op::Insert { b }), Op::Insert { b: nb }) => b.end = nb.end,
114 _ => ops.push(op),
115 }
116 }
117 Some(ops)
118}
119
120pub fn similarity(a: &str, b: &str) -> f64 {
122 let mut counts: HashMap<char, usize> = HashMap::new();
123 let mut len_a = 0usize;
124 for c in a.chars() {
125 *counts.entry(c).or_insert(0) += 1;
126 len_a += 1;
127 }
128 let mut len_b = 0usize;
129 let mut common = 0usize;
130 for c in b.chars() {
131 len_b += 1;
132 if let Some(n) = counts.get_mut(&c) {
133 if *n > 0 {
134 *n -= 1;
135 common += 1;
136 }
137 }
138 }
139 if len_a + len_b == 0 {
140 return 1.0;
141 }
142 2.0 * common as f64 / (len_a + len_b) as f64
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub enum BlockKind {
148 Paragraph,
149 Heading,
150 Code,
151 Table,
152 ListItem,
153 Quote,
154 Rule,
155 Footnote,
156 Html,
157 Other,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct BlockKey {
163 pub kind: BlockKind,
164 pub key: String,
166 pub text: String,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum RowKind {
173 Same,
174 Changed,
175 Added,
176 Deleted,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct Row {
182 pub kind: RowKind,
183 pub left: Option<usize>,
184 pub right: Option<usize>,
185}
186
187pub fn align_blocks(a: &[BlockKey], b: &[BlockKey]) -> Vec<Row> {
189 if a.len() + b.len() > DOC_MAX_BLOCKS {
190 return all_deleted_then_added(0..a.len(), 0..b.len());
191 }
192 let ka: Vec<&str> = a.iter().map(|x| x.key.as_str()).collect();
193 let kb: Vec<&str> = b.iter().map(|x| x.key.as_str()).collect();
194 let Some(ops) = diff_ops(&ka, &kb, MYERS_MAX_EDITS) else {
195 return all_deleted_then_added(0..a.len(), 0..b.len());
196 };
197
198 let mut rows = Vec::new();
199 let mut region: Option<(Range<usize>, Range<usize>)> = None;
201 for op in ops {
202 match op {
203 Op::Equal { a: ra, b: rb } => {
204 if let Some((da, ib)) = region.take() {
205 pair_region(a, b, da, ib, &mut rows);
206 }
207 for (i, j) in ra.zip(rb) {
208 rows.push(Row {
209 kind: RowKind::Same,
210 left: Some(i),
211 right: Some(j),
212 });
213 }
214 }
215 Op::Delete { a: ra } => {
216 let r = region.get_or_insert((ra.start..ra.start, 0..0));
217 r.0.end = ra.end;
218 }
219 Op::Insert { b: rb } => {
220 let r = region.get_or_insert((0..0, rb.start..rb.start));
221 if r.1.is_empty() && r.1.start == 0 {
222 r.1.start = rb.start;
223 }
224 r.1.end = rb.end;
225 }
226 }
227 }
228 if let Some((da, ib)) = region.take() {
229 pair_region(a, b, da, ib, &mut rows);
230 }
231 rows
232}
233
234fn all_deleted_then_added(da: Range<usize>, ib: Range<usize>) -> Vec<Row> {
236 let deleted = da.map(|i| Row {
237 kind: RowKind::Deleted,
238 left: Some(i),
239 right: None,
240 });
241 let added = ib.map(|j| Row {
242 kind: RowKind::Added,
243 left: None,
244 right: Some(j),
245 });
246 deleted.chain(added).collect()
247}
248
249fn char_counts(text: &str) -> (HashMap<char, usize>, usize) {
251 let mut counts = HashMap::new();
252 let mut len = 0;
253 for c in text.chars() {
254 *counts.entry(c).or_insert(0) += 1;
255 len += 1;
256 }
257 (counts, len)
258}
259
260fn block_similarity(
262 l: &BlockKey,
263 lc: &(HashMap<char, usize>, usize),
264 r: &BlockKey,
265 rc: &(HashMap<char, usize>, usize),
266) -> f64 {
267 if lc.1 == 0 && rc.1 == 0 {
268 return if l.kind == r.kind { 1.0 } else { 0.0 };
269 }
270 let (small, large) = if lc.0.len() <= rc.0.len() {
272 (&lc.0, &rc.0)
273 } else {
274 (&rc.0, &lc.0)
275 };
276 let common: usize = small
277 .iter()
278 .map(|(c, n)| large.get(c).map_or(0, |m| *n.min(m)))
279 .sum();
280 2.0 * common as f64 / (lc.1 + rc.1) as f64
281}
282
283fn pair_region(
285 a: &[BlockKey],
286 b: &[BlockKey],
287 da: Range<usize>,
288 ib: Range<usize>,
289 rows: &mut Vec<Row>,
290) {
291 if da.len() * ib.len() > PAIRING_MAX_PRODUCT {
292 rows.extend(all_deleted_then_added(da, ib));
293 return;
294 }
295 let (na, nb) = (da.len(), ib.len());
296 let ca: Vec<_> = a[da.clone()].iter().map(|x| char_counts(&x.text)).collect();
297 let cb: Vec<_> = b[ib.clone()].iter().map(|x| char_counts(&x.text)).collect();
298 let mut sim = vec![0.0f64; na * nb];
300 for i in 0..na {
301 for j in 0..nb {
302 sim[i * nb + j] = block_similarity(&a[da.start + i], &ca[i], &b[ib.start + j], &cb[j]);
303 }
304 }
305 let w = nb + 1;
308 let mut right_max = vec![0.0f64; (na + 1) * w];
309 let mut down_max = vec![0.0f64; (na + 1) * w];
310 for i in (0..na).rev() {
311 for j in (0..nb).rev() {
312 let v = sim[i * nb + j];
313 right_max[i * w + j] = v.max(right_max[i * w + j + 1]);
314 down_max[i * w + j] = v.max(down_max[(i + 1) * w + j]);
315 }
316 }
317 let (mut i, mut j) = (0, 0);
318 while i < na && j < nb {
319 let cur = sim[i * nb + j];
320 let best_r = right_max[i * w + j + 1];
322 let best_l = down_max[(i + 1) * w + j];
323 if cur >= SIMILARITY_THRESHOLD && cur >= best_r && cur >= best_l {
324 rows.push(Row {
325 kind: RowKind::Changed,
326 left: Some(da.start + i),
327 right: Some(ib.start + j),
328 });
329 i += 1;
330 j += 1;
331 } else if best_r > best_l {
332 rows.push(Row {
333 kind: RowKind::Added,
334 left: None,
335 right: Some(ib.start + j),
336 });
337 j += 1;
338 } else {
339 rows.push(Row {
340 kind: RowKind::Deleted,
341 left: Some(da.start + i),
342 right: None,
343 });
344 i += 1;
345 }
346 }
347 rows.extend(all_deleted_then_added(
348 da.start + i..da.end,
349 ib.start + j..ib.end,
350 ));
351}
352
353pub fn char_marks(a: &[char], b: &[char]) -> Option<(Vec<bool>, Vec<bool>)> {
356 if a.len() + b.len() > CHAR_DIFF_MAX_CHARS {
357 return None;
358 }
359 let ops = diff_ops(a, b, MYERS_MAX_EDITS)?;
360 let mut ma = vec![false; a.len()];
361 let mut mb = vec![false; b.len()];
362 for (idx, op) in ops.iter().enumerate() {
363 match op {
364 Op::Delete { a: r } => ma[r.clone()].fill(true),
365 Op::Insert { b: r } => mb[r.clone()].fill(true),
366 Op::Equal { a: ra, b: rb }
368 if idx > 0 && idx + 1 < ops.len() && ra.len() <= ABSORB_EQUAL_MAX =>
369 {
370 ma[ra.clone()].fill(true);
371 mb[rb.clone()].fill(true);
372 }
373 Op::Equal { .. } => {}
374 }
375 }
376 Some((ma, mb))
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 fn rebuild<T: Clone + Eq + std::fmt::Debug>(a: &[T], b: &[T], ops: &[Op]) {
384 let mut from_a = Vec::new();
385 let mut from_b = Vec::new();
386 for op in ops {
387 match op {
388 Op::Equal { a: ra, b: rb } => {
389 assert_eq!(a[ra.clone()], b[rb.clone()]);
390 from_a.extend_from_slice(&a[ra.clone()]);
391 from_b.extend_from_slice(&b[rb.clone()]);
392 }
393 Op::Delete { a: ra } => from_a.extend_from_slice(&a[ra.clone()]),
394 Op::Insert { b: rb } => from_b.extend_from_slice(&b[rb.clone()]),
395 }
396 }
397 assert_eq!(from_a, a);
398 assert_eq!(from_b, b);
399 }
400
401 fn has_equal(ops: &[Op]) -> bool {
402 ops.iter().any(|o| matches!(o, Op::Equal { .. }))
403 }
404 fn has_delete(ops: &[Op]) -> bool {
405 ops.iter().any(|o| matches!(o, Op::Delete { .. }))
406 }
407 fn has_insert(ops: &[Op]) -> bool {
408 ops.iter().any(|o| matches!(o, Op::Insert { .. }))
409 }
410
411 #[test]
415 fn diff_opsは両方空なら空の編集列を返す() {
416 let ops = diff_ops::<i32>(&[], &[], 10).unwrap();
417 assert!(!has_delete(&ops) && !has_insert(&ops));
418 rebuild::<i32>(&[], &[], &ops);
419 }
420
421 #[test]
423 fn diff_opsは同一列ならequalだけを返す() {
424 let a = [1, 2, 3];
425 let ops = diff_ops(&a, &a, 10).unwrap();
426 assert!(has_equal(&ops));
427 assert!(!has_delete(&ops) && !has_insert(&ops));
428 rebuild(&a, &a, &ops);
429 }
430
431 #[test]
433 fn diff_opsは全置換なら削除と挿入だけでequalを含まない() {
434 let (a, b) = ([1, 2, 3], [4, 5, 6]);
435 let ops = diff_ops(&a, &b, 100).unwrap();
436 assert!(!has_equal(&ops));
437 assert!(has_delete(&ops) && has_insert(&ops));
438 rebuild(&a, &b, &ops);
439 }
440
441 #[test]
443 fn diff_opsは挿入のみなら削除を含まない() {
444 let (a, b) = ([1, 3], [1, 2, 3]);
445 let ops = diff_ops(&a, &b, 100).unwrap();
446 assert!(has_insert(&ops) && !has_delete(&ops));
447 rebuild(&a, &b, &ops);
448 }
449
450 #[test]
452 fn diff_opsは削除のみなら挿入を含まない() {
453 let (a, b) = ([1, 2, 3], [1, 3]);
454 let ops = diff_ops(&a, &b, 100).unwrap();
455 assert!(has_delete(&ops) && !has_insert(&ops));
456 rebuild(&a, &b, &ops);
457 }
458
459 #[test]
461 fn diff_opsは編集距離がmax_editsを超えるとnoneで等しければsome() {
462 let (a, b) = ([1, 2, 3], [4, 5, 6]);
463 assert!(diff_ops(&a, &b, 5).is_none());
464 let ops = diff_ops(&a, &b, 6).expect("distance 6 is within max_edits 6");
465 rebuild(&a, &b, &ops);
466 }
467
468 #[test]
470 fn diff_opsは混在した変更でも両列を復元できる() {
471 let a: Vec<char> = "kitten sitting".chars().collect();
472 let b: Vec<char> = "sitting kitten".chars().collect();
473 let ops = diff_ops(&a, &b, MYERS_MAX_EDITS).unwrap();
474 rebuild(&a, &b, &ops);
475 }
476
477 #[test]
481 fn similarityは両方空なら1() {
482 assert_eq!(similarity("", ""), 1.0);
483 }
484
485 #[test]
487 fn similarityは片側だけ空なら0() {
488 assert_eq!(similarity("", "abc"), 0.0);
489 assert_eq!(similarity("abc", ""), 0.0);
490 }
491
492 #[test]
494 fn similarityは同一文字列なら1で共通が無ければ0() {
495 assert_eq!(similarity("abc", "abc"), 1.0);
496 assert_eq!(similarity("abc", "xyz"), 0.0);
497 }
498
499 #[test]
501 fn similarityは文字の多重集合の重なりで定まる() {
502 let v = similarity("aab", "abb");
504 assert!((v - 4.0 / 6.0).abs() < 1e-9, "{v}");
505 assert_eq!(similarity("abcd", "dcba"), 1.0);
507 let w = similarity("abcd", "abxy");
508 assert!((w - 0.5).abs() < 1e-9, "{w}");
509 }
510
511 fn bk(kind: BlockKind, key: &str, text: &str) -> BlockKey {
514 BlockKey {
515 kind,
516 key: key.to_string(),
517 text: text.to_string(),
518 }
519 }
520
521 fn para(key: &str, text: &str) -> BlockKey {
522 bk(BlockKind::Paragraph, key, text)
523 }
524
525 fn count(rows: &[Row], kind: RowKind) -> usize {
526 rows.iter().filter(|r| r.kind == kind).count()
527 }
528
529 fn text20(keep: usize, other: bool) -> String {
531 let base: Vec<char> = ('a'..='t').collect();
532 let alt: Vec<char> = ('A'..='T').collect();
533 (0..20)
534 .map(|i| if i < keep || !other { base[i] } else { alt[i] })
535 .collect()
536 }
537
538 #[test]
540 fn align_blocksは同一列ならすべてsameで添字が揃う() {
541 let a = vec![para("1", "x"), para("2", "y"), para("3", "z")];
542 let rows = align_blocks(&a, &a.clone());
543 assert_eq!(rows.len(), 3);
544 for (i, r) in rows.iter().enumerate() {
545 assert_eq!(r.kind, RowKind::Same);
546 assert_eq!((r.left, r.right), (Some(i), Some(i)));
547 }
548 }
549
550 #[test]
552 fn align_blocksは両方空なら行も空() {
553 assert!(align_blocks(&[], &[]).is_empty());
554 }
555
556 #[test]
558 fn align_blocksは片側だけのブロックを追加と削除にする() {
559 let a = vec![para("1", "x")];
560 let rows = align_blocks(&a, &[]);
561 assert_eq!(rows.len(), 1);
562 assert_eq!(rows[0].kind, RowKind::Deleted);
563 assert_eq!((rows[0].left, rows[0].right), (Some(0), None));
564 let rows = align_blocks(&[], &a);
565 assert_eq!(rows.len(), 1);
566 assert_eq!(rows[0].kind, RowKind::Added);
567 assert_eq!((rows[0].left, rows[0].right), (None, Some(0)));
568 }
569
570 #[test]
572 fn align_blocksは類似度が閾値以上の置換区間のブロックを対にする() {
573 let a = vec![para("l", &text20(10, false))];
575 let b = vec![para("r", &text20(10, true))];
576 let rows = align_blocks(&a, &b);
577 assert_eq!(rows.len(), 1);
578 assert_eq!(rows[0].kind, RowKind::Changed);
579 assert_eq!((rows[0].left, rows[0].right), (Some(0), Some(0)));
580 }
581
582 #[test]
584 fn align_blocksは類似度がちょうど0_45なら対にする() {
585 let a = vec![para("l", &text20(9, false))];
587 let b = vec![para("r", &text20(9, true))];
588 assert!((similarity(&a[0].text, &b[0].text) - 0.45).abs() < 1e-9);
589 let rows = align_blocks(&a, &b);
590 assert_eq!(count(&rows, RowKind::Changed), 1);
591 }
592
593 #[test]
595 fn align_blocksは類似度が閾値未満なら対にせず削除と追加にする() {
596 let a = vec![para("l", &text20(8, false))];
598 let b = vec![para("r", &text20(8, true))];
599 let rows = align_blocks(&a, &b);
600 assert_eq!(count(&rows, RowKind::Changed), 0);
601 assert_eq!(count(&rows, RowKind::Deleted), 1);
602 assert_eq!(count(&rows, RowKind::Added), 1);
603 }
604
605 #[test]
607 fn align_blocksは先読みして良い相手が後ろにあれば手前を単独の追加にする() {
608 let l = vec![para("l", &text20(0, false))];
610 let r = vec![
611 para(
612 "r0",
613 &format!("{}{}", &text20(0, false)[..10], "XXXXXXXXXX"),
614 ),
615 para("r1", &text20(0, false)),
616 ];
617 let rows = align_blocks(&l, &r);
618 assert_eq!(count(&rows, RowKind::Changed), 1);
619 let changed = rows.iter().find(|r| r.kind == RowKind::Changed).unwrap();
620 assert_eq!((changed.left, changed.right), (Some(0), Some(1)));
621 let added = rows.iter().find(|r| r.kind == RowKind::Added).unwrap();
622 assert_eq!(added.right, Some(0));
623 assert_eq!(count(&rows, RowKind::Deleted), 0);
624 }
625
626 #[test]
628 fn align_blocksは対にしたブロックの順序を保つ() {
629 let a = vec![para("l0", "alpha beta"), para("l1", "gamma delta")];
630 let b = vec![para("r0", "alpha beta!"), para("r1", "gamma delta!")];
631 let rows = align_blocks(&a, &b);
632 let pairs: Vec<_> = rows
633 .iter()
634 .filter(|r| r.kind == RowKind::Changed)
635 .map(|r| (r.left.unwrap(), r.right.unwrap()))
636 .collect();
637 assert_eq!(pairs, vec![(0, 0), (1, 1)]);
638 }
639
640 #[test]
642 fn align_blocksは空テキストで種類が同じブロックを対にする() {
643 let a = vec![bk(BlockKind::Paragraph, "img-a", "")];
644 let b = vec![bk(BlockKind::Paragraph, "img-b", "")];
645 let rows = align_blocks(&a, &b);
646 assert_eq!(rows.len(), 1);
647 assert_eq!(rows[0].kind, RowKind::Changed);
648 }
649
650 #[test]
652 fn align_blocksは空テキストで種類が違うブロックを対にしない() {
653 let a = vec![bk(BlockKind::Paragraph, "img-a", "")];
654 let b = vec![bk(BlockKind::ListItem, "img-b", "")];
655 let rows = align_blocks(&a, &b);
656 assert_eq!(count(&rows, RowKind::Changed), 0);
657 assert_eq!(count(&rows, RowKind::Deleted), 1);
658 assert_eq!(count(&rows, RowKind::Added), 1);
659 }
660
661 fn many(prefix: &str, n: usize, same_text: bool) -> Vec<BlockKey> {
662 (0..n)
663 .map(|i| {
664 let text = if same_text {
665 format!("text number {i}")
666 } else {
667 format!("{prefix}{i}")
668 };
669 para(&format!("{prefix}{i}"), &text)
670 })
671 .collect()
672 }
673
674 #[test]
676 fn align_blocksは積が上限ちょうどなら対づけする() {
677 let a = many("l", 100, true);
678 let b = many("r", 100, true);
679 assert_eq!(a.len() * b.len(), PAIRING_MAX_PRODUCT);
680 let rows = align_blocks(&a, &b);
681 assert_eq!(count(&rows, RowKind::Changed), 100);
682 }
683
684 #[test]
686 fn align_blocksは大きな置換区間でも似ていないブロックを速く削除と追加にする() {
687 let a: Vec<BlockKey> = (0..1000)
689 .map(|i| para(&format!("l{i}"), &"a".repeat(200)))
690 .collect();
691 let b = vec![para("r0", &"b".repeat(200))];
692 let rows = align_blocks(&a, &b);
693 assert_eq!(rows.len(), 1001);
694 assert_eq!(count(&rows, RowKind::Changed), 0);
695 assert_eq!(count(&rows, RowKind::Deleted), 1000);
696 assert_eq!(count(&rows, RowKind::Added), 1);
697 assert!(rows[..1000].iter().all(|r| r.kind == RowKind::Deleted));
699 assert_eq!(rows[1000].kind, RowKind::Added);
700 }
701
702 #[test]
704 fn align_blocksは積が上限を超えると対づけせずすべて削除と追加にする() {
705 let a = many("l", 101, true);
706 let b = many("r", 100, true);
707 assert!(a.len() * b.len() > PAIRING_MAX_PRODUCT);
708 let rows = align_blocks(&a, &b);
709 assert_eq!(count(&rows, RowKind::Changed), 0);
710 assert_eq!(count(&rows, RowKind::Deleted), 101);
711 assert_eq!(count(&rows, RowKind::Added), 100);
712 }
713
714 #[test]
716 fn align_blocksはブロック数の和が上限ちょうどなら通常どおり比べる() {
717 let a = many("k", DOC_MAX_BLOCKS / 2, false);
718 let rows = align_blocks(&a, &a.clone());
719 assert_eq!(rows.len(), DOC_MAX_BLOCKS / 2);
720 assert!(rows.iter().all(|r| r.kind == RowKind::Same));
721 }
722
723 #[test]
725 fn align_blocksはブロック数の和が上限を超えると全削除の後に全追加にする() {
726 let a = many("k", DOC_MAX_BLOCKS / 2 + 1, false);
727 let b = many("k", DOC_MAX_BLOCKS / 2, false);
728 let rows = align_blocks(&a, &b);
729 assert_eq!(rows.len(), a.len() + b.len());
730 let (del, add) = rows.split_at(a.len());
731 assert!(del
732 .iter()
733 .all(|r| r.kind == RowKind::Deleted && r.right.is_none()));
734 assert!(add
735 .iter()
736 .all(|r| r.kind == RowKind::Added && r.left.is_none()));
737 assert_eq!(del[0].left, Some(0));
738 assert_eq!(add[0].right, Some(0));
739 }
740
741 fn chars(s: &str) -> Vec<char> {
744 s.chars().collect()
745 }
746
747 #[test]
749 fn char_marksは同一なら印を付けない() {
750 let a = chars("同じ文字列");
751 let (ma, mb) = char_marks(&a, &a.clone()).unwrap();
752 assert_eq!(ma, vec![false; 5]);
753 assert_eq!(mb, vec![false; 5]);
754 }
755
756 #[test]
758 fn char_marksは変わった文字だけに印を付ける() {
759 let (ma, mb) = char_marks(&chars("abcdefgh"), &chars("abXdefgh")).unwrap();
760 assert_eq!(
761 ma,
762 vec![false, false, true, false, false, false, false, false]
763 );
764 assert_eq!(
765 mb,
766 vec![false, false, true, false, false, false, false, false]
767 );
768 }
769
770 #[test]
772 fn char_marksは2文字以下の一致に挟まれた変更を1つにまとめる() {
773 let (ma, mb) = char_marks(&chars("AAxxBB"), &chars("CCxxDD")).unwrap();
774 assert_eq!(ma, vec![true; 6]);
775 assert_eq!(mb, vec![true; 6]);
776 }
777
778 #[test]
780 fn char_marksは1文字の一致に挟まれた変更もまとめる() {
781 let (ma, mb) = char_marks(&chars("AAxBB"), &chars("CCxDD")).unwrap();
782 assert_eq!(ma, vec![true; 5]);
783 assert_eq!(mb, vec![true; 5]);
784 }
785
786 #[test]
788 fn char_marksは3文字の一致に挟まれた変更はまとめない() {
789 let (ma, mb) = char_marks(&chars("AAxxxBB"), &chars("CCxxxDD")).unwrap();
790 assert_eq!(ma, vec![true, true, false, false, false, true, true]);
791 assert_eq!(mb, vec![true, true, false, false, false, true, true]);
792 }
793
794 #[test]
796 fn char_marksは挿入と削除を別々の側に印付けする() {
797 let (ma, mb) = char_marks(&chars("abcdefgh"), &chars("abcdXXefgh")).unwrap();
798 assert_eq!(ma.len(), 8);
799 assert_eq!(mb.len(), 10);
800 assert!(ma.iter().all(|m| !m));
801 assert_eq!(mb.iter().filter(|m| **m).count(), 2);
802 assert!(mb[4] && mb[5]);
803 }
804
805 #[test]
807 fn char_marksは文字数の和が上限ちょうどなら印を返す() {
808 let a = vec!['a'; CHAR_DIFF_MAX_CHARS / 2];
809 let b = vec!['a'; CHAR_DIFF_MAX_CHARS / 2];
810 assert!(char_marks(&a, &b).is_some());
811 }
812
813 #[test]
815 fn char_marksは文字数の和が上限を超えるとnone() {
816 let a = vec!['a'; CHAR_DIFF_MAX_CHARS / 2];
817 let b = vec!['a'; CHAR_DIFF_MAX_CHARS / 2 + 1];
818 assert!(char_marks(&a, &b).is_none());
819 }
820
821 #[test]
823 fn char_marksは編集距離が上限を超えるとnone() {
824 let a = vec!['a'; CHAR_DIFF_MAX_CHARS / 2];
826 let b = vec!['b'; CHAR_DIFF_MAX_CHARS / 2];
827 assert!(char_marks(&a, &b).is_none());
828 }
829
830 #[test]
832 fn char_marksは編集距離が上限以内なら印を返す() {
833 let a = vec!['a'; 500];
834 let b = vec!['b'; 500];
835 let (ma, mb) = char_marks(&a, &b).unwrap();
836 assert!(ma.iter().all(|m| *m) && mb.iter().all(|m| *m));
837 }
838}