From 697c416e109600ce3438fb70d2475d7e030d61c1 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Tue, 4 Sep 2018 15:42:06 +0100 Subject: [PATCH] Auto convert source to Kotlin --- .../fraser/neil/plaintext/diff_match_patch.kt | 2503 +++++++++++++++++ .../name/fraser/neil/plaintext/Speedtest.kt | 58 + .../name/fraser/neil/plaintext/Speedtest1.txt | 230 ++ .../name/fraser/neil/plaintext/Speedtest2.txt | 188 ++ .../neil/plaintext/diff_match_patch_test.kt | 1392 +++++++++ 5 files changed, 4371 insertions(+) create mode 100644 kotlin/src/name/fraser/neil/plaintext/diff_match_patch.kt create mode 100644 kotlin/tests/name/fraser/neil/plaintext/Speedtest.kt create mode 100644 kotlin/tests/name/fraser/neil/plaintext/Speedtest1.txt create mode 100644 kotlin/tests/name/fraser/neil/plaintext/Speedtest2.txt create mode 100644 kotlin/tests/name/fraser/neil/plaintext/diff_match_patch_test.kt diff --git a/kotlin/src/name/fraser/neil/plaintext/diff_match_patch.kt b/kotlin/src/name/fraser/neil/plaintext/diff_match_patch.kt new file mode 100644 index 0000000..b0d932f --- /dev/null +++ b/kotlin/src/name/fraser/neil/plaintext/diff_match_patch.kt @@ -0,0 +1,2503 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package name.fraser.neil.plaintext + +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.net.URLEncoder +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +class diff_match_patch { + + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + /** + * Number of seconds to map a diff before giving up (0 for infinity). + */ + var Diff_Timeout = 1.0f + /** + * Cost of an empty edit operation in terms of edit characters. + */ + var Diff_EditCost: Short = 4 + /** + * At what point is no match declared (0.0 = perfection, 1.0 = very loose). + */ + var Match_Threshold = 0.5f + /** + * How far to search for a match (0 = exact location, 1000+ = broad match). + * A match this many characters away from the expected location will add + * 1.0 to the score (0.0 is a perfect match). + */ + var Match_Distance = 1000 + /** + * When deleting a large block of text (over ~64 characters), how close do + * the contents have to be to match the expected contents. (0.0 = perfection, + * 1.0 = very loose). Note that Match_Threshold controls how closely the + * end points of a delete need to match. + */ + var Patch_DeleteThreshold = 0.5f + /** + * Chunk size for context length. + */ + var Patch_Margin: Short = 4 + + /** + * The number of bits in an int. + */ + private val Match_MaxBits: Short = 32 + + // Define some regex patterns for matching boundaries. + private val BLANKLINEEND = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL) + private val BLANKLINESTART = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL) + + /** + * Internal class for returning results from diff_linesToChars(). + * Other less paranoid languages just use a three-element array. + */ + protected class LinesToCharsResult( + var chars1: String, var chars2: String, + var lineArray: List + ) + + // DIFF FUNCTIONS + + /** + * The data structure representing a diff is a Linked list of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + enum class Operation { + DELETE, INSERT, EQUAL + } + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return Linked List of Diff objects. + */ + @JvmOverloads + fun diff_main( + text1: String, text2: String, + checklines: Boolean = true + ): LinkedList { + // Set a deadline by which time the diff must be complete. + val deadline: Long + if (Diff_Timeout <= 0) { + deadline = java.lang.Long.MAX_VALUE + } else { + deadline = System.currentTimeMillis() + (Diff_Timeout * 1000).toLong() + } + return diff_main(text1, text2, checklines, deadline) + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout instead. + * @return Linked List of Diff objects. + */ + private fun diff_main( + text1: String?, text2: String?, + checklines: Boolean, deadline: Long + ): LinkedList { + var text1 = text1 + var text2 = text2 + // Check for null inputs. + if (text1 == null || text2 == null) { + throw IllegalArgumentException("Null inputs. (diff_main)") + } + + // Check for equality (speedup). + val diffs: LinkedList + if (text1 == text2) { + diffs = LinkedList() + if (text1.length != 0) { + diffs.add(Diff(Operation.EQUAL, text1)) + } + return diffs + } + + // Trim off common prefix (speedup). + var commonlength = diff_commonPrefix(text1, text2) + val commonprefix = text1.substring(0, commonlength) + text1 = text1.substring(commonlength) + text2 = text2.substring(commonlength) + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2) + val commonsuffix = text1.substring(text1.length - commonlength) + text1 = text1.substring(0, text1.length - commonlength) + text2 = text2.substring(0, text2.length - commonlength) + + // Compute the diff on the middle block. + diffs = diff_compute(text1, text2, checklines, deadline) + + // Restore the prefix and suffix. + if (commonprefix.length != 0) { + diffs.addFirst(Diff(Operation.EQUAL, commonprefix)) + } + if (commonsuffix.length != 0) { + diffs.addLast(Diff(Operation.EQUAL, commonsuffix)) + } + + diff_cleanupMerge(diffs) + return diffs + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private fun diff_compute( + text1: String, text2: String, + checklines: Boolean, deadline: Long + ): LinkedList { + var diffs = LinkedList() + + if (text1.length == 0) { + // Just add some text (speedup). + diffs.add(Diff(Operation.INSERT, text2)) + return diffs + } + + if (text2.length == 0) { + // Just delete some text (speedup). + diffs.add(Diff(Operation.DELETE, text1)) + return diffs + } + + val longtext = if (text1.length > text2.length) text1 else text2 + val shorttext = if (text1.length > text2.length) text2 else text1 + val i = longtext.indexOf(shorttext) + if (i != -1) { + // Shorter text is inside the longer text (speedup). + val op = if (text1.length > text2.length) + Operation.DELETE + else + Operation.INSERT + diffs.add(Diff(op, longtext.substring(0, i))) + diffs.add(Diff(Operation.EQUAL, shorttext)) + diffs.add(Diff(op, longtext.substring(i + shorttext.length))) + return diffs + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.add(Diff(Operation.DELETE, text1)) + diffs.add(Diff(Operation.INSERT, text2)) + return diffs + } + + // Check to see if the problem can be split in two. + val hm = diff_halfMatch(text1, text2) + if (hm != null) { + // A half-match was found, sort out the return data. + val text1_a = hm[0] + val text1_b = hm[1] + val text2_a = hm[2] + val text2_b = hm[3] + val mid_common = hm[4] + // Send both pairs off for separate processing. + val diffs_a = diff_main( + text1_a, text2_a, + checklines, deadline + ) + val diffs_b = diff_main( + text1_b, text2_b, + checklines, deadline + ) + // Merge the results. + diffs = diffs_a + diffs.add(Diff(Operation.EQUAL, mid_common)) + diffs.addAll(diffs_b) + return diffs + } + + return if (checklines && text1.length > 100 && text2.length > 100) { + diff_lineMode(text1, text2, deadline) + } else diff_bisect(text1, text2, deadline) + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private fun diff_lineMode( + text1: String, text2: String, + deadline: Long + ): LinkedList { + var text1 = text1 + var text2 = text2 + // Scan the text on a line-by-line basis first. + val a = diff_linesToChars(text1, text2) + text1 = a.chars1 + text2 = a.chars2 + val linearray = a.lineArray + + val diffs = diff_main(text1, text2, false, deadline) + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray) + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs) + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.add(Diff(Operation.EQUAL, "")) + var count_delete = 0 + var count_insert = 0 + var text_delete = "" + var text_insert = "" + val pointer = diffs.listIterator() + var thisDiff: Diff? = pointer.next() + while (thisDiff != null) { + when (thisDiff.operation) { + diff_match_patch.Operation.INSERT -> { + count_insert++ + text_insert += thisDiff.text + } + diff_match_patch.Operation.DELETE -> { + count_delete++ + text_delete += thisDiff.text + } + diff_match_patch.Operation.EQUAL -> { + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + pointer.previous() + for (j in 0 until count_delete + count_insert) { + pointer.previous() + pointer.remove() + } + for (subDiff in diff_main( + text_delete, text_insert, false, + deadline + )) { + pointer.add(subDiff) + } + } + count_insert = 0 + count_delete = 0 + text_delete = "" + text_insert = "" + } + } + thisDiff = if (pointer.hasNext()) pointer.next() else null + } + diffs.removeLast() // Remove the dummy entry at the end. + + return diffs + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + protected fun diff_bisect( + text1: String, text2: String, + deadline: Long + ): LinkedList { + // Cache the text lengths to prevent multiple calls. + val text1_length = text1.length + val text2_length = text2.length + val max_d = (text1_length + text2_length + 1) / 2 + val v_length = 2 * max_d + val v1 = IntArray(v_length) + val v2 = IntArray(v_length) + for (x in 0 until v_length) { + v1[x] = -1 + v2[x] = -1 + } + v1[max_d + 1] = 0 + v2[max_d + 1] = 0 + val delta = text1_length - text2_length + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + val front = delta % 2 != 0 + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + var k1start = 0 + var k1end = 0 + var k2start = 0 + var k2end = 0 + for (d in 0 until max_d) { + // Bail out if deadline is reached. + if (System.currentTimeMillis() > deadline) { + break + } + + // Walk the front path one step. + var k1 = -d + k1start + while (k1 <= d - k1end) { + val k1_offset = max_d + k1 + var x1: Int + if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { + x1 = v1[k1_offset + 1] + } else { + x1 = v1[k1_offset - 1] + 1 + } + var y1 = x1 - k1 + while (x1 < text1_length && y1 < text2_length + && text1[x1] == text2[y1] + ) { + x1++ + y1++ + } + v1[k1_offset] = x1 + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2 + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2 + } else if (front) { + val k2_offset = max_d + delta - k1 + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + val x2 = text1_length - v2[k2_offset] + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline) + } + } + } + k1 += 2 + } + + // Walk the reverse path one step. + var k2 = -d + k2start + while (k2 <= d - k2end) { + val k2_offset = max_d + k2 + var x2: Int + if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { + x2 = v2[k2_offset + 1] + } else { + x2 = v2[k2_offset - 1] + 1 + } + var y2 = x2 - k2 + while (x2 < text1_length && y2 < text2_length + && text1[text1_length - x2 - 1] == text2[text2_length - y2 - 1] + ) { + x2++ + y2++ + } + v2[k2_offset] = x2 + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2 + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2 + } else if (!front) { + val k1_offset = max_d + delta - k2 + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + val x1 = v1[k1_offset] + val y1 = max_d + x1 - k1_offset + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2 + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline) + } + } + } + k2 += 2 + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + val diffs = LinkedList() + diffs.add(Diff(Operation.DELETE, text1)) + diffs.add(Diff(Operation.INSERT, text2)) + return diffs + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private fun diff_bisectSplit( + text1: String, text2: String, + x: Int, y: Int, deadline: Long + ): LinkedList { + val text1a = text1.substring(0, x) + val text2a = text2.substring(0, y) + val text1b = text1.substring(x) + val text2b = text2.substring(y) + + // Compute both diffs serially. + val diffs = diff_main(text1a, text2a, false, deadline) + val diffsb = diff_main(text1b, text2b, false, deadline) + + diffs.addAll(diffsb) + return diffs + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return An object containing the encoded text1, the encoded text2 and + * the List of unique strings. The zeroth element of the List of + * unique strings is intentionally blank. + */ + protected fun diff_linesToChars(text1: String, text2: String): LinesToCharsResult { + val lineArray = ArrayList() + val lineHash = HashMap() + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.add("") + + // Allocate 2/3rds of the space for text1, the rest for text2. + val chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash, 40000) + val chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash, 65535) + return LinesToCharsResult(chars1, chars2, lineArray) + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @param maxLines Maximum length of lineArray. + * @return Encoded string. + */ + private fun diff_linesToCharsMunge( + text: String, lineArray: MutableList, + lineHash: MutableMap, maxLines: Int + ): String { + var lineStart = 0 + var lineEnd = -1 + var line: String + val chars = StringBuilder() + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart) + if (lineEnd == -1) { + lineEnd = text.length - 1 + } + line = text.substring(lineStart, lineEnd + 1) + + if (lineHash.containsKey(line)) { + chars.append((lineHash[line] as Int).toChar().toString()) + } else { + if (lineArray.size == maxLines) { + // Bail out at 65535 because + // String.valueOf((char) 65536).equals(String.valueOf(((char) 0))) + line = text.substring(lineStart) + lineEnd = text.length + } + lineArray.add(line) + lineHash[line] = lineArray.size - 1 + chars.append((lineArray.size - 1).toChar().toString()) + } + lineStart = lineEnd + 1 + } + return chars.toString() + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param diffs List of Diff objects. + * @param lineArray List of unique strings. + */ + protected fun diff_charsToLines( + diffs: List, + lineArray: List + ) { + var text: StringBuilder + for (diff in diffs) { + text = StringBuilder() + for (j in 0 until diff.text!!.length) { + text.append(lineArray[diff.text!![j].toInt()]) + } + diff.text = text.toString() + } + } + + /** + * Determine the common prefix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + fun diff_commonPrefix(text1: String, text2: String): Int { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + val n = Math.min(text1.length, text2.length) + for (i in 0 until n) { + if (text1[i] != text2[i]) { + return i + } + } + return n + } + + /** + * Determine the common suffix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + fun diff_commonSuffix(text1: String?, text2: String?): Int { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + val text1_length = text1!!.length + val text2_length = text2!!.length + val n = Math.min(text1_length, text2_length) + for (i in 1..n) { + if (text1[text1_length - i] != text2[text2_length - i]) { + return i - 1 + } + } + return n + } + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected fun diff_commonOverlap(text1: String, text2: String): Int { + var text1 = text1 + var text2 = text2 + // Cache the text lengths to prevent multiple calls. + val text1_length = text1.length + val text2_length = text2.length + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0 + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length) + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length) + } + val text_length = Math.min(text1_length, text2_length) + // Quick check for the worst case. + if (text1 == text2) { + return text_length + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + var best = 0 + var length = 1 + while (true) { + val pattern = text1.substring(text_length - length) + val found = text2.indexOf(pattern) + if (found == -1) { + return best + } + length += found + if (found == 0 || text1.substring(text_length - length) == text2.substring(0, length)) { + best = length + length++ + } + } + } + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + protected fun diff_halfMatch(text1: String, text2: String): Array? { + if (Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null + } + val longtext = if (text1.length > text2.length) text1 else text2 + val shorttext = if (text1.length > text2.length) text2 else text1 + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + val hm1 = diff_halfMatchI( + longtext, shorttext, + (longtext.length + 3) / 4 + ) + // Check again based on the third quarter. + val hm2 = diff_halfMatchI( + longtext, shorttext, + (longtext.length + 1) / 2 + ) + val hm: Array + hm = if (hm1 == null && hm2 == null) { + return null + } else if (hm2 == null) { + hm1!! + } else if (hm1 == null) { + hm2 + } else { + // Both matched. Select the longest. + if (hm1[4].length > hm2[4].length) hm1 else hm2 + } + + // A half-match was found, sort out the return data. + return if (text1.length > text2.length) { + hm + //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + arrayOf(hm[2], hm[3], hm[0], hm[1], hm[4]) + } + } + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length substring within longtext. + * @return Five element String array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private fun diff_halfMatchI(longtext: String, shorttext: String, i: Int): Array? { + // Start with a 1/4 length substring at position i as a seed. + val seed = longtext.substring(i, i + longtext.length / 4) + var j = -1 + var best_common = "" + var best_longtext_a = "" + var best_longtext_b = "" + var best_shorttext_a = "" + var best_shorttext_b = "" + val run = { j = shorttext.indexOf(seed, j + 1); j } + while (run() != -1) { + val prefixLength = diff_commonPrefix( + longtext.substring(i), + shorttext.substring(j) + ) + val suffixLength = diff_commonSuffix( + longtext.substring(0, i), + shorttext.substring(0, j) + ) + if (best_common.length < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength) + best_longtext_a = longtext.substring(0, i - suffixLength) + best_longtext_b = longtext.substring(i + prefixLength) + best_shorttext_a = shorttext.substring(0, j - suffixLength) + best_shorttext_b = shorttext.substring(j + prefixLength) + } + } + return if (best_common.length * 2 >= longtext.length) { + arrayOf(best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common) + } else { + null + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + fun diff_cleanupSemantic(diffs: LinkedList) { + if (diffs.isEmpty()) { + return + } + var changes = false + val equalities = ArrayDeque() // Double-ended queue of qualities. + var lastEquality: String? = null // Always equal to equalities.peek().text + var pointer: MutableListIterator = diffs.listIterator() + // Number of characters that changed prior to the equality. + var length_insertions1 = 0 + var length_deletions1 = 0 + // Number of characters that changed after the equality. + var length_insertions2 = 0 + var length_deletions2 = 0 + var thisDiff: Diff? = pointer.next() + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + equalities.push(thisDiff) + length_insertions1 = length_insertions2 + length_deletions1 = length_deletions2 + length_insertions2 = 0 + length_deletions2 = 0 + lastEquality = thisDiff.text + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.INSERT) { + length_insertions2 += thisDiff.text!!.length + } else { + length_deletions2 += thisDiff.text!!.length + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastEquality != null && lastEquality.length <= Math.max(length_insertions1, length_deletions1) + && lastEquality.length <= Math.max(length_insertions2, length_deletions2) + ) { + //System.out.println("Splitting: '" + lastEquality + "'"); + // Walk back to offending equality. + while (thisDiff !== equalities.peek()) { + thisDiff = pointer.previous() + } + pointer.next() + + // Replace equality with a delete. + pointer.set(Diff(Operation.DELETE, lastEquality)) + // Insert a corresponding an insert. + pointer.add(Diff(Operation.INSERT, lastEquality)) + + equalities.pop() // Throw away the equality we just deleted. + if (!equalities.isEmpty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop() + } + if (equalities.isEmpty()) { + // There are no previous equalities, walk back to the start. + while (pointer.hasPrevious()) { + pointer.previous() + } + } else { + // There is a safe equality we can fall back to. + thisDiff = equalities.peek() + while (thisDiff !== pointer.previous()) { + // Intentionally empty loop. + } + } + + length_insertions1 = 0 // Reset the counters. + length_insertions2 = 0 + length_deletions1 = 0 + length_deletions2 = 0 + lastEquality = null + changes = true + } + } + thisDiff = if (pointer.hasNext()) pointer.next() else null + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs) + } + diff_cleanupSemanticLossless(diffs) + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = diffs.listIterator() + var prevDiff: Diff? = null + thisDiff = null + if (pointer.hasNext()) { + prevDiff = pointer.next() + if (pointer.hasNext()) { + thisDiff = pointer.next() + } + } + while (thisDiff != null) { + if (prevDiff!!.operation == Operation.DELETE && thisDiff.operation == Operation.INSERT) { + val deletion = prevDiff.text + val insertion = thisDiff.text + val overlap_length1 = this.diff_commonOverlap(deletion!!, insertion!!) + val overlap_length2 = this.diff_commonOverlap(insertion, deletion) + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion!!.length / 2.0 || overlap_length1 >= insertion.length / 2.0) { + // Overlap found. Insert an equality and trim the surrounding edits. + pointer.previous() + pointer.add( + Diff( + Operation.EQUAL, + insertion!!.substring(0, overlap_length1) + ) + ) + prevDiff.text = deletion.substring(0, deletion.length - overlap_length1) + thisDiff.text = insertion.substring(overlap_length1) + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } else { + if (overlap_length2 >= deletion!!.length / 2.0 || overlap_length2 >= insertion.length / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + pointer.previous() + pointer.add( + Diff( + Operation.EQUAL, + deletion.substring(0, overlap_length2) + ) + ) + prevDiff.operation = Operation.INSERT + prevDiff.text = insertion!!.substring(0, insertion.length - overlap_length2) + thisDiff.operation = Operation.DELETE + thisDiff.text = deletion.substring(overlap_length2) + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } + thisDiff = if (pointer.hasNext()) pointer.next() else null + } + prevDiff = thisDiff + thisDiff = if (pointer.hasNext()) pointer.next() else null + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs LinkedList of Diff objects. + */ + fun diff_cleanupSemanticLossless(diffs: LinkedList) { + var equality1: String? + var edit: String? + var equality2: String? + var commonString: String + var commonOffset: Int + var score: Int + var bestScore: Int + var bestEquality1: String + var bestEdit: String + var bestEquality2: String + // Create a new iterator at the start. + val pointer = diffs.listIterator() + var prevDiff: Diff? = if (pointer.hasNext()) pointer.next() else null + var thisDiff: Diff? = if (pointer.hasNext()) pointer.next() else null + var nextDiff: Diff? = if (pointer.hasNext()) pointer.next() else null + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff!!.operation == Operation.EQUAL && nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + equality1 = prevDiff.text + edit = thisDiff!!.text + equality2 = nextDiff.text + + // First, shift the edit as far left as possible. + commonOffset = diff_commonSuffix(equality1, edit) + if (commonOffset != 0) { + commonString = edit!!.substring(edit.length - commonOffset) + equality1 = equality1!!.substring(0, equality1.length - commonOffset) + edit = commonString + edit.substring(0, edit.length - commonOffset) + equality2 = commonString + equality2!! + } + + // Second, step character by character right, looking for the best fit. + bestEquality1 = equality1!! + bestEdit = edit!! + bestEquality2 = equality2!! + bestScore = diff_cleanupSemanticScore(equality1!!, edit) + diff_cleanupSemanticScore(edit!!, equality2) + while (edit!!.length != 0 && equality2!!.length != 0 + && edit[0] == equality2[0] + ) { + equality1 += edit[0] + edit = edit.substring(1) + equality2[0] + equality2 = equality2.substring(1) + score = diff_cleanupSemanticScore(equality1, edit) + diff_cleanupSemanticScore(edit, equality2) + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + } + } + + if (prevDiff.text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1.isNotEmpty()) { + prevDiff.text = bestEquality1 + } else { + pointer.previous() // Walk past nextDiff. + pointer.previous() // Walk past thisDiff. + pointer.previous() // Walk past prevDiff. + pointer.remove() // Delete prevDiff. + pointer.next() // Walk past thisDiff. + pointer.next() // Walk past nextDiff. + } + thisDiff.text = bestEdit + if (bestEquality2.isNotEmpty()) { + nextDiff.text = bestEquality2 + } else { + pointer.remove() // Delete nextDiff. + nextDiff = thisDiff + thisDiff = prevDiff + } + } + } + prevDiff = thisDiff + thisDiff = nextDiff + nextDiff = if (pointer.hasNext()) pointer.next() else null + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private fun diff_cleanupSemanticScore(one: String, two: String): Int { + if (one.isEmpty() || two.isEmpty()) { + // Edges are the best. + return 6 + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + val char1 = one[one.length - 1] + val char2 = two[0] + val nonAlphaNumeric1 = !Character.isLetterOrDigit(char1) + val nonAlphaNumeric2 = !Character.isLetterOrDigit(char2) + val whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1) + val whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2) + val lineBreak1 = whitespace1 && Character.getType(char1) == Character.CONTROL.toInt() + val lineBreak2 = whitespace2 && Character.getType(char2) == Character.CONTROL.toInt() + val blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find() + val blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find() + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5 + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4 + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3 + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2 + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1 + } + return 0 + } + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + fun diff_cleanupEfficiency(diffs: LinkedList) { + if (diffs.isEmpty()) { + return + } + var changes = false + val equalities = ArrayDeque() // Double-ended queue of equalities. + var lastEquality: String? = null // Always equal to equalities.peek().text + val pointer = diffs.listIterator() + // Is there an insertion operation before the last equality. + var pre_ins = false + // Is there a deletion operation before the last equality. + var pre_del = false + // Is there an insertion operation after the last equality. + var post_ins = false + // Is there a deletion operation after the last equality. + var post_del = false + var thisDiff: Diff? = pointer.next() + var safeDiff: Diff = thisDiff!! // The last Diff that is known to be unsplittable. + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + if (thisDiff.text!!.length < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + equalities.push(thisDiff) + pre_ins = post_ins + pre_del = post_del + lastEquality = thisDiff.text + } else { + // Not a candidate, and can never become one. + equalities.clear() + lastEquality = null + safeDiff = thisDiff + } + post_del = false + post_ins = post_del + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.DELETE) { + post_del = true + } else { + post_ins = true + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastEquality != null && (pre_ins && pre_del && post_ins && post_del || lastEquality.length < Diff_EditCost / 2 && ((if (pre_ins) 1 else 0) + (if (pre_del) 1 else 0) + + (if (post_ins) 1 else 0) + if (post_del) 1 else 0) == 3) + ) { + //System.out.println("Splitting: '" + lastEquality + "'"); + // Walk back to offending equality. + while (thisDiff !== equalities.peek()) { + thisDiff = pointer.previous() + } + pointer.next() + + // Replace equality with a delete. + pointer.set(Diff(Operation.DELETE, lastEquality)) + // Insert a corresponding an insert. + pointer.add(Diff(Operation.INSERT, lastEquality)) + + equalities.pop() // Throw away the equality we just deleted. + lastEquality = null + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_del = true + post_ins = post_del + equalities.clear() + safeDiff = thisDiff!! + } else { + if (!equalities.isEmpty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop() + } + thisDiff = if (equalities.isEmpty()) { + // There are no previous questionable equalities, + // walk back to the last known safe diff. + safeDiff + } else { + // There is an equality we can fall back to. + equalities.peek() + } + while (thisDiff !== pointer.previous()) { + // Intentionally empty loop. + } + post_del = false + post_ins = post_del + } + + changes = true + } + } + thisDiff = if (pointer.hasNext()) pointer.next() else null + } + + if (changes) { + diff_cleanupMerge(diffs) + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs LinkedList of Diff objects. + */ + fun diff_cleanupMerge(diffs: LinkedList) { + diffs.add(Diff(Operation.EQUAL, "")) // Add a dummy entry at the end. + var pointer: MutableListIterator = diffs.listIterator() + var count_delete = 0 + var count_insert = 0 + var text_delete = "" + var text_insert = "" + var thisDiff: Diff? = pointer.next() + var prevEqual: Diff? = null + var commonlength: Int + while (thisDiff != null) { + when (thisDiff.operation) { + diff_match_patch.Operation.INSERT -> { + count_insert++ + text_insert += thisDiff.text + prevEqual = null + } + diff_match_patch.Operation.DELETE -> { + count_delete++ + text_delete += thisDiff.text + prevEqual = null + } + diff_match_patch.Operation.EQUAL -> { + if (count_delete + count_insert > 1) { + val both_types = count_delete != 0 && count_insert != 0 + // Delete the offending records. + pointer.previous() // Reverse direction. + while (count_delete-- > 0) { + pointer.previous() + pointer.remove() + } + while (count_insert-- > 0) { + pointer.previous() + pointer.remove() + } + if (both_types) { + // Factor out any common prefixies. + commonlength = diff_commonPrefix(text_insert, text_delete) + if (commonlength != 0) { + if (pointer.hasPrevious()) { + thisDiff = pointer.previous() + assert(thisDiff.operation == Operation.EQUAL) { "Previous diff should have been an equality." } + thisDiff.text += text_insert.substring(0, commonlength) + pointer.next() + } else { + pointer.add( + Diff( + Operation.EQUAL, + text_insert.substring(0, commonlength) + ) + ) + } + text_insert = text_insert.substring(commonlength) + text_delete = text_delete.substring(commonlength) + } + // Factor out any common suffixies. + commonlength = diff_commonSuffix(text_insert, text_delete) + if (commonlength != 0) { + thisDiff = pointer.next() + thisDiff.text = text_insert.substring(text_insert.length - commonlength) + thisDiff.text!! + text_insert = text_insert.substring(0, text_insert.length - commonlength) + text_delete = text_delete.substring(0, text_delete.length - commonlength) + pointer.previous() + } + } + // Insert the merged records. + if (text_delete.length != 0) { + pointer.add(Diff(Operation.DELETE, text_delete)) + } + if (text_insert.length != 0) { + pointer.add(Diff(Operation.INSERT, text_insert)) + } + // Step forward to the equality. + thisDiff = if (pointer.hasNext()) pointer.next() else null + } else if (prevEqual != null) { + // Merge this equality with the previous one. + prevEqual.text += thisDiff.text + pointer.remove() + thisDiff = pointer.previous() + pointer.next() // Forward direction + } + count_insert = 0 + count_delete = 0 + text_delete = "" + text_insert = "" + prevEqual = thisDiff + } + } + thisDiff = if (pointer.hasNext()) pointer.next() else null + } + if (diffs.last.text!!.length == 0) { + diffs.removeLast() // Remove the dummy entry at the end. + } + + /* + * Second pass: look for single edits surrounded on both sides by equalities + * which can be shifted sideways to eliminate an equality. + * e.g: ABAC -> ABAC + */ + var changes = false + // Create a new iterator at the start. + // (As opposed to walking the current one back.) + pointer = diffs.listIterator() + var prevDiff: Diff? = if (pointer.hasNext()) pointer.next() else null + thisDiff = if (pointer.hasNext()) pointer.next() else null + var nextDiff: Diff? = if (pointer.hasNext()) pointer.next() else null + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff!!.operation == Operation.EQUAL && nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (thisDiff!!.text!!.endsWith(prevDiff.text!!)) { + // Shift the edit over the previous equality. + thisDiff.text = prevDiff.text!! + + thisDiff.text!!.substring(0, thisDiff.text!!.length - prevDiff.text!!.length) + nextDiff.text = prevDiff.text!! + nextDiff.text!! + pointer.previous() // Walk past nextDiff. + pointer.previous() // Walk past thisDiff. + pointer.previous() // Walk past prevDiff. + pointer.remove() // Delete prevDiff. + pointer.next() // Walk past thisDiff. + thisDiff = pointer.next() // Walk past nextDiff. + nextDiff = if (pointer.hasNext()) pointer.next() else null + changes = true + } else if (thisDiff.text!!.startsWith(nextDiff.text!!)) { + // Shift the edit over the next equality. + prevDiff.text += nextDiff.text + thisDiff.text = thisDiff.text!!.substring(nextDiff.text!!.length) + nextDiff.text!! + pointer.remove() // Delete nextDiff. + nextDiff = if (pointer.hasNext()) pointer.next() else null + changes = true + } + } + prevDiff = thisDiff + thisDiff = nextDiff + nextDiff = if (pointer.hasNext()) pointer.next() else null + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs) + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs List of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + fun diff_xIndex(diffs: List, loc: Int): Int { + var chars1 = 0 + var chars2 = 0 + var last_chars1 = 0 + var last_chars2 = 0 + var lastDiff: Diff? = null + for (aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text!!.length + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text!!.length + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff + break + } + last_chars1 = chars1 + last_chars2 = chars2 + } + return if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + last_chars2 + } else last_chars2 + (loc - last_chars1) + // Add the remaining character length. + } + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs List of Diff objects. + * @return HTML representation. + */ + fun diff_prettyHtml(diffs: List): String { + val html = StringBuilder() + for (aDiff in diffs) { + val text = aDiff.text!!.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
") + when (aDiff.operation) { + diff_match_patch.Operation.INSERT -> html.append("").append(text) + .append("") + diff_match_patch.Operation.DELETE -> html.append("").append(text) + .append("") + diff_match_patch.Operation.EQUAL -> html.append("").append(text).append("") + } + } + return html.toString() + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs List of Diff objects. + * @return Source text. + */ + fun diff_text1(diffs: List): String { + val text = StringBuilder() + for (aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + text.append(aDiff.text) + } + } + return text.toString() + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs List of Diff objects. + * @return Destination text. + */ + fun diff_text2(diffs: List): String { + val text = StringBuilder() + for (aDiff in diffs) { + if (aDiff.operation != Operation.DELETE) { + text.append(aDiff.text) + } + } + return text.toString() + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs List of Diff objects. + * @return Number of changes. + */ + fun diff_levenshtein(diffs: List): Int { + var levenshtein = 0 + var insertions = 0 + var deletions = 0 + for (aDiff in diffs) { + when (aDiff.operation) { + diff_match_patch.Operation.INSERT -> insertions += aDiff.text!!.length + diff_match_patch.Operation.DELETE -> deletions += aDiff.text!!.length + diff_match_patch.Operation.EQUAL -> { + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions) + insertions = 0 + deletions = 0 + } + } + } + levenshtein += Math.max(insertions, deletions) + return levenshtein + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param diffs List of Diff objects. + * @return Delta text. + */ + fun diff_toDelta(diffs: List): String { + val text = StringBuilder() + for (aDiff in diffs) { + when (aDiff.operation) { + diff_match_patch.Operation.INSERT -> try { + text.append("+").append( + URLEncoder.encode(aDiff.text, "UTF-8") + .replace('+', ' ') + ).append("\t") + } catch (e: UnsupportedEncodingException) { + // Not likely on modern system. + throw Error("This system does not support UTF-8.", e) + } + + diff_match_patch.Operation.DELETE -> text.append("-").append(aDiff.text!!.length).append("\t") + diff_match_patch.Operation.EQUAL -> text.append("=").append(aDiff.text!!.length).append("\t") + } + } + var delta = text.toString() + if (delta.length != 0) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length - 1) + delta = unescapeForEncodeUriCompatability(delta) + } + return delta + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws IllegalArgumentException If invalid input. + */ + @Throws(IllegalArgumentException::class) + fun diff_fromDelta(text1: String, delta: String): LinkedList { + val diffs = LinkedList() + var pointer = 0 // Cursor in text1 + val tokens = delta.split("\t".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (token in tokens) { + if (token.length == 0) { + // Blank tokens are ok (from a trailing \t). + continue + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + var param = token.substring(1) + when (token[0]) { + '+' -> { + // decode would change all "+" to " " + param = param.replace("+", "%2B") + try { + param = URLDecoder.decode(param, "UTF-8") + } catch (e: UnsupportedEncodingException) { + // Not likely on modern system. + throw Error("This system does not support UTF-8.", e) + } catch (e: IllegalArgumentException) { + // Malformed URI sequence. + throw IllegalArgumentException( + "Illegal escape in diff_fromDelta: $param", e + ) + } + + diffs.add(Diff(Operation.INSERT, param)) + } + '-', + // Fall through. + '=' -> { + val n: Int + try { + n = Integer.parseInt(param) + } catch (e: NumberFormatException) { + throw IllegalArgumentException( + "Invalid number in diff_fromDelta: $param", e + ) + } + + if (n < 0) { + throw IllegalArgumentException( + "Negative number in diff_fromDelta: $param" + ) + } + val text: String + try { + val run = { pointer += n; pointer } + text = text1.substring(pointer, run()) + } catch (e: StringIndexOutOfBoundsException) { + throw IllegalArgumentException( + "Delta length (" + pointer + + ") larger than source text length (" + text1.length + + ").", e + ) + } + + if (token[0] == '=') { + diffs.add(Diff(Operation.EQUAL, text)) + } else { + diffs.add(Diff(Operation.DELETE, text)) + } + } + else -> + // Anything else is an error. + throw IllegalArgumentException( + "Invalid diff operation in diff_fromDelta: " + token[0] + ) + } + } + if (pointer != text1.length) { + throw IllegalArgumentException( + "Delta length (" + pointer + + ") smaller than source text length (" + text1.length + ")." + ) + } + return diffs + } + + // MATCH FUNCTIONS + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + fun match_main(text: String?, pattern: String?, loc: Int): Int { + var loc = loc + // Check for null inputs. + if (text == null || pattern == null) { + throw IllegalArgumentException("Null inputs. (match_main)") + } + + loc = Math.max(0, Math.min(loc, text.length)) + return if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + 0 + } else if (text.isEmpty()) { + // Nothing to match. + -1 + } else if (loc + pattern.length <= text.length && text.substring(loc, loc + pattern.length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + loc + } else { + // Do a fuzzy compare. + match_bitap(text, pattern, loc) + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected fun match_bitap(text: String, pattern: String, loc: Int): Int { + assert(Match_MaxBits.toInt() == 0 || pattern.length <= Match_MaxBits) { "Pattern too long for this application." } + + // Initialise the alphabet. + val s = match_alphabet(pattern) + + // Highest score beyond which we give up. + var score_threshold = Match_Threshold.toDouble() + // Is there a nearby exact match? (speedup) + var best_loc = text.indexOf(pattern, loc) + if (best_loc != -1) { + score_threshold = Math.min( + match_bitapScore(0, best_loc, loc, pattern), + score_threshold + ) + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length) + if (best_loc != -1) { + score_threshold = Math.min( + match_bitapScore(0, best_loc, loc, pattern), + score_threshold + ) + } + } + + // Initialise the bit arrays. + val matchmask = 1 shl pattern.length - 1 + best_loc = -1 + + var bin_min: Int + var bin_mid: Int + var bin_max = pattern.length + text.length + // Empty initialization added to appease Java compiler. + var last_rd = IntArray(0) + for (d in 0 until pattern.length) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0 + bin_mid = bin_max + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) <= score_threshold) { + bin_min = bin_mid + } else { + bin_max = bin_mid + } + bin_mid = (bin_max - bin_min) / 2 + bin_min + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid + var start = Math.max(1, loc - bin_mid + 1) + val finish = Math.min(loc + bin_mid, text.length) + pattern.length + + val rd = IntArray(finish + 2) + rd[finish + 1] = (1 shl d) - 1 + for (j in finish downTo start) { + val charMatch: Int + if (text.length <= j - 1 || !s.containsKey(text[j - 1])) { + // Out of range. + charMatch = 0 + } else { + charMatch = s[text[j - 1]]!! + } + if (d == 0) { + // First pass: exact match. + rd[j] = rd[j + 1] shl 1 or 1 and charMatch + } else { + // Subsequent passes: fuzzy match. + rd[j] = (rd[j + 1] shl 1 or 1 and charMatch + or (last_rd[j + 1] or last_rd[j] shl 1 or 1) or last_rd[j + 1]) + } + if (rd[j] and matchmask != 0) { + val score = match_bitapScore(d, j - 1, loc, pattern) + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score + best_loc = j - 1 + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc) + } else { + // Already passed loc, downhill from here on in. + break + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break + } + last_rd = rd + } + return best_loc + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private fun match_bitapScore(e: Int, x: Int, loc: Int, pattern: String): Double { + val accuracy = e.toFloat() / pattern.length + val proximity = Math.abs(loc - x) + return if (Match_Distance == 0) { + // Dodge divide by zero error. + if (proximity == 0) accuracy.toDouble() else 1.0 + } else (accuracy + proximity / Match_Distance.toFloat()).toDouble() + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected fun match_alphabet(pattern: String): Map { + val s = HashMap() + val char_pattern = pattern.toCharArray() + for (c in char_pattern) { + s[c] = 0 + } + for ((i, c) in char_pattern.withIndex()) { + s[c] = s[c]!! or (1 shl pattern.length - i - 1) + } + return s + } + + // PATCH FUNCTIONS + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected fun patch_addContext(patch: Patch, text: String) { + if (text.length == 0) { + return + } + var pattern = text.substring(patch.start2, patch.start2 + patch.length1) + var padding = 0 + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) && pattern.length < Match_MaxBits.toInt() - Patch_Margin.toInt() - Patch_Margin.toInt()) { + padding += Patch_Margin.toInt() + pattern = text.substring( + Math.max(0, patch.start2 - padding), + Math.min(text.length, patch.start2 + patch.length1 + padding) + ) + } + // Add one chunk for good luck. + padding += Patch_Margin.toInt() + + // Add the prefix. + val prefix = text.substring( + Math.max(0, patch.start2 - padding), + patch.start2 + ) + if (prefix.length != 0) { + patch.diffs.addFirst(Diff(Operation.EQUAL, prefix)) + } + // Add the suffix. + val suffix = text.substring( + patch.start2 + patch.length1, + Math.min(text.length, patch.start2 + patch.length1 + padding) + ) + if (suffix.length != 0) { + patch.diffs.addLast(Diff(Operation.EQUAL, suffix)) + } + + // Roll back the start points. + patch.start1 -= prefix.length + patch.start2 -= prefix.length + // Extend the lengths. + patch.length1 += prefix.length + suffix.length + patch.length2 += prefix.length + suffix.length + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + fun patch_make(text1: String?, text2: String?): LinkedList { + if (text1 == null || text2 == null) { + throw IllegalArgumentException("Null inputs. (patch_make)") + } + // No diffs provided, compute our own. + val diffs = diff_main(text1, text2, true) + if (diffs.size > 2) { + diff_cleanupSemantic(diffs) + diff_cleanupEfficiency(diffs) + } + return patch_make(text1, diffs) + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + fun patch_make(diffs: LinkedList?): LinkedList { + if (diffs == null) { + throw IllegalArgumentException("Null inputs. (patch_make)") + } + // No origin string provided, compute our own. + val text1 = diff_text1(diffs) + return patch_make(text1, diffs) + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + @Deprecated("Prefer patch_make(String text1, LinkedList diffs).") + fun patch_make( + text1: String, text2: String, + diffs: LinkedList + ): LinkedList { + return patch_make(text1, diffs) + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + fun patch_make(text1: String?, diffs: LinkedList?): LinkedList { + if (text1 == null || diffs == null) { + throw IllegalArgumentException("Null inputs. (patch_make)") + } + + val patches = LinkedList() + if (diffs.isEmpty()) { + return patches // Get rid of the null case. + } + var patch = Patch() + var char_count1 = 0 // Number of characters into the text1 string. + var char_count2 = 0 // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + var prepatch_text: String = text1 + var postpatch_text: String = text1 + for (aDiff in diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1 + patch.start2 = char_count2 + } + + when (aDiff.operation) { + diff_match_patch.Operation.INSERT -> { + patch.diffs.add(aDiff) + patch.length2 += aDiff.text!!.length + postpatch_text = (postpatch_text.substring(0, char_count2) + + aDiff.text + postpatch_text.substring(char_count2)) + } + diff_match_patch.Operation.DELETE -> { + patch.length1 += aDiff.text!!.length + patch.diffs.add(aDiff) + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + aDiff.text!!.length) + } + diff_match_patch.Operation.EQUAL -> { + if (aDiff.text!!.length <= 2 * Patch_Margin + && !patch.diffs.isEmpty() && aDiff !== diffs.last + ) { + // Small equality inside a patch. + patch.diffs.add(aDiff) + patch.length1 += aDiff.text!!.length + patch.length2 += aDiff.text!!.length + } + + if (aDiff.text!!.length >= 2 * Patch_Margin && !patch.diffs.isEmpty()) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text) + patches.add(patch) + patch = Patch() + // Unlike Unidiff, our patch lists have a rolling context. + // https://github.com/google/diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text + char_count1 = char_count2 + } + } + } + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text!!.length + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text!!.length + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text) + patches.add(patch) + } + + return patches + } + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + fun patch_deepCopy(patches: LinkedList): LinkedList { + val patchesCopy = LinkedList() + for (aPatch in patches) { + val patchCopy = Patch() + for (aDiff in aPatch.diffs) { + val diffCopy = Diff(aDiff.operation, aDiff.text) + patchCopy.diffs.add(diffCopy) + } + patchCopy.start1 = aPatch.start1 + patchCopy.start2 = aPatch.start2 + patchCopy.length1 = aPatch.length1 + patchCopy.length2 = aPatch.length2 + patchesCopy.add(patchCopy) + } + return patchesCopy + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * boolean values. + */ + fun patch_apply(patches: LinkedList, text: String): Array { + var patches = patches + var text = text + if (patches.isEmpty()) { + return arrayOf(text, BooleanArray(0)) + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches) + + val nullPadding = patch_addPadding(patches) + text = nullPadding + text + nullPadding + patch_splitMax(patches) + + var x = 0 + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0 + val results = BooleanArray(patches.size) + for (aPatch in patches) { + val expected_loc = aPatch.start2 + delta + val text1 = diff_text1(aPatch.diffs) + var start_loc: Int + var end_loc = -1 + if (text1.length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main( + text, + text1.substring(0, this.Match_MaxBits.toInt()), expected_loc + ) + if (start_loc != -1) { + end_loc = match_main( + text, + text1.substring(text1.length - this.Match_MaxBits), + expected_loc + text1.length - this.Match_MaxBits + ) + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1 + } + } + } else { + start_loc = match_main(text, text1, expected_loc) + } + if (start_loc == -1) { + // No match found. :( + results[x] = false + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1 + } else { + // Found a match. :) + results[x] = true + delta = start_loc - expected_loc + val text2: String + if (end_loc == -1) { + text2 = text.substring( + start_loc, + Math.min(start_loc + text1.length, text.length) + ) + } else { + text2 = text.substring( + start_loc, + Math.min(end_loc + this.Match_MaxBits, text.length) + ) + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = (text.substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.substring(start_loc + text1.length)) + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + val diffs = diff_main(text1, text2, false) + if (text1.length > this.Match_MaxBits && diff_levenshtein(diffs) / text1.length.toFloat() > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false + } else { + diff_cleanupSemanticLossless(diffs) + var index1 = 0 + for (aDiff in aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + val index2 = diff_xIndex(diffs, index1) + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = (text.substring(0, start_loc + index2) + aDiff.text + + text.substring(start_loc + index2)) + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.substring(0, start_loc + index2) + text.substring( + start_loc + diff_xIndex( + diffs, + index1 + aDiff.text!!.length + ) + ) + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text!!.length + } + } + } + } + } + x++ + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length) + return arrayOf(text, results) + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + fun patch_addPadding(patches: LinkedList): String { + val paddingLength = this.Patch_Margin + var nullPadding = "" + for (x in 1..paddingLength) { + nullPadding += x.toChar().toString() + } + + // Bump all the patches forward. + for (aPatch in patches) { + aPatch.start1 += paddingLength.toInt() + aPatch.start2 += paddingLength.toInt() + } + + // Add some padding on start of first diff. + var patch = patches.first + var diffs = patch.diffs + if (diffs.isEmpty() || diffs.first.operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addFirst(Diff(Operation.EQUAL, nullPadding)) + patch.start1 -= paddingLength.toInt() // Should be 0. + patch.start2 -= paddingLength.toInt() // Should be 0. + patch.length1 += paddingLength.toInt() + patch.length2 += paddingLength.toInt() + } else if (paddingLength > diffs.first.text!!.length) { + // Grow first equality. + val firstDiff = diffs.first + val extraLength = paddingLength - firstDiff.text!!.length + firstDiff.text = nullPadding.substring(firstDiff.text!!.length) + firstDiff.text!! + patch.start1 -= extraLength + patch.start2 -= extraLength + patch.length1 += extraLength + patch.length2 += extraLength + } + + // Add some padding on end of last diff. + patch = patches.last + diffs = patch.diffs + if (diffs.isEmpty() || diffs.last.operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addLast(Diff(Operation.EQUAL, nullPadding)) + patch.length1 += paddingLength.toInt() + patch.length2 += paddingLength.toInt() + } else if (paddingLength > diffs.last.text!!.length) { + // Grow last equality. + val lastDiff = diffs.last + val extraLength = paddingLength - lastDiff.text!!.length + lastDiff.text += nullPadding.substring(0, extraLength) + patch.length1 += extraLength + patch.length2 += extraLength + } + + return nullPadding + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches LinkedList of Patch objects. + */ + fun patch_splitMax(patches: LinkedList) { + val patch_size = Match_MaxBits + var precontext: String + var postcontext: String + var patch: Patch + var start1: Int + var start2: Int + var empty: Boolean + var diff_type: Operation? + var diff_text: String? + val pointer = patches.listIterator() + var bigpatch: Patch? = if (pointer.hasNext()) pointer.next() else null + while (bigpatch != null) { + if (bigpatch.length1 <= Match_MaxBits) { + bigpatch = if (pointer.hasNext()) pointer.next() else null + continue + } + // Remove the big old patch. + pointer.remove() + start1 = bigpatch.start1 + start2 = bigpatch.start2 + precontext = "" + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = Patch() + empty = true + patch.start1 = start1 - precontext.length + patch.start2 = start2 - precontext.length + if (precontext.length != 0) { + patch.length2 = precontext.length + patch.length1 = patch.length2 + patch.diffs.add(Diff(Operation.EQUAL, precontext)) + } + while (!bigpatch.diffs.isEmpty() && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.first.operation + diff_text = bigpatch.diffs.first.text + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text!!.length + start2 += diff_text.length + patch.diffs.addLast(bigpatch.diffs.removeFirst()) + empty = false + } else if (diff_type == Operation.DELETE && patch.diffs.size == 1 + && patch.diffs.first.operation == Operation.EQUAL + && diff_text!!.length > 2 * patch_size + ) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length + start1 += diff_text.length + empty = false + patch.diffs.add(Diff(diff_type, diff_text)) + bigpatch.diffs.removeFirst() + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text!!.substring( + 0, Math.min( + diff_text.length, + patch_size.toInt() - patch.length1 - Patch_Margin.toInt() + ) + ) + patch.length1 += diff_text.length + start1 += diff_text.length + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.length + start2 += diff_text.length + } else { + empty = false + } + patch.diffs.add(Diff(diff_type, diff_text)) + if (diff_text == bigpatch.diffs.first.text) { + bigpatch.diffs.removeFirst() + } else { + bigpatch.diffs.first.text = bigpatch.diffs.first.text!! + .substring(diff_text.length) + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs) + precontext = precontext.substring(Math.max(0, precontext.length - Patch_Margin)) + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).length > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin.toInt()) + } else { + postcontext = diff_text1(bigpatch.diffs) + } + if (postcontext.length != 0) { + patch.length1 += postcontext.length + patch.length2 += postcontext.length + if (!patch.diffs.isEmpty() && patch.diffs.last.operation == Operation.EQUAL) { + patch.diffs.last.text += postcontext + } else { + patch.diffs.add(Diff(Operation.EQUAL, postcontext)) + } + } + if (!empty) { + pointer.add(patch) + } + } + bigpatch = if (pointer.hasNext()) pointer.next() else null + } + } + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + fun patch_toText(patches: List): String { + val text = StringBuilder() + for (aPatch in patches) { + text.append(aPatch) + } + return text.toString() + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws IllegalArgumentException If invalid input. + */ + @Throws(IllegalArgumentException::class) + fun patch_fromText(textline: String): List { + val patches = LinkedList() + if (textline.isEmpty()) { + return patches + } + val textList = Arrays.asList(*textline.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + val text = LinkedList(textList) + var patch: Patch + val patchHeader = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$") + var m: Matcher + var sign: Char + var line: String + while (!text.isEmpty()) { + m = patchHeader.matcher(text.first) + if (!m.matches()) { + throw IllegalArgumentException( + "Invalid patch string: " + text.first + ) + } + patch = Patch() + patches.add(patch) + patch.start1 = Integer.parseInt(m.group(1)) + if (m.group(2).length == 0) { + patch.start1-- + patch.length1 = 1 + } else if (m.group(2) == "0") { + patch.length1 = 0 + } else { + patch.start1-- + patch.length1 = Integer.parseInt(m.group(2)) + } + + patch.start2 = Integer.parseInt(m.group(3)) + when { + m.group(4).isEmpty() -> { + patch.start2-- + patch.length2 = 1 + } + m.group(4) == "0" -> patch.length2 = 0 + else -> { + patch.start2-- + patch.length2 = Integer.parseInt(m.group(4)) + } + } + text.removeFirst() + + while (!text.isEmpty()) { + try { + sign = text.first[0] + } catch (e: IndexOutOfBoundsException) { + // Blank line? Whatever. + text.removeFirst() + continue + } + + line = text.first.substring(1) + line = line.replace("+", "%2B") // decode would change all "+" to " " + try { + line = URLDecoder.decode(line, "UTF-8") + } catch (e: UnsupportedEncodingException) { + // Not likely on modern system. + throw Error("This system does not support UTF-8.", e) + } catch (e: IllegalArgumentException) { + // Malformed URI sequence. + throw IllegalArgumentException( + "Illegal escape in patch_fromText: $line", e + ) + } + + if (sign == '-') { + // Deletion. + patch.diffs.add(Diff(Operation.DELETE, line)) + } else if (sign == '+') { + // Insertion. + patch.diffs.add(Diff(Operation.INSERT, line)) + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(Diff(Operation.EQUAL, line)) + } else if (sign == '@') { + // Start of next patch. + break + } else { + // WTF? + throw IllegalArgumentException( + "Invalid patch mode '$sign' in: $line" + ) + } + text.removeFirst() + } + } + return patches + } + + /** + * Class representing one diff operation. + */ + class Diff + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + ( + /** + * One of: INSERT, DELETE or EQUAL. + */ + var operation: Operation?, + /** + * The text associated with this diff operation. + */ + var text: String? + )// Construct a diff with the specified operation and text. + { + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + override fun toString(): String { + val prettyText = this.text!!.replace('\n', '\u00b6') + return "Diff(" + this.operation + ",\"" + prettyText + "\")" + } + + /** + * Create a numeric hash value for a Diff. + * This function is not used by DMP. + * @return Hash value. + */ + override fun hashCode(): Int { + val prime = 31 + var result = if (operation == null) 0 else operation!!.hashCode() + result += prime * if (text == null) 0 else text!!.hashCode() + return result + } + + /** + * Is this Diff equivalent to another Diff? + * @param obj Another Diff to compare against. + * @return true or false. + */ + override fun equals(obj: Any?): Boolean { + if (this === obj) { + return true + } + if (obj == null) { + return false + } + if (javaClass != obj.javaClass) { + return false + } + val other = obj as Diff? + if (operation != other!!.operation) { + return false + } + if (text == null) { + if (other.text != null) { + return false + } + } else if (text != other.text) { + return false + } + return true + } + } + + /** + * Class representing one patch operation. + */ + class Patch { + var diffs: LinkedList = LinkedList() + var start1: Int = 0 + var start2: Int = 0 + var length1: Int = 0 + var length2: Int = 0 + + /** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indices are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + override fun toString(): String { + val coords1: String + val coords2: String + if (this.length1 == 0) { + coords1 = this.start1.toString() + ",0" + } else if (this.length1 == 1) { + coords1 = Integer.toString(this.start1 + 1) + } else { + coords1 = (this.start1 + 1).toString() + "," + this.length1 + } + if (this.length2 == 0) { + coords2 = this.start2.toString() + ",0" + } else if (this.length2 == 1) { + coords2 = Integer.toString(this.start2 + 1) + } else { + coords2 = (this.start2 + 1).toString() + "," + this.length2 + } + val text = StringBuilder() + text.append("@@ -").append(coords1).append(" +").append(coords2) + .append(" @@\n") + // Escape the body of the patch with %xx notation. + for (aDiff in this.diffs) { + when (aDiff.operation) { + diff_match_patch.Operation.INSERT -> text.append('+') + diff_match_patch.Operation.DELETE -> text.append('-') + diff_match_patch.Operation.EQUAL -> text.append(' ') + } + try { + text.append(URLEncoder.encode(aDiff.text, "UTF-8").replace('+', ' ')) + .append("\n") + } catch (e: UnsupportedEncodingException) { + // Not likely on modern system. + throw Error("This system does not support UTF-8.", e) + } + } + return unescapeForEncodeUriCompatability(text.toString()) + } + } + + companion object { + + /** + * Unescape selected chars for compatability with JavaScript's encodeURI. + * In speed critical applications this could be dropped since the + * receiving application will certainly decode these fine. + * Note that this function is case-sensitive. Thus "%3f" would not be + * unescaped. But this is ok because it is only called with the output of + * URLEncoder.encode which returns uppercase hex. + * + * Example: "%3F" -> "?", "%24" -> "$", etc. + * + * @param str The string to escape. + * @return The escaped string. + */ + private fun unescapeForEncodeUriCompatability(str: String): String { + return str.replace("%21", "!").replace("%7E", "~") + .replace("%27", "'").replace("%28", "(").replace("%29", ")") + .replace("%3B", ";").replace("%2F", "/").replace("%3F", "?") + .replace("%3A", ":").replace("%40", "@").replace("%26", "&") + .replace("%3D", "=").replace("%2B", "+").replace("%24", "$") + .replace("%2C", ",").replace("%23", "#") + } + } +} +/** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ diff --git a/kotlin/tests/name/fraser/neil/plaintext/Speedtest.kt b/kotlin/tests/name/fraser/neil/plaintext/Speedtest.kt new file mode 100644 index 0000000..0363e15 --- /dev/null +++ b/kotlin/tests/name/fraser/neil/plaintext/Speedtest.kt @@ -0,0 +1,58 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +/** + * Diff Speed Test + * + * Compile from diff-match-patch/java with: + * javac -d classes src/name/fraser/neil/plaintext/diff_match_patch.java tests/name/fraser/neil/plaintext/Speedtest.java + * Execute with: + * java -classpath classes name/fraser/neil/plaintext/Speedtest + * + * @author fraser@google.com (Neil Fraser) + */ + +package name.fraser.neil.plaintext + +import java.io.BufferedReader +import java.io.FileReader +import java.io.IOException + +object Speedtest { + + @Throws(IOException::class) + @JvmStatic + fun main(args: Array) { + val text1 = readFile("tests/name/fraser/neil/plaintext/Speedtest1.txt") + val text2 = readFile("tests/name/fraser/neil/plaintext/Speedtest2.txt") + + val dmp = diff_match_patch() + dmp.Diff_Timeout = 0 + + // Execute one reverse diff as a warmup. + dmp.diff_main(text2, text1, false) + + val start_time = System.nanoTime() + dmp.diff_main(text1, text2, false) + val end_time = System.nanoTime() + System.out.printf("Elapsed time: %f\n", (end_time - start_time) / 1000000000.0) + } + + @Throws(IOException::class) + private fun readFile(filename: String): String { + // Read a file from disk and return the text contents. + val sb = StringBuilder() + val input = FileReader(filename) + val bufRead = BufferedReader(input) + try { + var line: String? = bufRead.readLine() + while (line != null) { + sb.append(line).append('\n') + line = bufRead.readLine() + } + } finally { + bufRead.close() + input.close() + } + return sb.toString() + } +} diff --git a/kotlin/tests/name/fraser/neil/plaintext/Speedtest1.txt b/kotlin/tests/name/fraser/neil/plaintext/Speedtest1.txt new file mode 100644 index 0000000..54b438f --- /dev/null +++ b/kotlin/tests/name/fraser/neil/plaintext/Speedtest1.txt @@ -0,0 +1,230 @@ +This is a '''list of newspapers published by [[Journal Register Company]]'''. + +The company owns daily and weekly newspapers, other print media properties and newspaper-affiliated local Websites in the [[U.S.]] states of [[Connecticut]], [[Michigan]], [[New York]], [[Ohio]] and [[Pennsylvania]], organized in six geographic "clusters":[http://www.journalregister.com/newspapers.html Journal Register Company: Our Newspapers], accessed February 10, 2008. + +== Capital-Saratoga == +Three dailies, associated weeklies and [[pennysaver]]s in greater [[Albany, New York]]; also [http://www.capitalcentral.com capitalcentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''The Oneida Daily Dispatch'' {{WS|oneidadispatch.com}} of [[Oneida, New York]] +* ''[[The Record (Troy)|The Record]]'' {{WS|troyrecord.com}} of [[Troy, New York]] +* ''[[The Saratogian]]'' {{WS|saratogian.com}} of [[Saratoga Springs, New York]] +* Weeklies: +** ''Community News'' {{WS|cnweekly.com}} weekly of [[Clifton Park, New York]] +** ''Rome Observer'' of [[Rome, New York]] +** ''Life & Times of Utica'' of [[Utica, New York]] + +== Connecticut == +Five dailies, associated weeklies and [[pennysaver]]s in the state of [[Connecticut]]; also [http://www.ctcentral.com CTcentral.com], [http://www.ctcarsandtrucks.com CTCarsAndTrucks.com] and [http://www.jobsinct.com JobsInCT.com]. + +* ''The Middletown Press'' {{WS|middletownpress.com}} of [[Middletown, Connecticut|Middletown]] +* ''[[New Haven Register]]'' {{WS|newhavenregister.com}} of [[New Haven, Connecticut|New Haven]] +* ''The Register Citizen'' {{WS|registercitizen.com}} of [[Torrington, Connecticut|Torrington]] + +* [[New Haven Register#Competitors|Elm City Newspapers]] {{WS|ctcentral.com}} +** ''The Advertiser'' of [[East Haven, Connecticut|East Haven]] +** ''Hamden Chronicle'' of [[Hamden, Connecticut|Hamden]] +** ''Milford Weekly'' of [[Milford, Connecticut|Milford]] +** ''The Orange Bulletin'' of [[Orange, Connecticut|Orange]] +** ''The Post'' of [[North Haven, Connecticut|North Haven]] +** ''Shelton Weekly'' of [[Shelton, Connecticut|Shelton]] +** ''The Stratford Bard'' of [[Stratford, Connecticut|Stratford]] +** ''Wallingford Voice'' of [[Wallingford, Connecticut|Wallingford]] +** ''West Haven News'' of [[West Haven, Connecticut|West Haven]] +* Housatonic Publications +** ''The New Milford Times'' {{WS|newmilfordtimes.com}} of [[New Milford, Connecticut|New Milford]] +** ''The Brookfield Journal'' of [[Brookfield, Connecticut|Brookfield]] +** ''The Kent Good Times Dispatch'' of [[Kent, Connecticut|Kent]] +** ''The Bethel Beacon'' of [[Bethel, Connecticut|Bethel]] +** ''The Litchfield Enquirer'' of [[Litchfield, Connecticut|Litchfield]] +** ''Litchfield County Times'' of [[Litchfield, Connecticut|Litchfield]] +* Imprint Newspapers {{WS|imprintnewspapers.com}} +** ''West Hartford News'' of [[West Hartford, Connecticut|West Hartford]] +** ''Windsor Journal'' of [[Windsor, Connecticut|Windsor]] +** ''Windsor Locks Journal'' of [[Windsor Locks, Connecticut|Windsor Locks]] +** ''Avon Post'' of [[Avon, Connecticut|Avon]] +** ''Farmington Post'' of [[Farmington, Connecticut|Farmington]] +** ''Simsbury Post'' of [[Simsbury, Connecticut|Simsbury]] +** ''Tri-Town Post'' of [[Burlington, Connecticut|Burlington]], [[Canton, Connecticut|Canton]] and [[Harwinton, Connecticut|Harwinton]] +* Minuteman Publications +** ''[[Fairfield Minuteman]]'' of [[Fairfield, Connecticut|Fairfield]] +** ''The Westport Minuteman'' {{WS|westportminuteman.com}} of [[Westport, Connecticut|Westport]] +* Shoreline Newspapers weeklies: +** ''Branford Review'' of [[Branford, Connecticut|Branford]] +** ''Clinton Recorder'' of [[Clinton, Connecticut|Clinton]] +** ''The Dolphin'' of [[Naval Submarine Base New London]] in [[New London, Connecticut|New London]] +** ''Main Street News'' {{WS|ctmainstreetnews.com}} of [[Essex, Connecticut|Essex]] +** ''Pictorial Gazette'' of [[Old Saybrook, Connecticut|Old Saybrook]] +** ''Regional Express'' of [[Colchester, Connecticut|Colchester]] +** ''Regional Standard'' of [[Colchester, Connecticut|Colchester]] +** ''Shoreline Times'' {{WS|shorelinetimes.com}} of [[Guilford, Connecticut|Guilford]] +** ''Shore View East'' of [[Madison, Connecticut|Madison]] +** ''Shore View West'' of [[Guilford, Connecticut|Guilford]] +* Other weeklies: +** ''Registro'' {{WS|registroct.com}} of [[New Haven, Connecticut|New Haven]] +** ''Thomaston Express'' {{WS|thomastownexpress.com}} of [[Thomaston, Connecticut|Thomaston]] +** ''Foothills Traders'' {{WS|foothillstrader.com}} of Torrington, Bristol, Canton + +== Michigan == +Four dailies, associated weeklies and [[pennysaver]]s in the state of [[Michigan]]; also [http://www.micentralhomes.com MIcentralhomes.com] and [http://www.micentralautos.com MIcentralautos.com] +* ''[[Oakland Press]]'' {{WS|theoaklandpress.com}} of [[Oakland, Michigan|Oakland]] +* ''Daily Tribune'' {{WS|dailytribune.com}} of [[Royal Oak, Michigan|Royal Oak]] +* ''Macomb Daily'' {{WS|macombdaily.com}} of [[Mt. Clemens, Michigan|Mt. Clemens]] +* ''[[Morning Sun]]'' {{WS|themorningsun.com}} of [[Mount Pleasant, Michigan|Mount Pleasant]] +* Heritage Newspapers {{WS|heritage.com}} +** ''Belleville View'' +** ''Ile Camera'' +** ''Monroe Guardian'' +** ''Ypsilanti Courier'' +** ''News-Herald'' +** ''Press & Guide'' +** ''Chelsea Standard & Dexter Leader'' +** ''Manchester Enterprise'' +** ''Milan News-Leader'' +** ''Saline Reporter'' +* Independent Newspapers {{WS|sourcenewspapers.com}} +** ''Advisor'' +** ''Source'' +* Morning Star {{WS|morningstarpublishing.com}} +** ''Alma Reminder'' +** ''Alpena Star'' +** ''Antrim County News'' +** ''Carson City Reminder'' +** ''The Leader & Kalkaskian'' +** ''Ogemaw/Oscoda County Star'' +** ''Petoskey/Charlevoix Star'' +** ''Presque Isle Star'' +** ''Preview Community Weekly'' +** ''Roscommon County Star'' +** ''St. Johns Reminder'' +** ''Straits Area Star'' +** ''The (Edmore) Advertiser'' +* Voice Newspapers {{WS|voicenews.com}} +** ''Armada Times'' +** ''Bay Voice'' +** ''Blue Water Voice'' +** ''Downriver Voice'' +** ''Macomb Township Voice'' +** ''North Macomb Voice'' +** ''Weekend Voice'' +** ''Suburban Lifestyles'' {{WS|suburbanlifestyles.com}} + +== Mid-Hudson == +One daily, associated magazines in the [[Hudson River Valley]] of [[New York]]; also [http://www.midhudsoncentral.com MidHudsonCentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''[[Daily Freeman]]'' {{WS|dailyfreeman.com}} of [[Kingston, New York]] + +== Ohio == +Two dailies, associated magazines and three shared Websites, all in the state of [[Ohio]]: [http://www.allaroundcleveland.com AllAroundCleveland.com], [http://www.allaroundclevelandcars.com AllAroundClevelandCars.com] and [http://www.allaroundclevelandjobs.com AllAroundClevelandJobs.com]. + +* ''[[The News-Herald (Ohio)|The News-Herald]]'' {{WS|news-herald.com}} of [[Willoughby, Ohio|Willoughby]] +* ''[[The Morning Journal]]'' {{WS|morningjournal.com}} of [[Lorain, Ohio|Lorain]] + +== Philadelphia area == +Seven dailies and associated weeklies and magazines in [[Pennsylvania]] and [[New Jersey]], and associated Websites: [http://www.allaroundphilly.com AllAroundPhilly.com], [http://www.jobsinnj.com JobsInNJ.com], [http://www.jobsinpa.com JobsInPA.com], and [http://www.phillycarsearch.com PhillyCarSearch.com]. + +* ''The Daily Local'' {{WS|dailylocal.com}} of [[West Chester, Pennsylvania|West Chester]] +* ''[[Delaware County Daily and Sunday Times]] {{WS|delcotimes.com}} of Primos +* ''[[The Mercury (Pennsylvania)|The Mercury]]'' {{WS|pottstownmercury.com}} of [[Pottstown, Pennsylvania|Pottstown]] +* ''The Phoenix'' {{WS|phoenixvillenews.com}} of [[Phoenixville, Pennsylvania|Phoenixville]] +* ''[[The Reporter (Lansdale)|The Reporter]]'' {{WS|thereporteronline.com}} of [[Lansdale, Pennsylvania|Lansdale]] +* ''The Times Herald'' {{WS|timesherald.com}} of [[Norristown, Pennsylvania|Norristown]] +* ''[[The Trentonian]]'' {{WS|trentonian.com}} of [[Trenton, New Jersey]] + +* Weeklies +** ''El Latino Expreso'' of [[Trenton, New Jersey]] +** ''La Voz'' of [[Norristown, Pennsylvania]] +** ''The Village News'' of [[Downingtown, Pennsylvania]] +** ''The Times Record'' of [[Kennett Square, Pennsylvania]] +** ''The Tri-County Record'' {{WS|tricountyrecord.com}} of [[Morgantown, Pennsylvania]] +** ''News of Delaware County'' {{WS|newsofdelawarecounty.com}}of [[Havertown, Pennsylvania]] +** ''Main Line Times'' {{WS|mainlinetimes.com}}of [[Ardmore, Pennsylvania]] +** ''Penny Pincher'' of [[Pottstown, Pennsylvania]] +** ''Town Talk'' {{WS|towntalknews.com}} of [[Ridley, Pennsylvania]] +* Chesapeake Publishing {{WS|pa8newsgroup.com}} +** ''Solanco Sun Ledger'' of [[Quarryville, Pennsylvania]] +** ''Columbia Ledger'' of [[Columbia, Pennsylvania]] +** ''Coatesville Ledger'' of [[Downingtown, Pennsylvania]] +** ''Parkesburg Post Ledger'' of [[Quarryville, Pennsylvania]] +** ''Downingtown Ledger'' of [[Downingtown, Pennsylvania]] +** ''The Kennett Paper'' of [[Kennett Square, Pennsylvania]] +** ''Avon Grove Sun'' of [[West Grove, Pennsylvania]] +** ''Oxford Tribune'' of [[Oxford, Pennsylvania]] +** ''Elizabethtown Chronicle'' of [[Elizabethtown, Pennsylvania]] +** ''Donegal Ledger'' of [[Donegal, Pennsylvania]] +** ''Chadds Ford Post'' of [[Chadds Ford, Pennsylvania]] +** ''The Central Record'' of [[Medford, New Jersey]] +** ''Maple Shade Progress'' of [[Maple Shade, New Jersey]] +* Intercounty Newspapers {{WS|buckslocalnews.com}} +** ''The Review'' of Roxborough, Pennsylvania +** ''The Recorder'' of [[Conshohocken, Pennsylvania]] +** ''The Leader'' of [[Mount Airy, Pennsylvania|Mount Airy]] and West Oak Lake, Pennsylvania +** ''The Pennington Post'' of [[Pennington, New Jersey]] +** ''The Bristol Pilot'' of [[Bristol, Pennsylvania]] +** ''Yardley News'' of [[Yardley, Pennsylvania]] +** ''New Hope Gazette'' of [[New Hope, Pennsylvania]] +** ''Doylestown Patriot'' of [[Doylestown, Pennsylvania]] +** ''Newtown Advance'' of [[Newtown, Pennsylvania]] +** ''The Plain Dealer'' of [[Williamstown, New Jersey]] +** ''News Report'' of [[Sewell, New Jersey]] +** ''Record Breeze'' of [[Berlin, New Jersey]] +** ''Newsweekly'' of [[Moorestown, New Jersey]] +** ''Haddon Herald'' of [[Haddonfield, New Jersey]] +** ''New Egypt Press'' of [[New Egypt, New Jersey]] +** ''Community News'' of [[Pemberton, New Jersey]] +** ''Plymouth Meeting Journal'' of [[Plymouth Meeting, Pennsylvania]] +** ''Lafayette Hill Journal'' of [[Lafayette Hill, Pennsylvania]] +* Montgomery Newspapers {{WS|montgomerynews.com}} +** ''Ambler Gazette'' of [[Ambler, Pennsylvania]] +** ''Central Bucks Life'' of [[Bucks County, Pennsylvania]] +** ''The Colonial'' of [[Plymouth Meeting, Pennsylvania]] +** ''Glenside News'' of [[Glenside, Pennsylvania]] +** ''The Globe'' of [[Lower Moreland Township, Pennsylvania]] +** ''Main Line Life'' of [[Ardmore, Pennsylvania]] +** ''Montgomery Life'' of [[Fort Washington, Pennsylvania]] +** ''North Penn Life'' of [[Lansdale, Pennsylvania]] +** ''Perkasie News Herald'' of [[Perkasie, Pennsylvania]] +** ''Public Spirit'' of [[Hatboro, Pennsylvania]] +** ''Souderton Independent'' of [[Souderton, Pennsylvania]] +** ''Springfield Sun'' of [[Springfield, Pennsylvania]] +** ''Spring-Ford Reporter'' of [[Royersford, Pennsylvania]] +** ''Times Chronicle'' of [[Jenkintown, Pennsylvania]] +** ''Valley Item'' of [[Perkiomenville, Pennsylvania]] +** ''Willow Grove Guide'' of [[Willow Grove, Pennsylvania]] +* News Gleaner Publications (closed December 2008) {{WS|newsgleaner.com}} +** ''Life Newspapers'' of [[Philadelphia, Pennsylvania]] +* Suburban Publications +** ''The Suburban & Wayne Times'' {{WS|waynesuburban.com}} of [[Wayne, Pennsylvania]] +** ''The Suburban Advertiser'' of [[Exton, Pennsylvania]] +** ''The King of Prussia Courier'' of [[King of Prussia, Pennsylvania]] +* Press Newspapers {{WS|countypressonline.com}} +** ''County Press'' of [[Newtown Square, Pennsylvania]] +** ''Garnet Valley Press'' of [[Glen Mills, Pennsylvania]] +** ''Haverford Press'' of [[Newtown Square, Pennsylvania]] (closed January 2009) +** ''Hometown Press'' of [[Glen Mills, Pennsylvania]] (closed January 2009) +** ''Media Press'' of [[Newtown Square, Pennsylvania]] (closed January 2009) +** ''Springfield Press'' of [[Springfield, Pennsylvania]] +* Berks-Mont Newspapers {{WS|berksmontnews.com}} +** ''The Boyertown Area Times'' of [[Boyertown, Pennsylvania]] +** ''The Kutztown Area Patriot'' of [[Kutztown, Pennsylvania]] +** ''The Hamburg Area Item'' of [[Hamburg, Pennsylvania]] +** ''The Southern Berks News'' of [[Exeter Township, Berks County, Pennsylvania]] +** ''The Free Press'' of [[Quakertown, Pennsylvania]] +** ''The Saucon News'' of [[Quakertown, Pennsylvania]] +** ''Westside Weekly'' of [[Reading, Pennsylvania]] + +* Magazines +** ''Bucks Co. Town & Country Living'' +** ''Chester Co. Town & Country Living'' +** ''Montomgery Co. Town & Country Living'' +** ''Garden State Town & Country Living'' +** ''Montgomery Homes'' +** ''Philadelphia Golfer'' +** ''Parents Express'' +** ''Art Matters'' + +{{JRC}} + +==References== + + +[[Category:Journal Register publications|*]] diff --git a/kotlin/tests/name/fraser/neil/plaintext/Speedtest2.txt b/kotlin/tests/name/fraser/neil/plaintext/Speedtest2.txt new file mode 100644 index 0000000..8f25a80 --- /dev/null +++ b/kotlin/tests/name/fraser/neil/plaintext/Speedtest2.txt @@ -0,0 +1,188 @@ +This is a '''list of newspapers published by [[Journal Register Company]]'''. + +The company owns daily and weekly newspapers, other print media properties and newspaper-affiliated local Websites in the [[U.S.]] states of [[Connecticut]], [[Michigan]], [[New York]], [[Ohio]], [[Pennsylvania]] and [[New Jersey]], organized in six geographic "clusters":[http://www.journalregister.com/publications.html Journal Register Company: Our Publications], accessed April 21, 2010. + +== Capital-Saratoga == +Three dailies, associated weeklies and [[pennysaver]]s in greater [[Albany, New York]]; also [http://www.capitalcentral.com capitalcentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''The Oneida Daily Dispatch'' {{WS|oneidadispatch.com}} of [[Oneida, New York]] +* ''[[The Record (Troy)|The Record]]'' {{WS|troyrecord.com}} of [[Troy, New York]] +* ''[[The Saratogian]]'' {{WS|saratogian.com}} of [[Saratoga Springs, New York]] +* Weeklies: +** ''Community News'' {{WS|cnweekly.com}} weekly of [[Clifton Park, New York]] +** ''Rome Observer'' {{WS|romeobserver.com}} of [[Rome, New York]] +** ''WG Life '' {{WS|saratogian.com/wglife/}} of [[Wilton, New York]] +** ''Ballston Spa Life '' {{WS|saratogian.com/bspalife}} of [[Ballston Spa, New York]] +** ''Greenbush Life'' {{WS|troyrecord.com/greenbush}} of [[Troy, New York]] +** ''Latham Life'' {{WS|troyrecord.com/latham}} of [[Latham, New York]] +** ''River Life'' {{WS|troyrecord.com/river}} of [[Troy, New York]] + +== Connecticut == +Three dailies, associated weeklies and [[pennysaver]]s in the state of [[Connecticut]]; also [http://www.ctcentral.com CTcentral.com], [http://www.ctcarsandtrucks.com CTCarsAndTrucks.com] and [http://www.jobsinct.com JobsInCT.com]. + +* ''The Middletown Press'' {{WS|middletownpress.com}} of [[Middletown, Connecticut|Middletown]] +* ''[[New Haven Register]]'' {{WS|newhavenregister.com}} of [[New Haven, Connecticut|New Haven]] +* ''The Register Citizen'' {{WS|registercitizen.com}} of [[Torrington, Connecticut|Torrington]] + +* Housatonic Publications +** ''The Housatonic Times'' {{WS|housatonictimes.com}} of [[New Milford, Connecticut|New Milford]] +** ''Litchfield County Times'' {{WS|countytimes.com}} of [[Litchfield, Connecticut|Litchfield]] + +* Minuteman Publications +** ''[[Fairfield Minuteman]]'' {{WS|fairfieldminuteman.com}}of [[Fairfield, Connecticut|Fairfield]] +** ''The Westport Minuteman'' {{WS|westportminuteman.com}} of [[Westport, Connecticut|Westport]] + +* Shoreline Newspapers +** ''The Dolphin'' {{WS|dolphin-news.com}} of [[Naval Submarine Base New London]] in [[New London, Connecticut|New London]] +** ''Shoreline Times'' {{WS|shorelinetimes.com}} of [[Guilford, Connecticut|Guilford]] + +* Foothills Media Group {{WS|foothillsmediagroup.com}} +** ''Thomaston Express'' {{WS|thomastonexpress.com}} of [[Thomaston, Connecticut|Thomaston]] +** ''Good News About Torrington'' {{WS|goodnewsabouttorrington.com}} of [[Torrington, Connecticut|Torrington]] +** ''Granby News'' {{WS|foothillsmediagroup.com/granby}} of [[Granby, Connecticut|Granby]] +** ''Canton News'' {{WS|foothillsmediagroup.com/canton}} of [[Canton, Connecticut|Canton]] +** ''Avon News'' {{WS|foothillsmediagroup.com/avon}} of [[Avon, Connecticut|Avon]] +** ''Simsbury News'' {{WS|foothillsmediagroup.com/simsbury}} of [[Simsbury, Connecticut|Simsbury]] +** ''Litchfield News'' {{WS|foothillsmediagroup.com/litchfield}} of [[Litchfield, Connecticut|Litchfield]] +** ''Foothills Trader'' {{WS|foothillstrader.com}} of Torrington, Bristol, Canton + +* Other weeklies +** ''The Milford-Orange Bulletin'' {{WS|ctbulletin.com}} of [[Orange, Connecticut|Orange]] +** ''The Post-Chronicle'' {{WS|ctpostchronicle.com}} of [[North Haven, Connecticut|North Haven]] +** ''West Hartford News'' {{WS|westhartfordnews.com}} of [[West Hartford, Connecticut|West Hartford]] + +* Magazines +** ''The Connecticut Bride'' {{WS|connecticutmag.com}} +** ''Connecticut Magazine'' {{WS|theconnecticutbride.com}} +** ''Passport Magazine'' {{WS|passport-mag.com}} + +== Michigan == +Four dailies, associated weeklies and [[pennysaver]]s in the state of [[Michigan]]; also [http://www.micentralhomes.com MIcentralhomes.com] and [http://www.micentralautos.com MIcentralautos.com] +* ''[[Oakland Press]]'' {{WS|theoaklandpress.com}} of [[Oakland, Michigan|Oakland]] +* ''Daily Tribune'' {{WS|dailytribune.com}} of [[Royal Oak, Michigan|Royal Oak]] +* ''Macomb Daily'' {{WS|macombdaily.com}} of [[Mt. Clemens, Michigan|Mt. Clemens]] +* ''[[Morning Sun]]'' {{WS|themorningsun.com}} of [[Mount Pleasant, Michigan|Mount Pleasant]] + +* Heritage Newspapers {{WS|heritage.com}} +** ''Belleville View'' {{WS|bellevilleview.com}} +** ''Ile Camera'' {{WS|thenewsherald.com/ile_camera}} +** ''Monroe Guardian'' {{WS|monreguardian.com}} +** ''Ypsilanti Courier'' {{WS|ypsilanticourier.com}} +** ''News-Herald'' {{WS|thenewsherald.com}} +** ''Press & Guide'' {{WS|pressandguide.com}} +** ''Chelsea Standard & Dexter Leader'' {{WS|chelseastandard.com}} +** ''Manchester Enterprise'' {{WS|manchesterguardian.com}} +** ''Milan News-Leader'' {{WS|milannews.com}} +** ''Saline Reporter'' {{WS|salinereporter.com}} +* Independent Newspapers +** ''Advisor'' {{WS|sourcenewspapers.com}} +** ''Source'' {{WS|sourcenewspapers.com}} +* Morning Star {{WS|morningstarpublishing.com}} +** ''The Leader & Kalkaskian'' {{WS|leaderandkalkaskian.com}} +** ''Grand Traverse Insider'' {{WS|grandtraverseinsider.com}} +** ''Alma Reminder'' +** ''Alpena Star'' +** ''Ogemaw/Oscoda County Star'' +** ''Presque Isle Star'' +** ''St. Johns Reminder'' + +* Voice Newspapers {{WS|voicenews.com}} +** ''Armada Times'' +** ''Bay Voice'' +** ''Blue Water Voice'' +** ''Downriver Voice'' +** ''Macomb Township Voice'' +** ''North Macomb Voice'' +** ''Weekend Voice'' + +== Mid-Hudson == +One daily, associated magazines in the [[Hudson River Valley]] of [[New York]]; also [http://www.midhudsoncentral.com MidHudsonCentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''[[Daily Freeman]]'' {{WS|dailyfreeman.com}} of [[Kingston, New York]] +* ''Las Noticias'' {{WS|lasnoticiasny.com}} of [[Kingston, New York]] + +== Ohio == +Two dailies, associated magazines and three shared Websites, all in the state of [[Ohio]]: [http://www.allaroundcleveland.com AllAroundCleveland.com], [http://www.allaroundclevelandcars.com AllAroundClevelandCars.com] and [http://www.allaroundclevelandjobs.com AllAroundClevelandJobs.com]. + +* ''[[The News-Herald (Ohio)|The News-Herald]]'' {{WS|news-herald.com}} of [[Willoughby, Ohio|Willoughby]] +* ''[[The Morning Journal]]'' {{WS|morningjournal.com}} of [[Lorain, Ohio|Lorain]] +* ''El Latino Expreso'' {{WS|lorainlatino.com}} of [[Lorain, Ohio|Lorain]] + +== Philadelphia area == +Seven dailies and associated weeklies and magazines in [[Pennsylvania]] and [[New Jersey]], and associated Websites: [http://www.allaroundphilly.com AllAroundPhilly.com], [http://www.jobsinnj.com JobsInNJ.com], [http://www.jobsinpa.com JobsInPA.com], and [http://www.phillycarsearch.com PhillyCarSearch.com]. + +* ''[[The Daily Local News]]'' {{WS|dailylocal.com}} of [[West Chester, Pennsylvania|West Chester]] +* ''[[Delaware County Daily and Sunday Times]] {{WS|delcotimes.com}} of Primos [[Upper Darby Township, Pennsylvania]] +* ''[[The Mercury (Pennsylvania)|The Mercury]]'' {{WS|pottstownmercury.com}} of [[Pottstown, Pennsylvania|Pottstown]] +* ''[[The Reporter (Lansdale)|The Reporter]]'' {{WS|thereporteronline.com}} of [[Lansdale, Pennsylvania|Lansdale]] +* ''The Times Herald'' {{WS|timesherald.com}} of [[Norristown, Pennsylvania|Norristown]] +* ''[[The Trentonian]]'' {{WS|trentonian.com}} of [[Trenton, New Jersey]] + +* Weeklies +* ''The Phoenix'' {{WS|phoenixvillenews.com}} of [[Phoenixville, Pennsylvania]] +** ''El Latino Expreso'' {{WS|njexpreso.com}} of [[Trenton, New Jersey]] +** ''La Voz'' {{WS|lavozpa.com}} of [[Norristown, Pennsylvania]] +** ''The Tri County Record'' {{WS|tricountyrecord.com}} of [[Morgantown, Pennsylvania]] +** ''Penny Pincher'' {{WS|pennypincherpa.com}}of [[Pottstown, Pennsylvania]] + +* Chesapeake Publishing {{WS|southernchestercountyweeklies.com}} +** ''The Kennett Paper'' {{WS|kennettpaper.com}} of [[Kennett Square, Pennsylvania]] +** ''Avon Grove Sun'' {{WS|avongrovesun.com}} of [[West Grove, Pennsylvania]] +** ''The Central Record'' {{WS|medfordcentralrecord.com}} of [[Medford, New Jersey]] +** ''Maple Shade Progress'' {{WS|mapleshadeprogress.com}} of [[Maple Shade, New Jersey]] + +* Intercounty Newspapers {{WS|buckslocalnews.com}} {{WS|southjerseylocalnews.com}} +** ''The Pennington Post'' {{WS|penningtonpost.com}} of [[Pennington, New Jersey]] +** ''The Bristol Pilot'' {{WS|bristolpilot.com}} of [[Bristol, Pennsylvania]] +** ''Yardley News'' {{WS|yardleynews.com}} of [[Yardley, Pennsylvania]] +** ''Advance of Bucks County'' {{WS|advanceofbucks.com}} of [[Newtown, Pennsylvania]] +** ''Record Breeze'' {{WS|recordbreeze.com}} of [[Berlin, New Jersey]] +** ''Community News'' {{WS|sjcommunitynews.com}} of [[Pemberton, New Jersey]] + +* Montgomery Newspapers {{WS|montgomerynews.com}} +** ''Ambler Gazette'' {{WS|amblergazette.com}} of [[Ambler, Pennsylvania]] +** ''The Colonial'' {{WS|colonialnews.com}} of [[Plymouth Meeting, Pennsylvania]] +** ''Glenside News'' {{WS|glensidenews.com}} of [[Glenside, Pennsylvania]] +** ''The Globe'' {{WS|globenewspaper.com}} of [[Lower Moreland Township, Pennsylvania]] +** ''Montgomery Life'' {{WS|montgomerylife.com}} of [[Fort Washington, Pennsylvania]] +** ''North Penn Life'' {{WS|northpennlife.com}} of [[Lansdale, Pennsylvania]] +** ''Perkasie News Herald'' {{WS|perkasienewsherald.com}} of [[Perkasie, Pennsylvania]] +** ''Public Spirit'' {{WS|thepublicspirit.com}} of [[Hatboro, Pennsylvania]] +** ''Souderton Independent'' {{WS|soudertonindependent.com}} of [[Souderton, Pennsylvania]] +** ''Springfield Sun'' {{WS|springfieldsun.com}} of [[Springfield, Pennsylvania]] +** ''Spring-Ford Reporter'' {{WS|springfordreporter.com}} of [[Royersford, Pennsylvania]] +** ''Times Chronicle'' {{WS|thetimeschronicle.com}} of [[Jenkintown, Pennsylvania]] +** ''Valley Item'' {{WS|valleyitem.com}} of [[Perkiomenville, Pennsylvania]] +** ''Willow Grove Guide'' {{WS|willowgroveguide.com}} of [[Willow Grove, Pennsylvania]] +** ''The Review'' {{WS|roxreview.com}} of [[Roxborough, Philadelphia, Pennsylvania]] + +* Main Line Media News {{WS|mainlinemedianews.com}} +** ''Main Line Times'' {{WS|mainlinetimes.com}} of [[Ardmore, Pennsylvania]] +** ''Main Line Life'' {{WS|mainlinelife.com}} of [[Ardmore, Pennsylvania]] +** ''The King of Prussia Courier'' {{WS|kingofprussiacourier.com}} of [[King of Prussia, Pennsylvania]] + +* Delaware County News Network {{WS|delconewsnetwork.com}} +** ''News of Delaware County'' {{WS|newsofdelawarecounty.com}} of [[Havertown, Pennsylvania]] +** ''County Press'' {{WS|countypressonline.com}} of [[Newtown Square, Pennsylvania]] +** ''Garnet Valley Press'' {{WS|countypressonline.com}} of [[Glen Mills, Pennsylvania]] +** ''Springfield Press'' {{WS|countypressonline.com}} of [[Springfield, Pennsylvania]] +** ''Town Talk'' {{WS|towntalknews.com}} of [[Ridley, Pennsylvania]] + +* Berks-Mont Newspapers {{WS|berksmontnews.com}} +** ''The Boyertown Area Times'' {{WS|berksmontnews.com/boyertown_area_times}} of [[Boyertown, Pennsylvania]] +** ''The Kutztown Area Patriot'' {{WS|berksmontnews.com/kutztown_area_patriot}} of [[Kutztown, Pennsylvania]] +** ''The Hamburg Area Item'' {{WS|berksmontnews.com/hamburg_area_item}} of [[Hamburg, Pennsylvania]] +** ''The Southern Berks News'' {{WS|berksmontnews.com/southern_berks_news}} of [[Exeter Township, Berks County, Pennsylvania]] +** ''Community Connection'' {{WS|berksmontnews.com/community_connection}} of [[Boyertown, Pennsylvania]] + +* Magazines +** ''Bucks Co. Town & Country Living'' {{WS|buckscountymagazine.com}} +** ''Parents Express'' {{WS|parents-express.com}} +** ''Real Men, Rednecks'' {{WS|realmenredneck.com}} + +{{JRC}} + +==References== + + +[[Category:Journal Register publications|*]] diff --git a/kotlin/tests/name/fraser/neil/plaintext/diff_match_patch_test.kt b/kotlin/tests/name/fraser/neil/plaintext/diff_match_patch_test.kt new file mode 100644 index 0000000..53938c9 --- /dev/null +++ b/kotlin/tests/name/fraser/neil/plaintext/diff_match_patch_test.kt @@ -0,0 +1,1392 @@ +/* + * Diff Match and Patch -- Test harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Compile from diff-match-patch/java with: + * kotlinc src/name/fraser/neil/plaintext/diff_match_patch.java tests/name/fraser/neil/plaintext/diff_match_patch_test.kt -include-runtime -d diff_match_patch_test.jar + * Execute with: + * java -classpath classes name/fraser/neil/plaintext/diff_match_patch_test <-- Wrong + */ + +package name.fraser.neil.plaintext + +import java.util.ArrayList +import java.util.Arrays +import java.util.HashMap +import java.util.LinkedList + +import io.tpro.timelineview.CacheAppendMode.INSERT +import name.fraser.neil.plaintext.diff_match_patch +import name.fraser.neil.plaintext.diff_match_patch.Diff +import name.fraser.neil.plaintext.diff_match_patch.LinesToCharsResult +import name.fraser.neil.plaintext.diff_match_patch.Patch +import retrofit2.http.DELETE + +object diff_match_patch_test { + + private var dmp: diff_match_patch? = null + private val DELETE = diff_match_patch.Operation.DELETE + private val EQUAL = diff_match_patch.Operation.EQUAL + private val INSERT = diff_match_patch.Operation.INSERT + + // DIFF TEST FUNCTIONS + + fun testDiffCommonPrefix() { + // Detect any common prefix. + assertEquals("diff_commonPrefix: Null case.", 0, dmp!!.diff_commonPrefix("abc", "xyz")) + + assertEquals("diff_commonPrefix: Non-null case.", 4, dmp!!.diff_commonPrefix("1234abcdef", "1234xyz")) + + assertEquals("diff_commonPrefix: Whole case.", 4, dmp!!.diff_commonPrefix("1234", "1234xyz")) + } + + fun testDiffCommonSuffix() { + // Detect any common suffix. + assertEquals("diff_commonSuffix: Null case.", 0, dmp!!.diff_commonSuffix("abc", "xyz")) + + assertEquals("diff_commonSuffix: Non-null case.", 4, dmp!!.diff_commonSuffix("abcdef1234", "xyz1234")) + + assertEquals("diff_commonSuffix: Whole case.", 4, dmp!!.diff_commonSuffix("1234", "xyz1234")) + } + + fun testDiffCommonOverlap() { + // Detect any suffix/prefix overlap. + assertEquals("diff_commonOverlap: Null case.", 0, dmp!!.diff_commonOverlap("", "abcd")) + + assertEquals("diff_commonOverlap: Whole case.", 3, dmp!!.diff_commonOverlap("abc", "abcd")) + + assertEquals("diff_commonOverlap: No overlap.", 0, dmp!!.diff_commonOverlap("123456", "abcd")) + + assertEquals("diff_commonOverlap: Overlap.", 3, dmp!!.diff_commonOverlap("123456xxx", "xxxabcd")) + + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + assertEquals("diff_commonOverlap: Unicode.", 0, dmp!!.diff_commonOverlap("fi", "\ufb01i")) + } + + fun testDiffHalfmatch() { + // Detect a halfmatch. + dmp!!.Diff_Timeout = 1 + assertNull("diff_halfMatch: No match #1.", dmp!!.diff_halfMatch("1234567890", "abcdef")) + + assertNull("diff_halfMatch: No match #2.", dmp!!.diff_halfMatch("12345", "23")) + + assertArrayEquals( + "diff_halfMatch: Single Match #1.", + arrayOf("12", "90", "a", "z", "345678"), + dmp!!.diff_halfMatch("1234567890", "a345678z") + ) + + assertArrayEquals( + "diff_halfMatch: Single Match #2.", + arrayOf("a", "z", "12", "90", "345678"), + dmp!!.diff_halfMatch("a345678z", "1234567890") + ) + + assertArrayEquals( + "diff_halfMatch: Single Match #3.", + arrayOf("abc", "z", "1234", "0", "56789"), + dmp!!.diff_halfMatch("abc56789z", "1234567890") + ) + + assertArrayEquals( + "diff_halfMatch: Single Match #4.", + arrayOf("a", "xyz", "1", "7890", "23456"), + dmp!!.diff_halfMatch("a23456xyz", "1234567890") + ) + + assertArrayEquals( + "diff_halfMatch: Multiple Matches #1.", + arrayOf("12123", "123121", "a", "z", "1234123451234"), + dmp!!.diff_halfMatch("121231234123451234123121", "a1234123451234z") + ) + + assertArrayEquals( + "diff_halfMatch: Multiple Matches #2.", + arrayOf("", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="), + dmp!!.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=") + ) + + assertArrayEquals( + "diff_halfMatch: Multiple Matches #3.", + arrayOf("-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"), + dmp!!.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy") + ) + + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assertArrayEquals( + "diff_halfMatch: Non-optimal halfmatch.", + arrayOf("qHillo", "w", "x", "Hulloy", "HelloHe"), + dmp!!.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy") + ) + + dmp!!.Diff_Timeout = 0 + assertNull("diff_halfMatch: Optimal no halfmatch.", dmp!!.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")) + } + + fun testDiffLinesToChars() { + // Convert lines down to characters. + val tmpVector = ArrayList() + tmpVector.add("") + tmpVector.add("alpha\n") + tmpVector.add("beta\n") + assertLinesToCharsResultEquals( + "diff_linesToChars: Shared lines.", + LinesToCharsResult("\u0001\u0002\u0001", "\u0002\u0001\u0002", tmpVector), + dmp!!.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n") + ) + + tmpVector.clear() + tmpVector.add("") + tmpVector.add("alpha\r\n") + tmpVector.add("beta\r\n") + tmpVector.add("\r\n") + assertLinesToCharsResultEquals( + "diff_linesToChars: Empty string and blank lines.", + LinesToCharsResult("", "\u0001\u0002\u0003\u0003", tmpVector), + dmp!!.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n") + ) + + tmpVector.clear() + tmpVector.add("") + tmpVector.add("a") + tmpVector.add("b") + assertLinesToCharsResultEquals( + "diff_linesToChars: No linebreaks.", + LinesToCharsResult("\u0001", "\u0002", tmpVector), + dmp!!.diff_linesToChars("a", "b") + ) + + // More than 256 to reveal any 8-bit limitations. + val n = 300 + tmpVector.clear() + val lineList = StringBuilder() + val charList = StringBuilder() + for (i in 1 until n + 1) { + tmpVector.add((i).toString() + "\n") + lineList.append((i).toString() + "\n") + charList.append(i.toChar().toString()) + } + assertEquals("Test initialization fail #1.", n, tmpVector.size) + val lines = lineList.toString() + val chars = charList.toString() + assertEquals("Test initialization fail #2.", n, chars.length) + tmpVector.add(0, "") + assertLinesToCharsResultEquals( + "diff_linesToChars: More than 256.", + LinesToCharsResult(chars, "", tmpVector), + dmp!!.diff_linesToChars(lines, "") + ) + } + + fun testDiffCharsToLines() { + // First check that Diff equality works. + assertTrue("diff_charsToLines: Equality #1.", Diff(EQUAL, "a").equals(Diff(EQUAL, "a"))) + + assertEquals("diff_charsToLines: Equality #2.", Diff(EQUAL, "a"), Diff(EQUAL, "a")) + + // Convert chars up to lines. + var diffs = diffList(Diff(EQUAL, "\u0001\u0002\u0001"), Diff(INSERT, "\u0002\u0001\u0002")) + val tmpVector = ArrayList() + tmpVector.add("") + tmpVector.add("alpha\n") + tmpVector.add("beta\n") + dmp!!.diff_charsToLines(diffs, tmpVector) + assertEquals( + "diff_charsToLines: Shared lines.", + diffList(Diff(EQUAL, "alpha\nbeta\nalpha\n"), Diff(INSERT, "beta\nalpha\nbeta\n")), + diffs + ) + + // More than 256 to reveal any 8-bit limitations. + val n = 300 + tmpVector.clear() + var lineList = StringBuilder() + val charList = StringBuilder() + for (i in 1 until n + 1) { + tmpVector.add((i).toString() + "\n") + lineList.append((i).toString() + "\n") + charList.append(i.toChar().toString()) + } + assertEquals("Test initialization fail #3.", n, tmpVector.size) + val lines = lineList.toString() + var chars = charList.toString() + assertEquals("Test initialization fail #4.", n, chars.length) + tmpVector.add(0, "") + diffs = diffList(Diff(DELETE, chars)) + dmp!!.diff_charsToLines(diffs, tmpVector) + assertEquals("diff_charsToLines: More than 256.", diffList(Diff(DELETE, lines)), diffs) + + // More than 65536 to verify any 16-bit limitation. + lineList = StringBuilder() + for (i in 0..65999) { + lineList.append((i).toString() + "\n") + } + chars = lineList.toString() + val results = dmp!!.diff_linesToChars(chars, "") + diffs = diffList(Diff(INSERT, results.chars1)) + dmp!!.diff_charsToLines(diffs, results.lineArray) + assertEquals("diff_charsToLines: More than 65536.", chars, diffs.getFirst().text) + } + + fun testDiffCleanupMerge() { + // Cleanup a messy diff. + var diffs = diffList() + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Null case.", diffList(), diffs) + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "b"), Diff(INSERT, "c")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals( + "diff_cleanupMerge: No change case.", + diffList(Diff(EQUAL, "a"), Diff(DELETE, "b"), Diff(INSERT, "c")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "a"), Diff(EQUAL, "b"), Diff(EQUAL, "c")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Merge equalities.", diffList(Diff(EQUAL, "abc")), diffs) + + diffs = diffList(Diff(DELETE, "a"), Diff(DELETE, "b"), Diff(DELETE, "c")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Merge deletions.", diffList(Diff(DELETE, "abc")), diffs) + + diffs = diffList(Diff(INSERT, "a"), Diff(INSERT, "b"), Diff(INSERT, "c")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Merge insertions.", diffList(Diff(INSERT, "abc")), diffs) + + diffs = diffList( + Diff(DELETE, "a"), + Diff(INSERT, "b"), + Diff(DELETE, "c"), + Diff(INSERT, "d"), + Diff(EQUAL, "e"), + Diff(EQUAL, "f") + ) + dmp!!.diff_cleanupMerge(diffs) + assertEquals( + "diff_cleanupMerge: Merge interweave.", + diffList(Diff(DELETE, "ac"), Diff(INSERT, "bd"), Diff(EQUAL, "ef")), + diffs + ) + + diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "abc"), Diff(DELETE, "dc")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals( + "diff_cleanupMerge: Prefix and suffix detection.", + diffList(Diff(EQUAL, "a"), Diff(DELETE, "d"), Diff(INSERT, "b"), Diff(EQUAL, "c")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "x"), Diff(DELETE, "a"), Diff(INSERT, "abc"), Diff(DELETE, "dc"), Diff(EQUAL, "y")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals( + "diff_cleanupMerge: Prefix and suffix detection with equalities.", + diffList(Diff(EQUAL, "xa"), Diff(DELETE, "d"), Diff(INSERT, "b"), Diff(EQUAL, "cy")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "a"), Diff(INSERT, "ba"), Diff(EQUAL, "c")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Slide edit left.", diffList(Diff(INSERT, "ab"), Diff(EQUAL, "ac")), diffs) + + diffs = diffList(Diff(EQUAL, "c"), Diff(INSERT, "ab"), Diff(EQUAL, "a")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Slide edit right.", diffList(Diff(EQUAL, "ca"), Diff(INSERT, "ba")), diffs) + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "b"), Diff(EQUAL, "c"), Diff(DELETE, "ac"), Diff(EQUAL, "x")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals( + "diff_cleanupMerge: Slide edit left recursive.", + diffList(Diff(DELETE, "abc"), Diff(EQUAL, "acx")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "x"), Diff(DELETE, "ca"), Diff(EQUAL, "c"), Diff(DELETE, "b"), Diff(EQUAL, "a")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals( + "diff_cleanupMerge: Slide edit right recursive.", + diffList(Diff(EQUAL, "xca"), Diff(DELETE, "cba")), + diffs + ) + + diffs = diffList(Diff(DELETE, "b"), Diff(INSERT, "ab"), Diff(EQUAL, "c")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Empty merge.", diffList(Diff(INSERT, "a"), Diff(EQUAL, "bc")), diffs) + + diffs = diffList(Diff(EQUAL, ""), Diff(INSERT, "a"), Diff(EQUAL, "b")) + dmp!!.diff_cleanupMerge(diffs) + assertEquals("diff_cleanupMerge: Empty equality.", diffList(Diff(INSERT, "a"), Diff(EQUAL, "b")), diffs) + } + + fun testDiffCleanupSemanticLossless() { + // Slide diffs to match logical boundaries. + var diffs = diffList() + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals("diff_cleanupSemanticLossless: Null case.", diffList(), diffs) + + diffs = diffList(Diff(EQUAL, "AAA\r\n\r\nBBB"), Diff(INSERT, "\r\nDDD\r\n\r\nBBB"), Diff(EQUAL, "\r\nEEE")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Blank lines.", + diffList(Diff(EQUAL, "AAA\r\n\r\n"), Diff(INSERT, "BBB\r\nDDD\r\n\r\n"), Diff(EQUAL, "BBB\r\nEEE")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "AAA\r\nBBB"), Diff(INSERT, " DDD\r\nBBB"), Diff(EQUAL, " EEE")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Line boundaries.", + diffList(Diff(EQUAL, "AAA\r\n"), Diff(INSERT, "BBB DDD\r\n"), Diff(EQUAL, "BBB EEE")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "The c"), Diff(INSERT, "ow and the c"), Diff(EQUAL, "at.")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Word boundaries.", + diffList(Diff(EQUAL, "The "), Diff(INSERT, "cow and the "), Diff(EQUAL, "cat.")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "The-c"), Diff(INSERT, "ow-and-the-c"), Diff(EQUAL, "at.")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Alphanumeric boundaries.", + diffList(Diff(EQUAL, "The-"), Diff(INSERT, "cow-and-the-"), Diff(EQUAL, "cat.")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "a"), Diff(EQUAL, "ax")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Hitting the start.", + diffList(Diff(DELETE, "a"), Diff(EQUAL, "aax")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "xa"), Diff(DELETE, "a"), Diff(EQUAL, "a")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Hitting the end.", + diffList(Diff(EQUAL, "xaa"), Diff(DELETE, "a")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "The xxx. The "), Diff(INSERT, "zzz. The "), Diff(EQUAL, "yyy.")) + dmp!!.diff_cleanupSemanticLossless(diffs) + assertEquals( + "diff_cleanupSemanticLossless: Sentence boundaries.", + diffList(Diff(EQUAL, "The xxx."), Diff(INSERT, " The zzz."), Diff(EQUAL, " The yyy.")), + diffs + ) + } + + fun testDiffCleanupSemantic() { + // Cleanup semantically trivial equalities. + var diffs = diffList() + dmp!!.diff_cleanupSemantic(diffs) + assertEquals("diff_cleanupSemantic: Null case.", diffList(), diffs) + + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "cd"), Diff(EQUAL, "12"), Diff(DELETE, "e")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: No elimination #1.", + diffList(Diff(DELETE, "ab"), Diff(INSERT, "cd"), Diff(EQUAL, "12"), Diff(DELETE, "e")), + diffs + ) + + diffs = diffList(Diff(DELETE, "abc"), Diff(INSERT, "ABC"), Diff(EQUAL, "1234"), Diff(DELETE, "wxyz")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: No elimination #2.", + diffList(Diff(DELETE, "abc"), Diff(INSERT, "ABC"), Diff(EQUAL, "1234"), Diff(DELETE, "wxyz")), + diffs + ) + + diffs = diffList(Diff(DELETE, "a"), Diff(EQUAL, "b"), Diff(DELETE, "c")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals("diff_cleanupSemantic: Simple elimination.", diffList(Diff(DELETE, "abc"), Diff(INSERT, "b")), diffs) + + diffs = diffList(Diff(DELETE, "ab"), Diff(EQUAL, "cd"), Diff(DELETE, "e"), Diff(EQUAL, "f"), Diff(INSERT, "g")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: Backpass elimination.", + diffList(Diff(DELETE, "abcdef"), Diff(INSERT, "cdfg")), + diffs + ) + + diffs = diffList( + Diff(INSERT, "1"), + Diff(EQUAL, "A"), + Diff(DELETE, "B"), + Diff(INSERT, "2"), + Diff(EQUAL, "_"), + Diff(INSERT, "1"), + Diff(EQUAL, "A"), + Diff(DELETE, "B"), + Diff(INSERT, "2") + ) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: Multiple elimination.", + diffList(Diff(DELETE, "AB_AB"), Diff(INSERT, "1A2_1A2")), + diffs + ) + + diffs = diffList(Diff(EQUAL, "The c"), Diff(DELETE, "ow and the c"), Diff(EQUAL, "at.")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: Word boundaries.", + diffList(Diff(EQUAL, "The "), Diff(DELETE, "cow and the "), Diff(EQUAL, "cat.")), + diffs + ) + + diffs = diffList(Diff(DELETE, "abcxx"), Diff(INSERT, "xxdef")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: No overlap elimination.", + diffList(Diff(DELETE, "abcxx"), Diff(INSERT, "xxdef")), + diffs + ) + + diffs = diffList(Diff(DELETE, "abcxxx"), Diff(INSERT, "xxxdef")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: Overlap elimination.", + diffList(Diff(DELETE, "abc"), Diff(EQUAL, "xxx"), Diff(INSERT, "def")), + diffs + ) + + diffs = diffList(Diff(DELETE, "xxxabc"), Diff(INSERT, "defxxx")) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: Reverse overlap elimination.", + diffList(Diff(INSERT, "def"), Diff(EQUAL, "xxx"), Diff(DELETE, "abc")), + diffs + ) + + diffs = diffList( + Diff(DELETE, "abcd1212"), + Diff(INSERT, "1212efghi"), + Diff(EQUAL, "----"), + Diff(DELETE, "A3"), + Diff(INSERT, "3BC") + ) + dmp!!.diff_cleanupSemantic(diffs) + assertEquals( + "diff_cleanupSemantic: Two overlap eliminations.", + diffList( + Diff(DELETE, "abcd"), + Diff(EQUAL, "1212"), + Diff(INSERT, "efghi"), + Diff(EQUAL, "----"), + Diff(DELETE, "A"), + Diff(EQUAL, "3"), + Diff(INSERT, "BC") + ), + diffs + ) + } + + fun testDiffCleanupEfficiency() { + // Cleanup operationally trivial equalities. + dmp!!.Diff_EditCost = 4 + var diffs = diffList() + dmp!!.diff_cleanupEfficiency(diffs) + assertEquals("diff_cleanupEfficiency: Null case.", diffList(), diffs) + + diffs = + diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "wxyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")) + dmp!!.diff_cleanupEfficiency(diffs) + assertEquals( + "diff_cleanupEfficiency: No elimination.", + diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "wxyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")), + diffs + ) + + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "xyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")) + dmp!!.diff_cleanupEfficiency(diffs) + assertEquals( + "diff_cleanupEfficiency: Four-edit elimination.", + diffList(Diff(DELETE, "abxyzcd"), Diff(INSERT, "12xyz34")), + diffs + ) + + diffs = diffList(Diff(INSERT, "12"), Diff(EQUAL, "x"), Diff(DELETE, "cd"), Diff(INSERT, "34")) + dmp!!.diff_cleanupEfficiency(diffs) + assertEquals( + "diff_cleanupEfficiency: Three-edit elimination.", + diffList(Diff(DELETE, "xcd"), Diff(INSERT, "12x34")), + diffs + ) + + diffs = diffList( + Diff(DELETE, "ab"), + Diff(INSERT, "12"), + Diff(EQUAL, "xy"), + Diff(INSERT, "34"), + Diff(EQUAL, "z"), + Diff(DELETE, "cd"), + Diff(INSERT, "56") + ) + dmp!!.diff_cleanupEfficiency(diffs) + assertEquals( + "diff_cleanupEfficiency: Backpass elimination.", + diffList(Diff(DELETE, "abxyzcd"), Diff(INSERT, "12xy34z56")), + diffs + ) + + dmp!!.Diff_EditCost = 5 + diffs = + diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "wxyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")) + dmp!!.diff_cleanupEfficiency(diffs) + assertEquals( + "diff_cleanupEfficiency: High cost elimination.", + diffList(Diff(DELETE, "abwxyzcd"), Diff(INSERT, "12wxyz34")), + diffs + ) + dmp!!.Diff_EditCost = 4 + } + + fun testDiffPrettyHtml() { + // Pretty print. + val diffs = diffList(Diff(EQUAL, "a\n"), Diff(DELETE, "b"), Diff(INSERT, "c&d")) + assertEquals( + "diff_prettyHtml:", + "
<B>b</B>c&d", + dmp!!.diff_prettyHtml(diffs) + ) + } + + fun testDiffText() { + // Compute the source and destination texts. + val diffs = diffList( + Diff(EQUAL, "jump"), + Diff(DELETE, "s"), + Diff(INSERT, "ed"), + Diff(EQUAL, " over "), + Diff(DELETE, "the"), + Diff(INSERT, "a"), + Diff(EQUAL, " lazy") + ) + assertEquals("diff_text1:", "jumps over the lazy", dmp!!.diff_text1(diffs)) + assertEquals("diff_text2:", "jumped over a lazy", dmp!!.diff_text2(diffs)) + } + + fun testDiffDelta() { + // Convert a diff into delta string. + var diffs = diffList( + Diff(EQUAL, "jump"), + Diff(DELETE, "s"), + Diff(INSERT, "ed"), + Diff(EQUAL, " over "), + Diff(DELETE, "the"), + Diff(INSERT, "a"), + Diff(EQUAL, " lazy"), + Diff(INSERT, "old dog") + ) + var text1 = dmp!!.diff_text1(diffs) + assertEquals("diff_text1: Base text.", "jumps over the lazy", text1) + + var delta = dmp!!.diff_toDelta(diffs) + assertEquals("diff_toDelta:", "=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta) + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: Normal.", diffs, dmp!!.diff_fromDelta(text1, delta)) + + // Generates error (19 < 20). + try { + dmp!!.diff_fromDelta(text1 + "x", delta) + fail("diff_fromDelta: Too long.") + } catch (ex: IllegalArgumentException) { + // Exception expected. + } + + // Generates error (19 > 18). + try { + dmp!!.diff_fromDelta(text1.substring(1), delta) + fail("diff_fromDelta: Too short.") + } catch (ex: IllegalArgumentException) { + // Exception expected. + } + + // Generates error (%c3%xy invalid Unicode). + try { + dmp!!.diff_fromDelta("", "+%c3%xy") + fail("diff_fromDelta: Invalid character.") + } catch (ex: IllegalArgumentException) { + // Exception expected. + } + + // Test deltas with special characters. + diffs = diffList( + Diff(EQUAL, "\u0680 \u0000 \t %"), + Diff(DELETE, "\u0681 \u0001 \n ^"), + Diff(INSERT, "\u0682 \u0002 \\ |") + ) + text1 = dmp!!.diff_text1(diffs) + assertEquals("diff_text1: Unicode text.", "\u0680 \u0000 \t %\u0681 \u0001 \n ^", text1) + + delta = dmp!!.diff_toDelta(diffs) + assertEquals("diff_toDelta: Unicode.", "=7\t-7\t+%DA%82 %02 %5C %7C", delta) + + assertEquals("diff_fromDelta: Unicode.", diffs, dmp!!.diff_fromDelta(text1, delta)) + + // Verify pool of unchanged characters. + diffs = diffList(Diff(INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")) + val text2 = dmp!!.diff_text2(diffs) + assertEquals("diff_text2: Unchanged characters.", "A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2) + + delta = dmp!!.diff_toDelta(diffs) + assertEquals("diff_toDelta: Unchanged characters.", "+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta) + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: Unchanged characters.", diffs, dmp!!.diff_fromDelta("", delta)) + + // 160 kb string. + var a = "abcdefghij" + for (i in 0..13) { + a += a + } + diffs = diffList(Diff(INSERT, a)) + delta = dmp!!.diff_toDelta(diffs) + assertEquals("diff_toDelta: 160kb string.", "+$a", delta) + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: 160kb string.", diffs, dmp!!.diff_fromDelta("", delta)) + } + + fun testDiffXIndex() { + // Translate a location in text1 to text2. + var diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "1234"), Diff(EQUAL, "xyz")) + assertEquals("diff_xIndex: Translation on equality.", 5, dmp!!.diff_xIndex(diffs, 2)) + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "1234"), Diff(EQUAL, "xyz")) + assertEquals("diff_xIndex: Translation on deletion.", 1, dmp!!.diff_xIndex(diffs, 3)) + } + + fun testDiffLevenshtein() { + var diffs = diffList(Diff(DELETE, "abc"), Diff(INSERT, "1234"), Diff(EQUAL, "xyz")) + assertEquals("diff_levenshtein: Levenshtein with trailing equality.", 4, dmp!!.diff_levenshtein(diffs)) + + diffs = diffList(Diff(EQUAL, "xyz"), Diff(DELETE, "abc"), Diff(INSERT, "1234")) + assertEquals("diff_levenshtein: Levenshtein with leading equality.", 4, dmp!!.diff_levenshtein(diffs)) + + diffs = diffList(Diff(DELETE, "abc"), Diff(EQUAL, "xyz"), Diff(INSERT, "1234")) + assertEquals("diff_levenshtein: Levenshtein with middle equality.", 7, dmp!!.diff_levenshtein(diffs)) + } + + fun testDiffBisect() { + // Normal. + val a = "cat" + val b = "map" + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + var diffs = diffList(Diff(DELETE, "c"), Diff(INSERT, "m"), Diff(EQUAL, "a"), Diff(DELETE, "t"), Diff(INSERT, "p")) + assertEquals("diff_bisect: Normal.", diffs, dmp!!.diff_bisect(a, b, java.lang.Long.MAX_VALUE)) + + // Timeout. + diffs = diffList(Diff(DELETE, "cat"), Diff(INSERT, "map")) + assertEquals("diff_bisect: Timeout.", diffs, dmp!!.diff_bisect(a, b, 0)) + } + + fun testDiffMain() { + // Perform a trivial diff. + var diffs = diffList() + assertEquals("diff_main: Null case.", diffs, dmp!!.diff_main("", "", false)) + + diffs = diffList(Diff(EQUAL, "abc")) + assertEquals("diff_main: Equality.", diffs, dmp!!.diff_main("abc", "abc", false)) + + diffs = diffList(Diff(EQUAL, "ab"), Diff(INSERT, "123"), Diff(EQUAL, "c")) + assertEquals("diff_main: Simple insertion.", diffs, dmp!!.diff_main("abc", "ab123c", false)) + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "123"), Diff(EQUAL, "bc")) + assertEquals("diff_main: Simple deletion.", diffs, dmp!!.diff_main("a123bc", "abc", false)) + + diffs = diffList(Diff(EQUAL, "a"), Diff(INSERT, "123"), Diff(EQUAL, "b"), Diff(INSERT, "456"), Diff(EQUAL, "c")) + assertEquals("diff_main: Two insertions.", diffs, dmp!!.diff_main("abc", "a123b456c", false)) + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "123"), Diff(EQUAL, "b"), Diff(DELETE, "456"), Diff(EQUAL, "c")) + assertEquals("diff_main: Two deletions.", diffs, dmp!!.diff_main("a123b456c", "abc", false)) + + // Perform a real diff. + // Switch off the timeout. + dmp!!.Diff_Timeout = 0 + diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "b")) + assertEquals("diff_main: Simple case #1.", diffs, dmp!!.diff_main("a", "b", false)) + + diffs = diffList( + Diff(DELETE, "Apple"), + Diff(INSERT, "Banana"), + Diff(EQUAL, "s are a"), + Diff(INSERT, "lso"), + Diff(EQUAL, " fruit.") + ) + assertEquals( + "diff_main: Simple case #2.", + diffs, + dmp!!.diff_main("Apples are a fruit.", "Bananas are also fruit.", false) + ) + + diffs = + diffList(Diff(DELETE, "a"), Diff(INSERT, "\u0680"), Diff(EQUAL, "x"), Diff(DELETE, "\t"), Diff(INSERT, "\u0000")) + assertEquals("diff_main: Simple case #3.", diffs, dmp!!.diff_main("ax\t", "\u0680x\u0000", false)) + + diffs = diffList( + Diff(DELETE, "1"), + Diff(EQUAL, "a"), + Diff(DELETE, "y"), + Diff(EQUAL, "b"), + Diff(DELETE, "2"), + Diff(INSERT, "xab") + ) + assertEquals("diff_main: Overlap #1.", diffs, dmp!!.diff_main("1ayb2", "abxab", false)) + + diffs = diffList(Diff(INSERT, "xaxcx"), Diff(EQUAL, "abc"), Diff(DELETE, "y")) + assertEquals("diff_main: Overlap #2.", diffs, dmp!!.diff_main("abcy", "xaxcxabc", false)) + + diffs = diffList( + Diff(DELETE, "ABCD"), + Diff(EQUAL, "a"), + Diff(DELETE, "="), + Diff(INSERT, "-"), + Diff(EQUAL, "bcd"), + Diff(DELETE, "="), + Diff(INSERT, "-"), + Diff(EQUAL, "efghijklmnopqrs"), + Diff(DELETE, "EFGHIJKLMNOefg") + ) + assertEquals( + "diff_main: Overlap #3.", + diffs, + dmp!!.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false) + ) + + diffs = diffList( + Diff(INSERT, " "), + Diff(EQUAL, "a"), + Diff(INSERT, "nd"), + Diff(EQUAL, " [[Pennsylvania]]"), + Diff(DELETE, " and [[New") + ) + assertEquals( + "diff_main: Large equality.", + diffs, + dmp!!.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false) + ) + + dmp!!.Diff_Timeout = 0.1f // 100ms + var a = + "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n" + var b = + "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n" + // Increase the text lengths by 1024 times to ensure a timeout. + for (i in 0..9) { + a += a + b += b + } + val startTime = System.currentTimeMillis() + dmp!!.diff_main(a, b) + val endTime = System.currentTimeMillis() + // Test that we took at least the timeout period. + assertTrue("diff_main: Timeout min.", dmp!!.Diff_Timeout * 1000 <= endTime - startTime) + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + assertTrue("diff_main: Timeout max.", dmp!!.Diff_Timeout * 1000 * 2 > endTime - startTime) + dmp!!.Diff_Timeout = 0 + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + a = + "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n" + b = + "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n" + assertEquals("diff_main: Simple line-mode.", dmp!!.diff_main(a, b, true), dmp!!.diff_main(a, b, false)) + + a = + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + b = + "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij" + assertEquals("diff_main: Single line-mode.", dmp!!.diff_main(a, b, true), dmp!!.diff_main(a, b, false)) + + a = + "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n" + b = + "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n" + val texts_linemode = diff_rebuildtexts(dmp!!.diff_main(a, b, true)) + val texts_textmode = diff_rebuildtexts(dmp!!.diff_main(a, b, false)) + assertArrayEquals("diff_main: Overlap line-mode.", texts_textmode, texts_linemode) + + // Test null inputs. + try { + dmp!!.diff_main(null, null) + fail("diff_main: Null inputs.") + } catch (ex: IllegalArgumentException) { + // Error expected. + } + } + + // MATCH TEST FUNCTIONS + + fun testMatchAlphabet() { + // Initialise the bitmasks for Bitap. + val bitmask: MutableMap + bitmask = HashMap() + bitmask['a'] = 4 + bitmask['b'] = 2 + bitmask['c'] = 1 + assertEquals("match_alphabet: Unique.", bitmask, dmp!!.match_alphabet("abc")) + + bitmask = HashMap() + bitmask['a'] = 37 + bitmask['b'] = 18 + bitmask['c'] = 8 + assertEquals("match_alphabet: Duplicates.", bitmask, dmp!!.match_alphabet("abcaba")) + } + + fun testMatchBitap() { + // Bitap algorithm. + dmp!!.Match_Distance = 100 + dmp!!.Match_Threshold = 0.5f + assertEquals("match_bitap: Exact match #1.", 5, dmp!!.match_bitap("abcdefghijk", "fgh", 5)) + + assertEquals("match_bitap: Exact match #2.", 5, dmp!!.match_bitap("abcdefghijk", "fgh", 0)) + + assertEquals("match_bitap: Fuzzy match #1.", 4, dmp!!.match_bitap("abcdefghijk", "efxhi", 0)) + + assertEquals("match_bitap: Fuzzy match #2.", 2, dmp!!.match_bitap("abcdefghijk", "cdefxyhijk", 5)) + + assertEquals("match_bitap: Fuzzy match #3.", -1, dmp!!.match_bitap("abcdefghijk", "bxy", 1)) + + assertEquals("match_bitap: Overflow.", 2, dmp!!.match_bitap("123456789xx0", "3456789x0", 2)) + + assertEquals("match_bitap: Before start match.", 0, dmp!!.match_bitap("abcdef", "xxabc", 4)) + + assertEquals("match_bitap: Beyond end match.", 3, dmp!!.match_bitap("abcdef", "defyy", 4)) + + assertEquals("match_bitap: Oversized pattern.", 0, dmp!!.match_bitap("abcdef", "xabcdefy", 0)) + + dmp!!.Match_Threshold = 0.4f + assertEquals("match_bitap: Threshold #1.", 4, dmp!!.match_bitap("abcdefghijk", "efxyhi", 1)) + + dmp!!.Match_Threshold = 0.3f + assertEquals("match_bitap: Threshold #2.", -1, dmp!!.match_bitap("abcdefghijk", "efxyhi", 1)) + + dmp!!.Match_Threshold = 0.0f + assertEquals("match_bitap: Threshold #3.", 1, dmp!!.match_bitap("abcdefghijk", "bcdef", 1)) + + dmp!!.Match_Threshold = 0.5f + assertEquals("match_bitap: Multiple select #1.", 0, dmp!!.match_bitap("abcdexyzabcde", "abccde", 3)) + + assertEquals("match_bitap: Multiple select #2.", 8, dmp!!.match_bitap("abcdexyzabcde", "abccde", 5)) + + dmp!!.Match_Distance = 10 // Strict location. + assertEquals("match_bitap: Distance test #1.", -1, dmp!!.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)) + + assertEquals("match_bitap: Distance test #2.", 0, dmp!!.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)) + + dmp!!.Match_Distance = 1000 // Loose location. + assertEquals("match_bitap: Distance test #3.", 0, dmp!!.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)) + } + + fun testMatchMain() { + // Full match. + assertEquals("match_main: Equality.", 0, dmp!!.match_main("abcdef", "abcdef", 1000)) + + assertEquals("match_main: Null text.", -1, dmp!!.match_main("", "abcdef", 1)) + + assertEquals("match_main: Null pattern.", 3, dmp!!.match_main("abcdef", "", 3)) + + assertEquals("match_main: Exact match.", 3, dmp!!.match_main("abcdef", "de", 3)) + + assertEquals("match_main: Beyond end match.", 3, dmp!!.match_main("abcdef", "defy", 4)) + + assertEquals("match_main: Oversized pattern.", 0, dmp!!.match_main("abcdef", "abcdefy", 0)) + + dmp!!.Match_Threshold = 0.7f + assertEquals( + "match_main: Complex match.", + 4, + dmp!!.match_main("I am the very model of a modern major general.", " that berry ", 5) + ) + dmp!!.Match_Threshold = 0.5f + + // Test null inputs. + try { + dmp!!.match_main(null, null, 0) + fail("match_main: Null inputs.") + } catch (ex: IllegalArgumentException) { + // Error expected. + } + } + + // PATCH TEST FUNCTIONS + + fun testPatchObj() { + // Patch Object. + val p = Patch() + p.start1 = 20 + p.start2 = 21 + p.length1 = 18 + p.length2 = 17 + p.diffs = diffList( + Diff(EQUAL, "jump"), + Diff(DELETE, "s"), + Diff(INSERT, "ed"), + Diff(EQUAL, " over "), + Diff(DELETE, "the"), + Diff(INSERT, "a"), + Diff(EQUAL, "\nlaz") + ) + val strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n" + assertEquals("Patch: toString.", strp, p.toString()) + } + + fun testPatchFromText() { + assertTrue("patch_fromText: #0.", dmp!!.patch_fromText("").isEmpty()) + + val strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n" + assertEquals("patch_fromText: #1.", strp, dmp!!.patch_fromText(strp).get(0).toString()) + + assertEquals( + "patch_fromText: #2.", + "@@ -1 +1 @@\n-a\n+b\n", + dmp!!.patch_fromText("@@ -1 +1 @@\n-a\n+b\n").get(0).toString() + ) + + assertEquals( + "patch_fromText: #3.", + "@@ -1,3 +0,0 @@\n-abc\n", + dmp!!.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n").get(0).toString() + ) + + assertEquals( + "patch_fromText: #4.", + "@@ -0,0 +1,3 @@\n+abc\n", + dmp!!.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n").get(0).toString() + ) + + // Generates error. + try { + dmp!!.patch_fromText("Bad\nPatch\n") + fail("patch_fromText: #5.") + } catch (ex: IllegalArgumentException) { + // Exception expected. + } + } + + fun testPatchToText() { + var strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n" + val patches: List + patches = dmp!!.patch_fromText(strp) + assertEquals("patch_toText: Single.", strp, dmp!!.patch_toText(patches)) + + strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n" + patches = dmp!!.patch_fromText(strp) + assertEquals("patch_toText: Dual.", strp, dmp!!.patch_toText(patches)) + } + + fun testPatchAddContext() { + dmp!!.Patch_Margin = 4 + val p: Patch + p = dmp!!.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").get(0) + dmp!!.patch_addContext(p, "The quick brown fox jumps over the lazy dog.") + assertEquals( + "patch_addContext: Simple case.", + "@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", + p.toString() + ) + + p = dmp!!.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").get(0) + dmp!!.patch_addContext(p, "The quick brown fox jumps.") + assertEquals( + "patch_addContext: Not enough trailing context.", + "@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", + p.toString() + ) + + p = dmp!!.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").get(0) + dmp!!.patch_addContext(p, "The quick brown fox jumps.") + assertEquals( + "patch_addContext: Not enough leading context.", + "@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", + p.toString() + ) + + p = dmp!!.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").get(0) + dmp!!.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes.") + assertEquals( + "patch_addContext: Ambiguity.", + "@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", + p.toString() + ) + } + + fun testPatchMake() { + val patches: LinkedList + patches = dmp!!.patch_make("", "") + assertEquals("patch_make: Null case.", "", dmp!!.patch_toText(patches)) + + var text1 = "The quick brown fox jumps over the lazy dog." + var text2 = "That quick brown fox jumped over a lazy dog." + var expectedPatch = + "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n" + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = dmp!!.patch_make(text2, text1) + assertEquals("patch_make: Text2+Text1 inputs.", expectedPatch, dmp!!.patch_toText(patches)) + + expectedPatch = + "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n" + patches = dmp!!.patch_make(text1, text2) + assertEquals("patch_make: Text1+Text2 inputs.", expectedPatch, dmp!!.patch_toText(patches)) + + var diffs = dmp!!.diff_main(text1, text2, false) + patches = dmp!!.patch_make(diffs) + assertEquals("patch_make: Diff input.", expectedPatch, dmp!!.patch_toText(patches)) + + patches = dmp!!.patch_make(text1, diffs) + assertEquals("patch_make: Text1+Diff inputs.", expectedPatch, dmp!!.patch_toText(patches)) + + patches = dmp!!.patch_make(text1, text2, diffs) + assertEquals("patch_make: Text1+Text2+Diff inputs (deprecated).", expectedPatch, dmp!!.patch_toText(patches)) + + patches = dmp!!.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?") + assertEquals( + "patch_toText: Character encoding.", + "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", + dmp!!.patch_toText(patches) + ) + + diffs = diffList(Diff(DELETE, "`1234567890-=[]\\;',./"), Diff(INSERT, "~!@#$%^&*()_+{}|:\"<>?")) + assertEquals( + "patch_fromText: Character decoding.", + diffs, + dmp!!.patch_fromText("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n").get( + 0 + ).diffs + ) + + text1 = "" + for (x in 0..99) { + text1 += "abcdef" + } + text2 = text1 + "123" + expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n" + patches = dmp!!.patch_make(text1, text2) + assertEquals("patch_make: Long string with repeats.", expectedPatch, dmp!!.patch_toText(patches)) + + // Test null inputs. + try { + dmp!!.patch_make(null) + fail("patch_make: Null inputs.") + } catch (ex: IllegalArgumentException) { + // Error expected. + } + } + + fun testPatchSplitMax() { + // Assumes that Match_MaxBits is 32. + val patches: LinkedList + patches = dmp!!.patch_make( + "abcdefghijklmnopqrstuvwxyz01234567890", + "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0" + ) + dmp!!.patch_splitMax(patches) + assertEquals( + "patch_splitMax: #1.", + "@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", + dmp!!.patch_toText(patches) + ) + + patches = dmp!!.patch_make( + "abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", + "abcdefuvwxyz" + ) + val oldToText = dmp!!.patch_toText(patches) + dmp!!.patch_splitMax(patches) + assertEquals("patch_splitMax: #2.", oldToText, dmp!!.patch_toText(patches)) + + patches = dmp!!.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc") + dmp!!.patch_splitMax(patches) + assertEquals( + "patch_splitMax: #3.", + "@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", + dmp!!.patch_toText(patches) + ) + + patches = dmp!!.patch_make( + "abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", + "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1" + ) + dmp!!.patch_splitMax(patches) + assertEquals( + "patch_splitMax: #4.", + "@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", + dmp!!.patch_toText(patches) + ) + } + + fun testPatchAddPadding() { + val patches: LinkedList + patches = dmp!!.patch_make("", "test") + assertEquals("patch_addPadding: Both edges full.", "@@ -0,0 +1,4 @@\n+test\n", dmp!!.patch_toText(patches)) + dmp!!.patch_addPadding(patches) + assertEquals( + "patch_addPadding: Both edges full.", + "@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", + dmp!!.patch_toText(patches) + ) + + patches = dmp!!.patch_make("XY", "XtestY") + assertEquals( + "patch_addPadding: Both edges partial.", + "@@ -1,2 +1,6 @@\n X\n+test\n Y\n", + dmp!!.patch_toText(patches) + ) + dmp!!.patch_addPadding(patches) + assertEquals( + "patch_addPadding: Both edges partial.", + "@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", + dmp!!.patch_toText(patches) + ) + + patches = dmp!!.patch_make("XXXXYYYY", "XXXXtestYYYY") + assertEquals( + "patch_addPadding: Both edges none.", + "@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", + dmp!!.patch_toText(patches) + ) + dmp!!.patch_addPadding(patches) + assertEquals( + "patch_addPadding: Both edges none.", + "@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", + dmp!!.patch_toText(patches) + ) + } + + fun testPatchApply() { + dmp!!.Match_Distance = 1000 + dmp!!.Match_Threshold = 0.5f + dmp!!.Patch_DeleteThreshold = 0.5f + val patches: LinkedList + patches = dmp!!.patch_make("", "") + var results = dmp!!.patch_apply(patches, "Hello world.") + var boolArray = results[1] as BooleanArray + var resultStr = results[0] + "\t" + boolArray.size + assertEquals("patch_apply: Null case.", "Hello world.\t0", resultStr) + + patches = + dmp!!.patch_make("The quick brown fox jumps over the lazy dog.", "That quick brown fox jumped over a lazy dog.") + results = dmp!!.patch_apply(patches, "The quick brown fox jumps over the lazy dog.") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals("patch_apply: Exact match.", "That quick brown fox jumped over a lazy dog.\ttrue\ttrue", resultStr) + + results = dmp!!.patch_apply(patches, "The quick red rabbit jumps over the tired tiger.") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals( + "patch_apply: Partial match.", + "That quick red rabbit jumped over a tired tiger.\ttrue\ttrue", + resultStr + ) + + results = dmp!!.patch_apply(patches, "I am the very model of a modern major general.") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals( + "patch_apply: Failed match.", + "I am the very model of a modern major general.\tfalse\tfalse", + resultStr + ) + + patches = dmp!!.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = + dmp!!.patch_apply(patches, "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals("patch_apply: Big delete, small change.", "xabcy\ttrue\ttrue", resultStr) + + patches = dmp!!.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = + dmp!!.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals( + "patch_apply: Big delete, big change 1.", + "xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue", + resultStr + ) + + dmp!!.Patch_DeleteThreshold = 0.6f + patches = dmp!!.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = + dmp!!.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals("patch_apply: Big delete, big change 2.", "xabcy\ttrue\ttrue", resultStr) + dmp!!.Patch_DeleteThreshold = 0.5f + + // Compensate for failed patch. + dmp!!.Match_Threshold = 0.0f + dmp!!.Match_Distance = 0 + patches = dmp!!.patch_make( + "abcdefghijklmnopqrstuvwxyz--------------------1234567890", + "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890" + ) + results = dmp!!.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1] + assertEquals( + "patch_apply: Compensate for failed patch.", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue", + resultStr + ) + dmp!!.Match_Threshold = 0.5f + dmp!!.Match_Distance = 1000 + + patches = dmp!!.patch_make("", "test") + var patchStr = dmp!!.patch_toText(patches) + dmp!!.patch_apply(patches, "") + assertEquals("patch_apply: No side effects.", patchStr, dmp!!.patch_toText(patches)) + + patches = dmp!!.patch_make("The quick brown fox jumps over the lazy dog.", "Woof") + patchStr = dmp!!.patch_toText(patches) + dmp!!.patch_apply(patches, "The quick brown fox jumps over the lazy dog.") + assertEquals("patch_apply: No side effects with major delete.", patchStr, dmp!!.patch_toText(patches)) + + patches = dmp!!.patch_make("", "test") + results = dmp!!.patch_apply(patches, "") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + assertEquals("patch_apply: Edge exact match.", "test\ttrue", resultStr) + + patches = dmp!!.patch_make("XY", "XtestY") + results = dmp!!.patch_apply(patches, "XY") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + assertEquals("patch_apply: Near edge exact match.", "XtestY\ttrue", resultStr) + + patches = dmp!!.patch_make("y", "y123") + results = dmp!!.patch_apply(patches, "x") + boolArray = results[1] as BooleanArray + resultStr = results[0] + "\t" + boolArray[0] + assertEquals("patch_apply: Edge partial match.", "x123\ttrue", resultStr) + } + + private fun assertEquals(error_msg: String, a: Any, b: Any) { + if (a.toString() != b.toString()) { + throw Error( + "assertEquals fail:\n Expected: " + a + "\n Actual: " + b + + "\n" + error_msg + ) + } + } + + private fun assertTrue(error_msg: String, a: Boolean) { + if (!a) { + throw Error("assertTrue fail: $error_msg") + } + } + + private fun assertNull(error_msg: String, n: Any?) { + if (n != null) { + throw Error("assertNull fail: $error_msg") + } + } + + private fun fail(error_msg: String) { + throw Error("Fail: $error_msg") + } + + private fun assertArrayEquals(error_msg: String, a: Array, b: Array) { + val list_a = Arrays.asList(*a) + val list_b = Arrays.asList(*b) + assertEquals(error_msg, list_a, list_b) + } + + private fun assertLinesToCharsResultEquals( + error_msg: String, + a: LinesToCharsResult, b: LinesToCharsResult + ) { + assertEquals(error_msg, a.chars1, b.chars1) + assertEquals(error_msg, a.chars2, b.chars2) + assertEquals(error_msg, a.lineArray, b.lineArray) + } + + // Construct the two texts which made up the diff originally. + private fun diff_rebuildtexts(diffs: LinkedList): Array { + val text = arrayOf("", "") + for (myDiff in diffs) { + if (myDiff.operation !== diff_match_patch.Operation.INSERT) { + text[0] += myDiff.text + } + if (myDiff.operation !== diff_match_patch.Operation.DELETE) { + text[1] += myDiff.text + } + } + return text + } + + // Private function for quickly building lists of diffs. + private fun diffList(vararg diffs: Diff): LinkedList { + return LinkedList(Arrays.asList(*diffs)) + } + + @JvmStatic + fun main(args: Array) { + dmp = diff_match_patch() + + testDiffCommonPrefix() + testDiffCommonSuffix() + testDiffCommonOverlap() + testDiffHalfmatch() + testDiffLinesToChars() + testDiffCharsToLines() + testDiffCleanupMerge() + testDiffCleanupSemanticLossless() + testDiffCleanupSemantic() + testDiffCleanupEfficiency() + testDiffPrettyHtml() + testDiffText() + testDiffDelta() + testDiffXIndex() + testDiffLevenshtein() + testDiffBisect() + testDiffMain() + + testMatchAlphabet() + testMatchBitap() + testMatchMain() + + testPatchObj() + testPatchFromText() + testPatchToText() + testPatchAddContext() + testPatchMake() + testPatchSplitMax() + testPatchAddPadding() + testPatchApply() + + println("All tests passed.") + } +}