Java tutorial
/* * Copyright 2016 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.template.soy.error; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableSet; import javax.annotation.Nullable; /** Utility methods for constructing Soy error messages. */ public final class SoyErrors { /** * Given a collection of strings and a name that isn't contained in it. Return a message that * suggests one of the names. * * <p>Returns the empty string if {@code allNames} is empty or there is no close match. */ public static String getDidYouMeanMessage(Iterable<String> allNames, String wrongName) { String closestName = getClosest(allNames, wrongName); if (closestName != null) { return String.format(" Did you mean '%s'?", closestName); } return ""; } /** * Same as {@link #getDidYouMeanMessage(Iterable, String)} but with some additional heuristics for * proto fields. */ public static String getDidYouMeanMessageForProtoFields(ImmutableSet<String> fields, String fieldName) { // TODO(lukes): when we have map/case enum support add more cases here. if (fields.contains(fieldName + "List")) { return String.format(" Did you mean '%sList'?", fieldName); } else { return getDidYouMeanMessage(fields, fieldName); } } /** * Returns the member of {@code allNames} that is closest to {@code wrongName}, or {@code null} if * {@code allNames} is empty. * * <p>The distance metric is a case insensitive Levenshtein distance. * * @throws IllegalArgumentException if {@code wrongName} is a member of {@code allNames} */ @Nullable @VisibleForTesting static String getClosest(Iterable<String> allNames, String wrongName) { // only suggest matches that are closer than this. This magic heuristic is based on what llvm // and javac do int shortest = (wrongName.length() + 2) / 3 + 1; String closestName = null; for (String otherName : allNames) { if (otherName.equals(wrongName)) { throw new IllegalArgumentException("'" + wrongName + "' is contained in " + allNames); } int distance = distance(otherName, wrongName, shortest); if (distance < shortest) { shortest = distance; closestName = otherName; if (distance == 0) { return closestName; } } } return closestName; } /** * Performs a case insensitive Levenshtein edit distance based on the 2 rows implementation. * * @param s The first string * @param t The second string * @param maxDistance The distance to beat, if we can't do better, stop trying * @return an integer describing the number of edits needed to transform s into t * @see "https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows" */ private static int distance(String s, String t, int maxDistance) { // create two work vectors of integer distances // it is possible to reduce this to only one array, but performance isn't that important here. // We could also avoid calculating a lot of the entries by taking maxDistance into account in // the inner loop. This would only be worth optimizing if it showed up in a profile. int[] v0 = new int[t.length() + 1]; int[] v1 = new int[t.length() + 1]; // initialize v0 (the previous row of distances) // this row is A[0][i]: edit distance for an empty s // the distance is just the number of characters to delete from t for (int i = 0; i < v0.length; i++) { v0[i] = i; } for (int i = 0; i < s.length(); i++) { // calculate v1 (current row distances) from the previous row v0 // first element of v1 is A[i+1][0] // edit distance is delete (i+1) chars from s to match empty t v1[0] = i + 1; int bestThisRow = v1[0]; char sChar = Ascii.toLowerCase(s.charAt(i)); // use formula to fill in the rest of the row for (int j = 0; j < t.length(); j++) { char tChar = Ascii.toLowerCase(t.charAt(j)); v1[j + 1] = Math.min(v1[j] + 1, // deletion Math.min(v0[j + 1] + 1, // insertion v0[j] + ((sChar == tChar) ? 0 : 1))); // substitution bestThisRow = Math.min(bestThisRow, v1[j + 1]); } if (bestThisRow > maxDistance) { // if we couldn't possibly do better than maxDistance, stop trying. return maxDistance + 1; } // swap v1 (current row) to v0 (previous row) for next iteration. no need to clear previous // row since we always update all of v1 on each iteration. int[] tmp = v0; v0 = v1; v1 = tmp; } // The best answer is the last slot in v0 (due to the swap on the last iteration) return v0[t.length()]; } }