com.google.gwt.dev.util.editdistance.GeneralEditDistances Maven / Gradle / Ivy
/*
* Copyright 2010 Google Inc.
*
* 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 com.google.gwt.dev.util.editdistance;
/**
* A collection of instance generators for the GeneralEditDistance interface.
*/
public class GeneralEditDistances {
/**
* Chooses the best implementation of Levenshtein string edit distance
* available at the current time.
*/
/*
* As of 2007-08-23, the best algorithm known (to the author=mwyoung) for
* short strings is one due to Eugene Myers, except for the special case
* where the distance limit is 0 or 1. The Myers algorithm also has good
* worst-case performance for long strings when the edit distance is not
* reasonably bounded.
*
* When there is a good bound, a variant of the Ukkonen algorithm due to
* Berghel and Roach (modified by Michael Young to use linear space)
* is faster for long strings.
*
* Note that other algorithms that perform better in some cases for running
* text searches do not outperform Myers for rigid distance computations.
* Notably:
* Navarro/Baeza-Yates (Algorithmica 23,2) simulates an NFA with an
* epsilon-cycle on the initial state (appropriate for running texts)
* and reports success without computing exact distance. When adjusted
* to a fixed starting point and computing distance, its state machine
* is larger and it underperforms.
*
* BITAP (Baeza-Yates/Gonnet, Manber/Wu) also simulates an NFA, and
* Navarro claims that it wins for small patterns and small limits for
* running search. Experiments with a Java implementation showed that
* it beat Myers on pure string edit distance only for limits where the
* special 0-1 limit applied, where special-case comparison beats all.
*
* A survey of algorithms for running text search by Navarro appeared
* in ACM Computing Surveys 33#1: http://portal.acm.org/citation.cfm?id=375365
* Another algorithm (Four Russians) that Navarro claims superior for very
* long patterns and high limits was not evaluated for inclusion here.
* Filtering algorithms also improve running search, but do not help
* for pure edit distance.
*/
private static class Levenshtein implements GeneralEditDistance {
/**
* Long+bounded implementation class: distance-only Berghel-Roach.
*/
private ModifiedBerghelRoachEditDistance berghel;
/**
* Short/unbounded implementation class: Myers bit-parallel.
*/
private MyersBitParallelEditDistance myers;
/**
* Saved pattern, for specialized comparisons.
*/
private final CharSequence pattern;
/**
* Length of saved pattern.
*/
private final int patternLength;
private Levenshtein(CharSequence pattern) {
this.pattern = pattern;
this.patternLength = pattern.length();
}
public GeneralEditDistance duplicate() {
Levenshtein dup = new Levenshtein(pattern);
/* Duplicate the Myers engine, as it is cheaper than rebuilding */
if (this.myers != null) {
dup.myers = (MyersBitParallelEditDistance) this.myers.duplicate();
}
/* Do not duplicate the Berghel engine; it provides no savings. */
return dup;
}
public int getDistance(CharSequence target, int limit) {
/* When the limit is 0 or 1, specialized comparisons are much faster. */
if (limit <= 1) {
return limit == 0 ?
(pattern.equals(target) ? 0 : 1) :
atMostOneError(pattern, target);
}
/*
* The best algorithm for long strings depends on the resulting
* edit distance (or the limit placed on it). Without further
* information on the likelihood of a low distance, we guess
* based on the provided limit. We currently lean toward using
* the Myers algorithm unless we are pretty sure that the
* Berghel-Roach algorithm will win (based on the limit).
*
* Note that when the string lengths are small (fewer characters
* than bits in a long), Myers wins regardless of limit.
*/
if ((patternLength > 64)
&& (limit < (target.length() / 10))) {
if (berghel == null) {
berghel = ModifiedBerghelRoachEditDistance.getInstance(pattern);
}
return berghel.getDistance(target, limit);
}
if (myers == null) {
myers = MyersBitParallelEditDistance.getInstance(pattern);
}
return myers.getDistance(target, limit);
}
}
/**
* Compares two strings for at most one insert/delete/substitute difference.
* Since operations cannot be composed, a simple case analysis is possible.
*
* @param s1 one string to be compared
* @param s2 the other string to be compared
* @return Levenshtein edit distance if no greater than 1;
* otherwise, more than 1
*/
public static int atMostOneError(CharSequence s1, CharSequence s2) {
int s1Length = s1.length();
int s2Length = s2.length();
int errors = 0; /* running count of edits required */
switch(s2Length - s1Length) {
/*
* Strings are the same length. No single insert/delete is possible;
* at most one substitution can be present.
*/
case 0:
for (int i = 0; i < s2Length; i++) {
if ((s2.charAt(i) != s1.charAt(i)) && (errors++ != 0)) {
break;
}
}
return errors;
/*
* Strings differ in length by 1, so we have an insertion
* (and therefore cannot have any other substitutions).
*/
case 1: /* s2Length > s1Length */
for (int i = 0; i < s1Length; i++) {
if (s2.charAt(i) != s1.charAt(i)) {
for (; i < s1Length; i++) {
if (s2.charAt(i + 1) != s1.charAt(i)) {
return 2;
}
}
return 1;
}
}
return 1;
/* Same as above case, with strings reversed */
case -1: /* s1Length > s2Length */
for (int i = 0; i < s2Length; i++) {
if (s2.charAt(i) != s1.charAt(i)) {
for (; i < s2Length; i++) {
if (s2.charAt(i) != s1.charAt(i + 1)) {
return 2;
}
}
return 1;
}
}
return 1;
/* Edit distance is at least difference in lengths; more than 1 here. */
default:
return 2;
}
}
/**
* Generates an GeneralEditDistance engine for a particular pattern string
* based on Levenshtein distance. Caller must ensure that the
* pattern does not change (consider using pattern.toString() if
* necessary) as long as the generated object is to be used.
*
* @param pattern a string from which distance computations are desired
* @return an engine for computing Levenshtein distances from that pattern
*/
public static GeneralEditDistance
getLevenshteinDistance(CharSequence pattern) {
return new Levenshtein(pattern);
}
private GeneralEditDistances() { }
}