Skip to content

Commit 1573a12

Browse files
committed
[CODEC-125] Implement a Beider-Morse phonetic matching codec. First commit. Thank you to Matthew Pocock for the contribution. TODO: PMD rules show code that needs to be changed to use StringBuffer in PhoneticEngine.
git-svn-id: https://svn.apache.org/repos/asf/commons/proper/codec/trunk@1151311 13f79535-47bb-0310-9956-ffa450edef68
1 parent 5d72a46 commit 1573a12

137 files changed

Lines changed: 9504 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@
174174
<role>Streaming Base64</role>
175175
</roles>
176176
</contributor>
177+
<contributor>
178+
<name>Matthew Pocock</name>
179+
<email>turingatemyhamster@gmail.com</email>
180+
<roles>
181+
<role>Beinder-Morse phonetic matching</role>
182+
</roles>
183+
</contributor>
177184
</contributors>
178185
<!-- Codec should depend on very little -->
179186
<dependencies>
@@ -202,6 +209,7 @@
202209
</properties>
203210
<build>
204211
<sourceDirectory>src/java</sourceDirectory>
212+
<resources><resource><directory>src/resources</directory></resource></resources>
205213
<testSourceDirectory>src/test</testSourceDirectory>
206214
<plugins>
207215
<plugin>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
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+
* <p>
25+
* Encodes strings into their Beider-Morse phonetic encoding.
26+
* </p>
27+
* <p>
28+
* Beider-Morse phonetic encodings are optimised for family names. However, they may be useful for a wide range of words.
29+
* </p>
30+
* <p>
31+
* This encoder is intentionally mutable to allow dynamic configuration through bean properties. As such, it is mutable, and may not be
32+
* thread-safe. If you require a guaranteed thread-safe encoding then use {@link PhoneticEngine} directly.
33+
* </p>
34+
*
35+
* @author Apache Software Foundation
36+
* @since 2.0
37+
*/
38+
public class BeiderMorseEncoder implements StringEncoder {
39+
// a cached object
40+
private PhoneticEngine engine = new PhoneticEngine(NameType.GENERIC, RuleType.APPROX, true);
41+
42+
public Object encode(Object source) throws EncoderException {
43+
if (!(source instanceof String)) {
44+
throw new EncoderException("BeiderMorseEncoder encode parameter is not of type String");
45+
}
46+
return encode((String) source);
47+
}
48+
49+
public String encode(String source) throws EncoderException {
50+
if (source == null) {
51+
return null;
52+
}
53+
return this.engine.encode(source);
54+
}
55+
56+
/**
57+
* Gets the name type currently in operation.
58+
*
59+
* @return the NameType currently being used
60+
*/
61+
public NameType getNameType() {
62+
return this.engine.getNameType();
63+
}
64+
65+
/**
66+
* Gets the rule type currently in operation.
67+
*
68+
* @return the RuleType currently being used
69+
*/
70+
public RuleType getRuleType() {
71+
return this.engine.getRuleType();
72+
}
73+
74+
/**
75+
* Discovers if multiple possible encodings are concatenated.
76+
*
77+
* @return true if multiple encodings are concatenated, false if just the first one is returned
78+
*/
79+
public boolean isConcat() {
80+
return this.engine.isConcat();
81+
}
82+
83+
/**
84+
* Sets how multiple possible phonetic encodings are combined.
85+
*
86+
* @param concat
87+
* true if multiple encodings are to be combined with a '|', false if just the first one is to be considered
88+
*/
89+
public void setConcat(boolean concat) {
90+
this.engine = new PhoneticEngine(this.engine.getNameType(), this.engine.getRuleType(), concat);
91+
}
92+
93+
/**
94+
* Sets the type of name. Use {@link NameType#GENERIC} unless you specifically want phoentic encodings optimized for Ashkenazi or
95+
* Sephardic Jewish family names.
96+
*
97+
* @param nameType
98+
* the NameType in use
99+
*/
100+
public void setNameType(NameType nameType) {
101+
this.engine = new PhoneticEngine(nameType, this.engine.getRuleType(), this.engine.isConcat());
102+
}
103+
104+
/**
105+
* Sets the rule type to apply. This will widen or narrow the range of phonetic encodings considered.
106+
*
107+
* @param ruleType
108+
* {@link RuleType#APPROX} or {@link RuleType#EXACT} for approximate or exact phonetic matches
109+
*/
110+
public void setRuleType(RuleType ruleType) {
111+
this.engine = new PhoneticEngine(this.engine.getNameType(), ruleType, this.engine.isConcat());
112+
}
113+
114+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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+
18+
package org.apache.commons.codec.language.bm;
19+
20+
import java.io.InputStream;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.Collections;
24+
import java.util.EnumMap;
25+
import java.util.HashSet;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Scanner;
29+
import java.util.Set;
30+
import java.util.regex.Pattern;
31+
32+
/**
33+
* <p>
34+
* Language guessing utility.
35+
* </p>
36+
* <p>
37+
* This class encapsulates rules used to guess the possible languages that a word originates from. This is done by reference to a whole
38+
* series of rules distributed in resource files.
39+
* </p>
40+
* <p>
41+
* Instances of this class are typically managed through the static factory method instance(). Unless you are developing your own language
42+
* guessing rules, you will not need to interact with this class directly.
43+
* </p>
44+
* <p>
45+
* This class is intended to be immutable and thread-safe.
46+
* </p>
47+
* <h2>Lang resources</h2
48+
* <p>
49+
* Language guessing rules are typically loaded from resource files. These are UTF-8 encoded text files. They are systematically named
50+
* following the pattern: <blockquote>org/apache/commons/codec/language/bm/lang.txt</blockquote> The format of these resources is the
51+
* following:
52+
* </p>
53+
* <ul>
54+
* <li><b>Rules:</b> whitespace separated strings. There should be 3 columns to each row, and these will be interpreted as:
55+
* <ol>
56+
* <li>pattern: a regular expression.</li>
57+
* <li>languages: a '+'-separated list of languages.</li>
58+
* <li>acceptOnMatch: 'true' or 'false' indicating if a match rules in or rules out the language.</li>
59+
* </ol>
60+
* </li>
61+
* <li><b>End-of-line comments:</b> Any occurance of '//' will cause all text following on that line to be discarded as a comment.</li>
62+
* <li><b>Multi-line comments:</b> Any line starting with '/*' will start multi-line commenting mode. This will skip all content until a
63+
* line ending in '*' and '/' is found.</li>
64+
* <li><b>Blank lines:</b> All blank lines will be skipped.</li>
65+
* </ul>
66+
* <p/>
67+
* Port of lang.php
68+
*
69+
* @author Apache Software Foundation
70+
* @since 2.0
71+
*/
72+
public class Lang {
73+
74+
private static class LangRule {
75+
private boolean acceptOnMatch;
76+
private Set<String> languages;
77+
private Pattern pattern;
78+
79+
private LangRule(Pattern pattern, Set<String> languages, boolean acceptOnMatch) {
80+
this.pattern = pattern;
81+
this.languages = languages;
82+
this.acceptOnMatch = acceptOnMatch;
83+
}
84+
85+
public boolean matches(String txt) {
86+
return this.pattern.matcher(txt).find();
87+
}
88+
}
89+
90+
private static final Map<NameType, Lang> Langs = new EnumMap<NameType, Lang>(NameType.class);
91+
92+
private static final String LANGUAGE_RULES_RN = "org/apache/commons/codec/language/bm/lang.txt";
93+
94+
static {
95+
for (NameType s : NameType.values()) {
96+
Langs.put(s, loadFromResource(LANGUAGE_RULES_RN, Languages.instance(s)));
97+
}
98+
}
99+
100+
/**
101+
* Gets a Lang instance for one of the supported NameTypes.
102+
*
103+
* @param nameType
104+
* the NameType to look up
105+
* @return a Lang encapsulating the language guessing rules for that name type
106+
*/
107+
public static Lang instance(NameType nameType) {
108+
return Langs.get(nameType);
109+
}
110+
111+
/**
112+
* <p>
113+
* Loads language rules from a resource.
114+
* </p>
115+
* <p>
116+
* In normal use, you will obtain instances of Lang through the {@link #instance(NameType)} method. You will only need to call this
117+
* yourself if you are developing custom language mapping rules.
118+
* </p>
119+
*
120+
* @param languageRulesResourceName
121+
* the fully-qualified resource name to load
122+
* @param languages
123+
* the languages that these rules will support
124+
* @return a Lang encapsulating the loaded language-guessing rules.
125+
*/
126+
public static Lang loadFromResource(String languageRulesResourceName, Languages languages) {
127+
List<LangRule> rules = new ArrayList<LangRule>();
128+
InputStream lRulesIS = Lang.class.getClassLoader().getResourceAsStream(languageRulesResourceName);
129+
130+
if (lRulesIS == null) {
131+
throw new IllegalStateException("Unable to resolve required resource:" + LANGUAGE_RULES_RN);
132+
}
133+
134+
Scanner scanner = new Scanner(lRulesIS, ResourceConstants.ENCODING);
135+
boolean inExtendedComment = false;
136+
while (scanner.hasNextLine()) {
137+
String rawLine = scanner.nextLine();
138+
String line = rawLine;
139+
140+
if (inExtendedComment) {
141+
if (line.endsWith(ResourceConstants.EXT_CMT_END)) {
142+
inExtendedComment = false;
143+
} else {
144+
// discard doc comment line
145+
}
146+
} else {
147+
if (line.startsWith(ResourceConstants.EXT_CMT_START)) {
148+
inExtendedComment = true;
149+
} else {
150+
// discard comments
151+
int cmtI = line.indexOf(ResourceConstants.CMT);
152+
if (cmtI >= 0) {
153+
// System.err.println("index of comment: " + cmtI);
154+
line = line.substring(0, cmtI);
155+
}
156+
157+
// trim leading-trailing whitespace
158+
line = line.trim();
159+
160+
if (line.length() == 0)
161+
continue; // empty lines can be safely skipped
162+
163+
// split it up
164+
String[] parts = line.split("\\s+");
165+
// System.err.println("part count: " + parts.length);
166+
167+
if (parts.length != 3) {
168+
// fixme: we really need to log this somewhere
169+
System.err.println("Warning: malformed line '" + rawLine + "'");
170+
continue;
171+
}
172+
173+
Pattern pattern = Pattern.compile(parts[0]);
174+
String[] langs = parts[1].split("\\+");
175+
boolean accept = parts[2].equals("true");
176+
177+
rules.add(new LangRule(pattern, new HashSet<String>(Arrays.asList(langs)), accept));
178+
}
179+
}
180+
}
181+
182+
return new Lang(rules, languages);
183+
}
184+
185+
private final Languages languages;
186+
private final List<LangRule> rules;
187+
188+
private Lang(List<LangRule> rules, Languages languages) {
189+
this.rules = Collections.unmodifiableList(rules);
190+
this.languages = languages;
191+
}
192+
193+
/**
194+
* Guesses the language of a word.
195+
*
196+
* @param text
197+
* the word
198+
* @return the language that the word originates from or {@link Languages#ANY} if there was no unique match
199+
*/
200+
public String guessLanguage(String text) {
201+
Set<String> ls = guessLanguages(text);
202+
if (ls.size() == 1) {
203+
return ls.iterator().next();
204+
} else {
205+
return Languages.ANY;
206+
}
207+
}
208+
209+
/**
210+
* Guesses the languages of a word.
211+
*
212+
* @param text
213+
* the word
214+
* @return a Set of Strings of language names that are potential matches for the word
215+
*/
216+
public Set<String> guessLanguages(String text) {
217+
text = text.toLowerCase(); // todo: locale?
218+
// System.out.println("Testing text: '" + text + "'");
219+
220+
Set<String> langs = new HashSet<String>(this.languages.getLanguages());
221+
for (LangRule rule : this.rules) {
222+
if (rule.matches(text)) {
223+
// System.out.println("Rule " + rule.pattern + " matches " + text);
224+
if (rule.acceptOnMatch) {
225+
// System.out.println("Retaining " + rule.languages);
226+
langs.retainAll(rule.languages);
227+
} else {
228+
// System.out.println("Removing " + rule.languages);
229+
langs.removeAll(rule.languages);
230+
}
231+
// System.out.println("Current languages: " + langs);
232+
} else {
233+
// System.out.println("Rule " + rule.pattern + " does not match " + text);
234+
}
235+
}
236+
237+
return langs;
238+
}
239+
}

0 commit comments

Comments
 (0)