OSDN Git Service

5458ba1631e6f84a797dacc1c15d783711f20fd3
[neighbornote/NeighborNote.git] / src / com / swabunga / spell / engine / GenericTransformator.java
1 /*\r
2 Jazzy - a Java library for Spell Checking\r
3 Copyright (C) 2001 Mindaugas Idzelis\r
4 Full text of license can be found in LICENSE.txt\r
5 \r
6 This library is free software; you can redistribute it and/or\r
7 modify it under the terms of the GNU Lesser General Public\r
8 License as published by the Free Software Foundation; either\r
9 version 2.1 of the License, or (at your option) any later version.\r
10 \r
11 This library is distributed in the hope that it will be useful,\r
12 but WITHOUT ANY WARRANTY; without even the implied warranty of\r
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
14 Lesser General Public License for more details.\r
15 \r
16 You should have received a copy of the GNU Lesser General Public\r
17 License along with this library; if not, write to the Free Software\r
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\r
19 */\r
20 package com.swabunga.spell.engine;\r
21 \r
22 import java.io.BufferedReader;\r
23 import java.io.File;\r
24 import java.io.FileInputStream;\r
25 import java.io.FileReader;\r
26 import java.io.IOException;\r
27 import java.io.InputStreamReader;\r
28 import java.io.Reader;\r
29 import java.util.HashMap;\r
30 import java.util.Vector;\r
31 \r
32 import com.swabunga.util.StringUtility;\r
33 \r
34 /**\r
35  * A Generic implementation of a transformator takes an \r
36  * <a href="http://aspell.net/man-html/Phonetic-Code.html">\r
37  * aspell phonetics file</a> and constructs some sort of transformation \r
38  * table using the inner class TransformationRule.\r
39  * </p>\r
40  * Basically, each transformation rule represent a line in the phonetic file.\r
41  * One line contains two groups of characters separated by white space(s).\r
42  * The first group is the <em>match expression</em>. \r
43  * The <em>match expression</em> describe letters to associate with a syllable.\r
44  * The second group is the <em>replacement expression</em> giving the phonetic \r
45  * equivalent of the <em>match expression</em>.\r
46  *\r
47  * @see SpellDictionaryASpell SpellDictionaryASpell for information on getting\r
48  * phonetic files for aspell.\r
49  *\r
50  * @author Robert Gustavsson (robert@lindesign.se)\r
51  */\r
52 public class GenericTransformator implements Transformator {\r
53 \r
54 \r
55   /**\r
56    * This replace list is used if no phonetic file is supplied or it doesn't\r
57    * contain the alphabet.\r
58    */\r
59   private static final char[] defaultEnglishAlphabet = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};\r
60 \r
61   /**\r
62    * The alphabet start marker.\r
63    * @see GenericTransformator#KEYWORD_ALPHBET KEYWORD_ALPHBET\r
64    */\r
65   public static final char ALPHABET_START = '[';\r
66   /**\r
67    * The alphabet end marker.\r
68    * @see GenericTransformator#KEYWORD_ALPHBET KEYWORD_ALPHBET\r
69    */\r
70   public static final char ALPHABET_END = ']';\r
71   /**\r
72    * Phonetic file keyword indicating that a different alphabet is used \r
73    * for this language. The keyword must be followed an\r
74    * {@link GenericTransformator#ALPHABET_START ALPHABET_START} marker, \r
75    * a list of characters defining the alphabet and a\r
76    * {@link GenericTransformator#ALPHABET_END ALPHABET_END} marker.\r
77    */\r
78   public static final String KEYWORD_ALPHBET = "alphabet";\r
79   /**\r
80    * Phonetic file lines starting with the keywords are skipped. \r
81    * The key words are: version, followup, collapse_result.\r
82    * Comments, starting with '#', are also skipped to the end of line.\r
83    */\r
84   public static final String[] IGNORED_KEYWORDS = {"version", "followup", "collapse_result"};\r
85 \r
86   /**\r
87    * Start a group of characters which can be appended to the match expression\r
88    * of the phonetic file.\r
89    */\r
90   public static final char STARTMULTI = '(';\r
91   /**\r
92    * End a group of characters which can be appended to the match expression\r
93    * of the phonetic file.\r
94    */\r
95   public static final char ENDMULTI = ')';\r
96   /**\r
97    * During phonetic transformation of a word each numeric character is\r
98    * replaced by this DIGITCODE.\r
99    */\r
100   public static final String DIGITCODE = "0";\r
101   /**\r
102    * Phonetic file character code indicating that the replace expression\r
103    * is empty.\r
104    */\r
105   public static final String REPLACEVOID = "_";\r
106 \r
107   private Object[] ruleArray = null;\r
108   private char[] alphabetString = defaultEnglishAlphabet;\r
109 \r
110   /**\r
111    * Construct a transformation table from the phonetic file\r
112    * @param phonetic the phonetic file as specified in aspell\r
113    * @throws java.io.IOException indicates a problem while reading\r
114    * the phonetic file\r
115    */\r
116   public GenericTransformator(File phonetic) throws IOException {\r
117     buildRules(new BufferedReader(new FileReader(phonetic)));\r
118     alphabetString = washAlphabetIntoReplaceList(getReplaceList());\r
119 \r
120   }\r
121 \r
122   /**\r
123    * Construct a transformation table from the phonetic file\r
124    * @param phonetic the phonetic file as specified in aspell\r
125    * @param encoding the character set required\r
126    * @throws java.io.IOException indicates a problem while reading\r
127    * the phonetic file\r
128    */\r
129   public GenericTransformator(File phonetic, String encoding) throws IOException {\r
130     buildRules(new BufferedReader(new InputStreamReader(new FileInputStream(phonetic), encoding)));\r
131     alphabetString = washAlphabetIntoReplaceList(getReplaceList());\r
132   }\r
133 \r
134   /**\r
135    * Construct a transformation table from the phonetic file\r
136    * @param phonetic the phonetic file as specified in aspell. The file is\r
137    * supplied as a reader.\r
138    * @throws java.io.IOException indicates a problem while reading\r
139    * the phonetic information\r
140    */\r
141   public GenericTransformator(Reader phonetic) throws IOException {\r
142     buildRules(new BufferedReader(phonetic));\r
143     alphabetString = washAlphabetIntoReplaceList(getReplaceList());\r
144   }\r
145 \r
146   /**\r
147    * Goes through an alphabet and makes sure that only one of those letters\r
148    * that are coded equally will be in the replace list.\r
149    * In other words, it removes any letters in the alphabet\r
150    * that are redundant phonetically.\r
151    *\r
152    * This is done to improve speed in the getSuggestion method.\r
153    *\r
154    * @param alphabet The complete alphabet to wash.\r
155    * @return The washed alphabet to be used as replace list.\r
156    */\r
157   @SuppressWarnings("unchecked")\r
158 private char[] washAlphabetIntoReplaceList(char[] alphabet) {\r
159 \r
160     HashMap letters = new HashMap(alphabet.length);\r
161 \r
162     for (char element : alphabet) {\r
163       String tmp = String.valueOf(element);\r
164       String code = transform(tmp);\r
165       if (!letters.containsKey(code)) {\r
166         letters.put(code, new Character(element));\r
167       }\r
168     }\r
169 \r
170     Object[] tmpCharacters = letters.values().toArray();\r
171     char[] washedArray = new char[tmpCharacters.length];\r
172 \r
173     for (int i = 0; i < tmpCharacters.length; i++) {\r
174       washedArray[i] = ((Character) tmpCharacters[i]).charValue();\r
175     }\r
176 \r
177     return washedArray;\r
178   }\r
179 \r
180 \r
181   /**\r
182    * Takes out all single character replacements and put them in a char array.\r
183    * This array can later be used for adding or changing letters in getSuggestion().\r
184    * @return char[] An array of chars with replacements characters\r
185    */\r
186   @SuppressWarnings("unchecked")\r
187 public char[] getCodeReplaceList() {\r
188     char[] replacements;\r
189     TransformationRule rule;\r
190     Vector tmp = new Vector();\r
191 \r
192     if (ruleArray == null)\r
193       return null;\r
194     for (Object element : ruleArray) {\r
195       rule = (TransformationRule) element;\r
196       if (rule.getReplaceExp().length() == 1)\r
197         tmp.addElement(rule.getReplaceExp());\r
198     }\r
199     replacements = new char[tmp.size()];\r
200     for (int i = 0; i < tmp.size(); i++) {\r
201       replacements[i] = ((String) tmp.elementAt(i)).charAt(0);\r
202     }\r
203     return replacements;\r
204   }\r
205 \r
206   /**\r
207    * Builds up an char array with the chars in the alphabet of the language as it was read from the\r
208    * alphabet tag in the phonetic file.\r
209    * @return char[] An array of chars representing the alphabet or null if no alphabet was available.\r
210    */\r
211   public char[] getReplaceList() {\r
212     return alphabetString;\r
213   }\r
214 \r
215   /**\r
216    * Builds the phonetic code of the word.\r
217    * @param word the word to transform\r
218    * @return the phonetic transformation of the word\r
219    */\r
220   public String transform(String word) {\r
221 \r
222     if (ruleArray == null)\r
223       return null;\r
224 \r
225     TransformationRule rule;\r
226     StringBuffer str = new StringBuffer(word.toUpperCase());\r
227     int strLength = str.length();\r
228     int startPos = 0, add = 1;\r
229 \r
230     while (startPos < strLength) {\r
231 \r
232       add = 1;\r
233       if (Character.isDigit(str.charAt(startPos))) {\r
234         StringUtility.replace(str, startPos, startPos + DIGITCODE.length(), DIGITCODE);\r
235         startPos += add;\r
236         continue;\r
237       }\r
238 \r
239       for (Object element : ruleArray) {\r
240         //System.out.println("Testing rule#:"+i);\r
241         rule = (TransformationRule) element;\r
242         if (rule.startsWithExp() && startPos > 0)\r
243           continue;\r
244         if (startPos + rule.lengthOfMatch() > strLength) {\r
245           continue;\r
246         }\r
247         if (rule.isMatching(str, startPos)) {\r
248           String replaceExp = rule.getReplaceExp();\r
249 \r
250           add = replaceExp.length();\r
251           StringUtility.replace(str, startPos, startPos + rule.getTakeOut(), replaceExp);\r
252           strLength -= rule.getTakeOut();\r
253           strLength += add;\r
254           //System.out.println("Replacing with rule#:"+i+" add="+add);\r
255           break;\r
256         }\r
257       }\r
258       startPos += add;\r
259     }\r
260     //System.out.println(word);\r
261     //System.out.println(str.toString());\r
262     return str.toString();\r
263   }\r
264 \r
265   // Used to build up the transformastion table.\r
266   @SuppressWarnings("unchecked")\r
267 private void buildRules(BufferedReader in) throws IOException {\r
268     String read = null;\r
269     Vector ruleList = new Vector();\r
270     while ((read = in.readLine()) != null) {\r
271       buildRule(realTrimmer(read), ruleList);\r
272     }\r
273     ruleArray = new TransformationRule[ruleList.size()];\r
274     ruleList.copyInto(ruleArray);\r
275   }\r
276 \r
277   // Here is where the real work of reading the phonetics file is done.\r
278   @SuppressWarnings("unchecked")\r
279 private void buildRule(String str, Vector ruleList) {\r
280     if (str.length() < 1)\r
281       return;\r
282     for (String element : IGNORED_KEYWORDS) {\r
283       if (str.startsWith(element))\r
284         return;\r
285     }\r
286 \r
287     // A different alphabet is used for this language, will be read into\r
288     // the alphabetString variable.\r
289     if (str.startsWith(KEYWORD_ALPHBET)) {\r
290       int start = str.indexOf(ALPHABET_START);\r
291       int end = str.lastIndexOf(ALPHABET_END);\r
292       if (end != -1 && start != -1) {\r
293         alphabetString = str.substring(++start, end).toCharArray();\r
294       }\r
295       return;\r
296     }\r
297 \r
298     // str contains two groups of characters separated by white space(s).\r
299     // The fisrt group is the "match expression". The second group is the \r
300     // "replacement expression" giving the phonetic equivalent of the \r
301     // "match expression".\r
302     TransformationRule rule = null;\r
303     StringBuffer matchExp = new StringBuffer();\r
304     StringBuffer replaceExp = new StringBuffer();\r
305     boolean start = false,\r
306         end = false;\r
307     int takeOutPart = 0,\r
308         matchLength = 0;\r
309     boolean match = true,\r
310         inMulti = false;\r
311     for (int i = 0; i < str.length(); i++) {\r
312       if (Character.isWhitespace(str.charAt(i))) {\r
313         match = false;\r
314       } else {\r
315         if (match) {\r
316           if (!isReservedChar(str.charAt(i))) {\r
317             matchExp.append(str.charAt(i));\r
318             if (!inMulti) {\r
319               takeOutPart++;\r
320               matchLength++;\r
321             }\r
322             if (str.charAt(i) == STARTMULTI || str.charAt(i) == ENDMULTI)\r
323               inMulti = !inMulti;\r
324           }\r
325           if (str.charAt(i) == '-')\r
326             takeOutPart--;\r
327           if (str.charAt(i) == '^')\r
328             start = true;\r
329           if (str.charAt(i) == '$')\r
330             end = true;\r
331         } else {\r
332           replaceExp.append(str.charAt(i));\r
333         }\r
334       }\r
335     }\r
336     if (replaceExp.toString().equals(REPLACEVOID)) {\r
337       replaceExp = new StringBuffer("");\r
338       //System.out.println("Changing _ to \"\" for "+matchExp.toString());\r
339     }\r
340     rule = new TransformationRule(matchExp.toString(), replaceExp.toString(), takeOutPart, matchLength, start, end);\r
341     //System.out.println(rule.toString());\r
342     ruleList.addElement(rule);\r
343   }\r
344 \r
345   // Chars with special meaning to aspell. Not everyone is implemented here.\r
346   private boolean isReservedChar(char ch) {\r
347     if (ch == '<' || ch == '>' || ch == '^' || ch == '$' || ch == '-' || Character.isDigit(ch))\r
348       return true;\r
349     return false;\r
350   }\r
351 \r
352   // Trims off everything we don't care about.\r
353   private String realTrimmer(String row) {\r
354     int pos = row.indexOf('#');\r
355     if (pos != -1) {\r
356       row = row.substring(0, pos);\r
357     }\r
358     return row.trim();\r
359   }\r
360 \r
361   // Inner Classes\r
362   /*\r
363   * Holds the match string and the replace string and all the rule attributes.\r
364   * Is responsible for indicating matches.\r
365   */\r
366   private class TransformationRule {\r
367 \r
368     private final String replace;\r
369     private final char[] match;\r
370     // takeOut=number of chars to replace;\r
371     // matchLength=length of matching string counting multies as one.\r
372     private final int takeOut, matchLength;\r
373     private final boolean start, end;\r
374 \r
375     // Construktor\r
376     public TransformationRule(String match, String replace, int takeout, int matchLength, boolean start, boolean end) {\r
377       this.match = match.toCharArray();\r
378       this.replace = replace;\r
379       this.takeOut = takeout;\r
380       this.matchLength = matchLength;\r
381       this.start = start;\r
382       this.end = end;\r
383     }\r
384 \r
385     /*\r
386     * Returns true if word from pos and forward matches the match string.\r
387     * Precondition: wordPos+matchLength<word.length()\r
388     */\r
389     public boolean isMatching(StringBuffer word, int wordPos) {\r
390       boolean matching = true, inMulti = false, multiMatch = false;\r
391       char matchCh;\r
392 \r
393       for (char element : match) {\r
394         matchCh = element;\r
395         if (matchCh == STARTMULTI || matchCh == ENDMULTI) {\r
396           inMulti = !inMulti;\r
397           if (!inMulti)\r
398             matching = matching & multiMatch;\r
399           else\r
400             multiMatch = false;\r
401         } else {\r
402           if (matchCh != word.charAt(wordPos)) {\r
403             if (inMulti)\r
404               multiMatch = multiMatch | false;\r
405             else\r
406               matching = false;\r
407           } else {\r
408             if (inMulti)\r
409               multiMatch = multiMatch | true;\r
410             else\r
411               matching = true;\r
412           }\r
413           if (!inMulti)\r
414             wordPos++;\r
415           if (!matching)\r
416             break;\r
417         }\r
418       }\r
419       if (end && wordPos != word.length())\r
420         matching = false;\r
421       return matching;\r
422     }\r
423 \r
424     public String getReplaceExp() {\r
425       return replace;\r
426     }\r
427 \r
428     public int getTakeOut() {\r
429       return takeOut;\r
430     }\r
431 \r
432     public boolean startsWithExp() {\r
433       return start;\r
434     }\r
435 \r
436     public int lengthOfMatch() {\r
437       return matchLength;\r
438     }\r
439 \r
440     // Just for debugging purposes.\r
441     @Override\r
442         public String toString() {\r
443       return "Match:" + String.valueOf(match) + " Replace:" + replace + " TakeOut:" + takeOut + " MatchLength:" + matchLength + " Start:" + start + " End:" + end;\r
444     }\r
445 \r
446   }\r
447 }\r