content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You 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 org.apache.commons.codec.language;
  
  import org.apache.commons.codec.EncoderException;
  import org.apache.commons.codec.StringEncoder;
  
  /**
   * Encodes a string into a Metaphone value. 
   * <p>
   * Initial Java implementation by <CITE>William B. Brogden. December, 1997</CITE>. 
   * Permission given by <CITE>wbrogden</CITE> for code to be used anywhere.
   * </p>
   * <p>
   * <CITE>Hanging on the Metaphone</CITE> by <CITE>Lawrence Philips</CITE> in <CITE>Computer Language of Dec. 1990, p
   * 39.</CITE>
   * </p>
   * <p>
   * Note, that this does not match the algorithm that ships with PHP, or the algorithm 
   * found in the Perl <a href="http://search.cpan.org/~mschwern/Text-Metaphone-1.96/Metaphone.pm">Text:Metaphone-1.96</a>.
   * They have had undocumented changes from the originally published algorithm. 
   * For more information, see <a href="https://issues.apache.org/jira/browse/CODEC-57">CODEC-57</a>.
   * </p>
   * 
   * @author Apache Software Foundation
   * @version $Id: Metaphone.java 1080867 2011-03-12 06:06:46Z ggregory $
   */
  public class Metaphone implements StringEncoder {
  
      /**
       * Five values in the English language 
       */
      private static final String VOWELS = "AEIOU" ;
  
      /**
       * Variable used in Metaphone algorithm
       */
      private static final String FRONTV = "EIY"   ;
  
      /**
       * Variable used in Metaphone algorithm
       */
      private static final String VARSON = "CSPTG" ;
  
      /**
       * The max code length for metaphone is 4
       */
      private int maxCodeLen = 4 ;
  
      /**
       * Creates an instance of the Metaphone encoder
       */
      public Metaphone() {
          super();
      }
  
      /**
       * Find the metaphone value of a String. This is similar to the
       * soundex algorithm, but better at finding similar sounding words.
       * All input is converted to upper case.
       * Limitations: Input format is expected to be a single ASCII word
       * with only characters in the A - Z range, no punctuation or numbers.
       *
       * @param txt String to find the metaphone code for
       * @return A metaphone code corresponding to the String supplied
       */
      public String metaphone(String txt) {
          boolean hard = false ;
          if ((txt == null) || (txt.length() == 0)) {
              return "" ;
          }
          // single character is itself
          if (txt.length() == 1) {
              return txt.toUpperCase(java.util.Locale.ENGLISH) ;
          }
        
          char[] inwd = txt.toUpperCase(java.util.Locale.ENGLISH).toCharArray() ;
        
          StringBuffer local = new StringBuffer(40); // manipulate
          StringBuffer code = new StringBuffer(10) ; //   output
          // handle initial 2 characters exceptions
          switch(inwd[0]) {
          case 'K' : 
          case 'G' : 
          case 'P' : /* looking for KN, etc*/
              if (inwd[1] == 'N') {
                  local.append(inwd, 1, inwd.length - 1);
              } else {
                  local.append(inwd);
              }
              break;
          case 'A': /* looking for AE */
              if (inwd[1] == 'E') {
                  local.append(inwd, 1, inwd.length - 1);
              } else {
                  local.append(inwd);
              }
              break;
          case 'W' : /* looking for WR or WH */
              if (inwd[1] == 'R') {   // WR -> R
                  local.append(inwd, 1, inwd.length - 1); 
                  break ;
              }
              if (inwd[1] == 'H') {
                  local.append(inwd, 1, inwd.length - 1);
                  local.setCharAt(0, 'W'); // WH -> W
              } else {
                  local.append(inwd);
              }
              break;
          case 'X' : /* initial X becomes S */
              inwd[0] = 'S';
              local.append(inwd);
              break ;
          default :
              local.append(inwd);
          } // now local has working string with initials fixed
  
          int wdsz = local.length();
          int n = 0 ;
  
          while ((code.length() < this.getMaxCodeLen()) && 
                 (n < wdsz) ) { // max code size of 4 works well
              char symb = local.charAt(n) ;
              // remove duplicate letters except C
              if ((symb != 'C') && (isPreviousChar( local, n, symb )) ) {
                  n++ ;
              } else { // not dup
                  switch(symb) {
                  case 'A' : case 'E' : case 'I' : case 'O' : case 'U' :
                      if (n == 0) { 
                          code.append(symb);
                      }
                      break ; // only use vowel if leading char
                  case 'B' :
                      if ( isPreviousChar(local, n, 'M') && 
                           isLastChar(wdsz, n) ) { // B is silent if word ends in MB
                          break;
                      }
                      code.append(symb);
                      break;
                  case 'C' : // lots of C special cases
                      /* discard if SCI, SCE or SCY */
                      if ( isPreviousChar(local, n, 'S') && 
                           !isLastChar(wdsz, n) && 
                           (FRONTV.indexOf(local.charAt(n + 1)) >= 0) ) { 
                          break;
                      }
                      if (regionMatch(local, n, "CIA")) { // "CIA" -> X
                          code.append('X'); 
                          break;
                      }
                      if (!isLastChar(wdsz, n) && 
                          (FRONTV.indexOf(local.charAt(n + 1)) >= 0)) {
                          code.append('S');
                          break; // CI,CE,CY -> S
                      }
                      if (isPreviousChar(local, n, 'S') &&
                          isNextChar(local, n, 'H') ) { // SCH->sk
                          code.append('K') ; 
                          break ;
                      }
                      if (isNextChar(local, n, 'H')) { // detect CH
                          if ((n == 0) && 
                              (wdsz >= 3) && 
                              isVowel(local,2) ) { // CH consonant -> K consonant
                              code.append('K');
                          } else { 
                              code.append('X'); // CHvowel -> X
                          }
                      } else { 
                          code.append('K');
                      }
                      break ;
                  case 'D' :
                      if (!isLastChar(wdsz, n + 1) && 
                          isNextChar(local, n, 'G') && 
                          (FRONTV.indexOf(local.charAt(n + 2)) >= 0)) { // DGE DGI DGY -> J 
                          code.append('J'); n += 2 ;
                      } else { 
                          code.append('T');
                      }
                      break ;
                  case 'G' : // GH silent at end or before consonant
                      if (isLastChar(wdsz, n + 1) && 
                          isNextChar(local, n, 'H')) {
                          break;
                      }
                      if (!isLastChar(wdsz, n + 1) &&  
                          isNextChar(local,n,'H') && 
                          !isVowel(local,n+2)) {
                          break;
                      }
                      if ((n > 0) && 
                          ( regionMatch(local, n, "GN") ||
                            regionMatch(local, n, "GNED") ) ) {
                          break; // silent G
                      }
                      if (isPreviousChar(local, n, 'G')) {
                          // NOTE: Given that duplicated chars are removed, I don't see how this can ever be true
                          hard = true ;
                      } else {
                          hard = false ;
                      }
                      if (!isLastChar(wdsz, n) && 
                          (FRONTV.indexOf(local.charAt(n + 1)) >= 0) && 
                          (!hard)) {
                          code.append('J');
                      } else {
                          code.append('K');
                      }
                      break ;
                  case 'H':
                      if (isLastChar(wdsz, n)) {
                          break ; // terminal H
                      }
                      if ((n > 0) && 
                          (VARSON.indexOf(local.charAt(n - 1)) >= 0)) {
                          break;
                      }
                      if (isVowel(local,n+1)) {
                          code.append('H'); // Hvowel
                      }
                      break;
                  case 'F': 
                  case 'J' : 
                  case 'L' :
                  case 'M': 
                  case 'N' : 
                  case 'R' :
                      code.append(symb); 
                      break;
                  case 'K' :
                      if (n > 0) { // not initial
                          if (!isPreviousChar(local, n, 'C')) {
                              code.append(symb);
                          }
                      } else {
                          code.append(symb); // initial K
                      }
                      break ;
                  case 'P' :
                      if (isNextChar(local,n,'H')) {
                          // PH -> F
                          code.append('F');
                      } else {
                          code.append(symb);
                      }
                      break ;
                  case 'Q' :
                      code.append('K');
                      break;
                  case 'S' :
                      if (regionMatch(local,n,"SH") || 
                          regionMatch(local,n,"SIO") || 
                          regionMatch(local,n,"SIA")) {
                          code.append('X');
                      } else {
                          code.append('S');
                      }
                      break;
                  case 'T' :
                      if (regionMatch(local,n,"TIA") || 
                          regionMatch(local,n,"TIO")) {
                          code.append('X'); 
                          break;
                      }
                      if (regionMatch(local,n,"TCH")) {
                          // Silent if in "TCH"
                          break;
                      }
                      // substitute numeral 0 for TH (resembles theta after all)
                      if (regionMatch(local,n,"TH")) {
                          code.append('0');
                      } else {
                          code.append('T');
                      }
                      break ;
                  case 'V' :
                      code.append('F'); break ;
                  case 'W' : case 'Y' : // silent if not followed by vowel
                      if (!isLastChar(wdsz,n) && 
                          isVowel(local,n+1)) {
                          code.append(symb);
                      }
                      break ;
                  case 'X' :
                      code.append('K'); code.append('S');
                      break ;
                  case 'Z' :
                      code.append('S'); break ;
                  } // end switch
                  n++ ;
              } // end else from symb != 'C'
              if (code.length() > this.getMaxCodeLen()) { 
                  code.setLength(this.getMaxCodeLen()); 
              }
          }
          return code.toString();
      }
  
      private boolean isVowel(StringBuffer string, int index) {
          return VOWELS.indexOf(string.charAt(index)) >= 0;
      }
  
      private boolean isPreviousChar(StringBuffer string, int index, char c) {
          boolean matches = false;
          if( index > 0 &&
              index < string.length() ) {
              matches = string.charAt(index - 1) == c;
          }
          return matches;
      }
  
      private boolean isNextChar(StringBuffer string, int index, char c) {
          boolean matches = false;
          if( index >= 0 &&
              index < string.length() - 1 ) {
              matches = string.charAt(index + 1) == c;
          }
          return matches;
      }
  
      private boolean regionMatch(StringBuffer string, int index, String test) {
          boolean matches = false;
          if( index >= 0 &&
              (index + test.length() - 1) < string.length() ) {
              String substring = string.substring( index, index + test.length());
              matches = substring.equals( test );
          }
          return matches;
      }
  
      private boolean isLastChar(int wdsz, int n) {
          return n + 1 == wdsz;
      } 
      
      
      /**
       * Encodes an Object using the metaphone algorithm.  This method
       * is provided in order to satisfy the requirements of the
       * Encoder interface, and will throw an EncoderException if the
       * supplied object is not of type java.lang.String.
       *
       * @param pObject Object to encode
       * @return An object (or type java.lang.String) containing the 
       *         metaphone code which corresponds to the String supplied.
       * @throws EncoderException if the parameter supplied is not
       *                          of type java.lang.String
       */
      public Object encode(Object pObject) throws EncoderException {
          if (!(pObject instanceof String)) {
              throw new EncoderException("Parameter supplied to Metaphone encode is not of type java.lang.String"); 
          }
          return metaphone((String) pObject);
      }
  
      /**
       * Encodes a String using the Metaphone algorithm. 
       *
       * @param pString String object to encode
       * @return The metaphone code corresponding to the String supplied
       */
      public String encode(String pString) {
          return metaphone(pString);   
      }
  
      /**
       * Tests is the metaphones of two strings are identical.
       *
       * @param str1 First of two strings to compare
       * @param str2 Second of two strings to compare
       * @return <code>true</code> if the metaphones of these strings are identical, 
       *        <code>false</code> otherwise.
       */
      public boolean isMetaphoneEqual(String str1, String str2) {
          return metaphone(str1).equals(metaphone(str2));
      }
  
      /**
       * Returns the maxCodeLen.
       * @return int
       */
      public int getMaxCodeLen() { return this.maxCodeLen; }
  
      /**
       * Sets the maxCodeLen.
       * @param maxCodeLen The maxCodeLen to set
       */
      public void setMaxCodeLen(int maxCodeLen) { this.maxCodeLen = maxCodeLen; }
  
  }
  