001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.codec.language.bm; 019 020import org.apache.commons.codec.EncoderException; 021import org.apache.commons.codec.StringEncoder; 022 023/** 024 * Encodes strings into their Beider-Morse phonetic encoding. 025 * <p> 026 * Beider-Morse phonetic encodings are optimized for family names. However, they may be useful for a wide range of 027 * words. 028 * </p> 029 * <p> 030 * This encoder is intentionally mutable to allow dynamic configuration through bean properties. As such, it is mutable, 031 * and may not be thread-safe. If you require a guaranteed thread-safe encoding then use {@link PhoneticEngine} 032 * directly. 033 * </p> 034 * <h2>Encoding overview</h2> 035 * <p> 036 * Beider-Morse phonetic encodings is a multi-step process. Firstly, a table of rules is consulted to guess what 037 * language the word comes from. For example, if it ends in "{@code ault}" then it infers that the word is French. 038 * Next, the word is translated into a phonetic representation using a language-specific phonetics table. Some runs of 039 * letters can be pronounced in multiple ways, and a single run of letters may be potentially broken up into phonemes at 040 * different places, so this stage results in a set of possible language-specific phonetic representations. Lastly, this 041 * language-specific phonetic representation is processed by a table of rules that re-writes it phonetically taking into 042 * account systematic pronunciation differences between languages, to move it towards a pan-indo-european phonetic 043 * representation. Again, sometimes there are multiple ways this could be done and sometimes things that can be 044 * pronounced in several ways in the source language have only one way to represent them in this average phonetic 045 * language, so the result is again a set of phonetic spellings. 046 * </p> 047 * <p> 048 * Some names are treated as having multiple parts. This can be due to two things. Firstly, they may be hyphenated. In 049 * this case, each individual hyphenated word is encoded, and then these are combined end-to-end for the final encoding. 050 * Secondly, some names have standard prefixes, for example, "{@code Mac/Mc}" in Scottish (English) names. As 051 * sometimes it is ambiguous whether the prefix is intended or is an accident of the spelling, the word is encoded once 052 * with the prefix and once without it. The resulting encoding contains one and then the other result. 053 * </p> 054 * <h2>Encoding format</h2> 055 * <p> 056 * Individual phonetic spellings of an input word are represented in upper- and lower-case roman characters. Where there 057 * are multiple possible phonetic representations, these are joined with a pipe ({@code |}) character. If multiple 058 * hyphenated words where found, or if the word may contain a name prefix, each encoded word is placed in ellipses and 059 * these blocks are then joined with hyphens. For example, "{@code d'ortley}" has a possible prefix. The form 060 * without prefix encodes to "{@code ortlaj|ortlej}", while the form with prefix encodes to " 061 * {@code dortlaj|dortlej}". Thus, the full, combined encoding is "{@code (ortlaj|ortlej)-(dortlaj|dortlej)}". 062 * </p> 063 * <p> 064 * The encoded forms are often quite a bit longer than the input strings. This is because a single input may have many 065 * potential phonetic interpretations. For example, "{@code Renault}" encodes to " 066 * {@code rYnDlt|rYnalt|rYnult|rinDlt|rinalt|rinult}". The {@code APPROX} rules will tend to produce larger 067 * encodings as they consider a wider range of possible, approximate phonetic interpretations of the original word. 068 * Down-stream applications may wish to further process the encoding for indexing or lookup purposes, for example, by 069 * splitting on pipe ({@code |}) and indexing under each of these alternatives. 070 * </p> 071 * <p> 072 * <b>Note</b>: this version of the Beider-Morse encoding is equivalent with v3.4 of the reference implementation. 073 * </p> 074 * @see <a href="https://stevemorse.org/phonetics/bmpm.htm">Beider-Morse Phonetic Matching</a> 075 * @see <a href="https://stevemorse.org/phoneticinfo.htm">Reference implementation</a> 076 * 077 * <p> 078 * This class is Not ThreadSafe 079 * </p> 080 * @since 1.6 081 */ 082public class BeiderMorseEncoder implements StringEncoder { 083 // Implementation note: This class is a spring-friendly facade to PhoneticEngine. It allows read/write configuration 084 // of an immutable PhoneticEngine instance that will be delegated to for the actual encoding. 085 086 // a cached object 087 private PhoneticEngine engine = new PhoneticEngine(NameType.GENERIC, RuleType.APPROX, true); 088 089 @Override 090 public Object encode(final Object source) throws EncoderException { 091 if (!(source instanceof String)) { 092 throw new EncoderException("BeiderMorseEncoder encode parameter is not of type String"); 093 } 094 return encode((String) source); 095 } 096 097 @Override 098 public String encode(final String source) throws EncoderException { 099 if (source == null) { 100 return null; 101 } 102 return this.engine.encode(source); 103 } 104 105 /** 106 * Gets the name type currently in operation. 107 * 108 * @return the NameType currently being used 109 */ 110 public NameType getNameType() { 111 return this.engine.getNameType(); 112 } 113 114 /** 115 * Gets the rule type currently in operation. 116 * 117 * @return the RuleType currently being used 118 */ 119 public RuleType getRuleType() { 120 return this.engine.getRuleType(); 121 } 122 123 /** 124 * Discovers if multiple possible encodings are concatenated. 125 * 126 * @return true if multiple encodings are concatenated, false if just the first one is returned 127 */ 128 public boolean isConcat() { 129 return this.engine.isConcat(); 130 } 131 132 /** 133 * Sets how multiple possible phonetic encodings are combined. 134 * 135 * @param concat 136 * true if multiple encodings are to be combined with a '|', false if just the first one is 137 * to be considered 138 */ 139 public void setConcat(final boolean concat) { 140 this.engine = new PhoneticEngine(this.engine.getNameType(), 141 this.engine.getRuleType(), 142 concat, 143 this.engine.getMaxPhonemes()); 144 } 145 146 /** 147 * Sets the number of maximum of phonemes that shall be considered by the engine. 148 * 149 * @param maxPhonemes 150 * the maximum number of phonemes returned by the engine 151 * @since 1.7 152 */ 153 public void setMaxPhonemes(final int maxPhonemes) { 154 this.engine = new PhoneticEngine(this.engine.getNameType(), 155 this.engine.getRuleType(), 156 this.engine.isConcat(), 157 maxPhonemes); 158 } 159 160 /** 161 * Sets the type of name. Use {@link NameType#GENERIC} unless you specifically want phonetic encodings 162 * optimized for Ashkenazi or Sephardic Jewish family names. 163 * 164 * @param nameType 165 * the NameType in use 166 */ 167 public void setNameType(final NameType nameType) { 168 this.engine = new PhoneticEngine(nameType, 169 this.engine.getRuleType(), 170 this.engine.isConcat(), 171 this.engine.getMaxPhonemes()); 172 } 173 174 /** 175 * Sets the rule type to apply. This will widen or narrow the range of phonetic encodings considered. 176 * 177 * @param ruleType 178 * {@link RuleType#APPROX} or {@link RuleType#EXACT} for approximate or exact phonetic matches 179 */ 180 public void setRuleType(final RuleType ruleType) { 181 this.engine = new PhoneticEngine(this.engine.getNameType(), 182 ruleType, 183 this.engine.isConcat(), 184 this.engine.getMaxPhonemes()); 185 } 186 187}