DoubleMetaphone.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.  *      https://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.codec.language;

  18. import org.apache.commons.codec.EncoderException;
  19. import org.apache.commons.codec.StringEncoder;
  20. import org.apache.commons.codec.binary.StringUtils;

  21. /**
  22.  * Encodes a string into a double metaphone value. This Implementation is based on the algorithm by <CITE>Lawrence
  23.  * Philips</CITE>.
  24.  * <p>
  25.  * This class is conditionally thread-safe. The instance field for the maximum code length is mutable
  26.  * {@link #setMaxCodeLen(int)} but is not volatile, and accesses are not synchronized. If an instance of the class is
  27.  * shared between threads, the caller needs to ensure that suitable synchronization is used to ensure safe publication
  28.  * of the value between threads, and must not invoke {@link #setMaxCodeLen(int)} after initial setup.
  29.  * </p>
  30.  *
  31.  * @see <a href="https://drdobbs.com/the-double-metaphone-search-algorithm/184401251?pgno=2">Dr. Dobbs Original Article</a>
  32.  * @see <a href="https://en.wikipedia.org/wiki/Metaphone">Wikipedia Metaphone</a>
  33.  */
  34. public class DoubleMetaphone implements StringEncoder {

  35.     /**
  36.      * Stores results, since there is the optional alternate encoding.
  37.      */
  38.     public class DoubleMetaphoneResult {

  39.         private final StringBuilder primary = new StringBuilder(getMaxCodeLen());
  40.         private final StringBuilder alternate = new StringBuilder(getMaxCodeLen());
  41.         private final int maxLength;

  42.         /**
  43.          * Constructs a new instance.
  44.          *
  45.          * @param maxLength The maximum length.
  46.          */
  47.         public DoubleMetaphoneResult(final int maxLength) {
  48.             this.maxLength = maxLength;
  49.         }

  50.         /**
  51.          * Appends the given value as primary and alternative.
  52.          *
  53.          * @param value The value to append.
  54.          */
  55.         public void append(final char value) {
  56.             appendPrimary(value);
  57.             appendAlternate(value);
  58.         }

  59.         /**
  60.          * Appends the given primary and alternative values.
  61.          *
  62.          * @param primary   The primary value.
  63.          * @param alternate The alternate value.
  64.          */
  65.         public void append(final char primary, final char alternate) {
  66.             appendPrimary(primary);
  67.             appendAlternate(alternate);
  68.         }

  69.         /**
  70.          * Appends the given value as primary and alternative.
  71.          *
  72.          * @param value The value to append.
  73.          */
  74.         public void append(final String value) {
  75.             appendPrimary(value);
  76.             appendAlternate(value);
  77.         }

  78.         /**
  79.          * Appends the given primary and alternative values.
  80.          *
  81.          * @param primary   The primary value.
  82.          * @param alternate The alternate value.
  83.          */
  84.         public void append(final String primary, final String alternate) {
  85.             appendPrimary(primary);
  86.             appendAlternate(alternate);
  87.         }

  88.         /**
  89.          * Appends the given value as alternative.
  90.          *
  91.          * @param value The value to append.
  92.          */
  93.         public void appendAlternate(final char value) {
  94.             if (this.alternate.length() < this.maxLength) {
  95.                 this.alternate.append(value);
  96.             }
  97.         }

  98.         /**
  99.          * Appends the given value as alternative.
  100.          *
  101.          * @param value The value to append.
  102.          */
  103.         public void appendAlternate(final String value) {
  104.             final int addChars = this.maxLength - this.alternate.length();
  105.             if (value.length() <= addChars) {
  106.                 this.alternate.append(value);
  107.             } else {
  108.                 this.alternate.append(value, 0, addChars);
  109.             }
  110.         }

  111.         /**
  112.          * Appends the given value as primary.
  113.          *
  114.          * @param value The value to append.
  115.          */
  116.         public void appendPrimary(final char value) {
  117.             if (this.primary.length() < this.maxLength) {
  118.                 this.primary.append(value);
  119.             }
  120.         }

  121.         /**
  122.          * Appends the given value as primary.
  123.          *
  124.          * @param value The value to append.
  125.          */
  126.         public void appendPrimary(final String value) {
  127.             final int addChars = this.maxLength - this.primary.length();
  128.             if (value.length() <= addChars) {
  129.                 this.primary.append(value);
  130.             } else {
  131.                 this.primary.append(value, 0, addChars);
  132.             }
  133.         }

  134.         /**
  135.          * Gets the alternate string.
  136.          *
  137.          * @return the alternate string.
  138.          */
  139.         public String getAlternate() {
  140.             return this.alternate.toString();
  141.         }

  142.         /**
  143.          * Gets the primary string.
  144.          *
  145.          * @return the primary string.
  146.          */
  147.         public String getPrimary() {
  148.             return this.primary.toString();
  149.         }

  150.         /**
  151.          * Tests whether this result is complete.
  152.          *
  153.          * @return whether this result is complete.
  154.          */
  155.         public boolean isComplete() {
  156.             return this.primary.length() >= this.maxLength && this.alternate.length() >= this.maxLength;
  157.         }
  158.     }

  159.     /**
  160.      * "Vowels" to test.
  161.      */
  162.     private static final String VOWELS = "AEIOUY";

  163.     /**
  164.      * Prefixes when present which are not pronounced.
  165.      */
  166.     private static final String[] SILENT_START = { "GN", "KN", "PN", "WR", "PS" };

  167.     private static final String[] L_R_N_M_B_H_F_V_W_SPACE = { "L", "R", "N", "M", "B", "H", "F", "V", "W", " " };
  168.     private static final String[] ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER = { "ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER" };
  169.     private static final String[] L_T_K_S_N_M_B_Z = { "L", "T", "K", "S", "N", "M", "B", "Z" };

  170.     /**
  171.      * Tests whether {@code value} contains any of the {@code criteria} starting at index {@code start} and matching up to length {@code length}.
  172.      *
  173.      * @param value    The value to test.
  174.      * @param start    Where in {@code value} to start testing.
  175.      * @param length   How many to test.
  176.      * @param criteria The search criteria.
  177.      * @return Whether there was a match.
  178.      */
  179.     protected static boolean contains(final String value, final int start, final int length, final String... criteria) {
  180.         boolean result = false;
  181.         if (start >= 0 && start + length <= value.length()) {
  182.             final String target = value.substring(start, start + length);
  183.             for (final String element : criteria) {
  184.                 if (target.equals(element)) {
  185.                     result = true;
  186.                     break;
  187.                 }
  188.             }
  189.         }
  190.         return result;
  191.     }

  192.     /**
  193.      * Maximum length of an encoding, default is 4
  194.      */
  195.     private int maxCodeLen = 4;

  196.     /**
  197.      * Constructs a new instance.
  198.      */
  199.     public DoubleMetaphone() {
  200.         // empty
  201.     }

  202.     /**
  203.      * Gets the character at index {@code index} if available, or {@link Character#MIN_VALUE} if out of bounds.
  204.      *
  205.      * @param value The String to query.
  206.      * @param index A string index.
  207.      * @return The character at the index or {@link Character#MIN_VALUE} if out of bounds.
  208.      */
  209.     protected char charAt(final String value, final int index) {
  210.         if (index < 0 || index >= value.length()) {
  211.             return Character.MIN_VALUE;
  212.         }
  213.         return value.charAt(index);
  214.     }

  215.     /**
  216.      * Cleans the input.
  217.      */
  218.     private String cleanInput(String input) {
  219.         if (input == null) {
  220.             return null;
  221.         }
  222.         input = input.trim();
  223.         if (input.isEmpty()) {
  224.             return null;
  225.         }
  226.         return input.toUpperCase(java.util.Locale.ENGLISH);
  227.     }

  228.     /**
  229.      * Complex condition 0 for 'C'.
  230.      */
  231.     private boolean conditionC0(final String value, final int index) {
  232.         if (contains(value, index, 4, "CHIA")) {
  233.             return true;
  234.         }
  235.         if (index <= 1) {
  236.             return false;
  237.         }
  238.         if (isVowel(charAt(value, index - 2))) {
  239.             return false;
  240.         }
  241.         if (!contains(value, index - 1, 3, "ACH")) {
  242.             return false;
  243.         }
  244.         final char c = charAt(value, index + 2);
  245.         return c != 'I' && c != 'E' ||
  246.                 contains(value, index - 2, 6, "BACHER", "MACHER");
  247.     }

  248.     /**
  249.      * Complex condition 0 for 'CH'.
  250.      */
  251.     private boolean conditionCH0(final String value, final int index) {
  252.         if (index != 0) {
  253.             return false;
  254.         }
  255.         if (!contains(value, index + 1, 5, "HARAC", "HARIS") &&
  256.                    !contains(value, index + 1, 3, "HOR", "HYM", "HIA", "HEM")) {
  257.             return false;
  258.         }
  259.         return !contains(value, 0, 5, "CHORE");
  260.     }

  261.     /**
  262.      * Complex condition 1 for 'CH'.
  263.      */
  264.     private boolean conditionCH1(final String value, final int index) {
  265.         return contains(value, 0, 4, "VAN ", "VON ") || contains(value, 0, 3, "SCH") ||
  266.                 contains(value, index - 2, 6, "ORCHES", "ARCHIT", "ORCHID") ||
  267.                 contains(value, index + 2, 1, "T", "S") ||
  268.                 (contains(value, index - 1, 1, "A", "O", "U", "E") || index == 0) &&
  269.                  (contains(value, index + 2, 1, L_R_N_M_B_H_F_V_W_SPACE) || index + 1 == value.length() - 1);
  270.     }

  271.     /**
  272.      * Complex condition 0 for 'L'.
  273.      */
  274.     private boolean conditionL0(final String value, final int index) {
  275.         if (index == value.length() - 3 &&
  276.             contains(value, index - 1, 4, "ILLO", "ILLA", "ALLE")) {
  277.             return true;
  278.         }
  279.         return (contains(value, value.length() - 2, 2, "AS", "OS") ||
  280.                 contains(value, value.length() - 1, 1, "A", "O")) &&
  281.                 contains(value, index - 1, 4, "ALLE");
  282.     }

  283.     //-- BEGIN HANDLERS --//

  284.     /**
  285.      * Complex condition 0 for 'M'.
  286.      */
  287.     private boolean conditionM0(final String value, final int index) {
  288.         if (charAt(value, index + 1) == 'M') {
  289.             return true;
  290.         }
  291.         return contains(value, index - 1, 3, "UMB") &&
  292.                (index + 1 == value.length() - 1 || contains(value, index + 2, 2, "ER"));
  293.     }

  294.     /**
  295.      * Encode a value with Double Metaphone.
  296.      *
  297.      * @param value String to encode
  298.      * @return an encoded string
  299.      */
  300.     public String doubleMetaphone(final String value) {
  301.         return doubleMetaphone(value, false);
  302.     }

  303.     /**
  304.      * Encode a value with Double Metaphone, optionally using the alternate encoding.
  305.      *
  306.      * @param value String to encode
  307.      * @param alternate use alternate encode
  308.      * @return an encoded string
  309.      */
  310.     public String doubleMetaphone(String value, final boolean alternate) {
  311.         value = cleanInput(value);
  312.         if (value == null) {
  313.             return null;
  314.         }

  315.         final boolean slavoGermanic = isSlavoGermanic(value);
  316.         int index = isSilentStart(value) ? 1 : 0;

  317.         final DoubleMetaphoneResult result = new DoubleMetaphoneResult(getMaxCodeLen());

  318.         while (!result.isComplete() && index <= value.length() - 1) {
  319.             switch (value.charAt(index)) {
  320.             case 'A':
  321.             case 'E':
  322.             case 'I':
  323.             case 'O':
  324.             case 'U':
  325.             case 'Y':
  326.                 index = handleAEIOUY(result, index);
  327.                 break;
  328.             case 'B':
  329.                 result.append('P');
  330.                 index = charAt(value, index + 1) == 'B' ? index + 2 : index + 1;
  331.                 break;
  332.             case '\u00C7':
  333.                 // A C with a Cedilla
  334.                 result.append('S');
  335.                 index++;
  336.                 break;
  337.             case 'C':
  338.                 index = handleC(value, result, index);
  339.                 break;
  340.             case 'D':
  341.                 index = handleD(value, result, index);
  342.                 break;
  343.             case 'F':
  344.                 result.append('F');
  345.                 index = charAt(value, index + 1) == 'F' ? index + 2 : index + 1;
  346.                 break;
  347.             case 'G':
  348.                 index = handleG(value, result, index, slavoGermanic);
  349.                 break;
  350.             case 'H':
  351.                 index = handleH(value, result, index);
  352.                 break;
  353.             case 'J':
  354.                 index = handleJ(value, result, index, slavoGermanic);
  355.                 break;
  356.             case 'K':
  357.                 result.append('K');
  358.                 index = charAt(value, index + 1) == 'K' ? index + 2 : index + 1;
  359.                 break;
  360.             case 'L':
  361.                 index = handleL(value, result, index);
  362.                 break;
  363.             case 'M':
  364.                 result.append('M');
  365.                 index = conditionM0(value, index) ? index + 2 : index + 1;
  366.                 break;
  367.             case 'N':
  368.                 result.append('N');
  369.                 index = charAt(value, index + 1) == 'N' ? index + 2 : index + 1;
  370.                 break;
  371.             case '\u00D1':
  372.                 // N with a tilde (spanish ene)
  373.                 result.append('N');
  374.                 index++;
  375.                 break;
  376.             case 'P':
  377.                 index = handleP(value, result, index);
  378.                 break;
  379.             case 'Q':
  380.                 result.append('K');
  381.                 index = charAt(value, index + 1) == 'Q' ? index + 2 : index + 1;
  382.                 break;
  383.             case 'R':
  384.                 index = handleR(value, result, index, slavoGermanic);
  385.                 break;
  386.             case 'S':
  387.                 index = handleS(value, result, index, slavoGermanic);
  388.                 break;
  389.             case 'T':
  390.                 index = handleT(value, result, index);
  391.                 break;
  392.             case 'V':
  393.                 result.append('F');
  394.                 index = charAt(value, index + 1) == 'V' ? index + 2 : index + 1;
  395.                 break;
  396.             case 'W':
  397.                 index = handleW(value, result, index);
  398.                 break;
  399.             case 'X':
  400.                 index = handleX(value, result, index);
  401.                 break;
  402.             case 'Z':
  403.                 index = handleZ(value, result, index, slavoGermanic);
  404.                 break;
  405.             default:
  406.                 index++;
  407.                 break;
  408.             }
  409.         }

  410.         return alternate ? result.getAlternate() : result.getPrimary();
  411.     }

  412.     /**
  413.      * Encode the value using DoubleMetaphone.  It will only work if
  414.      * {@code obj} is a {@code String} (like {@code Metaphone}).
  415.      *
  416.      * @param obj Object to encode (should be of type String)
  417.      * @return An encoded Object (will be of type String)
  418.      * @throws EncoderException encode parameter is not of type String
  419.      */
  420.     @Override
  421.     public Object encode(final Object obj) throws EncoderException {
  422.         if (!(obj instanceof String)) {
  423.             throw new EncoderException("DoubleMetaphone encode parameter is not of type String");
  424.         }
  425.         return doubleMetaphone((String) obj);
  426.     }

  427.     /**
  428.      * Encode the value using DoubleMetaphone.
  429.      *
  430.      * @param value String to encode
  431.      * @return An encoded String
  432.      */
  433.     @Override
  434.     public String encode(final String value) {
  435.         return doubleMetaphone(value);
  436.     }

  437.     /**
  438.      * Returns the maxCodeLen.
  439.      * @return int
  440.      */
  441.     public int getMaxCodeLen() {
  442.         return this.maxCodeLen;
  443.     }

  444.     /**
  445.      * Handles 'A', 'E', 'I', 'O', 'U', and 'Y' cases.
  446.      */
  447.     private int handleAEIOUY(final DoubleMetaphoneResult result, final int index) {
  448.         if (index == 0) {
  449.             result.append('A');
  450.         }
  451.         return index + 1;
  452.     }

  453.     /**
  454.      * Handles 'C' cases.
  455.      */
  456.     private int handleC(final String value, final DoubleMetaphoneResult result, int index) {
  457.         if (conditionC0(value, index)) {  // very confusing, moved out
  458.             result.append('K');
  459.             index += 2;
  460.         } else if (index == 0 && contains(value, index, 6, "CAESAR")) {
  461.             result.append('S');
  462.             index += 2;
  463.         } else if (contains(value, index, 2, "CH")) {
  464.             index = handleCH(value, result, index);
  465.         } else if (contains(value, index, 2, "CZ") &&
  466.                    !contains(value, index - 2, 4, "WICZ")) {
  467.             //-- "Czerny" --//
  468.             result.append('S', 'X');
  469.             index += 2;
  470.         } else if (contains(value, index + 1, 3, "CIA")) {
  471.             //-- "focaccia" --//
  472.             result.append('X');
  473.             index += 3;
  474.         } else if (contains(value, index, 2, "CC") &&
  475.                    !(index == 1 && charAt(value, 0) == 'M')) {
  476.             //-- double "cc" but not "McClelland" --//
  477.             return handleCC(value, result, index);
  478.         } else if (contains(value, index, 2, "CK", "CG", "CQ")) {
  479.             result.append('K');
  480.             index += 2;
  481.         } else if (contains(value, index, 2, "CI", "CE", "CY")) {
  482.             //-- Italian vs. English --//
  483.             if (contains(value, index, 3, "CIO", "CIE", "CIA")) {
  484.                 result.append('S', 'X');
  485.             } else {
  486.                 result.append('S');
  487.             }
  488.             index += 2;
  489.         } else {
  490.             result.append('K');
  491.             if (contains(value, index + 1, 2, " C", " Q", " G")) {
  492.                 //-- Mac Caffrey, Mac Gregor --//
  493.                 index += 3;
  494.             } else if (contains(value, index + 1, 1, "C", "K", "Q") &&
  495.                        !contains(value, index + 1, 2, "CE", "CI")) {
  496.                 index += 2;
  497.             } else {
  498.                 index++;
  499.             }
  500.         }

  501.         return index;
  502.     }

  503.     /**
  504.      * Handles 'CC' cases.
  505.      */
  506.     private int handleCC(final String value, final DoubleMetaphoneResult result, int index) {
  507.         if (contains(value, index + 2, 1, "I", "E", "H") &&
  508.             !contains(value, index + 2, 2, "HU")) {
  509.             //-- "bellocchio" but not "bacchus" --//
  510.             if (index == 1 && charAt(value, index - 1) == 'A' ||
  511.                 contains(value, index - 1, 5, "UCCEE", "UCCES")) {
  512.                 //-- "accident", "accede", "succeed" --//
  513.                 result.append("KS");
  514.             } else {
  515.                 //-- "bacci", "bertucci", other Italian --//
  516.                 result.append('X');
  517.             }
  518.             index += 3;
  519.         } else {    // Pierce's rule
  520.             result.append('K');
  521.             index += 2;
  522.         }

  523.         return index;
  524.     }

  525.     /**
  526.      * Handles 'CH' cases.
  527.      */
  528.     private int handleCH(final String value, final DoubleMetaphoneResult result, final int index) {
  529.         if (index > 0 && contains(value, index, 4, "CHAE")) {   // Michael
  530.             result.append('K', 'X');
  531.             return index + 2;
  532.         }
  533.         if (conditionCH0(value, index)) {
  534.             //-- Greek roots ("chemistry", "chorus", etc.) --//
  535.             result.append('K');
  536.             return index + 2;
  537.         }
  538.         if (conditionCH1(value, index)) {
  539.             //-- Germanic, Greek, or otherwise 'ch' for 'kh' sound --//
  540.             result.append('K');
  541.             return index + 2;
  542.         }
  543.         if (index > 0) {
  544.             if (contains(value, 0, 2, "MC")) {
  545.                 result.append('K');
  546.             } else {
  547.                 result.append('X', 'K');
  548.             }
  549.         } else {
  550.             result.append('X');
  551.         }
  552.         return index + 2;
  553.     }

  554.     /**
  555.      * Handles 'D' cases.
  556.      */
  557.     private int handleD(final String value, final DoubleMetaphoneResult result, int index) {
  558.         if (contains(value, index, 2, "DG")) {
  559.             //-- "Edge" --//
  560.             if (contains(value, index + 2, 1, "I", "E", "Y")) {
  561.                 result.append('J');
  562.                 index += 3;
  563.                 //-- "Edgar" --//
  564.             } else {
  565.                 result.append("TK");
  566.                 index += 2;
  567.             }
  568.         } else if (contains(value, index, 2, "DT", "DD")) {
  569.             result.append('T');
  570.             index += 2;
  571.         } else {
  572.             result.append('T');
  573.             index++;
  574.         }
  575.         return index;
  576.     }

  577.     /**
  578.      * Handles 'G' cases.
  579.      */
  580.     private int handleG(final String value, final DoubleMetaphoneResult result, int index,
  581.                         final boolean slavoGermanic) {
  582.         if (charAt(value, index + 1) == 'H') {
  583.             index = handleGH(value, result, index);
  584.         } else if (charAt(value, index + 1) == 'N') {
  585.             if (index == 1 && isVowel(charAt(value, 0)) && !slavoGermanic) {
  586.                 result.append("KN", "N");
  587.             } else if (!contains(value, index + 2, 2, "EY") &&
  588.                        charAt(value, index + 1) != 'Y' && !slavoGermanic) {
  589.                 result.append("N", "KN");
  590.             } else {
  591.                 result.append("KN");
  592.             }
  593.             index += 2;
  594.         } else if (contains(value, index + 1, 2, "LI") && !slavoGermanic) {
  595.             result.append("KL", "L");
  596.             index += 2;
  597.         } else if (index == 0 &&
  598.                    (charAt(value, index + 1) == 'Y' ||
  599.                     contains(value, index + 1, 2, ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER))) {
  600.             //-- -ges-, -gep-, -gel-, -gie- at beginning --//
  601.             result.append('K', 'J');
  602.             index += 2;
  603.         } else if ((contains(value, index + 1, 2, "ER") ||
  604.                     charAt(value, index + 1) == 'Y') &&
  605.                    !contains(value, 0, 6, "DANGER", "RANGER", "MANGER") &&
  606.                    !contains(value, index - 1, 1, "E", "I") &&
  607.                    !contains(value, index - 1, 3, "RGY", "OGY")) {
  608.             //-- -ger-, -gy- --//
  609.             result.append('K', 'J');
  610.             index += 2;
  611.         } else if (contains(value, index + 1, 1, "E", "I", "Y") ||
  612.                    contains(value, index - 1, 4, "AGGI", "OGGI")) {
  613.             //-- Italian "biaggi" --//
  614.             if (contains(value, 0, 4, "VAN ", "VON ") ||
  615.                 contains(value, 0, 3, "SCH") ||
  616.                 contains(value, index + 1, 2, "ET")) {
  617.                 //-- obvious germanic --//
  618.                 result.append('K');
  619.             } else if (contains(value, index + 1, 3, "IER")) {
  620.                 result.append('J');
  621.             } else {
  622.                 result.append('J', 'K');
  623.             }
  624.             index += 2;
  625.         } else {
  626.             if (charAt(value, index + 1) == 'G') {
  627.                 index += 2;
  628.             } else {
  629.                 index++;
  630.             }
  631.             result.append('K');
  632.         }
  633.         return index;
  634.     }

  635.     /**
  636.      * Handles 'GH' cases.
  637.      */
  638.     private int handleGH(final String value, final DoubleMetaphoneResult result, int index) {
  639.         if (index > 0 && !isVowel(charAt(value, index - 1))) {
  640.             result.append('K');
  641.             index += 2;
  642.         } else if (index == 0) {
  643.             if (charAt(value, index + 2) == 'I') {
  644.                 result.append('J');
  645.             } else {
  646.                 result.append('K');
  647.             }
  648.             index += 2;
  649.         } else if (index > 1 && contains(value, index - 2, 1, "B", "H", "D") ||
  650.                    index > 2 && contains(value, index - 3, 1, "B", "H", "D") ||
  651.                    index > 3 && contains(value, index - 4, 1, "B", "H")) {
  652.             //-- Parker's rule (with some further refinements) - "hugh"
  653.             index += 2;
  654.         } else {
  655.             if (index > 2 && charAt(value, index - 1) == 'U' &&
  656.                 contains(value, index - 3, 1, "C", "G", "L", "R", "T")) {
  657.                 //-- "laugh", "McLaughlin", "cough", "gough", "rough", "tough"
  658.                 result.append('F');
  659.             } else if (index > 0 && charAt(value, index - 1) != 'I') {
  660.                 result.append('K');
  661.             }
  662.             index += 2;
  663.         }
  664.         return index;
  665.     }

  666.     /**
  667.      * Handles 'H' cases.
  668.      */
  669.     private int handleH(final String value, final DoubleMetaphoneResult result, int index) {
  670.         //-- only keep if first & before vowel or between 2 vowels --//
  671.         if ((index == 0 || isVowel(charAt(value, index - 1))) &&
  672.             isVowel(charAt(value, index + 1))) {
  673.             result.append('H');
  674.             index += 2;
  675.             //-- also takes car of "HH" --//
  676.         } else {
  677.             index++;
  678.         }
  679.         return index;
  680.     }

  681.     /**
  682.      * Handles 'J' cases.
  683.      */
  684.     private int handleJ(final String value, final DoubleMetaphoneResult result, int index,
  685.                         final boolean slavoGermanic) {
  686.         if (contains(value, index, 4, "JOSE") || contains(value, 0, 4, "SAN ")) {
  687.                 //-- obvious Spanish, "Jose", "San Jacinto" --//
  688.                 if (index == 0 && charAt(value, index + 4) == ' ' ||
  689.                      value.length() == 4 || contains(value, 0, 4, "SAN ")) {
  690.                     result.append('H');
  691.                 } else {
  692.                     result.append('J', 'H');
  693.                 }
  694.                 index++;
  695.             } else {
  696.                 if (index == 0 && !contains(value, index, 4, "JOSE")) {
  697.                     result.append('J', 'A');
  698.                 } else if (isVowel(charAt(value, index - 1)) && !slavoGermanic &&
  699.                            (charAt(value, index + 1) == 'A' || charAt(value, index + 1) == 'O')) {
  700.                     result.append('J', 'H');
  701.                 } else if (index == value.length() - 1) {
  702.                     result.append('J', ' ');
  703.                 } else if (!contains(value, index + 1, 1, L_T_K_S_N_M_B_Z) &&
  704.                            !contains(value, index - 1, 1, "S", "K", "L")) {
  705.                     result.append('J');
  706.                 }

  707.                 if (charAt(value, index + 1) == 'J') {
  708.                     index += 2;
  709.                 } else {
  710.                     index++;
  711.                 }
  712.             }
  713.         return index;
  714.     }

  715.     /**
  716.      * Handles 'L' cases.
  717.      */
  718.     private int handleL(final String value, final DoubleMetaphoneResult result, int index) {
  719.         if (charAt(value, index + 1) == 'L') {
  720.             if (conditionL0(value, index)) {
  721.                 result.appendPrimary('L');
  722.             } else {
  723.                 result.append('L');
  724.             }
  725.             index += 2;
  726.         } else {
  727.             index++;
  728.             result.append('L');
  729.         }
  730.         return index;
  731.     }

  732.     /**
  733.      * Handles 'P' cases.
  734.      */
  735.     private int handleP(final String value, final DoubleMetaphoneResult result, int index) {
  736.         if (charAt(value, index + 1) == 'H') {
  737.             result.append('F');
  738.             index += 2;
  739.         } else {
  740.             result.append('P');
  741.             index = contains(value, index + 1, 1, "P", "B") ? index + 2 : index + 1;
  742.         }
  743.         return index;
  744.     }

  745.     /**
  746.      * Handles 'R' cases.
  747.      */
  748.     private int handleR(final String value, final DoubleMetaphoneResult result, final int index,
  749.                         final boolean slavoGermanic) {
  750.         if (index == value.length() - 1 && !slavoGermanic &&
  751.             contains(value, index - 2, 2, "IE") &&
  752.             !contains(value, index - 4, 2, "ME", "MA")) {
  753.             result.appendAlternate('R');
  754.         } else {
  755.             result.append('R');
  756.         }
  757.         return charAt(value, index + 1) == 'R' ? index + 2 : index + 1;
  758.     }

  759.     //-- BEGIN CONDITIONS --//

  760.     /**
  761.      * Handles 'S' cases.
  762.      */
  763.     private int handleS(final String value, final DoubleMetaphoneResult result, int index,
  764.                         final boolean slavoGermanic) {
  765.         if (contains(value, index - 1, 3, "ISL", "YSL")) {
  766.             //-- special cases "island", "isle", "carlisle", "carlysle" --//
  767.             index++;
  768.         } else if (index == 0 && contains(value, index, 5, "SUGAR")) {
  769.             //-- special case "sugar-" --//
  770.             result.append('X', 'S');
  771.             index++;
  772.         } else if (contains(value, index, 2, "SH")) {
  773.             if (contains(value, index + 1, 4, "HEIM", "HOEK", "HOLM", "HOLZ")) {
  774.                 //-- germanic --//
  775.                 result.append('S');
  776.             } else {
  777.                 result.append('X');
  778.             }
  779.             index += 2;
  780.         } else if (contains(value, index, 3, "SIO", "SIA") || contains(value, index, 4, "SIAN")) {
  781.             //-- Italian and Armenian --//
  782.             if (slavoGermanic) {
  783.                 result.append('S');
  784.             } else {
  785.                 result.append('S', 'X');
  786.             }
  787.             index += 3;
  788.         } else if (index == 0 && contains(value, index + 1, 1, "M", "N", "L", "W") ||
  789.                    contains(value, index + 1, 1, "Z")) {
  790.             //-- german & anglicisations, for example "smith" match "schmidt" //
  791.             // "snider" match "schneider" --//
  792.             //-- also, -sz- in slavic language although in hungarian it //
  793.             //   is pronounced "s" --//
  794.             result.append('S', 'X');
  795.             index = contains(value, index + 1, 1, "Z") ? index + 2 : index + 1;
  796.         } else if (contains(value, index, 2, "SC")) {
  797.             index = handleSC(value, result, index);
  798.         } else {
  799.             if (index == value.length() - 1 && contains(value, index - 2, 2, "AI", "OI")) {
  800.                 //-- french for example "resnais", "artois" --//
  801.                 result.appendAlternate('S');
  802.             } else {
  803.                 result.append('S');
  804.             }
  805.             index = contains(value, index + 1, 1, "S", "Z") ? index + 2 : index + 1;
  806.         }
  807.         return index;
  808.     }

  809.     /**
  810.      * Handles 'SC' cases.
  811.      */
  812.     private int handleSC(final String value, final DoubleMetaphoneResult result, final int index) {
  813.         if (charAt(value, index + 2) == 'H') {
  814.             //-- Schlesinger's rule --//
  815.             if (contains(value, index + 3, 2, "OO", "ER", "EN", "UY", "ED", "EM")) {
  816.                 //-- Dutch origin, for example "school", "schooner" --//
  817.                 if (contains(value, index + 3, 2, "ER", "EN")) {
  818.                     //-- "schermerhorn", "schenker" --//
  819.                     result.append("X", "SK");
  820.                 } else {
  821.                     result.append("SK");
  822.                 }
  823.             } else if (index == 0 && !isVowel(charAt(value, 3)) && charAt(value, 3) != 'W') {
  824.                 result.append('X', 'S');
  825.             } else {
  826.                 result.append('X');
  827.             }
  828.         } else if (contains(value, index + 2, 1, "I", "E", "Y")) {
  829.             result.append('S');
  830.         } else {
  831.             result.append("SK");
  832.         }
  833.         return index + 3;
  834.     }

  835.     /**
  836.      * Handles 'T' cases.
  837.      */
  838.     private int handleT(final String value, final DoubleMetaphoneResult result, int index) {
  839.         if (contains(value, index, 4, "TION") || contains(value, index, 3, "TIA", "TCH")) {
  840.             result.append('X');
  841.             index += 3;
  842.         } else if (contains(value, index, 2, "TH") || contains(value, index, 3, "TTH")) {
  843.             if (contains(value, index + 2, 2, "OM", "AM") ||
  844.                 //-- special case "thomas", "thames" or germanic --//
  845.                 contains(value, 0, 4, "VAN ", "VON ") ||
  846.                 contains(value, 0, 3, "SCH")) {
  847.                 result.append('T');
  848.             } else {
  849.                 result.append('0', 'T');
  850.             }
  851.             index += 2;
  852.         } else {
  853.             result.append('T');
  854.             index = contains(value, index + 1, 1, "T", "D") ? index + 2 : index + 1;
  855.         }
  856.         return index;
  857.     }

  858.     /**
  859.      * Handles 'W' cases.
  860.      */
  861.     private int handleW(final String value, final DoubleMetaphoneResult result, int index) {
  862.         if (contains(value, index, 2, "WR")) {
  863.             //-- can also be in middle of word --//
  864.             result.append('R');
  865.             index += 2;
  866.         } else if (index == 0 && (isVowel(charAt(value, index + 1)) ||
  867.                            contains(value, index, 2, "WH"))) {
  868.             if (isVowel(charAt(value, index + 1))) {
  869.                 //-- Wasserman should match Vasserman --//
  870.                 result.append('A', 'F');
  871.             } else {
  872.                 //-- need Uomo to match Womo --//
  873.                 result.append('A');
  874.             }
  875.             index++;
  876.         } else if (index == value.length() - 1 && isVowel(charAt(value, index - 1)) ||
  877.                    contains(value, index - 1, 5, "EWSKI", "EWSKY", "OWSKI", "OWSKY") ||
  878.                    contains(value, 0, 3, "SCH")) {
  879.             //-- Arnow should match Arnoff --//
  880.             result.appendAlternate('F');
  881.             index++;
  882.         } else if (contains(value, index, 4, "WICZ", "WITZ")) {
  883.             //-- Polish for example "filipowicz" --//
  884.             result.append("TS", "FX");
  885.             index += 4;
  886.         } else {
  887.             index++;
  888.         }
  889.         return index;
  890.     }

  891.     /**
  892.      * Handles 'X' cases.
  893.      */
  894.     private int handleX(final String value, final DoubleMetaphoneResult result, int index) {
  895.         if (index == 0) {
  896.             result.append('S');
  897.             index++;
  898.         } else {
  899.             if (!(index == value.length() - 1 &&
  900.                   (contains(value, index - 3, 3, "IAU", "EAU") ||
  901.                    contains(value, index - 2, 2, "AU", "OU")))) {
  902.                 //-- French for example breaux --//
  903.                 result.append("KS");
  904.             }
  905.             index = contains(value, index + 1, 1, "C", "X") ? index + 2 : index + 1;
  906.         }
  907.         return index;
  908.     }

  909.     //-- BEGIN HELPER FUNCTIONS --//

  910.     /**
  911.      * Handles 'Z' cases.
  912.      */
  913.     private int handleZ(final String value, final DoubleMetaphoneResult result, int index,
  914.                         final boolean slavoGermanic) {
  915.         if (charAt(value, index + 1) == 'H') {
  916.             //-- Chinese pinyin for example "zhao" or Angelina "Zhang" --//
  917.             result.append('J');
  918.             index += 2;
  919.         } else {
  920.             if (contains(value, index + 1, 2, "ZO", "ZI", "ZA") ||
  921.                 slavoGermanic && index > 0 && charAt(value, index - 1) != 'T') {
  922.                 result.append("S", "TS");
  923.             } else {
  924.                 result.append('S');
  925.             }
  926.             index = charAt(value, index + 1) == 'Z' ? index + 2 : index + 1;
  927.         }
  928.         return index;
  929.     }

  930.     /**
  931.      * Check if the Double Metaphone values of two {@code String} values
  932.      * are equal.
  933.      *
  934.      * @param value1 The left-hand side of the encoded {@link String#equals(Object)}.
  935.      * @param value2 The right-hand side of the encoded {@link String#equals(Object)}.
  936.      * @return {@code true} if the encoded {@code String}s are equal;
  937.      *          {@code false} otherwise.
  938.      * @see #isDoubleMetaphoneEqual(String,String,boolean)
  939.      */
  940.     public boolean isDoubleMetaphoneEqual(final String value1, final String value2) {
  941.         return isDoubleMetaphoneEqual(value1, value2, false);
  942.     }

  943.     /**
  944.      * Check if the Double Metaphone values of two {@code String} values
  945.      * are equal, optionally using the alternate value.
  946.      *
  947.      * @param value1 The left-hand side of the encoded {@link String#equals(Object)}.
  948.      * @param value2 The right-hand side of the encoded {@link String#equals(Object)}.
  949.      * @param alternate use the alternate value if {@code true}.
  950.      * @return {@code true} if the encoded {@code String}s are equal;
  951.      *          {@code false} otherwise.
  952.      */
  953.     public boolean isDoubleMetaphoneEqual(final String value1, final String value2, final boolean alternate) {
  954.         return StringUtils.equals(doubleMetaphone(value1, alternate), doubleMetaphone(value2, alternate));
  955.     }

  956.     /**
  957.      * Determines whether or not the value starts with a silent letter.  It will
  958.      * return {@code true} if the value starts with any of 'GN', 'KN',
  959.      * 'PN', 'WR' or 'PS'.
  960.      */
  961.     private boolean isSilentStart(final String value) {
  962.         boolean result = false;
  963.         for (final String element : SILENT_START) {
  964.             if (value.startsWith(element)) {
  965.                 result = true;
  966.                 break;
  967.             }
  968.         }
  969.         return result;
  970.     }

  971.     /**
  972.      * Determines whether or not a value is of slavo-germanic origin. A value is
  973.      * of slavo-germanic origin if it contains any of 'W', 'K', 'CZ', or 'WITZ'.
  974.      */
  975.     private boolean isSlavoGermanic(final String value) {
  976.         return value.indexOf('W') > -1 || value.indexOf('K') > -1 ||
  977.                 value.contains("CZ") || value.contains("WITZ");
  978.     }

  979.     /**
  980.      * Determines whether or not a character is a vowel or not
  981.      */
  982.     private boolean isVowel(final char ch) {
  983.         return VOWELS.indexOf(ch) != -1;
  984.     }

  985.     //-- BEGIN INNER CLASSES --//

  986.     /**
  987.      * Sets the maxCodeLen.
  988.      * @param maxCodeLen The maxCodeLen to set
  989.      */
  990.     public void setMaxCodeLen(final int maxCodeLen) {
  991.         this.maxCodeLen = maxCodeLen;
  992.     }
  993. }