LevenshteinDetailedDistance.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.text.similarity;

  18. import java.util.Arrays;

  19. /**
  20.  * An algorithm for measuring the difference between two character sequences.
  21.  *
  22.  * <p>
  23.  * This is the number of changes needed to change one sequence into another,
  24.  * where each change is a single character modification (deletion, insertion
  25.  * or substitution).
  26.  * </p>
  27.  *
  28.  * @since 1.0
  29.  */
  30. public class LevenshteinDetailedDistance implements EditDistance<LevenshteinResults> {

  31.     /**
  32.      * Default instance.
  33.      */
  34.     private static final LevenshteinDetailedDistance DEFAULT_INSTANCE = new LevenshteinDetailedDistance();
  35.     /**
  36.      * Threshold.
  37.      */
  38.     private final Integer threshold;

  39.     /**
  40.      * <p>
  41.      * This returns the default instance that uses a version
  42.      * of the algorithm that does not use a threshold parameter.
  43.      * </p>
  44.      *
  45.      * @see LevenshteinDetailedDistance#getDefaultInstance()
  46.      */
  47.     public LevenshteinDetailedDistance() {
  48.         this(null);
  49.     }

  50.     /**
  51.      * If the threshold is not null, distance calculations will be limited to a maximum length.
  52.      *
  53.      * <p>If the threshold is null, the unlimited version of the algorithm will be used.</p>
  54.      *
  55.      * @param threshold If this is null then distances calculations will not be limited. This may not be negative.
  56.      */
  57.     public LevenshteinDetailedDistance(final Integer threshold) {
  58.         if (threshold != null && threshold < 0) {
  59.             throw new IllegalArgumentException("Threshold must not be negative");
  60.         }
  61.         this.threshold = threshold;
  62.     }

  63.     /**
  64.      * <p>Find the Levenshtein distance between two Strings.</p>
  65.      *
  66.      * <p>A higher score indicates a greater distance.</p>
  67.      *
  68.      * <p>The previous implementation of the Levenshtein distance algorithm
  69.      * was from <a href="http://www.merriampark.com/ld.htm">http://www.merriampark.com/ld.htm</a></p>
  70.      *
  71.      * <p>Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError
  72.      * which can occur when my Java implementation is used with very large strings.<br>
  73.      * This implementation of the Levenshtein distance algorithm
  74.      * is from <a href="http://www.merriampark.com/ldjava.htm">http://www.merriampark.com/ldjava.htm</a></p>
  75.      *
  76.      * <pre>
  77.      * distance.apply(null, *)             = IllegalArgumentException
  78.      * distance.apply(*, null)             = IllegalArgumentException
  79.      * distance.apply("","")               = 0
  80.      * distance.apply("","a")              = 1
  81.      * distance.apply("aaapppp", "")       = 7
  82.      * distance.apply("frog", "fog")       = 1
  83.      * distance.apply("fly", "ant")        = 3
  84.      * distance.apply("elephant", "hippo") = 7
  85.      * distance.apply("hippo", "elephant") = 7
  86.      * distance.apply("hippo", "zzzzzzzz") = 8
  87.      * distance.apply("hello", "hallo")    = 1
  88.      * </pre>
  89.      *
  90.      * @param left the first string, must not be null
  91.      * @param right the second string, must not be null
  92.      * @return result distance, or -1
  93.      * @throws IllegalArgumentException if either String input {@code null}
  94.      */
  95.     @Override
  96.     public LevenshteinResults apply(final CharSequence left, final CharSequence right) {
  97.         if (threshold != null) {
  98.             return limitedCompare(left, right, threshold);
  99.         }
  100.         return unlimitedCompare(left, right);
  101.     }

  102.     /**
  103.      * Gets the default instance.
  104.      *
  105.      * @return the default instace
  106.      */
  107.     public static LevenshteinDetailedDistance getDefaultInstance() {
  108.         return DEFAULT_INSTANCE;
  109.     }

  110.     /**
  111.      * Gets the distance threshold.
  112.      *
  113.      * @return the distance threshold
  114.      */
  115.     public Integer getThreshold() {
  116.         return threshold;
  117.     }

  118.     /**
  119.      * Find the Levenshtein distance between two CharSequences if it's less than or
  120.      * equal to a given threshold.
  121.      *
  122.      * <p>
  123.      * This implementation follows from Algorithms on Strings, Trees and
  124.      * Sequences by Dan Gusfield and Chas Emerick's implementation of the
  125.      * Levenshtein distance algorithm from <a
  126.      * href="http://www.merriampark.com/ld.htm"
  127.      * >http://www.merriampark.com/ld.htm</a>
  128.      * </p>
  129.      *
  130.      * <pre>
  131.      * limitedCompare(null, *, *)             = IllegalArgumentException
  132.      * limitedCompare(*, null, *)             = IllegalArgumentException
  133.      * limitedCompare(*, *, -1)               = IllegalArgumentException
  134.      * limitedCompare("","", 0)               = 0
  135.      * limitedCompare("aaapppp", "", 8)       = 7
  136.      * limitedCompare("aaapppp", "", 7)       = 7
  137.      * limitedCompare("aaapppp", "", 6))      = -1
  138.      * limitedCompare("elephant", "hippo", 7) = 7
  139.      * limitedCompare("elephant", "hippo", 6) = -1
  140.      * limitedCompare("hippo", "elephant", 7) = 7
  141.      * limitedCompare("hippo", "elephant", 6) = -1
  142.      * </pre>
  143.      *
  144.      * @param left the first string, must not be null
  145.      * @param right the second string, must not be null
  146.      * @param threshold the target threshold, must not be negative
  147.      * @return result distance, or -1
  148.      */
  149.     private static LevenshteinResults limitedCompare(CharSequence left, CharSequence right, final int threshold) { //NOPMD
  150.         if (left == null || right == null) {
  151.             throw new IllegalArgumentException("Strings must not be null");
  152.         }
  153.         if (threshold < 0) {
  154.             throw new IllegalArgumentException("Threshold must not be negative");
  155.         }

  156.         /*
  157.          * This implementation only computes the distance if it's less than or
  158.          * equal to the threshold value, returning -1 if it's greater. The
  159.          * advantage is performance: unbounded distance is O(nm), but a bound of
  160.          * k allows us to reduce it to O(km) time by only computing a diagonal
  161.          * stripe of width 2k + 1 of the cost table. It is also possible to use
  162.          * this to compute the unbounded Levenshtein distance by starting the
  163.          * threshold at 1 and doubling each time until the distance is found;
  164.          * this is O(dm), where d is the distance.
  165.          *
  166.          * One subtlety comes from needing to ignore entries on the border of
  167.          * our stripe eg. p[] = |#|#|#|* d[] = *|#|#|#| We must ignore the entry
  168.          * to the left of the leftmost member We must ignore the entry above the
  169.          * rightmost member
  170.          *
  171.          * Another subtlety comes from our stripe running off the matrix if the
  172.          * strings aren't of the same size. Since string s is always swapped to
  173.          * be the shorter of the two, the stripe will always run off to the
  174.          * upper right instead of the lower left of the matrix.
  175.          *
  176.          * As a concrete example, suppose s is of length 5, t is of length 7,
  177.          * and our threshold is 1. In this case we're going to walk a stripe of
  178.          * length 3. The matrix would look like so:
  179.          *
  180.          * <pre>
  181.          *    1 2 3 4 5
  182.          * 1 |#|#| | | |
  183.          * 2 |#|#|#| | |
  184.          * 3 | |#|#|#| |
  185.          * 4 | | |#|#|#|
  186.          * 5 | | | |#|#|
  187.          * 6 | | | | |#|
  188.          * 7 | | | | | |
  189.          * </pre>
  190.          *
  191.          * Note how the stripe leads off the table as there is no possible way
  192.          * to turn a string of length 5 into one of length 7 in edit distance of
  193.          * 1.
  194.          *
  195.          * Additionally, this implementation decreases memory usage by using two
  196.          * single-dimensional arrays and swapping them back and forth instead of
  197.          * allocating an entire n by m matrix. This requires a few minor
  198.          * changes, such as immediately returning when it's detected that the
  199.          * stripe has run off the matrix and initially filling the arrays with
  200.          * large values so that entries we don't compute are ignored.
  201.          *
  202.          * See Algorithms on Strings, Trees and Sequences by Dan Gusfield for
  203.          * some discussion.
  204.          */

  205.         int n = left.length(); // length of left
  206.         int m = right.length(); // length of right

  207.         // if one string is empty, the edit distance is necessarily the length of the other
  208.         if (n == 0) {
  209.             return m <= threshold ? new LevenshteinResults(m, m, 0, 0) : new LevenshteinResults(-1, 0, 0, 0);
  210.         } else if (m == 0) {
  211.             return n <= threshold ? new LevenshteinResults(n, 0, n, 0) : new LevenshteinResults(-1, 0, 0, 0);
  212.         }

  213.         boolean swapped = false;
  214.         if (n > m) {
  215.             // swap the two strings to consume less memory
  216.             final CharSequence tmp = left;
  217.             left = right;
  218.             right = tmp;
  219.             n = m;
  220.             m = right.length();
  221.             swapped = true;
  222.         }

  223.         int[] p = new int[n + 1]; // 'previous' cost array, horizontally
  224.         int[] d = new int[n + 1]; // cost array, horizontally
  225.         int[] tempD; // placeholder to assist in swapping p and d
  226.         final int[][] matrix = new int[m + 1][n + 1];

  227.         //filling the first row and first column values in the matrix
  228.         for (int index = 0; index <= n; index++) {
  229.             matrix[0][index] = index;
  230.         }
  231.         for (int index = 0; index <= m; index++) {
  232.             matrix[index][0] = index;
  233.         }

  234.         // fill in starting table values
  235.         final int boundary = Math.min(n, threshold) + 1;
  236.         for (int i = 0; i < boundary; i++) {
  237.             p[i] = i;
  238.         }
  239.         // these fills ensure that the value above the rightmost entry of our
  240.         // stripe will be ignored in following loop iterations
  241.         Arrays.fill(p, boundary, p.length, Integer.MAX_VALUE);
  242.         Arrays.fill(d, Integer.MAX_VALUE);

  243.         // iterates through t
  244.         for (int j = 1; j <= m; j++) {
  245.             final char rightJ = right.charAt(j - 1); // jth character of right
  246.             d[0] = j;

  247.             // compute stripe indices, constrain to array size
  248.             final int min = Math.max(1, j - threshold);
  249.             final int max = j > Integer.MAX_VALUE - threshold ? n : Math.min(
  250.                     n, j + threshold);

  251.             // the stripe may lead off of the table if s and t are of different sizes
  252.             if (min > max) {
  253.                 return new LevenshteinResults(-1, 0, 0, 0);
  254.             }

  255.             // ignore entry left of leftmost
  256.             if (min > 1) {
  257.                 d[min - 1] = Integer.MAX_VALUE;
  258.             }

  259.             // iterates through [min, max] in s
  260.             for (int i = min; i <= max; i++) {
  261.                 if (left.charAt(i - 1) == rightJ) {
  262.                     // diagonally left and up
  263.                     d[i] = p[i - 1];
  264.                 } else {
  265.                     // 1 + minimum of cell to the left, to the top, diagonally left and up
  266.                     d[i] = 1 + Math.min(Math.min(d[i - 1], p[i]), p[i - 1]);
  267.                 }
  268.                 matrix[j][i] = d[i];
  269.             }

  270.             // copy current distance counts to 'previous row' distance counts
  271.             tempD = p;
  272.             p = d;
  273.             d = tempD;
  274.         }

  275.         // if p[n] is greater than the threshold, there's no guarantee on it being the correct distance
  276.         if (p[n] <= threshold) {
  277.             return findDetailedResults(left, right, matrix, swapped);
  278.         }
  279.         return new LevenshteinResults(-1, 0, 0, 0);
  280.     }

  281.     /**
  282.      * <p>Find the Levenshtein distance between two Strings.</p>
  283.      *
  284.      * <p>A higher score indicates a greater distance.</p>
  285.      *
  286.      * <p>The previous implementation of the Levenshtein distance algorithm
  287.      * was from <a href="http://www.merriampark.com/ld.htm">http://www.merriampark.com/ld.htm</a></p>
  288.      *
  289.      * <p>Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError
  290.      * which can occur when my Java implementation is used with very large strings.<br>
  291.      * This implementation of the Levenshtein distance algorithm
  292.      * is from <a href="http://www.merriampark.com/ldjava.htm">http://www.merriampark.com/ldjava.htm</a></p>
  293.      *
  294.      * <pre>
  295.      * unlimitedCompare(null, *)             = IllegalArgumentException
  296.      * unlimitedCompare(*, null)             = IllegalArgumentException
  297.      * unlimitedCompare("","")               = 0
  298.      * unlimitedCompare("","a")              = 1
  299.      * unlimitedCompare("aaapppp", "")       = 7
  300.      * unlimitedCompare("frog", "fog")       = 1
  301.      * unlimitedCompare("fly", "ant")        = 3
  302.      * unlimitedCompare("elephant", "hippo") = 7
  303.      * unlimitedCompare("hippo", "elephant") = 7
  304.      * unlimitedCompare("hippo", "zzzzzzzz") = 8
  305.      * unlimitedCompare("hello", "hallo")    = 1
  306.      * </pre>
  307.      *
  308.      * @param left the first String, must not be null
  309.      * @param right the second String, must not be null
  310.      * @return result distance, or -1
  311.      * @throws IllegalArgumentException if either String input {@code null}
  312.      */
  313.     private static LevenshteinResults unlimitedCompare(CharSequence left, CharSequence right) {
  314.         if (left == null || right == null) {
  315.             throw new IllegalArgumentException("Strings must not be null");
  316.         }

  317.         /*
  318.            The difference between this impl. and the previous is that, rather
  319.            than creating and retaining a matrix of size s.length() + 1 by t.length() + 1,
  320.            we maintain two single-dimensional arrays of length s.length() + 1.  The first, d,
  321.            is the 'current working' distance array that maintains the newest distance cost
  322.            counts as we iterate through the characters of String s.  Each time we increment
  323.            the index of String t we are comparing, d is copied to p, the second int[].  Doing so
  324.            allows us to retain the previous cost counts as required by the algorithm (taking
  325.            the minimum of the cost count to the left, up one, and diagonally up and to the left
  326.            of the current cost count being calculated).  (Note that the arrays aren't really
  327.            copied anymore, just switched...this is clearly much better than cloning an array
  328.            or doing a System.arraycopy() each time  through the outer loop.)

  329.            Effectively, the difference between the two implementations is this one does not
  330.            cause an out of memory condition when calculating the LD over two very large strings.
  331.          */

  332.         int n = left.length(); // length of left
  333.         int m = right.length(); // length of right

  334.         if (n == 0) {
  335.             return new LevenshteinResults(m, m, 0, 0);
  336.         } else if (m == 0) {
  337.             return new LevenshteinResults(n, 0, n, 0);
  338.         }
  339.         boolean swapped = false;
  340.         if (n > m) {
  341.             // swap the input strings to consume less memory
  342.             final CharSequence tmp = left;
  343.             left = right;
  344.             right = tmp;
  345.             n = m;
  346.             m = right.length();
  347.             swapped = true;
  348.         }

  349.         int[] p = new int[n + 1]; // 'previous' cost array, horizontally
  350.         int[] d = new int[n + 1]; // cost array, horizontally
  351.         int[] tempD; //placeholder to assist in swapping p and d
  352.         final int[][] matrix = new int[m + 1][n + 1];

  353.         // filling the first row and first column values in the matrix
  354.         for (int index = 0; index <= n; index++) {
  355.             matrix[0][index] = index;
  356.         }
  357.         for (int index = 0; index <= m; index++) {
  358.             matrix[index][0] = index;
  359.         }

  360.         // indexes into strings left and right
  361.         int i; // iterates through left
  362.         int j; // iterates through right

  363.         char rightJ; // jth character of right

  364.         int cost; // cost
  365.         for (i = 0; i <= n; i++) {
  366.             p[i] = i;
  367.         }

  368.         for (j = 1; j <= m; j++) {
  369.             rightJ = right.charAt(j - 1);
  370.             d[0] = j;

  371.             for (i = 1; i <= n; i++) {
  372.                 cost = left.charAt(i - 1) == rightJ ? 0 : 1;
  373.                 // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
  374.                 d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
  375.                 //filling the matrix
  376.                 matrix[j][i] = d[i];
  377.             }

  378.             // copy current distance counts to 'previous row' distance counts
  379.             tempD = p;
  380.             p = d;
  381.             d = tempD;
  382.         }
  383.         return findDetailedResults(left, right, matrix, swapped);
  384.     }

  385.     /**
  386.      * Finds count for each of the three [insert, delete, substitute] operations
  387.      * needed. This is based on the matrix formed based on the two character
  388.      * sequence.
  389.      *
  390.      * @param left character sequence which need to be converted from
  391.      * @param right character sequence which need to be converted to
  392.      * @param matrix two dimensional array containing
  393.      * @param swapped tells whether the value for left character sequence and right
  394.      *            character sequence were swapped to save memory
  395.      * @return result object containing the count of insert, delete and substitute and total count needed
  396.      */
  397.     private static LevenshteinResults findDetailedResults(final CharSequence left, final CharSequence right, final int[][] matrix,
  398.             final boolean swapped) {

  399.         int delCount = 0;
  400.         int addCount = 0;
  401.         int subCount = 0;

  402.         int rowIndex = right.length();
  403.         int columnIndex = left.length();

  404.         int dataAtLeft = 0;
  405.         int dataAtTop = 0;
  406.         int dataAtDiagonal = 0;
  407.         int data = 0;
  408.         boolean deleted = false;
  409.         boolean added = false;

  410.         while (rowIndex >= 0 && columnIndex >= 0) {

  411.             if (columnIndex == 0) {
  412.                 dataAtLeft = -1;
  413.             } else {
  414.                 dataAtLeft = matrix[rowIndex][columnIndex - 1];
  415.             }
  416.             if (rowIndex == 0) {
  417.                 dataAtTop = -1;
  418.             } else {
  419.                 dataAtTop = matrix[rowIndex - 1][columnIndex];
  420.             }
  421.             if (rowIndex > 0 && columnIndex > 0) {
  422.                 dataAtDiagonal = matrix[rowIndex - 1][columnIndex - 1];
  423.             } else {
  424.                 dataAtDiagonal = -1;
  425.             }
  426.             if (dataAtLeft == -1 && dataAtTop == -1 && dataAtDiagonal == -1) {
  427.                 break;
  428.             }
  429.             data = matrix[rowIndex][columnIndex];

  430.             // case in which the character at left and right are the same,
  431.             // in this case none of the counters will be incremented.
  432.             if (columnIndex > 0 && rowIndex > 0 && left.charAt(columnIndex - 1) == right.charAt(rowIndex - 1)) {
  433.                 columnIndex--;
  434.                 rowIndex--;
  435.                 continue;
  436.             }

  437.             // handling insert and delete cases.
  438.             deleted = false;
  439.             added = false;
  440.             if (data - 1 == dataAtLeft && (data <= dataAtDiagonal && data <= dataAtTop)
  441.                     || (dataAtDiagonal == -1 && dataAtTop == -1)) { // NOPMD
  442.                 columnIndex--;
  443.                 if (swapped) {
  444.                     addCount++;
  445.                     added = true;
  446.                 } else {
  447.                     delCount++;
  448.                     deleted = true;
  449.                 }
  450.             } else if (data - 1 == dataAtTop && (data <= dataAtDiagonal && data <= dataAtLeft)
  451.                     || (dataAtDiagonal == -1 && dataAtLeft == -1)) { // NOPMD
  452.                 rowIndex--;
  453.                 if (swapped) {
  454.                     delCount++;
  455.                     deleted = true;
  456.                 } else {
  457.                     addCount++;
  458.                     added = true;
  459.                 }
  460.             }

  461.             // substituted case
  462.             if (!added && !deleted) {
  463.                 subCount++;
  464.                 columnIndex--;
  465.                 rowIndex--;
  466.             }
  467.         }
  468.         return new LevenshteinResults(addCount + delCount + subCount, addCount, delCount, subCount);
  469.     }
  470. }