A faithful MoonBit port of Python's difflib: SequenceMatcher, Differ, ndiff, unified/context diffs, HtmlDiff and get_close_matches.
| Python | MoonBit |
|---|---|
| SequenceMatcher(isjunk, a, b, autojunk) | SequenceMatcher::new(isjunk?, a?, b?, autojunk?) (generic over T : Hash + Eq) |
| Match(a, b, size) | Match { a, b, size } |
| opcode tuples (tag, i1, i2, j1, j2) | Opcode { tag : Tag, i1, i2, j1, j2 } |
| get_close_matches(word, poss, n, cutoff) | get_close_matches(word, poss, n?, cutoff?, autojunk?) |
| Differ(linejunk, charjunk).compare(a, b) | Differ::new(linejunk?, charjunk?).compare(a, b) |
| ndiff, restore | ndiff, restore |
| unified_diff, context_diff | unified_diff, context_diff |
| diff_bytes(unified_diff, ...) | diff_bytes(Unified, ...) / diff_bytes(Context, ...) |
| HtmlDiff(...).make_file / make_table | HtmlDiff::new(...).make_file / make_table |
| IS_LINE_JUNK, IS_CHARACTER_JUNK | is_line_junk, is_character_junk |
| ValueError | DiffError::ValueError(message) (same messages) |
///|
test "sequence matcher" {
let s = @difflib.SequenceMatcher::new(
a="qabxcd".to_array(),
b="abycdf".to_array(),
)
inspect(
s
.get_opcodes()
.map(op => "\{op.tag} a[\{op.i1}:\{op.i2}] b[\{op.j1}:\{op.j2}]")
.join("\n"),
content=(
#|delete a[0:1] b[0:0]
#|equal a[1:3] b[0:2]
#|replace a[3:4] b[2:3]
#|equal a[4:6] b[3:5]
#|insert a[6:6] b[5:6]
),
)
inspect(s.ratio(), content="0.6666666666666666")
}///|
test "unified diff" {
let before = ["one\n", "two\n", "three\n", "four\n"]
let after = ["zero\n", "one\n", "tree\n", "four\n"]
inspect(
@difflib.unified_diff(
before,
after,
fromfile="before.txt",
tofile="after.txt",
).join(""),
content=(
#|--- before.txt
#|+++ after.txt
#|@@ -1,4 +1,4 @@
#|+zero
#| one
#|-two
#|-three
#|+tree
#| four
#|
),
)
}///|
test "ndiff" {
let a = ["one\n", "two\n", "three\n"]
let b = ["ore\n", "tree\n", "emu\n"]
let delta = @difflib.ndiff(a, b).to_array()
inspect(
delta.join(""),
content=(
#|- one
#|? ^
#|+ ore
#|? ^
#|- two
#|- three
#|? -
#|+ tree
#|+ emu
#|
),
)
assert_eq(@difflib.restore(delta.iter(), 1).to_array(), a)
assert_eq(@difflib.restore(delta.iter(), 2).to_array(), b)
}///|
test "close matches" {
debug_inspect(
@difflib.get_close_matches("appel", ["ape", "apple", "peach", "puppy"]),
content=(
#|["apple", "ape"]
),
)
}///|
test "html diff" {
let html = @difflib.HtmlDiff::new(wrapcolumn=40).make_file(
["a\n", "b\n"],
["a\n", "c\n"],
fromdesc="old",
todesc="new",
)
assert_true(html.has_prefix("\n<!DOCTYPE html>"))
}moon test # one backend; add --target js, native, wasm or allgit init .repos/cpython && cd .repos/cpython
git remote add origin https://github.com/python/cpython
git sparse-checkout set --no-cone /Lib/difflib.py /Lib/test/test_difflib.py /Lib/test/test_difflib_expect.html
git fetch --depth 1 --filter=blob:none origin 763b6edb0ec959bdfb78f098b76829365de36ca5
git checkout FETCH_HEAD && cd ../..
python3 tools/gen_corpus.py > corpus/corpus_fixtures_test.mbt
python3 tools/gen_html_fixtures.py .repos/cpython/Lib tools > html_fixtures_test.mbt
python3 tools/gen_templates.py .repos/cpython/Lib > html_templates.mbt
python3 tools/gen_charset.py > charset.mbt
moon fmt # generated files are committed in formatted formpub struct Differ {
// private fields
}test {
let text1 = [
" 1. Beautiful is better than ugly.\n", " 2. Explicit is better than implicit.\n",
" 3. Simple is better than complex.\n", " 4. Complex is better than complicated.\n",
]
let text2 = [
" 1. Beautiful is better than ugly.\n", " 3. Simple is better than complex.\n",
" 4. Complicated is better than complex.\n", " 5. Flat is better than nested.\n",
]
inspect(
@difflib.Differ::new().compare(text1, text2).join(""),
content=(
#| 1. Beautiful is better than ugly.
#|- 2. Explicit is better than implicit.
#|- 3. Simple is better than complex.
#|+ 3. Simple is better than complex.
#|? ++
#|- 4. Complex is better than complicated.
#|? ^ ---- ^
#|+ 4. Complicated is better than complex.
#|? ++++ ^ ^
#|+ 5. Flat is better than nested.
#|
),
)
}test {
let delta = @difflib.Differ::new().compare(["one\n", "two\n", "three\n"], [
"ore\n", "tree\n", "emu\n",
])
inspect(
delta.join(""),
content=(
#|- one
#|? ^
#|+ ore
#|? ^
#|- two
#|- three
#|? -
#|+ tree
#|+ emu
#|
),
)
}pub struct HtmlDiff {
// private fields
}test {
@difflib.HtmlDiff::reset_prefix_counter()
let table = @difflib.HtmlDiff::new().make_table(["a\n", "b\n"], ["a\n", "c\n"])
assert_true(table.contains("<span class=\"diff_sub\">b</span>"))
assert_true(table.contains("<span class=\"diff_add\">c</span>"))
}pub struct SequenceMatcher[T] {
// private fields
}test {
let s = @difflib.SequenceMatcher::new(
isjunk=c => c == ' ',
a="private Thread currentThread;".to_array(),
b="private volatile Thread currentThread;".to_array(),
)
inspect((s.ratio() * 100).round() / 100, content="0.87")
debug_inspect(
s.get_matching_blocks(),
content=(
#|[
#| { a: 0, b: 0, size: 8 },
#| { a: 8, b: 17, size: 21 },
#| { a: 29, b: 38, size: 0 },
#|]
),
)
}fn[T : Hash + Eq] SequenceMatcher::find_longest_match(self : SequenceMatcher[T], alo? : Int, ahi? : Int, blo? : Int, bhi? : Int) -> Matchtest {
let s = @difflib.SequenceMatcher::new(
a=" abcd".to_array(),
b="abcd abcd".to_array(),
)
debug_inspect(s.find_longest_match(), content="{ a: 0, b: 4, size: 5 }")
let s = @difflib.SequenceMatcher::new(
isjunk=c => c == ' ',
a=" abcd".to_array(),
b="abcd abcd".to_array(),
)
debug_inspect(s.find_longest_match(), content="{ a: 1, b: 0, size: 4 }")
}fn[T : Hash + Eq] SequenceMatcher::get_grouped_opcodes(self : SequenceMatcher[T], n? : Int) -> Iter[Array[Opcode]]test {
let a = Array::makei(39, i => (i + 1).to_string())
let b = a.copy()
b.insert(8, "i") // Make an insertion
b[20] = b[20] + "x" // Make a replacement
for _ in 0..<5 {
b.remove(23) |> ignore // Make a deletion
}
b[30] = b[30] + "y" // Make another replacement
let groups = @difflib.SequenceMatcher::new(a~, b~).get_grouped_opcodes()
inspect(
groups
.map(g => {
g.map(op => "\{op.tag} \{op.i1} \{op.i2} \{op.j1} \{op.j2}").join(", ")
})
.join("\n"),
content=(
#|equal 5 8 5 8, insert 8 8 8 9, equal 8 11 9 12
#|equal 16 19 17 20, replace 19 20 20 21, equal 20 22 21 23, delete 22 27 23 23, equal 27 30 23 26
#|equal 31 34 27 30, replace 34 35 30 31, equal 35 38 31 34
),
)
}test {
let s = @difflib.SequenceMatcher::new(
a="abxcd".to_array(),
b="abcd".to_array(),
)
debug_inspect(
s.get_matching_blocks(),
content=(
#|[
#| { a: 0, b: 0, size: 2 },
#| { a: 3, b: 2, size: 2 },
#| { a: 5, b: 4, size: 0 },
#|]
),
)
}test {
let a = "qabxcd"
let b = "abycdf"
let s = @difflib.SequenceMatcher::new(a=a.to_array(), b=b.to_array())
let lines = s
.get_opcodes()
.map(op => "\{op.tag} a[\{op.i1}:\{op.i2}] b[\{op.j1}:\{op.j2}]")
inspect(
lines.join("\n"),
content=(
#|delete a[0:1] b[0:0]
#|equal a[1:3] b[0:2]
#|replace a[3:4] b[2:3]
#|equal a[4:6] b[3:5]
#|insert a[6:6] b[5:6]
),
)
}fn[T : Hash + Eq] SequenceMatcher::new(isjunk? : (T) -> Bool, a? : Array[T], b? : Array[T], autojunk? : Bool) -> SequenceMatcher[T]test {
let s = @difflib.SequenceMatcher::new(
a="abcd".to_array(),
b="bcde".to_array(),
)
inspect(s.ratio(), content="0.75")
inspect(s.quick_ratio(), content="0.75")
inspect(s.real_quick_ratio(), content="1")
}fn[T : Hash + Eq] SequenceMatcher::set_seqs(self : SequenceMatcher[T], a : Array[T], b : Array[T]) -> Unittest {
let diff = @difflib.context_diff(
["one\n", "two\n", "three\n", "four\n"],
["zero\n", "one\n", "tree\n", "four\n"],
fromfile="Original",
tofile="Current",
)
inspect(
diff.join(""),
content=(
#|*** Original
#|--- Current
#|***************
#|*** 1,4 ****
#| one
#|! two
#|! three
#| four
#|--- 1,4 ----
#|+ zero
#| one
#|! tree
#| four
#|
),
)
}fn diff_bytes(format : DiffFormat, a : Array[Bytes], b : Array[Bytes], fromfile? : Bytes, tofile? : Bytes, fromfiledate? : Bytes, tofiledate? : Bytes, n? : Int, lineterm? : Bytes, autojunk? : Bool) -> Iter[Bytes]test {
let diff = @difflib.diff_bytes(
Unified,
[b"\xa3odz is a city in Poland."],
[b"\xc5\x81odz is a city in Poland."],
fromfile=b"\xb3odz.txt",
tofile=b"\xc5\x82odz.txt",
lineterm=b"",
)
let expected : Array[Bytes] = [
b"--- \xb3odz.txt", b"+++ \xc5\x82odz.txt", b"@@ -1 +1 @@", b"-\xa3odz is a city in Poland.",
b"+\xc5\x81odz is a city in Poland.",
]
assert_eq(diff.to_array(), expected)
}test {
debug_inspect(
@difflib.get_close_matches("appel", ["ape", "apple", "peach", "puppy"]),
content=(
#|["apple", "ape"]
),
)
let keywords = [
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from",
"global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass",
"raise", "return", "try", "while", "with", "yield",
]
debug_inspect(
@difflib.get_close_matches("wheel", keywords),
content=(
#|["while"]
),
)
debug_inspect(@difflib.get_close_matches("Apple", keywords), content="[]")
debug_inspect(
@difflib.get_close_matches("accept", keywords),
content=(
#|["except"]
),
)
}fn is_character_junk(ch : Char) -> Booltest {
inspect(@difflib.is_character_junk(' '), content="true")
inspect(@difflib.is_character_junk('\t'), content="true")
inspect(@difflib.is_character_junk('\n'), content="false")
inspect(@difflib.is_character_junk('x'), content="false")
}fn is_line_junk(line : String) -> Booltest {
inspect(@difflib.is_line_junk("\n"), content="true")
inspect(@difflib.is_line_junk(" # \n"), content="true")
inspect(@difflib.is_line_junk("hello\n"), content="false")
}test {
let diff = @difflib.ndiff(["one\n", "two\n", "three\n"], [
"ore\n", "tree\n", "emu\n",
])
inspect(
diff.join(""),
content=(
#|- one
#|? ^
#|+ ore
#|? ^
#|- two
#|- three
#|? -
#|+ tree
#|+ emu
#|
),
)
}test {
// keep the delta in an array: an `Iter` can be consumed only once
let diff = @difflib.ndiff(["one\n", "two\n", "three\n"], [
"ore\n", "tree\n", "emu\n",
]).to_array()
inspect(
@difflib.restore(diff.iter(), 1).join(""),
content="one\ntwo\nthree\n",
)
inspect(@difflib.restore(diff.iter(), 2).join(""), content="ore\ntree\nemu\n")
}test {
let diff = @difflib.unified_diff(
["one", "two", "three", "four"],
["zero", "one", "tree", "four"],
fromfile="Original",
tofile="Current",
fromfiledate="2005-01-26 23:30:50",
tofiledate="2010-04-02 10:20:52",
lineterm="",
)
inspect(
diff.join("\n"),
content=(
#|--- Original 2005-01-26 23:30:50
#|+++ Current 2010-04-02 10:20:52
#|@@ -1,4 +1,4 @@
#|+zero
#| one
#|-two
#|-three
#|+tree
#| four
),
)
}Install
Download zipA faithful MoonBit port of Python's difflib: SequenceMatcher, Differ, ndiff, unified/context diffs, HtmlDiff and get_close_matches.