View Javadoc
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  
18  package org.apache.commons.codec.language.bm;
19  
20  import org.apache.commons.codec.EncoderException;
21  import org.apache.commons.codec.StringEncoder;
22  
23  /**
24   * Encodes strings into their Beider-Morse phonetic encoding.
25   * <p>
26   * Beider-Morse phonetic encodings are optimized for family names. However, they may be useful for a wide range of words.
27   * </p>
28   * <p>
29   * This encoder is intentionally mutable to allow dynamic configuration through bean properties. As such, it is mutable, and may not be thread-safe. If you
30   * require a guaranteed thread-safe encoding then use {@link PhoneticEngine} directly.
31   * </p>
32   * <h2>Encoding overview</h2>
33   * <p>
34   * Beider-Morse phonetic encodings is a multi-step process. Firstly, a table of rules is consulted to guess what language the word comes from. For example, if
35   * it ends in "{@code ault}" then it infers that the word is French. Next, the word is translated into a phonetic representation using a language-specific
36   * phonetics table. Some runs of letters can be pronounced in multiple ways, and a single run of letters may be potentially broken up into phonemes at different
37   * places, so this stage results in a set of possible language-specific phonetic representations. Lastly, this language-specific phonetic representation is
38   * processed by a table of rules that re-writes it phonetically taking into account systematic pronunciation differences between languages, to move it towards a
39   * pan-indo-european phonetic representation. Again, sometimes there are multiple ways this could be done and sometimes things that can be pronounced in several
40   * ways in the source language have only one way to represent them in this average phonetic language, so the result is again a set of phonetic spellings.
41   * </p>
42   * <p>
43   * Some names are treated as having multiple parts. This can be due to two things. Firstly, they may be hyphenated. In this case, each individual hyphenated
44   * word is encoded, and then these are combined end-to-end for the final encoding. Secondly, some names have standard prefixes, for example, "{@code Mac/Mc}" in
45   * Scottish (English) names. As sometimes it is ambiguous whether the prefix is intended or is an accident of the spelling, the word is encoded once with the
46   * prefix and once without it. The resulting encoding contains one and then the other result.
47   * </p>
48   * <h2>Encoding format</h2>
49   * <p>
50   * Individual phonetic spellings of an input word are represented in upper- and lower-case roman characters. Where there are multiple possible phonetic
51   * representations, these are joined with a pipe ({@code |}) character. If multiple hyphenated words where found, or if the word may contain a name prefix, each
52   * encoded word is placed in ellipses and these blocks are then joined with hyphens. For example, "{@code d'ortley}" has a possible prefix. The form without
53   * prefix encodes to "{@code ortlaj|ortlej}", while the form with prefix encodes to " {@code dortlaj|dortlej}". Thus, the full, combined encoding is
54   * "{@code (ortlaj|ortlej)-(dortlaj|dortlej)}".
55   * </p>
56   * <p>
57   * The encoded forms are often quite a bit longer than the input strings. This is because a single input may have many potential phonetic interpretations. For
58   * example, "{@code Renault}" encodes to " {@code rYnDlt|rYnalt|rYnult|rinDlt|rinalt|rinult}". The {@code APPROX} rules will tend to produce larger encodings as
59   * they consider a wider range of possible, approximate phonetic interpretations of the original word. Down-stream applications may wish to further process the
60   * encoding for indexing or lookup purposes, for example, by splitting on pipe ({@code |}) and indexing under each of these alternatives.
61   * </p>
62   * <p>
63   * <strong>Note</strong>: this version of the Beider-Morse encoding is equivalent with v3.4 of the reference implementation.
64   * </p>
65   * <p>
66   * This class is Not ThreadSafe.
67   * </p>
68   *
69   * @see <a href="https://stevemorse.org/phonetics/bmpm.htm">Beider-Morse Phonetic Matching</a>
70   * @see <a href="https://stevemorse.org/phoneticinfo.htm">Reference implementation</a>
71   * @since 1.6
72   */
73  public class BeiderMorseEncoder implements StringEncoder {
74  
75      /**
76       * Creates a new builder for a Beider-Morse encoder.
77       *
78       * @since 1.23.0
79       */
80      public static final class Builder {
81  
82          private PhoneticEngine engine = PhoneticEngine.builder().get();
83  
84          private Builder() {
85              // empty.
86          }
87  
88          /**
89           * Gets a new Beider-Morse encoder with the current configuration.
90           *
91           * @return a new Beider-Morse encoder with the current configuration.
92           */
93          public BeiderMorseEncoder get() {
94              return new BeiderMorseEncoder(this);
95          }
96  
97          /**
98           * Sets the phonetic engine to use.
99           *
100          * @param engine the phonetic engine to use.
101          * @return this builder, for chaining.
102          */
103         public Builder setPhoneticEngine(final PhoneticEngine engine) {
104             this.engine = engine != null ? engine : PhoneticEngine.builder().get();
105             return this;
106         }
107     }
108 
109     /**
110      * Creates a new builder for a BeiderMorseEncoder.
111      *
112      * @return a new builder for a BeiderMorseEncoder.
113      * @since 1.23.0
114      */
115     public static Builder builder() {
116         return new Builder();
117     }
118 
119     /** A cached object. */
120     private PhoneticEngine engine = PhoneticEngine.builder().get();
121 
122     /**
123      * Constructs a new instance.
124      *
125      * @deprecated Use {@link #builder()} to create a new instance.
126      */
127     @Deprecated
128     public BeiderMorseEncoder() {
129         // empty
130     }
131 
132     private BeiderMorseEncoder(final Builder builder) {
133         engine = builder.engine;
134     }
135 
136     @Override
137     public Object encode(final Object source) throws EncoderException {
138         if (!(source instanceof String)) {
139             throw new EncoderException("BeiderMorseEncoder encode parameter is not of type String");
140         }
141         return encode((String) source);
142     }
143 
144     @Override
145     public String encode(final String source) throws EncoderException {
146         return source != null ? engine.encode(source) : null;
147     }
148 
149     /**
150      * Gets the name type currently in operation.
151      *
152      * @return The NameType currently being used.
153      */
154     public NameType getNameType() {
155         return engine.getNameType();
156     }
157 
158     /**
159      * Gets the rule type currently in operation.
160      *
161      * @return The RuleType currently being used.
162      */
163     public RuleType getRuleType() {
164         return engine.getRuleType();
165     }
166 
167     /**
168      * Tests whether multiple phonetic encodings are concatenated or just the first one is kept.
169      *
170      * @return true if multiple encodings are concatenated, false if just the first one is returned.
171      */
172     public boolean isConcat() {
173         return engine.isConcat();
174     }
175 
176     /**
177      * Sets how multiple possible phonetic encodings are combined.
178      *
179      * @param concat true if multiple encodings are to be combined with a '|', false if just the first one is to be considered.
180      */
181     public void setConcat(final boolean concat) {
182         engine = PhoneticEngine.builder().setAll(engine).setConcat(concat).get();
183     }
184 
185     /**
186      * Sets the number of maximum of phonemes that shall be considered by the engine.
187      * <p>
188      * A value less than 0 will reset the maximum number of phonemes to the default {@value PhoneticEngine.Builder#MAX_PHONEMES}.
189      * </p>
190      *
191      * @param maxPhonemes the maximum number of phonemes returned by the engine.
192      * @since 1.7
193      */
194     public void setMaxPhonemes(final int maxPhonemes) {
195         engine = PhoneticEngine.builder().setAll(engine).setMaxPhonemes(maxPhonemes).get();
196     }
197 
198     /**
199      * Sets the type of name. Use {@link NameType#GENERIC} unless you specifically want phonetic encodings optimized for Ashkenazi or Sephardic Jewish family
200      * names.
201      * <p>
202      * A null value will reset the name type to the default of {@link NameType#GENERIC}.
203      * </p>
204      *
205      * @param nameType the NameType in use.
206      */
207     public void setNameType(final NameType nameType) {
208         engine = PhoneticEngine.builder().setAll(engine).setNameType(nameType).get();
209     }
210 
211     /**
212      * Sets the rule type to apply. This will widen or narrow the range of phonetic encodings considered.
213      * <p>
214      * A null value will reset the rule type to the default of {@link RuleType#APPROX}.
215      * </p>
216      *
217      * @param ruleType {@link RuleType#APPROX} or {@link RuleType#EXACT} for approximate or exact phonetic matches.
218      */
219     public void setRuleType(final RuleType ruleType) {
220         engine = PhoneticEngine.builder().setAll(engine).setRuleType(ruleType).get();
221     }
222 }