blob: a180eaab28aee43b791017566fd09d115a87e146 [file] [log] [blame]
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Lucene.Net.Analysis.En;
namespace Lucene.Net.Analysis.En
{
public class KStemmer
{
private const int MaxWordLen = 50;
static private readonly String[] exceptionWords = {"aide", "bathe", "caste",
"cute", "dame", "dime", "doge", "done", "dune", "envelope", "gage",
"grille", "grippe", "lobe", "mane", "mare", "nape", "node", "pane",
"pate", "plane", "pope", "programme", "quite", "ripe", "rote", "rune",
"sage", "severe", "shoppe", "sine", "slime", "snipe", "steppe", "suite",
"swinge", "tare", "tine", "tope", "tripe", "twine"};
static private readonly String[][] directConflations = { new[] {"aging", "age"},
new[] {"going", "go"}, new[] {"goes", "go"}, new[] {"lying", "lie"}, new[] {"using", "use"},
new[] {"owing", "owe"}, new[] {"suing", "sue"}, new[] {"dying", "die"}, new[] {"tying", "tie"},
new[] {"vying", "vie"}, new[] {"aged", "age"}, new[] {"used", "use"},new[] {"vied", "vie"},
new[] {"cued", "cue"}, new[] {"died", "die"}, new[] {"eyed", "eye"}, new[] {"hued", "hue"},
new[] {"iced", "ice"}, new[] {"lied", "lie"}, new[] {"owed", "owe"}, new[] {"sued", "sue"},
new[] {"toed", "toe"}, new[] {"tied", "tie"}, new[] {"does", "do"}, new[] {"doing", "do"},
new[] {"aeronautical", "aeronautics"}, new[] {"mathematical", "mathematics"},
new[] {"political", "politics"}, new[] {"metaphysical", "metaphysics"},
new[] {"cylindrical", "cylinder"}, new[] {"nazism", "nazi"},
new[] {"ambiguity", "ambiguous"}, new[] {"barbarity", "barbarous"},
new[] {"credulity", "credulous"}, new[] {"generosity", "generous"},
new[] {"spontaneity", "spontaneous"}, new[] {"unanimity", "unanimous"},
new[] {"voracity", "voracious"}, new[] {"fled", "flee"}, new[] {"miscarriage", "miscarry"}};
static private readonly String[][] countryNationality = {
new[] {"afghan", "afghanistan"}, new[] {"african", "africa"},
new[] {"albanian", "albania"}, new[] {"algerian", "algeria"},
new[] {"american", "america"}, new[] {"andorran", "andorra"}, new[] {"angolan", "angola"},
new[] {"arabian", "arabia"}, new[] {"argentine", "argentina"},
new[] {"armenian", "armenia"}, new[] {"asian", "asia"}, new[] {"australian", "australia"},
new[] {"austrian", "austria"}, new[] {"azerbaijani", "azerbaijan"},
new[] {"azeri", "azerbaijan"}, new[] {"bangladeshi", "bangladesh"},
new[] {"belgian", "belgium"}, new[] {"bermudan", "bermuda"}, new[] {"bolivian", "bolivia"},
new[] {"bosnian", "bosnia"}, new[] {"botswanan", "botswana"},
new[] {"brazilian", "brazil"}, new[] {"british", "britain"},
new[] {"bulgarian", "bulgaria"}, new[] {"burmese", "burma"},
new[] {"californian", "california"}, new[] {"cambodian", "cambodia"},
new[] {"canadian", "canada"}, new[] {"chadian", "chad"}, new[] {"chilean", "chile"},
new[] {"chinese", "china"}, new[] {"colombian", "colombia"}, new[] {"croat", "croatia"},
new[] {"croatian", "croatia"}, new[] {"cuban", "cuba"}, new[] {"cypriot", "cyprus"},
new[] {"czechoslovakian", "czechoslovakia"}, new[] {"danish", "denmark"},
new[] {"egyptian", "egypt"}, new[] {"equadorian", "equador"},
new[] {"eritrean", "eritrea"}, new[] {"estonian", "estonia"},
new[] {"ethiopian", "ethiopia"}, new[] {"european", "europe"}, new[] {"fijian", "fiji"},
new[] {"filipino", "philippines"}, new[] {"finnish", "finland"},
new[] {"french", "france"}, new[] {"gambian", "gambia"}, new[] {"georgian", "georgia"},
new[] {"german", "germany"}, new[] {"ghanian", "ghana"}, new[] {"greek", "greece"},
new[] {"grenadan", "grenada"}, new[] {"guamian", "guam"},
new[] {"guatemalan", "guatemala"}, new[] {"guinean", "guinea"},
new[] {"guyanan", "guyana"}, new[] {"haitian", "haiti"}, new[] {"hawaiian", "hawaii"},
new[] {"holland", "dutch"}, new[] {"honduran", "honduras"}, new[] {"hungarian", "hungary"},
new[] {"icelandic", "iceland"}, new[] {"indonesian", "indonesia"},
new[] {"iranian", "iran"}, new[] {"iraqi", "iraq"}, new[] {"iraqui", "iraq"},
new[] {"irish", "ireland"}, new[] {"israeli", "israel"},
new[] {"italian", "italy"},
new[] {"jamaican", "jamaica"},
new[] {"japanese", "japan"},
new[] {"jordanian", "jordan"},
new[] {"kampuchean", "cambodia"},
new[] {"kenyan", "kenya"},
new[] {"korean", "korea"},
new[] {"kuwaiti", "kuwait"},
new[] {"lankan", "lanka"},
new[] {"laotian", "laos"},
new[] {"latvian", "latvia"},
new[] {"lebanese", "lebanon"},
new[] {"liberian", "liberia"},
new[] {"libyan", "libya"},
new[] {"lithuanian", "lithuania"},
new[] {"macedonian", "macedonia"},
new[] {"madagascan", "madagascar"},
new[] {"malaysian", "malaysia"},
new[] {"maltese", "malta"},
new[] {"mauritanian", "mauritania"},
new[] {"mexican", "mexico"},
new[] {"micronesian", "micronesia"},
new[] {"moldovan", "moldova"},
new[] {"monacan", "monaco"},
new[] {"mongolian", "mongolia"},
new[] {"montenegran", "montenegro"},
new[] {"moroccan", "morocco"},
new[] {"myanmar", "burma"},
new[] {"namibian", "namibia"},
new[] {"nepalese", "nepal"},
//new[] {"netherlands", "dutch"},
new[] {"nicaraguan", "nicaragua"}, new[] {"nigerian", "nigeria"},
new[] {"norwegian", "norway"}, new[] {"omani", "oman"}, new[] {"pakistani", "pakistan"},
new[] {"panamanian", "panama"}, new[] {"papuan", "papua"},
new[] {"paraguayan", "paraguay"}, new[] {"peruvian", "peru"},
new[] {"portuguese", "portugal"}, new[] {"romanian", "romania"},
new[] {"rumania", "romania"}, new[] {"rumanian", "romania"}, new[] {"russian", "russia"},
new[] {"rwandan", "rwanda"}, new[] {"samoan", "samoa"}, new[] {"scottish", "scotland"},
new[] {"serb", "serbia"}, new[] {"serbian", "serbia"}, new[] {"siam", "thailand"},
new[] {"siamese", "thailand"}, new[] {"slovakia", "slovak"}, new[] {"slovakian", "slovak"},
new[] {"slovenian", "slovenia"}, new[] {"somali", "somalia"},
new[] {"somalian", "somalia"}, new[] {"spanish", "spain"}, new[] {"swedish", "sweden"},
new[] {"swiss", "switzerland"}, new[] {"syrian", "syria"}, new[] {"taiwanese", "taiwan"},
new[] {"tanzanian", "tanzania"}, new[] {"texan", "texas"}, new[] {"thai", "thailand"},
new[] {"tunisian", "tunisia"}, new[] {"turkish", "turkey"}, new[] {"ugandan", "uganda"},
new[] {"ukrainian", "ukraine"}, new[] {"uruguayan", "uruguay"},
new[] {"uzbek", "uzbekistan"}, new[] {"venezuelan", "venezuela"},
new[] {"vietnamese", "viet"}, new[] {"virginian", "virginia"}, new[] {"yemeni", "yemen"},
new[] {"yugoslav", "yugoslavia"}, new[] {"yugoslavian", "yugoslavia"},
new[] {"zambian", "zambia"}, new[] {"zealander", "zealand"},
new[] {"zimbabwean", "zimbabwe"}};
static private readonly String[] supplementDict = {"aids", "applicator",
"capacitor", "digitize", "electromagnet", "ellipsoid", "exosphere",
"extensible", "ferromagnet", "graphics", "hydromagnet", "polygraph",
"toroid", "superconduct", "backscatter", "connectionism"};
static private readonly String[] properNouns = {"abrams", "achilles",
"acropolis", "adams", "agnes", "aires", "alexander", "alexis", "alfred",
"algiers", "alps", "amadeus", "ames", "amos", "andes", "angeles",
"annapolis", "antilles", "aquarius", "archimedes", "arkansas", "asher",
"ashly", "athens", "atkins", "atlantis", "avis", "bahamas", "bangor",
"barbados", "barger", "bering", "brahms", "brandeis", "brussels",
"bruxelles", "cairns", "camoros", "camus", "carlos", "celts", "chalker",
"charles", "cheops", "ching", "christmas", "cocos", "collins",
"columbus", "confucius", "conners", "connolly", "copernicus", "cramer",
"cyclops", "cygnus", "cyprus", "dallas", "damascus", "daniels", "davies",
"davis", "decker", "denning", "dennis", "descartes", "dickens", "doris",
"douglas", "downs", "dreyfus", "dukakis", "dulles", "dumfries",
"ecclesiastes", "edwards", "emily", "erasmus", "euphrates", "evans",
"everglades", "fairbanks", "federales", "fisher", "fitzsimmons",
"fleming", "forbes", "fowler", "france", "francis", "goering",
"goodling", "goths", "grenadines", "guiness", "hades", "harding",
"harris", "hastings", "hawkes", "hawking", "hayes", "heights",
"hercules", "himalayas", "hippocrates", "hobbs", "holmes", "honduras",
"hopkins", "hughes", "humphreys", "illinois", "indianapolis",
"inverness", "iris", "iroquois", "irving", "isaacs", "italy", "james",
"jarvis", "jeffreys", "jesus", "jones", "josephus", "judas", "julius",
"kansas", "keynes", "kipling", "kiwanis", "lansing", "laos", "leeds",
"levis", "leviticus", "lewis", "louis", "maccabees", "madras",
"maimonides", "maldive", "massachusetts", "matthews", "mauritius",
"memphis", "mercedes", "midas", "mingus", "minneapolis", "mohammed",
"moines", "morris", "moses", "myers", "myknos", "nablus", "nanjing",
"nantes", "naples", "neal", "netherlands", "nevis", "nostradamus",
"oedipus", "olympus", "orleans", "orly", "papas", "paris", "parker",
"pauling", "peking", "pershing", "peter", "peters", "philippines",
"phineas", "pisces", "pryor", "pythagoras", "queens", "rabelais",
"ramses", "reynolds", "rhesus", "rhodes", "richards", "robins",
"rodgers", "rogers", "rubens", "sagittarius", "seychelles", "socrates",
"texas", "thames", "thomas", "tiberias", "tunis", "venus", "vilnius",
"wales", "warner", "wilkins", "williams", "wyoming", "xmas", "yonkers",
"zeus", "frances", "aarhus", "adonis", "andrews", "angus", "antares",
"aquinas", "arcturus", "ares", "artemis", "augustus", "ayers",
"barnabas", "barnes", "becker", "bejing", "biggs", "billings", "boeing",
"boris", "borroughs", "briggs", "buenos", "calais", "caracas", "cassius",
"cerberus", "ceres", "cervantes", "chantilly", "chartres", "chester",
"connally", "conner", "coors", "cummings", "curtis", "daedalus",
"dionysus", "dobbs", "dolores", "edmonds"};
private class DictEntry
{
public readonly bool exception;
public readonly String root;
public DictEntry(String root, bool isException)
{
this.root = root;
this.exception = isException;
}
}
private static readonly IDictionary<string, DictEntry> dict_ht = initializeDictHash();
/***
* caching off private int maxCacheSize; private CharArrayMap<String> cache =
* null; private static final String SAME = "SAME"; // use if stemmed form is
* the same
***/
private readonly OpenStringBuilder word = new OpenStringBuilder(); // OpenStringBuilder
private int j; /* index of final letter in stem (within word) */
private int k; /*
* INDEX of final letter in word. You must add 1 to k to get
* the current length of word. When you want the length of
* word, use the method wordLength, which returns (k+1).
*/
/***
* private void initializeStemHash() { if (maxCacheSize > 0) cache = new
* CharArrayMap<String>(maxCacheSize,false); }
***/
private char finalChar()
{
return word.charAt(k);
}
private char penultChar()
{
return word.charAt(k - 1);
}
private bool isVowel(int index)
{
return !isCons(index);
}
private bool isCons(int index)
{
var ch = word.charAt(index);
if ((ch == 'a') || (ch == 'e') || (ch == 'i') || (ch == 'o') || (ch == 'u')) return false;
if ((ch != 'y') || (index == 0)) return true;
else return (!isCons(index - 1));
}
private static IDictionary<string, DictEntry> initializeDictHash()
{
DictEntry defaultEntry;
DictEntry entry;
IDictionary<string, DictEntry> d = new Dictionary<string, DictEntry>(1000);
for (int i = 0; i < exceptionWords.Length; i++)
{
if (!d.ContainsKey(exceptionWords[i]))
{
entry = new DictEntry(exceptionWords[i], true);
d.Add(exceptionWords[i], entry);
}
else
{
Debug.Write("Warning: Entry [" + exceptionWords[i]
+ "] already in dictionary 1");
}
}
for (int i = 0; i < directConflations.Length; i++)
{
if (!d.ContainsKey(directConflations[i][0]))
{
entry = new DictEntry(directConflations[i][1], false);
d.Add(directConflations[i][0], entry);
}
else
{
Debug.Write("Warning: Entry [" + directConflations[i][0]
+ "] already in dictionary 2");
}
}
for (int i = 0; i < countryNationality.Length; i++)
{
if (!d.ContainsKey(countryNationality[i][0]))
{
entry = new DictEntry(countryNationality[i][1], false);
d.Add(countryNationality[i][0], entry);
}
else
{
Debug.Write("Warning: Entry [" + countryNationality[i][0]
+ "] already in dictionary 3");
}
}
defaultEntry = new DictEntry(null, false);
string[] array = KStemData1.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
array = KStemData2.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
array = KStemData3.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
array = KStemData4.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
array = KStemData5.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
array = KStemData6.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
array = KStemData7.data;
for (int i = 0; i < array.Length; i++)
{
if (!d.ContainsKey(array[i]))
{
d.Add(array[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + array[i]
+ "] already in dictionary 4");
}
}
for (int i = 0; i < KStemData8.data.Length; i++)
{
if (!d.ContainsKey(KStemData8.data[i]))
{
d.Add(KStemData8.data[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + KStemData8.data[i]
+ "] already in dictionary 4");
}
}
for (int i = 0; i < supplementDict.Length; i++)
{
if (!d.ContainsKey(supplementDict[i]))
{
d.Add(supplementDict[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + supplementDict[i]
+ "] already in dictionary 5");
}
}
for (int i = 0; i < properNouns.Length; i++)
{
if (!d.ContainsKey(properNouns[i]))
{
d.Add(properNouns[i], defaultEntry);
}
else
{
Debug.Write("Warning: Entry [" + properNouns[i]
+ "] already in dictionary 6");
}
}
return d;
}
private bool isAlpha(char ch)
{
return ch >= 'a' && ch <= 'z'; // terms must be lowercased already
}
/* length of stem within word */
private int stemLength()
{
return j + 1;
}
private bool endsIn(char[] s)
{
if (s.Length > k) return false;
int r = word.length() - s.Length; /* length of word before this suffix */
j = k;
for (int r1 = r, i = 0; i < s.Length; i++, r1++)
{
if (s[i] != word.charAt(r1)) return false;
}
j = r - 1; /* index of the character BEFORE the posfix */
return true;
}
private bool endsIn(char a, char b)
{
if (2 > k) return false;
// check left to right since the endings have often already matched
if (word.charAt(k - 1) == a && word.charAt(k) == b)
{
j = k - 2;
return true;
}
return false;
}
private bool endsIn(char a, char b, char c)
{
if (3 > k) return false;
if (word.charAt(k - 2) == a && word.charAt(k - 1) == b
&& word.charAt(k) == c)
{
j = k - 3;
return true;
}
return false;
}
private bool endsIn(char a, char b, char c, char d)
{
if (4 > k) return false;
if (word.charAt(k - 3) == a && word.charAt(k - 2) == b
&& word.charAt(k - 1) == c && word.charAt(k) == d)
{
j = k - 4;
return true;
}
return false;
}
private DictEntry wordInDict()
{
/***
* if (matchedEntry != null) { if (dict_ht.get(word.getArray(), 0,
* word.size()) != matchedEntry) {
* System.out.println("Uh oh... cached entry doesn't match"); } return
* matchedEntry; }
***/
if (matchedEntry != null) return matchedEntry;
String w = word.toString();
DictEntry e = dict_ht.ContainsKey(w) ? dict_ht[w] : null;
if (e != null && !e.exception)
{
matchedEntry = e; // only cache if it's not an exception.
}
// lookups.add(word.toString());
return e;
}
/* Convert plurals to singular form, and '-ies' to 'y' */
private void plural()
{
if (word.charAt(k) == 's')
{
if (endsIn('i', 'e', 's'))
{
word.setLength(j + 3);
k--;
if (lookup()) /* ensure calories -> calorie */
return;
k++;
word.unsafeWrite('s');
setSuffix("y");
lookup();
}
else if (endsIn('e', 's'))
{
/* try just removing the "s" */
word.setLength(j + 2);
k--;
/*
* note: don't check for exceptions here. So, `aides' -> `aide', but
* `aided' -> `aid'. The exception for double s is used to prevent
* crosses -> crosse. This is actually correct if crosses is a plural
* noun (a type of racket used in lacrosse), but the verb is much more
* common
*/
/****
* YCS: this was the one place where lookup was not followed by return.
* So restructure it. if ((j>0)&&(lookup(word.toString())) &&
* !((word.charAt(j) == 's') && (word.charAt(j-1) == 's'))) return;
*****/
bool tryE = j > 0
&& !((word.charAt(j) == 's') && (word.charAt(j - 1) == 's'));
if (tryE && lookup()) return;
/* try removing the "es" */
word.setLength(j + 1);
k--;
if (lookup()) return;
/* the default is to retain the "e" */
word.unsafeWrite('e');
k++;
if (!tryE) lookup(); // if we didn't try the "e" ending before
return;
}
else
{
if (word.length() > 3 && penultChar() != 's' && !endsIn('o', 'u', 's'))
{
/* unless the word ends in "ous" or a double "s", remove the final "s" */
word.setLength(k);
k--;
lookup();
}
}
}
}
private void setSuffix(String s)
{
setSuff(s, s.Length);
}
/* replace old suffix with s */
private void setSuff(String s, int len)
{
word.setLength(j + 1);
for (int l = 0; l < len; l++)
{
word.unsafeWrite(s[l]);
}
k = j + len;
}
/* Returns true if the word is found in the dictionary */
// almost all uses of lookup() return immediately and are
// followed by another lookup in the dict. Store the match
// to avoid this double lookup.
DictEntry matchedEntry = null;
private bool lookup()
{
/******
* debugging code String thisLookup = word.toString(); bool added =
* lookups.add(thisLookup); if (!added) {
* System.out.println("######extra lookup:" + thisLookup); // occaasional
* extra lookups aren't necessarily errors... could happen by diff
* manipulations // throw new RuntimeException("######extra lookup:" +
* thisLookup); } else { // System.out.println("new lookup:" + thisLookup);
* }
******/
String w = word.toString();
matchedEntry = dict_ht.ContainsKey(w) ? dict_ht[w] : null;
return matchedEntry != null;
}
// Set<String> lookups = new HashSet<String>();
/* convert past tense (-ed) to present, and `-ied' to `y' */
private void pastTense()
{
/*
* Handle words less than 5 letters with a direct mapping This prevents
* (fled -> fl).
*/
if (word.length() <= 4) return;
if (endsIn('i', 'e', 'd'))
{
word.setLength(j + 3);
k--;
if (lookup()) /* we almost always want to convert -ied to -y, but */
return; /* this isn't true for short words (died->die) */
k++; /* I don't know any long words that this applies to, */
word.unsafeWrite('d'); /* but just in case... */
setSuffix("y");
lookup();
return;
}
/* the vowelInStem() is necessary so we don't stem acronyms */
if (endsIn('e', 'd') && vowelInStem())
{
/* see if the root ends in `e' */
word.setLength(j + 2);
k = j + 1;
DictEntry entry = wordInDict();
if (entry != null) if (!entry.exception) /*
* if it's in the dictionary and
* not an exception
*/
return;
/* try removing the "ed" */
word.setLength(j + 1);
k = j;
if (lookup()) return;
/*
* try removing a doubled consonant. if the root isn't found in the
* dictionary, the default is to leave it doubled. This will correctly
* capture `backfilled' -> `backfill' instead of `backfill' ->
* `backfille', and seems correct most of the time
*/
if (doubleC(k))
{
word.setLength(k);
k--;
if (lookup()) return;
word.unsafeWrite(word.charAt(k));
k++;
lookup();
return;
}
/* if we have a `un-' prefix, then leave the word alone */
/* (this will sometimes screw up with `under-', but we */
/* will take care of that later) */
if ((word.charAt(0) == 'u') && (word.charAt(1) == 'n'))
{
word.unsafeWrite('e');
word.unsafeWrite('d');
k = k + 2;
// nolookup()
return;
}
/*
* it wasn't found by just removing the `d' or the `ed', so prefer to end
* with an `e' (e.g., `microcoded' -> `microcode').
*/
word.setLength(j + 1);
word.unsafeWrite('e');
k = j + 1;
// nolookup() - we already tried the "e" ending
return;
}
}
/* return TRUE if word ends with a double consonant */
private bool doubleC(int i)
{
if (i < 1) return false;
if (word.charAt(i) != word.charAt(i - 1)) return false;
return (isCons(i));
}
private bool vowelInStem()
{
for (int i = 0; i < stemLength(); i++)
{
if (isVowel(i)) return true;
}
return false;
}
/* handle `-ing' endings */
private void aspect()
{
/*
* handle short words (aging -> age) via a direct mapping. This prevents
* (thing -> the) in the version of this routine that ignores inflectional
* variants that are mentioned in the dictionary (when the root is also
* present)
*/
if (word.length() <= 5) return;
/* the vowelinstem() is necessary so we don't stem acronyms */
if (endsIn('i', 'n', 'g') && vowelInStem())
{
/* try adding an `e' to the stem and check against the dictionary */
word.setCharAt(j + 1, 'e');
word.setLength(j + 2);
k = j + 1;
DictEntry entry = wordInDict();
if (entry != null)
{
if (!entry.exception) /* if it's in the dictionary and not an exception */
return;
}
/* adding on the `e' didn't work, so remove it */
word.setLength(k);
k--; /* note that `ing' has also been removed */
if (lookup()) return;
/* if I can remove a doubled consonant and get a word, then do so */
if (doubleC(k))
{
k--;
word.setLength(k + 1);
if (lookup()) return;
word.unsafeWrite(word.charAt(k)); /* restore the doubled consonant */
/* the default is to leave the consonant doubled */
/* (e.g.,`fingerspelling' -> `fingerspell'). Unfortunately */
/* `bookselling' -> `booksell' and `mislabelling' -> `mislabell'). */
/* Without making the algorithm significantly more complicated, this */
/* is the best I can do */
k++;
lookup();
return;
}
/*
* the word wasn't in the dictionary after removing the stem, and then
* checking with and without a final `e'. The default is to add an `e'
* unless the word ends in two consonants, so `microcoding' ->
* `microcode'. The two consonants restriction wouldn't normally be
* necessary, but is needed because we don't try to deal with prefixes and
* compounds, and most of the time it is correct (e.g., footstamping ->
* footstamp, not footstampe; however, decoupled -> decoupl). We can
* prevent almost all of the incorrect stems if we try to do some prefix
* analysis first
*/
if ((j > 0) && isCons(j) && isCons(j - 1))
{
k = j;
word.setLength(k + 1);
// nolookup() because we already did according to the comment
return;
}
word.setLength(j + 1);
word.unsafeWrite('e');
k = j + 1;
// nolookup(); we already tried an 'e' ending
return;
}
}
/*
* this routine deals with -ity endings. It accepts -ability, -ibility, and
* -ality, even without checking the dictionary because they are so
* productive. The first two are mapped to -ble, and the -ity is remove for
* the latter
*/
private void ityEndings()
{
int old_k = k;
if (endsIn('i', 't', 'y'))
{
word.setLength(j + 1); /* try just removing -ity */
k = j;
if (lookup()) return;
word.unsafeWrite('e'); /* try removing -ity and adding -e */
k = j + 1;
if (lookup()) return;
word.setCharAt(j + 1, 'i');
word.append("ty");
k = old_k;
/*
* the -ability and -ibility endings are highly productive, so just accept
* them
*/
if ((j > 0) && (word.charAt(j - 1) == 'i') && (word.charAt(j) == 'l'))
{
word.setLength(j - 1);
word.append("le"); /* convert to -ble */
k = j;
lookup();
return;
}
/* ditto for -ivity */
if ((j > 0) && (word.charAt(j - 1) == 'i') && (word.charAt(j) == 'v'))
{
word.setLength(j + 1);
word.unsafeWrite('e'); /* convert to -ive */
k = j + 1;
lookup();
return;
}
/* ditto for -ality */
if ((j > 0) && (word.charAt(j - 1) == 'a') && (word.charAt(j) == 'l'))
{
word.setLength(j + 1);
k = j;
lookup();
return;
}
/*
* if the root isn't in the dictionary, and the variant *is* there, then
* use the variant. This allows `immunity'->`immune', but prevents
* `capacity'->`capac'. If neither the variant nor the root form are in
* the dictionary, then remove the ending as a default
*/
if (lookup()) return;
/* the default is to remove -ity altogether */
word.setLength(j + 1);
k = j;
// nolookup(), we already did it.
return;
}
}
/* handle -ence and -ance */
private void nceEndings()
{
int old_k = k;
char word_char;
if (endsIn('n', 'c', 'e'))
{
word_char = word.charAt(j);
if (!((word_char == 'e') || (word_char == 'a'))) return;
word.setLength(j);
word.unsafeWrite('e'); /* try converting -e/ance to -e (adherance/adhere) */
k = j;
if (lookup()) return;
word.setLength(j); /*
* try removing -e/ance altogether
* (disappearance/disappear)
*/
k = j - 1;
if (lookup()) return;
word.unsafeWrite(word_char); /* restore the original ending */
word.append("nce");
k = old_k;
// nolookup() because we restored the original ending
}
return;
}
/* handle -ness */
private void nessEndings()
{
if (endsIn('n', 'e', 's', 's'))
{ /*
* this is a very productive endings, so
* just accept it
*/
word.setLength(j + 1);
k = j;
if (word.charAt(j) == 'i') word.setCharAt(j, 'y');
lookup();
}
return;
}
/* handle -ism */
private void ismEndings()
{
if (endsIn('i', 's', 'm'))
{ /*
* this is a very productive ending, so just
* accept it
*/
word.setLength(j + 1);
k = j;
lookup();
}
return;
}
/* this routine deals with -ment endings. */
private void mentEndings()
{
int old_k = k;
if (endsIn('m', 'e', 'n', 't'))
{
word.setLength(j + 1);
k = j;
if (lookup()) return;
word.append("ment");
k = old_k;
// nolookup
}
return;
}
/* this routine deals with -ize endings. */
private void izeEndings()
{
int old_k = k;
if (endsIn('i', 'z', 'e'))
{
word.setLength(j + 1); /* try removing -ize entirely */
k = j;
if (lookup()) return;
word.unsafeWrite('i');
if (doubleC(j))
{ /* allow for a doubled consonant */
word.setLength(j);
k = j - 1;
if (lookup()) return;
word.unsafeWrite(word.charAt(j - 1));
}
word.setLength(j + 1);
word.unsafeWrite('e'); /* try removing -ize and adding -e */
k = j + 1;
if (lookup()) return;
word.setLength(j + 1);
word.append("ize");
k = old_k;
// nolookup()
}
return;
}
/* handle -ency and -ancy */
private void ncyEndings()
{
if (endsIn('n', 'c', 'y'))
{
if (!((word.charAt(j) == 'e') || (word.charAt(j) == 'a'))) return;
word.setCharAt(j + 2, 't'); /* try converting -ncy to -nt */
word.setLength(j + 3);
k = j + 2;
if (lookup()) return;
word.setCharAt(j + 2, 'c'); /* the default is to convert it to -nce */
word.unsafeWrite('e');
k = j + 3;
lookup();
}
return;
}
/* handle -able and -ible */
private void bleEndings()
{
int old_k = k;
char word_char;
if (endsIn('b', 'l', 'e'))
{
if (!((word.charAt(j) == 'a') || (word.charAt(j) == 'i'))) return;
word_char = word.charAt(j);
word.setLength(j); /* try just removing the ending */
k = j - 1;
if (lookup()) return;
if (doubleC(k))
{ /* allow for a doubled consonant */
word.setLength(k);
k--;
if (lookup()) return;
k++;
word.unsafeWrite(word.charAt(k - 1));
}
word.setLength(j);
word.unsafeWrite('e'); /* try removing -a/ible and adding -e */
k = j;
if (lookup()) return;
word.setLength(j);
word.append("ate"); /* try removing -able and adding -ate */
/* (e.g., compensable/compensate) */
k = j + 2;
if (lookup()) return;
word.setLength(j);
word.unsafeWrite(word_char); /* restore the original values */
word.append("ble");
k = old_k;
// nolookup()
}
return;
}
/*
* handle -ic endings. This is fairly straightforward, but this is also the
* only place we try *expanding* an ending, -ic -> -ical. This is to handle
* cases like `canonic' -> `canonical'
*/
private void icEndings()
{
if (endsIn('i', 'c'))
{
word.setLength(j + 3);
word.append("al"); /* try converting -ic to -ical */
k = j + 4;
if (lookup()) return;
word.setCharAt(j + 1, 'y'); /* try converting -ic to -y */
word.setLength(j + 2);
k = j + 1;
if (lookup()) return;
word.setCharAt(j + 1, 'e'); /* try converting -ic to -e */
if (lookup()) return;
word.setLength(j + 1); /* try removing -ic altogether */
k = j;
if (lookup()) return;
word.append("ic"); /* restore the original ending */
k = j + 2;
// nolookup()
}
return;
}
private static readonly char[] ization = "ization".ToCharArray();
private static readonly char[] ition = "ition".ToCharArray();
private static readonly char[] ation = "ation".ToCharArray();
private static readonly char[] ication = "ication".ToCharArray();
/* handle some derivational endings */
/*
* this routine deals with -ion, -ition, -ation, -ization, and -ication. The
* -ization ending is always converted to -ize
*/
private void ionEndings()
{
int old_k = k;
if (!endsIn('i', 'o', 'n'))
{
return;
}
if (endsIn(ization))
{ /*
* the -ize ending is very productive, so simply
* accept it as the root
*/
word.setLength(j + 3);
word.unsafeWrite('e');
k = j + 3;
lookup();
return;
}
if (endsIn(ition))
{
word.setLength(j + 1);
word.unsafeWrite('e');
k = j + 1;
if (lookup()) /*
* remove -ition and add `e', and check against the
* dictionary
*/
return; /* (e.g., definition->define, opposition->oppose) */
/* restore original values */
word.setLength(j + 1);
word.append("ition");
k = old_k;
// nolookup()
}
else if (endsIn(ation))
{
word.setLength(j + 3);
word.unsafeWrite('e');
k = j + 3;
if (lookup()) /* remove -ion and add `e', and check against the dictionary */
return; /* (elmination -> eliminate) */
word.setLength(j + 1);
word.unsafeWrite('e'); /*
* remove -ation and add `e', and check against the
* dictionary
*/
k = j + 1;
if (lookup()) return;
word.setLength(j + 1);/*
* just remove -ation (resignation->resign) and
* check dictionary
*/
k = j;
if (lookup()) return;
/* restore original values */
word.setLength(j + 1);
word.append("ation");
k = old_k;
// nolookup()
}
/*
* test -ication after -ation is attempted (e.g., `complication->complicate'
* rather than `complication->comply')
*/
if (endsIn(ication))
{
word.setLength(j + 1);
word.unsafeWrite('y');
k = j + 1;
if (lookup()) /*
* remove -ication and add `y', and check against the
* dictionary
*/
return; /* (e.g., amplification -> amplify) */
/* restore original values */
word.setLength(j + 1);
word.append("ication");
k = old_k;
// nolookup()
}
// if (endsIn(ion)) {
if (true)
{ // we checked for this earlier... just need to set "j"
j = k - 3; // YCS
word.setLength(j + 1);
word.unsafeWrite('e');
k = j + 1;
if (lookup()) /* remove -ion and add `e', and check against the dictionary */
return;
word.setLength(j + 1);
k = j;
if (lookup()) /* remove -ion, and if it's found, treat that as the root */
return;
/* restore original values */
word.setLength(j + 1);
word.append("ion");
k = old_k;
// nolookup()
}
// nolookup(); all of the other paths restored original values
return;
}
/*
* this routine deals with -er, -or, -ier, and -eer. The -izer ending is
* always converted to -ize
*/
private void erAndOrEndings()
{
int old_k = k;
if (word.charAt(k) != 'r') return; // YCS
char word_char; /* so we can remember if it was -er or -or */
if (endsIn('i', 'z', 'e', 'r'))
{ /*
* -ize is very productive, so accept it
* as the root
*/
word.setLength(j + 4);
k = j + 3;
lookup();
return;
}
if (endsIn('e', 'r') || endsIn('o', 'r'))
{
word_char = word.charAt(j + 1);
if (doubleC(j))
{
word.setLength(j);
k = j - 1;
if (lookup()) return;
word.unsafeWrite(word.charAt(j - 1)); /* restore the doubled consonant */
}
if (word.charAt(j) == 'i')
{ /* do we have a -ier ending? */
word.setCharAt(j, 'y');
word.setLength(j + 1);
k = j;
if (lookup()) /* yes, so check against the dictionary */
return;
word.setCharAt(j, 'i'); /* restore the endings */
word.unsafeWrite('e');
}
if (word.charAt(j) == 'e')
{ /* handle -eer */
word.setLength(j);
k = j - 1;
if (lookup()) return;
word.unsafeWrite('e');
}
word.setLength(j + 2); /* remove the -r ending */
k = j + 1;
if (lookup()) return;
word.setLength(j + 1); /* try removing -er/-or */
k = j;
if (lookup()) return;
word.unsafeWrite('e'); /* try removing -or and adding -e */
k = j + 1;
if (lookup()) return;
word.setLength(j + 1);
word.unsafeWrite(word_char);
word.unsafeWrite('r'); /* restore the word to the way it was */
k = old_k;
// nolookup()
}
}
/*
* this routine deals with -ly endings. The -ally ending is always converted
* to -al Sometimes this will temporarily leave us with a non-word (e.g.,
* heuristically maps to heuristical), but then the -al is removed in the next
* step.
*/
private void lyEndings()
{
int old_k = k;
if (endsIn('l', 'y'))
{
word.setCharAt(j + 2, 'e'); /* try converting -ly to -le */
if (lookup()) return;
word.setCharAt(j + 2, 'y');
word.setLength(j + 1); /* try just removing the -ly */
k = j;
if (lookup()) return;
if ((j > 0) && (word.charAt(j - 1) == 'a') && (word.charAt(j) == 'l')) /*
* always
* convert
* -
* ally
* to
* -
* al
*/
return;
word.append("ly");
k = old_k;
if ((j > 0) && (word.charAt(j - 1) == 'a') && (word.charAt(j) == 'b'))
{ /*
* always
* convert
* -
* ably
* to
* -
* able
*/
word.setCharAt(j + 2, 'e');
k = j + 2;
return;
}
if (word.charAt(j) == 'i')
{ /* e.g., militarily -> military */
word.setLength(j);
word.unsafeWrite('y');
k = j;
if (lookup()) return;
word.setLength(j);
word.append("ily");
k = old_k;
}
word.setLength(j + 1); /* the default is to remove -ly */
k = j;
// nolookup()... we already tried removing the "ly" variant
}
return;
}
/*
* this routine deals with -al endings. Some of the endings from the previous
* routine are finished up here.
*/
private void alEndings()
{
int old_k = k;
if (word.length() < 4) return;
if (endsIn('a', 'l'))
{
word.setLength(j + 1);
k = j;
if (lookup()) /* try just removing the -al */
return;
if (doubleC(j))
{ /* allow for a doubled consonant */
word.setLength(j);
k = j - 1;
if (lookup()) return;
word.unsafeWrite(word.charAt(j - 1));
}
word.setLength(j + 1);
word.unsafeWrite('e'); /* try removing the -al and adding -e */
k = j + 1;
if (lookup()) return;
word.setLength(j + 1);
word.append("um"); /* try converting -al to -um */
/* (e.g., optimal - > optimum ) */
k = j + 2;
if (lookup()) return;
word.setLength(j + 1);
word.append("al"); /* restore the ending to the way it was */
k = old_k;
if ((j > 0) && (word.charAt(j - 1) == 'i') && (word.charAt(j) == 'c'))
{
word.setLength(j - 1); /* try removing -ical */
k = j - 2;
if (lookup()) return;
word.setLength(j - 1);
word.unsafeWrite('y');/* try turning -ical to -y (e.g., bibliographical) */
k = j - 1;
if (lookup()) return;
word.setLength(j - 1);
word.append("ic"); /* the default is to convert -ical to -ic */
k = j;
// nolookup() ... converting ical to ic means removing "al" which we
// already tried
// ERROR
lookup();
return;
}
if (word.charAt(j) == 'i')
{ /* sometimes -ial endings should be removed */
word.setLength(j); /* (sometimes it gets turned into -y, but we */
k = j - 1; /* aren't dealing with that case for now) */
if (lookup()) return;
word.append("ial");
k = old_k;
lookup();
}
}
return;
}
/*
* this routine deals with -ive endings. It normalizes some of the -ative
* endings directly, and also maps some -ive endings to -ion.
*/
private void iveEndings()
{
int old_k = k;
if (endsIn('i', 'v', 'e'))
{
word.setLength(j + 1); /* try removing -ive entirely */
k = j;
if (lookup()) return;
word.unsafeWrite('e'); /* try removing -ive and adding -e */
k = j + 1;
if (lookup()) return;
word.setLength(j + 1);
word.append("ive");
if ((j > 0) && (word.charAt(j - 1) == 'a') && (word.charAt(j) == 't'))
{
word.setCharAt(j - 1, 'e'); /* try removing -ative and adding -e */
word.setLength(j); /* (e.g., determinative -> determine) */
k = j - 1;
if (lookup()) return;
word.setLength(j - 1); /* try just removing -ative */
if (lookup()) return;
word.append("ative");
k = old_k;
}
/* try mapping -ive to -ion (e.g., injunctive/injunction) */
word.setCharAt(j + 2, 'o');
word.setCharAt(j + 3, 'n');
if (lookup()) return;
word.setCharAt(j + 2, 'v'); /* restore the original values */
word.setCharAt(j + 3, 'e');
k = old_k;
// nolookup()
}
return;
}
public KStemmer() { }
public String stem(String term)
{
bool changed = stem(term, term.Length);
if (!changed) return term;
return asString();
}
/**
* Returns the result of the stem (assuming the word was changed) as a String.
*/
public String asString()
{
String s = getString();
if (s != null) return s;
return word.toString();
}
// public CharSequence asCharSequence() {
// return result != null ? result : word;
// }
String getString()
{
return result;
}
char[] getChars()
{
return word.getArray();
}
int getLength()
{
return word.length();
}
String result;
private bool matched()
{
/***
* if (!lookups.contains(word.toString())) { throw new
* RuntimeException("didn't look up "+word.toString()+" prev="+prevLookup);
* }
***/
// lookup();
return matchedEntry != null;
}
/**
* Stems the text in the token. Returns true if changed.
*/
public bool stem(String term, int len)
{
len = term.Length; // HACK for API compatibility
result = null;
k = len - 1;
if ((k <= 1) || (k >= MaxWordLen - 1))
{
return false; // don't stem
}
// first check the stemmer dictionaries, and avoid using the
// cache if it's in there.
DictEntry entry = dict_ht.ContainsKey(term) ? dict_ht[term] : null;
if (entry != null)
{
if (entry.root != null)
{
result = entry.root;
return true;
}
return false;
}
/***
* caching off is normally faster if (cache == null) initializeStemHash();
*
* // now check the cache, before we copy chars to "word" if (cache != null)
* { String val = cache.get(term, 0, len); if (val != null) { if (val !=
* SAME) { result = val; return true; } return false; } }
***/
word.reset();
// allocate enough space so that an expansion is never needed
word.reserve(len + 10);
for (int i = 0; i < len; i++)
{
char ch = term[i];
if (!isAlpha(ch)) return false; // don't stem
// don't lowercase... it's a requirement that lowercase filter be
// used before this stemmer.
word.unsafeWrite(ch);
}
matchedEntry = null;
/***
* lookups.clear(); lookups.add(word.toString());
***/
/*
* This while loop will never be executed more than one time; it is here
* only to allow the break statement to be used to escape as soon as a word
* is recognized
*/
while (true)
{
// YCS: extra lookup()s were inserted so we don't need to
// do an extra wordInDict() here.
plural();
if (matched()) break;
pastTense();
if (matched()) break;
aspect();
if (matched()) break;
ityEndings();
if (matched()) break;
nessEndings();
if (matched()) break;
ionEndings();
if (matched()) break;
erAndOrEndings();
if (matched()) break;
lyEndings();
if (matched()) break;
alEndings();
if (matched()) break;
entry = wordInDict();
iveEndings();
if (matched()) break;
izeEndings();
if (matched()) break;
mentEndings();
if (matched()) break;
bleEndings();
if (matched()) break;
ismEndings();
if (matched()) break;
icEndings();
if (matched()) break;
ncyEndings();
if (matched()) break;
nceEndings();
matched();
break;
}
/*
* try for a direct mapping (allows for cases like `Italian'->`Italy' and
* `Italians'->`Italy')
*/
entry = matchedEntry;
if (entry != null)
{
result = entry.root; // may be null, which means that "word" is the stem
}
/***
* caching off is normally faster if (cache != null && cache.size() <
* maxCacheSize) { char[] key = new char[len]; System.arraycopy(term, 0,
* key, 0, len); if (result != null) { cache.put(key, result); } else {
* cache.put(key, word.toString()); } }
***/
/***
* if (entry == null) { if (!word.toString().equals(new String(term,0,len)))
* { System.out.println("CASE:" + word.toString() + "," + new
* String(term,0,len));
*
* } }
***/
// no entry matched means result is "word"
return true;
}
}
internal class OpenStringBuilder
{
protected char[] buf;
protected int len;
public OpenStringBuilder()
: this(32)
{
}
public OpenStringBuilder(int size)
{
buf = new char[size];
}
public OpenStringBuilder(char[] arr, int len)
{
set(arr, len);
}
public void setLength(int len) { this.len = len; }
public void set(char[] arr, int end)
{
this.buf = arr;
this.len = end;
}
public char[] getArray() { return buf; }
public int size() { return len; }
public int length() { return len; }
public int capacity() { return buf.Length; }
public OpenStringBuilder append(String csq)
{
return append(csq, 0, csq.Length);
}
public OpenStringBuilder append(String csq, int start, int end)
{
reserve(end - start);
for (int i = start; i < end; i++)
{
unsafeWrite(csq[i]);
}
return this;
}
public OpenStringBuilder append(char c)
{
write(c);
return this;
}
public char charAt(int index)
{
return buf[index];
}
public void setCharAt(int index, char ch)
{
buf[index] = ch;
}
public void unsafeWrite(char b)
{
buf[len++] = b;
}
public void unsafeWrite(int b) { unsafeWrite((char)b); }
public void unsafeWrite(char[] b, int off, int len)
{
Array.Copy(b, off, buf, this.len, len);
this.len += len;
}
protected void resize(int len)
{
char[] newbuf = new char[Math.Max(buf.Length << 1, len)];
Array.Copy(buf, newbuf, size());
buf = newbuf;
}
public void reserve(int num)
{
if (len + num > buf.Length) resize(len + num);
}
public void write(char b)
{
if (len >= buf.Length)
{
resize(len + 1);
}
unsafeWrite(b);
}
public void write(int b) { write((char)b); }
public void write(char[] b)
{
write(b, 0, b.Length);
}
public void write(char[] b, int off, int len)
{
reserve(len);
unsafeWrite(b, off, len);
}
public void write(OpenStringBuilder arr)
{
write(arr.buf, 0, len);
}
// public void write(String s) {
// reserve(s.Length);
// s.GetChars(0,s.Length,buf, len);
// len +=s.length();
// }
public void flush()
{
}
public void reset()
{
len = 0;
}
public char[] toCharArray()
{
char[] newbuf = new char[size()];
Array.Copy(buf, newbuf, size());
return newbuf;
}
public String toString()
{
return new String(buf, 0, size());
}
}
}