/*
Jazzy - a Java library for Spell Checking
Copyright (C) 2001 Mindaugas Idzelis
Full text of license can be found in LICENSE.txt
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.swabunga.spell.engine;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Vector;
import com.swabunga.util.StringUtility;
/**
* A Generic implementation of a transformator takes an aspell phonetics file and constructs some sort of transformation table using the
* inner class Rule.
*
* @author Robert Gustavsson (robert@lindesign.se)
*/
public class GenericTransformator implements Transformator {
/**
* This replace list is used if no phonetic file is supplied or it doesn't contain the alphabet.
*/
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' };
public static final char ALPHABET_START = '[';
public static final char ALPHABET_END = ']';
public static final String KEYWORD_ALPHBET = "alphabet";
public static final String[] IGNORED_KEYWORDS = { "version", "followup", "collapse_result" };
public static final char STARTMULTI = '(';
public static final char ENDMULTI = ')';
public static final String DIGITCODE = "0";
public static final String REPLACEVOID = "_";
private Object[] ruleArray = null;
private char[] alphabetString = defaultEnglishAlphabet;
public GenericTransformator(File phonetic) throws IOException {
buildRules(new BufferedReader(new FileReader(phonetic)));
alphabetString = washAlphabetIntoReplaceList(getReplaceList());
}
public GenericTransformator(File phonetic, String encoding) throws IOException {
buildRules(new BufferedReader(new InputStreamReader(new FileInputStream(phonetic), encoding)));
alphabetString = washAlphabetIntoReplaceList(getReplaceList());
}
public GenericTransformator(Reader phonetic) throws IOException {
buildRules(new BufferedReader(phonetic));
alphabetString = washAlphabetIntoReplaceList(getReplaceList());
}
/**
* Goes through an alphabet and makes sure that only one of those letters that are coded equally will be in the replace list. In other
* words, it removes any letters in the alphabet that are redundant phonetically.
*
* This is done to improve speed in the getSuggestion method.
*
* @param alphabet
* The complete alphabet to wash.
* @return The washed alphabet to be used as replace list.
*/
private char[] washAlphabetIntoReplaceList(char[] alphabet) {
HashMap letters = new HashMap(alphabet.length);
for (int i = 0; i < alphabet.length; i++) {
String tmp = String.valueOf(alphabet[i]);
String code = transform(tmp);
if (!letters.containsKey(code)) {
letters.put(code, new Character(alphabet[i]));
}
}
Object[] tmpCharacters = letters.values().toArray();
char[] washedArray = new char[tmpCharacters.length];
for (int i = 0; i < tmpCharacters.length; i++) {
washedArray[i] = ((Character) tmpCharacters[i]).charValue();
}
return washedArray;
}
/**
* Takes out all single character replacements and put them in a char array. This array can later be used for adding or changing letters
* in getSuggestion().
*
* @return char[] An array of chars with replacements characters
*/
public char[] getCodeReplaceList() {
char[] replacements;
TransformationRule rule;
Vector tmp = new Vector();
if (ruleArray == null) {
return null;
}
for (int i = 0; i < ruleArray.length; i++) {
rule = (TransformationRule) ruleArray[i];
if (rule.getReplaceExp().length() == 1) {
tmp.addElement(rule.getReplaceExp());
}
}
replacements = new char[tmp.size()];
for (int i = 0; i < tmp.size(); i++) {
replacements[i] = ((String) tmp.elementAt(i)).charAt(0);
}
return replacements;
}
/**
* Builds up an char array with the chars in the alphabet of the language as it was read from the alphabet tag in the phonetic file.
*
* @return char[] An array of chars representing the alphabet or null if no alphabet was available.
*/
@Override
public char[] getReplaceList() {
return alphabetString;
}
/**
* Returns the phonetic code of the word.
*/
@Override
public String transform(String word) {
if (ruleArray == null) {
return null;
}
TransformationRule rule;
StringBuffer str = new StringBuffer(word.toUpperCase());
int strLength = str.length();
int startPos = 0, add = 1;
while (startPos < strLength) {
add = 1;
if (Character.isDigit(str.charAt(startPos))) {
StringUtility.replace(str, startPos, startPos + DIGITCODE.length(), DIGITCODE);
startPos += add;
continue;
}
for (int i = 0; i < ruleArray.length; i++) {
// System.out.println("Testing rule#:"+i);
rule = (TransformationRule) ruleArray[i];
if (rule.startsWithExp() && startPos > 0) {
continue;
}
if (startPos + rule.lengthOfMatch() > strLength) {
continue;
}
if (rule.isMatching(str, startPos)) {
String replaceExp = rule.getReplaceExp();
add = replaceExp.length();
StringUtility.replace(str, startPos, startPos + rule.getTakeOut(), replaceExp);
strLength -= rule.getTakeOut();
strLength += add;
// System.out.println("Replacing with rule#:"+i+" add="+add);
break;
}
}
startPos += add;
}
// System.out.println(word);
// System.out.println(str.toString());
return str.toString();
}
// Used to build up the transformastion table.
private void buildRules(BufferedReader in) throws IOException {
String read = null;
Vector ruleList = new Vector();
while ((read = in.readLine()) != null) {
buildRule(realTrimmer(read), ruleList);
}
ruleArray = new TransformationRule[ruleList.size()];
ruleList.copyInto(ruleArray);
}
// Here is where the real work of reading the phonetics file is done.
private void buildRule(String str, Vector ruleList) {
if (str.length() < 1) {
return;
}
for (int i = 0; i < IGNORED_KEYWORDS.length; i++) {
if (str.startsWith(IGNORED_KEYWORDS[i])) {
return;
}
}
// A different alphabet is used for this language, will be read into
// the alphabetString variable.
if (str.startsWith(KEYWORD_ALPHBET)) {
int start = str.indexOf(ALPHABET_START);
int end = str.lastIndexOf(ALPHABET_END);
if (end != -1 && start != -1) {
alphabetString = str.substring(++start, end).toCharArray();
}
return;
}
TransformationRule rule = null;
StringBuffer matchExp = new StringBuffer();
StringBuffer replaceExp = new StringBuffer();
boolean start = false, end = false;
int takeOutPart = 0, matchLength = 0;
boolean match = true, inMulti = false;
for (int i = 0; i < str.length(); i++) {
if (Character.isWhitespace(str.charAt(i))) {
match = false;
} else {
if (match) {
if (!isReservedChar(str.charAt(i))) {
matchExp.append(str.charAt(i));
if (!inMulti) {
takeOutPart++;
matchLength++;
}
if (str.charAt(i) == STARTMULTI || str.charAt(i) == ENDMULTI) {
inMulti = !inMulti;
}
}
if (str.charAt(i) == '-') {
takeOutPart--;
}
if (str.charAt(i) == '^') {
start = true;
}
if (str.charAt(i) == '$') {
end = true;
}
} else {
replaceExp.append(str.charAt(i));
}
}
}
if (replaceExp.toString().equals(REPLACEVOID)) {
replaceExp = new StringBuffer("");
// System.out.println("Changing _ to \"\" for "+matchExp.toString());
}
rule = new TransformationRule(matchExp.toString(), replaceExp.toString(), takeOutPart, matchLength, start, end);
// System.out.println(rule.toString());
ruleList.addElement(rule);
}
// Chars with special meaning to aspell. Not everyone is implemented here.
private boolean isReservedChar(char ch) {
if (ch == '<' || ch == '>' || ch == '^' || ch == '$' || ch == '-' || Character.isDigit(ch)) {
return true;
}
return false;
}
// Trims off everything we don't care about.
private String realTrimmer(String row) {
int pos = row.indexOf('#');
if (pos != -1) {
row = row.substring(0, pos);
}
return row.trim();
}
// Inner Classes
/*
* Holds the match string and the replace string and all the rule attributes.
* Is responsible for indicating matches.
*/
private class TransformationRule {
private String replace;
private char[] match;
// takeOut=number of chars to replace;
// matchLength=length of matching string counting multies as one.
private int takeOut, matchLength;
private boolean start, end;
// Construktor
public TransformationRule(String match, String replace, int takeout, int matchLength, boolean start, boolean end) {
this.match = match.toCharArray();
this.replace = replace;
this.takeOut = takeout;
this.matchLength = matchLength;
this.start = start;
this.end = end;
}
/*
* Returns true if word from pos and forward matches the match string.
* Precondition: wordPos+matchLength<word.length()
*/
public boolean isMatching(StringBuffer word, int wordPos) {
boolean matching = true, inMulti = false, multiMatch = false;
char matchCh;
for (int matchPos = 0; matchPos < match.length; matchPos++) {
matchCh = match[matchPos];
if (matchCh == STARTMULTI || matchCh == ENDMULTI) {
inMulti = !inMulti;
if (!inMulti) {
matching = matching & multiMatch;
} else {
multiMatch = false;
}
} else {
if (matchCh != word.charAt(wordPos)) {
if (inMulti) {
multiMatch = multiMatch | false;
} else {
matching = false;
}
} else {
if (inMulti) {
multiMatch = multiMatch | true;
} else {
matching = true;
}
}
if (!inMulti) {
wordPos++;
}
if (!matching) {
break;
}
}
}
if (end && wordPos != word.length()) {
matching = false;
}
return matching;
}
public String getReplaceExp() {
return replace;
}
public int getTakeOut() {
return takeOut;
}
public boolean startsWithExp() {
return start;
}
public int lengthOfMatch() {
return matchLength;
}
// Just for debugging purposes.
@Override
public String toString() {
return "Match:" + String.valueOf(match) + " Replace:" + replace + " TakeOut:" + takeOut + " MatchLength:" + matchLength
+ " Start:" + start + " End:" + end;
}
}
}