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.  *      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.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 {@link #maxCodeLen} 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.  *
  30.  * @see <a href="http://drdobbs.com/184401251?pgno=2">Original Article</a>
  31.  * @see <a href="http://en.wikipedia.org/wiki/Metaphone">http://en.wikipedia.org/wiki/Metaphone</a>
  32.  *
  33.  * @version $Id: DoubleMetaphone.java 1634417 2014-10-27 00:42:28Z ggregory $
  34.  */
  35. public class DoubleMetaphone implements StringEncoder {

  36.     /**
  37.      * "Vowels" to test for
  38.      */
  39.     private static final String VOWELS = "AEIOUY";

  40.     /**
  41.      * Prefixes when present which are not pronounced
  42.      */
  43.     private static final String[] SILENT_START =
  44.         { "GN", "KN", "PN", "WR", "PS" };
  45.     private static final String[] L_R_N_M_B_H_F_V_W_SPACE =
  46.         { "L", "R", "N", "M", "B", "H", "F", "V", "W", " " };
  47.     private static final String[] ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER =
  48.         { "ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER" };
  49.     private static final String[] L_T_K_S_N_M_B_Z =
  50.         { "L", "T", "K", "S", "N", "M", "B", "Z" };

  51.     /**
  52.      * Maximum length of an encoding, default is 4
  53.      */
  54.     private int maxCodeLen = 4;

  55.     /**
  56.      * Creates an instance of this DoubleMetaphone encoder
  57.      */
  58.     public DoubleMetaphone() {
  59.         super();
  60.     }

  61.     /**
  62.      * Encode a value with Double Metaphone.
  63.      *
  64.      * @param value String to encode
  65.      * @return an encoded string
  66.      */
  67.     public String doubleMetaphone(final String value) {
  68.         return doubleMetaphone(value, false);
  69.     }

  70.     /**
  71.      * Encode a value with Double Metaphone, optionally using the alternate encoding.
  72.      *
  73.      * @param value String to encode
  74.      * @param alternate use alternate encode
  75.      * @return an encoded string
  76.      */
  77.     public String doubleMetaphone(String value, final boolean alternate) {
  78.         value = cleanInput(value);
  79.         if (value == null) {
  80.             return null;
  81.         }

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

  84.         final DoubleMetaphoneResult result = new DoubleMetaphoneResult(this.getMaxCodeLen());

  85.         while (!result.isComplete() && index <= value.length() - 1) {
  86.             switch (value.charAt(index)) {
  87.             case 'A':
  88.             case 'E':
  89.             case 'I':
  90.             case 'O':
  91.             case 'U':
  92.             case 'Y':
  93.                 index = handleAEIOUY(result, index);
  94.                 break;
  95.             case 'B':
  96.                 result.append('P');
  97.                 index = charAt(value, index + 1) == 'B' ? index + 2 : index + 1;
  98.                 break;
  99.             case '\u00C7':
  100.                 // A C with a Cedilla
  101.                 result.append('S');
  102.                 index++;
  103.                 break;
  104.             case 'C':
  105.                 index = handleC(value, result, index);
  106.                 break;
  107.             case 'D':
  108.                 index = handleD(value, result, index);
  109.                 break;
  110.             case 'F':
  111.                 result.append('F');
  112.                 index = charAt(value, index + 1) == 'F' ? index + 2 : index + 1;
  113.                 break;
  114.             case 'G':
  115.                 index = handleG(value, result, index, slavoGermanic);
  116.                 break;
  117.             case 'H':
  118.                 index = handleH(value, result, index);
  119.                 break;
  120.             case 'J':
  121.                 index = handleJ(value, result, index, slavoGermanic);
  122.                 break;
  123.             case 'K':
  124.                 result.append('K');
  125.                 index = charAt(value, index + 1) == 'K' ? index + 2 : index + 1;
  126.                 break;
  127.             case 'L':
  128.                 index = handleL(value, result, index);
  129.                 break;
  130.             case 'M':
  131.                 result.append('M');
  132.                 index = conditionM0(value, index) ? index + 2 : index + 1;
  133.                 break;
  134.             case 'N':
  135.                 result.append('N');
  136.                 index = charAt(value, index + 1) == 'N' ? index + 2 : index + 1;
  137.                 break;
  138.             case '\u00D1':
  139.                 // N with a tilde (spanish ene)
  140.                 result.append('N');
  141.                 index++;
  142.                 break;
  143.             case 'P':
  144.                 index = handleP(value, result, index);
  145.                 break;
  146.             case 'Q':
  147.                 result.append('K');
  148.                 index = charAt(value, index + 1) == 'Q' ? index + 2 : index + 1;
  149.                 break;
  150.             case 'R':
  151.                 index = handleR(value, result, index, slavoGermanic);
  152.                 break;
  153.             case 'S':
  154.                 index = handleS(value, result, index, slavoGermanic);
  155.                 break;
  156.             case 'T':
  157.                 index = handleT(value, result, index);
  158.                 break;
  159.             case 'V':
  160.                 result.append('F');
  161.                 index = charAt(value, index + 1) == 'V' ? index + 2 : index + 1;
  162.                 break;
  163.             case 'W':
  164.                 index = handleW(value, result, index);
  165.                 break;
  166.             case 'X':
  167.                 index = handleX(value, result, index);
  168.                 break;
  169.             case 'Z':
  170.                 index = handleZ(value, result, index, slavoGermanic);
  171.                 break;
  172.             default:
  173.                 index++;
  174.                 break;
  175.             }
  176.         }

  177.         return alternate ? result.getAlternate() : result.getPrimary();
  178.     }

  179.     /**
  180.      * Encode the value using DoubleMetaphone.  It will only work if
  181.      * <code>obj</code> is a <code>String</code> (like <code>Metaphone</code>).
  182.      *
  183.      * @param obj Object to encode (should be of type String)
  184.      * @return An encoded Object (will be of type String)
  185.      * @throws EncoderException encode parameter is not of type String
  186.      */
  187.     @Override
  188.     public Object encode(final Object obj) throws EncoderException {
  189.         if (!(obj instanceof String)) {
  190.             throw new EncoderException("DoubleMetaphone encode parameter is not of type String");
  191.         }
  192.         return doubleMetaphone((String) obj);
  193.     }

  194.     /**
  195.      * Encode the value using DoubleMetaphone.
  196.      *
  197.      * @param value String to encode
  198.      * @return An encoded String
  199.      */
  200.     @Override
  201.     public String encode(final String value) {
  202.         return doubleMetaphone(value);
  203.     }

  204.     /**
  205.      * Check if the Double Metaphone values of two <code>String</code> values
  206.      * are equal.
  207.      *
  208.      * @param value1 The left-hand side of the encoded {@link String#equals(Object)}.
  209.      * @param value2 The right-hand side of the encoded {@link String#equals(Object)}.
  210.      * @return <code>true</code> if the encoded <code>String</code>s are equal;
  211.      *          <code>false</code> otherwise.
  212.      * @see #isDoubleMetaphoneEqual(String,String,boolean)
  213.      */
  214.     public boolean isDoubleMetaphoneEqual(final String value1, final String value2) {
  215.         return isDoubleMetaphoneEqual(value1, value2, false);
  216.     }

  217.     /**
  218.      * Check if the Double Metaphone values of two <code>String</code> values
  219.      * are equal, optionally using the alternate value.
  220.      *
  221.      * @param value1 The left-hand side of the encoded {@link String#equals(Object)}.
  222.      * @param value2 The right-hand side of the encoded {@link String#equals(Object)}.
  223.      * @param alternate use the alternate value if <code>true</code>.
  224.      * @return <code>true</code> if the encoded <code>String</code>s are equal;
  225.      *          <code>false</code> otherwise.
  226.      */
  227.     public boolean isDoubleMetaphoneEqual(final String value1, final String value2, final boolean alternate) {
  228.         return StringUtils.equals(doubleMetaphone(value1, alternate), doubleMetaphone(value2, alternate));
  229.     }

  230.     /**
  231.      * Returns the maxCodeLen.
  232.      * @return int
  233.      */
  234.     public int getMaxCodeLen() {
  235.         return this.maxCodeLen;
  236.     }

  237.     /**
  238.      * Sets the maxCodeLen.
  239.      * @param maxCodeLen The maxCodeLen to set
  240.      */
  241.     public void setMaxCodeLen(final int maxCodeLen) {
  242.         this.maxCodeLen = maxCodeLen;
  243.     }

  244.     //-- BEGIN HANDLERS --//

  245.     /**
  246.      * Handles 'A', 'E', 'I', 'O', 'U', and 'Y' cases.
  247.      */
  248.     private int handleAEIOUY(final DoubleMetaphoneResult result, final int index) {
  249.         if (index == 0) {
  250.             result.append('A');
  251.         }
  252.         return index + 1;
  253.     }

  254.     /**
  255.      * Handles 'C' cases.
  256.      */
  257.     private int handleC(final String value, final DoubleMetaphoneResult result, int index) {
  258.         if (conditionC0(value, index)) {  // very confusing, moved out
  259.             result.append('K');
  260.             index += 2;
  261.         } else if (index == 0 && contains(value, index, 6, "CAESAR")) {
  262.             result.append('S');
  263.             index += 2;
  264.         } else if (contains(value, index, 2, "CH")) {
  265.             index = handleCH(value, result, index);
  266.         } else if (contains(value, index, 2, "CZ") &&
  267.                    !contains(value, index - 2, 4, "WICZ")) {
  268.             //-- "Czerny" --//
  269.             result.append('S', 'X');
  270.             index += 2;
  271.         } else if (contains(value, index + 1, 3, "CIA")) {
  272.             //-- "focaccia" --//
  273.             result.append('X');
  274.             index += 3;
  275.         } else if (contains(value, index, 2, "CC") &&
  276.                    !(index == 1 && charAt(value, 0) == 'M')) {
  277.             //-- double "cc" but not "McClelland" --//
  278.             return handleCC(value, result, index);
  279.         } else if (contains(value, index, 2, "CK", "CG", "CQ")) {
  280.             result.append('K');
  281.             index += 2;
  282.         } else if (contains(value, index, 2, "CI", "CE", "CY")) {
  283.             //-- Italian vs. English --//
  284.             if (contains(value, index, 3, "CIO", "CIE", "CIA")) {
  285.                 result.append('S', 'X');
  286.             } else {
  287.                 result.append('S');
  288.             }
  289.             index += 2;
  290.         } else {
  291.             result.append('K');
  292.             if (contains(value, index + 1, 2, " C", " Q", " G")) {
  293.                 //-- Mac Caffrey, Mac Gregor --//
  294.                 index += 3;
  295.             } else if (contains(value, index + 1, 1, "C", "K", "Q") &&
  296.                        !contains(value, index + 1, 2, "CE", "CI")) {
  297.                 index += 2;
  298.             } else {
  299.                 index++;
  300.             }
  301.         }

  302.         return index;
  303.     }

  304.     /**
  305.      * Handles 'CC' cases.
  306.      */
  307.     private int handleCC(final String value, final DoubleMetaphoneResult result, int index) {
  308.         if (contains(value, index + 2, 1, "I", "E", "H") &&
  309.             !contains(value, index + 2, 2, "HU")) {
  310.             //-- "bellocchio" but not "bacchus" --//
  311.             if ((index == 1 && charAt(value, index - 1) == 'A') ||
  312.                 contains(value, index - 1, 5, "UCCEE", "UCCES")) {
  313.                 //-- "accident", "accede", "succeed" --//
  314.                 result.append("KS");
  315.             } else {
  316.                 //-- "bacci", "bertucci", other Italian --//
  317.                 result.append('X');
  318.             }
  319.             index += 3;
  320.         } else {    // Pierce's rule
  321.             result.append('K');
  322.             index += 2;
  323.         }

  324.         return index;
  325.     }

  326.     /**
  327.      * Handles 'CH' cases.
  328.      */
  329.     private int handleCH(final String value, final DoubleMetaphoneResult result, final int index) {
  330.         if (index > 0 && contains(value, index, 4, "CHAE")) {   // Michael
  331.             result.append('K', 'X');
  332.             return index + 2;
  333.         } else if (conditionCH0(value, index)) {
  334.             //-- Greek roots ("chemistry", "chorus", etc.) --//
  335.             result.append('K');
  336.             return index + 2;
  337.         } else if (conditionCH1(value, index)) {
  338.             //-- Germanic, Greek, or otherwise 'ch' for 'kh' sound --//
  339.             result.append('K');
  340.             return index + 2;
  341.         } else {
  342.             if (index > 0) {
  343.                 if (contains(value, 0, 2, "MC")) {
  344.                     result.append('K');
  345.                 } else {
  346.                     result.append('X', 'K');
  347.                 }
  348.             } else {
  349.                 result.append('X');
  350.             }
  351.             return index + 2;
  352.         }
  353.     }

  354.     /**
  355.      * Handles 'D' cases.
  356.      */
  357.     private int handleD(final String value, final DoubleMetaphoneResult result, int index) {
  358.         if (contains(value, index, 2, "DG")) {
  359.             //-- "Edge" --//
  360.             if (contains(value, index + 2, 1, "I", "E", "Y")) {
  361.                 result.append('J');
  362.                 index += 3;
  363.                 //-- "Edgar" --//
  364.             } else {
  365.                 result.append("TK");
  366.                 index += 2;
  367.             }
  368.         } else if (contains(value, index, 2, "DT", "DD")) {
  369.             result.append('T');
  370.             index += 2;
  371.         } else {
  372.             result.append('T');
  373.             index++;
  374.         }
  375.         return index;
  376.     }

  377.     /**
  378.      * Handles 'G' cases.
  379.      */
  380.     private int handleG(final String value, final DoubleMetaphoneResult result, int index,
  381.                         final boolean slavoGermanic) {
  382.         if (charAt(value, index + 1) == 'H') {
  383.             index = handleGH(value, result, index);
  384.         } else if (charAt(value, index + 1) == 'N') {
  385.             if (index == 1 && isVowel(charAt(value, 0)) && !slavoGermanic) {
  386.                 result.append("KN", "N");
  387.             } else if (!contains(value, index + 2, 2, "EY") &&
  388.                        charAt(value, index + 1) != 'Y' && !slavoGermanic) {
  389.                 result.append("N", "KN");
  390.             } else {
  391.                 result.append("KN");
  392.             }
  393.             index = index + 2;
  394.         } else if (contains(value, index + 1, 2, "LI") && !slavoGermanic) {
  395.             result.append("KL", "L");
  396.             index += 2;
  397.         } else if (index == 0 &&
  398.                    (charAt(value, index + 1) == 'Y' ||
  399.                     contains(value, index + 1, 2, ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER))) {
  400.             //-- -ges-, -gep-, -gel-, -gie- at beginning --//
  401.             result.append('K', 'J');
  402.             index += 2;
  403.         } else if ((contains(value, index + 1, 2, "ER") ||
  404.                     charAt(value, index + 1) == 'Y') &&
  405.                    !contains(value, 0, 6, "DANGER", "RANGER", "MANGER") &&
  406.                    !contains(value, index - 1, 1, "E", "I") &&
  407.                    !contains(value, index - 1, 3, "RGY", "OGY")) {
  408.             //-- -ger-, -gy- --//
  409.             result.append('K', 'J');
  410.             index += 2;
  411.         } else if (contains(value, index + 1, 1, "E", "I", "Y") ||
  412.                    contains(value, index - 1, 4, "AGGI", "OGGI")) {
  413.             //-- Italian "biaggi" --//
  414.             if (contains(value, 0 ,4, "VAN ", "VON ") ||
  415.                 contains(value, 0, 3, "SCH") ||
  416.                 contains(value, index + 1, 2, "ET")) {
  417.                 //-- obvious germanic --//
  418.                 result.append('K');
  419.             } else if (contains(value, index + 1, 3, "IER")) {
  420.                 result.append('J');
  421.             } else {
  422.                 result.append('J', 'K');
  423.             }
  424.             index += 2;
  425.         } else if (charAt(value, index + 1) == 'G') {
  426.             index += 2;
  427.             result.append('K');
  428.         } else {
  429.             index++;
  430.             result.append('K');
  431.         }
  432.         return index;
  433.     }

  434.     /**
  435.      * Handles 'GH' cases.
  436.      */
  437.     private int handleGH(final String value, final DoubleMetaphoneResult result, int index) {
  438.         if (index > 0 && !isVowel(charAt(value, index - 1))) {
  439.             result.append('K');
  440.             index += 2;
  441.         } else if (index == 0) {
  442.             if (charAt(value, index + 2) == 'I') {
  443.                 result.append('J');
  444.             } else {
  445.                 result.append('K');
  446.             }
  447.             index += 2;
  448.         } else if ((index > 1 && contains(value, index - 2, 1, "B", "H", "D")) ||
  449.                    (index > 2 && contains(value, index - 3, 1, "B", "H", "D")) ||
  450.                    (index > 3 && contains(value, index - 4, 1, "B", "H"))) {
  451.             //-- Parker's rule (with some further refinements) - "hugh"
  452.             index += 2;
  453.         } else {
  454.             if (index > 2 && charAt(value, index - 1) == 'U' &&
  455.                 contains(value, index - 3, 1, "C", "G", "L", "R", "T")) {
  456.                 //-- "laugh", "McLaughlin", "cough", "gough", "rough", "tough"
  457.                 result.append('F');
  458.             } else if (index > 0 && charAt(value, index - 1) != 'I') {
  459.                 result.append('K');
  460.             }
  461.             index += 2;
  462.         }
  463.         return index;
  464.     }

  465.     /**
  466.      * Handles 'H' cases.
  467.      */
  468.     private int handleH(final String value, final DoubleMetaphoneResult result, int index) {
  469.         //-- only keep if first & before vowel or between 2 vowels --//
  470.         if ((index == 0 || isVowel(charAt(value, index - 1))) &&
  471.             isVowel(charAt(value, index + 1))) {
  472.             result.append('H');
  473.             index += 2;
  474.             //-- also takes car of "HH" --//
  475.         } else {
  476.             index++;
  477.         }
  478.         return index;
  479.     }

  480.     /**
  481.      * Handles 'J' cases.
  482.      */
  483.     private int handleJ(final String value, final DoubleMetaphoneResult result, int index,
  484.                         final boolean slavoGermanic) {
  485.         if (contains(value, index, 4, "JOSE") || contains(value, 0, 4, "SAN ")) {
  486.                 //-- obvious Spanish, "Jose", "San Jacinto" --//
  487.                 if ((index == 0 && (charAt(value, index + 4) == ' ') ||
  488.                      value.length() == 4) || contains(value, 0, 4, "SAN ")) {
  489.                     result.append('H');
  490.                 } else {
  491.                     result.append('J', 'H');
  492.                 }
  493.                 index++;
  494.             } else {
  495.                 if (index == 0 && !contains(value, index, 4, "JOSE")) {
  496.                     result.append('J', 'A');
  497.                 } else if (isVowel(charAt(value, index - 1)) && !slavoGermanic &&
  498.                            (charAt(value, index + 1) == 'A' || charAt(value, index + 1) == 'O')) {
  499.                     result.append('J', 'H');
  500.                 } else if (index == value.length() - 1) {
  501.                     result.append('J', ' ');
  502.                 } else if (!contains(value, index + 1, 1, L_T_K_S_N_M_B_Z) &&
  503.                            !contains(value, index - 1, 1, "S", "K", "L")) {
  504.                     result.append('J');
  505.                 }

  506.                 if (charAt(value, index + 1) == 'J') {
  507.                     index += 2;
  508.                 } else {
  509.                     index++;
  510.                 }
  511.             }
  512.         return index;
  513.     }

  514.     /**
  515.      * Handles 'L' cases.
  516.      */
  517.     private int handleL(final String value, final DoubleMetaphoneResult result, int index) {
  518.         if (charAt(value, index + 1) == 'L') {
  519.             if (conditionL0(value, index)) {
  520.                 result.appendPrimary('L');
  521.             } else {
  522.                 result.append('L');
  523.             }
  524.             index += 2;
  525.         } else {
  526.             index++;
  527.             result.append('L');
  528.         }
  529.         return index;
  530.     }

  531.     /**
  532.      * Handles 'P' cases.
  533.      */
  534.     private int handleP(final String value, final DoubleMetaphoneResult result, int index) {
  535.         if (charAt(value, index + 1) == 'H') {
  536.             result.append('F');
  537.             index += 2;
  538.         } else {
  539.             result.append('P');
  540.             index = contains(value, index + 1, 1, "P", "B") ? index + 2 : index + 1;
  541.         }
  542.         return index;
  543.     }

  544.     /**
  545.      * Handles 'R' cases.
  546.      */
  547.     private int handleR(final String value, final DoubleMetaphoneResult result, final int index,
  548.                         final boolean slavoGermanic) {
  549.         if (index == value.length() - 1 && !slavoGermanic &&
  550.             contains(value, index - 2, 2, "IE") &&
  551.             !contains(value, index - 4, 2, "ME", "MA")) {
  552.             result.appendAlternate('R');
  553.         } else {
  554.             result.append('R');
  555.         }
  556.         return charAt(value, index + 1) == 'R' ? index + 2 : index + 1;
  557.     }

  558.     /**
  559.      * Handles 'S' cases.
  560.      */
  561.     private int handleS(final String value, final DoubleMetaphoneResult result, int index,
  562.                         final boolean slavoGermanic) {
  563.         if (contains(value, index - 1, 3, "ISL", "YSL")) {
  564.             //-- special cases "island", "isle", "carlisle", "carlysle" --//
  565.             index++;
  566.         } else if (index == 0 && contains(value, index, 5, "SUGAR")) {
  567.             //-- special case "sugar-" --//
  568.             result.append('X', 'S');
  569.             index++;
  570.         } else if (contains(value, index, 2, "SH")) {
  571.             if (contains(value, index + 1, 4, "HEIM", "HOEK", "HOLM", "HOLZ")) {
  572.                 //-- germanic --//
  573.                 result.append('S');
  574.             } else {
  575.                 result.append('X');
  576.             }
  577.             index += 2;
  578.         } else if (contains(value, index, 3, "SIO", "SIA") || contains(value, index, 4, "SIAN")) {
  579.             //-- Italian and Armenian --//
  580.             if (slavoGermanic) {
  581.                 result.append('S');
  582.             } else {
  583.                 result.append('S', 'X');
  584.             }
  585.             index += 3;
  586.         } else if ((index == 0 && contains(value, index + 1, 1, "M", "N", "L", "W")) ||
  587.                    contains(value, index + 1, 1, "Z")) {
  588.             //-- german & anglicisations, e.g. "smith" match "schmidt" //
  589.             // "snider" match "schneider" --//
  590.             //-- also, -sz- in slavic language although in hungarian it //
  591.             //   is pronounced "s" --//
  592.             result.append('S', 'X');
  593.             index = contains(value, index + 1, 1, "Z") ? index + 2 : index + 1;
  594.         } else if (contains(value, index, 2, "SC")) {
  595.             index = handleSC(value, result, index);
  596.         } else {
  597.             if (index == value.length() - 1 && contains(value, index - 2, 2, "AI", "OI")) {
  598.                 //-- french e.g. "resnais", "artois" --//
  599.                 result.appendAlternate('S');
  600.             } else {
  601.                 result.append('S');
  602.             }
  603.             index = contains(value, index + 1, 1, "S", "Z") ? index + 2 : index + 1;
  604.         }
  605.         return index;
  606.     }

  607.     /**
  608.      * Handles 'SC' cases.
  609.      */
  610.     private int handleSC(final String value, final DoubleMetaphoneResult result, final int index) {
  611.         if (charAt(value, index + 2) == 'H') {
  612.             //-- Schlesinger's rule --//
  613.             if (contains(value, index + 3, 2, "OO", "ER", "EN", "UY", "ED", "EM")) {
  614.                 //-- Dutch origin, e.g. "school", "schooner" --//
  615.                 if (contains(value, index + 3, 2, "ER", "EN")) {
  616.                     //-- "schermerhorn", "schenker" --//
  617.                     result.append("X", "SK");
  618.                 } else {
  619.                     result.append("SK");
  620.                 }
  621.             } else {
  622.                 if (index == 0 && !isVowel(charAt(value, 3)) && charAt(value, 3) != 'W') {
  623.                     result.append('X', 'S');
  624.                 } else {
  625.                     result.append('X');
  626.                 }
  627.             }
  628.         } else if (contains(value, index + 2, 1, "I", "E", "Y")) {
  629.             result.append('S');
  630.         } else {
  631.             result.append("SK");
  632.         }
  633.         return index + 3;
  634.     }

  635.     /**
  636.      * Handles 'T' cases.
  637.      */
  638.     private int handleT(final String value, final DoubleMetaphoneResult result, int index) {
  639.         if (contains(value, index, 4, "TION")) {
  640.             result.append('X');
  641.             index += 3;
  642.         } else if (contains(value, index, 3, "TIA", "TCH")) {
  643.             result.append('X');
  644.             index += 3;
  645.         } else if (contains(value, index, 2, "TH") || contains(value, index, 3, "TTH")) {
  646.             if (contains(value, index + 2, 2, "OM", "AM") ||
  647.                 //-- special case "thomas", "thames" or germanic --//
  648.                 contains(value, 0, 4, "VAN ", "VON ") ||
  649.                 contains(value, 0, 3, "SCH")) {
  650.                 result.append('T');
  651.             } else {
  652.                 result.append('0', 'T');
  653.             }
  654.             index += 2;
  655.         } else {
  656.             result.append('T');
  657.             index = contains(value, index + 1, 1, "T", "D") ? index + 2 : index + 1;
  658.         }
  659.         return index;
  660.     }

  661.     /**
  662.      * Handles 'W' cases.
  663.      */
  664.     private int handleW(final String value, final DoubleMetaphoneResult result, int index) {
  665.         if (contains(value, index, 2, "WR")) {
  666.             //-- can also be in middle of word --//
  667.             result.append('R');
  668.             index += 2;
  669.         } else {
  670.             if (index == 0 && (isVowel(charAt(value, index + 1)) ||
  671.                                contains(value, index, 2, "WH"))) {
  672.                 if (isVowel(charAt(value, index + 1))) {
  673.                     //-- Wasserman should match Vasserman --//
  674.                     result.append('A', 'F');
  675.                 } else {
  676.                     //-- need Uomo to match Womo --//
  677.                     result.append('A');
  678.                 }
  679.                 index++;
  680.             } else if ((index == value.length() - 1 && isVowel(charAt(value, index - 1))) ||
  681.                        contains(value, index - 1, 5, "EWSKI", "EWSKY", "OWSKI", "OWSKY") ||
  682.                        contains(value, 0, 3, "SCH")) {
  683.                 //-- Arnow should match Arnoff --//
  684.                 result.appendAlternate('F');
  685.                 index++;
  686.             } else if (contains(value, index, 4, "WICZ", "WITZ")) {
  687.                 //-- Polish e.g. "filipowicz" --//
  688.                 result.append("TS", "FX");
  689.                 index += 4;
  690.             } else {
  691.                 index++;
  692.             }
  693.         }
  694.         return index;
  695.     }

  696.     /**
  697.      * Handles 'X' cases.
  698.      */
  699.     private int handleX(final String value, final DoubleMetaphoneResult result, int index) {
  700.         if (index == 0) {
  701.             result.append('S');
  702.             index++;
  703.         } else {
  704.             if (!((index == value.length() - 1) &&
  705.                   (contains(value, index - 3, 3, "IAU", "EAU") ||
  706.                    contains(value, index - 2, 2, "AU", "OU")))) {
  707.                 //-- French e.g. breaux --//
  708.                 result.append("KS");
  709.             }
  710.             index = contains(value, index + 1, 1, "C", "X") ? index + 2 : index + 1;
  711.         }
  712.         return index;
  713.     }

  714.     /**
  715.      * Handles 'Z' cases.
  716.      */
  717.     private int handleZ(final String value, final DoubleMetaphoneResult result, int index,
  718.                         final boolean slavoGermanic) {
  719.         if (charAt(value, index + 1) == 'H') {
  720.             //-- Chinese pinyin e.g. "zhao" or Angelina "Zhang" --//
  721.             result.append('J');
  722.             index += 2;
  723.         } else {
  724.             if (contains(value, index + 1, 2, "ZO", "ZI", "ZA") ||
  725.                 (slavoGermanic && (index > 0 && charAt(value, index - 1) != 'T'))) {
  726.                 result.append("S", "TS");
  727.             } else {
  728.                 result.append('S');
  729.             }
  730.             index = charAt(value, index + 1) == 'Z' ? index + 2 : index + 1;
  731.         }
  732.         return index;
  733.     }

  734.     //-- BEGIN CONDITIONS --//

  735.     /**
  736.      * Complex condition 0 for 'C'.
  737.      */
  738.     private boolean conditionC0(final String value, final int index) {
  739.         if (contains(value, index, 4, "CHIA")) {
  740.             return true;
  741.         } else if (index <= 1) {
  742.             return false;
  743.         } else if (isVowel(charAt(value, index - 2))) {
  744.             return false;
  745.         } else if (!contains(value, index - 1, 3, "ACH")) {
  746.             return false;
  747.         } else {
  748.             final char c = charAt(value, index + 2);
  749.             return (c != 'I' && c != 'E') ||
  750.                     contains(value, index - 2, 6, "BACHER", "MACHER");
  751.         }
  752.     }

  753.     /**
  754.      * Complex condition 0 for 'CH'.
  755.      */
  756.     private boolean conditionCH0(final String value, final int index) {
  757.         if (index != 0) {
  758.             return false;
  759.         } else if (!contains(value, index + 1, 5, "HARAC", "HARIS") &&
  760.                    !contains(value, index + 1, 3, "HOR", "HYM", "HIA", "HEM")) {
  761.             return false;
  762.         } else if (contains(value, 0, 5, "CHORE")) {
  763.             return false;
  764.         } else {
  765.             return true;
  766.         }
  767.     }

  768.     /**
  769.      * Complex condition 1 for 'CH'.
  770.      */
  771.     private boolean conditionCH1(final String value, final int index) {
  772.         return ((contains(value, 0, 4, "VAN ", "VON ") || contains(value, 0, 3, "SCH")) ||
  773.                 contains(value, index - 2, 6, "ORCHES", "ARCHIT", "ORCHID") ||
  774.                 contains(value, index + 2, 1, "T", "S") ||
  775.                 ((contains(value, index - 1, 1, "A", "O", "U", "E") || index == 0) &&
  776.                  (contains(value, index + 2, 1, L_R_N_M_B_H_F_V_W_SPACE) || index + 1 == value.length() - 1)));
  777.     }

  778.     /**
  779.      * Complex condition 0 for 'L'.
  780.      */
  781.     private boolean conditionL0(final String value, final int index) {
  782.         if (index == value.length() - 3 &&
  783.             contains(value, index - 1, 4, "ILLO", "ILLA", "ALLE")) {
  784.             return true;
  785.         } else if ((contains(value, value.length() - 2, 2, "AS", "OS") ||
  786.                     contains(value, value.length() - 1, 1, "A", "O")) &&
  787.                    contains(value, index - 1, 4, "ALLE")) {
  788.             return true;
  789.         } else {
  790.             return false;
  791.         }
  792.     }

  793.     /**
  794.      * Complex condition 0 for 'M'.
  795.      */
  796.     private boolean conditionM0(final String value, final int index) {
  797.         if (charAt(value, index + 1) == 'M') {
  798.             return true;
  799.         }
  800.         return contains(value, index - 1, 3, "UMB") &&
  801.                ((index + 1) == value.length() - 1 || contains(value, index + 2, 2, "ER"));
  802.     }

  803.     //-- BEGIN HELPER FUNCTIONS --//

  804.     /**
  805.      * Determines whether or not a value is of slavo-germanic origin. A value is
  806.      * of slavo-germanic origin if it contians any of 'W', 'K', 'CZ', or 'WITZ'.
  807.      */
  808.     private boolean isSlavoGermanic(final String value) {
  809.         return value.indexOf('W') > -1 || value.indexOf('K') > -1 ||
  810.             value.indexOf("CZ") > -1 || value.indexOf("WITZ") > -1;
  811.     }

  812.     /**
  813.      * Determines whether or not a character is a vowel or not
  814.      */
  815.     private boolean isVowel(final char ch) {
  816.         return VOWELS.indexOf(ch) != -1;
  817.     }

  818.     /**
  819.      * Determines whether or not the value starts with a silent letter.  It will
  820.      * return <code>true</code> if the value starts with any of 'GN', 'KN',
  821.      * 'PN', 'WR' or 'PS'.
  822.      */
  823.     private boolean isSilentStart(final String value) {
  824.         boolean result = false;
  825.         for (final String element : SILENT_START) {
  826.             if (value.startsWith(element)) {
  827.                 result = true;
  828.                 break;
  829.             }
  830.         }
  831.         return result;
  832.     }

  833.     /**
  834.      * Cleans the input.
  835.      */
  836.     private String cleanInput(String input) {
  837.         if (input == null) {
  838.             return null;
  839.         }
  840.         input = input.trim();
  841.         if (input.length() == 0) {
  842.             return null;
  843.         }
  844.         return input.toUpperCase(java.util.Locale.ENGLISH);
  845.     }

  846.     /*
  847.      * Gets the character at index <code>index</code> if available, otherwise
  848.      * it returns <code>Character.MIN_VALUE</code> so that there is some sort
  849.      * of a default.
  850.      */
  851.     protected char charAt(final String value, final int index) {
  852.         if (index < 0 || index >= value.length()) {
  853.             return Character.MIN_VALUE;
  854.         }
  855.         return value.charAt(index);
  856.     }

  857.     /*
  858.      * Determines whether <code>value</code> contains any of the criteria starting at index <code>start</code> and
  859.      * matching up to length <code>length</code>.
  860.      */
  861.     protected static boolean contains(final String value, final int start, final int length,
  862.                                       final String... criteria) {
  863.         boolean result = false;
  864.         if (start >= 0 && start + length <= value.length()) {
  865.             final String target = value.substring(start, start + length);

  866.             for (final String element : criteria) {
  867.                 if (target.equals(element)) {
  868.                     result = true;
  869.                     break;
  870.                 }
  871.             }
  872.         }
  873.         return result;
  874.     }

  875.     //-- BEGIN INNER CLASSES --//

  876.     /**
  877.      * Inner class for storing results, since there is the optional alternate encoding.
  878.      */
  879.     public class DoubleMetaphoneResult {

  880.         private final StringBuilder primary = new StringBuilder(getMaxCodeLen());
  881.         private final StringBuilder alternate = new StringBuilder(getMaxCodeLen());
  882.         private final int maxLength;

  883.         public DoubleMetaphoneResult(final int maxLength) {
  884.             this.maxLength = maxLength;
  885.         }

  886.         public void append(final char value) {
  887.             appendPrimary(value);
  888.             appendAlternate(value);
  889.         }

  890.         public void append(final char primary, final char alternate) {
  891.             appendPrimary(primary);
  892.             appendAlternate(alternate);
  893.         }

  894.         public void appendPrimary(final char value) {
  895.             if (this.primary.length() < this.maxLength) {
  896.                 this.primary.append(value);
  897.             }
  898.         }

  899.         public void appendAlternate(final char value) {
  900.             if (this.alternate.length() < this.maxLength) {
  901.                 this.alternate.append(value);
  902.             }
  903.         }

  904.         public void append(final String value) {
  905.             appendPrimary(value);
  906.             appendAlternate(value);
  907.         }

  908.         public void append(final String primary, final String alternate) {
  909.             appendPrimary(primary);
  910.             appendAlternate(alternate);
  911.         }

  912.         public void appendPrimary(final String value) {
  913.             final int addChars = this.maxLength - this.primary.length();
  914.             if (value.length() <= addChars) {
  915.                 this.primary.append(value);
  916.             } else {
  917.                 this.primary.append(value.substring(0, addChars));
  918.             }
  919.         }

  920.         public void appendAlternate(final String value) {
  921.             final int addChars = this.maxLength - this.alternate.length();
  922.             if (value.length() <= addChars) {
  923.                 this.alternate.append(value);
  924.             } else {
  925.                 this.alternate.append(value.substring(0, addChars));
  926.             }
  927.         }

  928.         public String getPrimary() {
  929.             return this.primary.toString();
  930.         }

  931.         public String getAlternate() {
  932.             return this.alternate.toString();
  933.         }

  934.         public boolean isComplete() {
  935.             return this.primary.length() >= this.maxLength &&
  936.                    this.alternate.length() >= this.maxLength;
  937.         }
  938.     }
  939. }