Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 164 additions & 12 deletions dev/README-hunspell-dictionaries.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package opennlp.tools.stemmer.hunspell;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/** Affix tables with entry counts and source locations for validation errors. */
final class HunspellAffixTable {

/** Prevents construction. */
private HunspellAffixTable() { }

/**
* An entry including the directive name.
*
* @param fields The tokenized entry.
* @param line The one-based source line.
*/
record Entry(String[] fields, int line) { }

/**
* Checks a table header, entry count, and field counts.
*
* @param lines The tokenized affix content.
* @param tag The table directive.
* @param minimumFields The minimum fields in an entry, including the directive.
* @param maximumFields The maximum fields in an entry, including the directive.
* @return The entries, or null if the file has no declaration for this table.
* @throws IOException If the header, entries, or count is invalid.
*/
static List<Entry> read(String[][] lines, String tag, int minimumFields, int maximumFields)
throws IOException {
final List<Entry> entries = new ArrayList<>();
int count = -1;
for (int i = 0; i < lines.length; i++) {
final String[] fields = lines[i];
if (fields.length == 0 || !tag.equals(fields[0])) {
continue;
}
if (count < 0) {
try {
if (fields.length != 2) {
throw new NumberFormatException();
}
count = Integer.parseInt(fields[1]);
if (count < 0) {
throw new NumberFormatException();
}
} catch (NumberFormatException e) {
throw new IOException("invalid " + tag + " count at line " + (i + 1), e);
}
} else {
if (fields.length < minimumFields || fields.length > maximumFields || entries.size() == count) {
throw new IOException("invalid " + tag + " entry at line " + (i + 1));
}
entries.add(new Entry(fields, i + 1));
}
}
if (count < 0) {
return null;
}
if (entries.size() != count) {
throw new IOException(tag + " header specifies " + count + " entries but found " + entries.size());
}
return entries;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package opennlp.tools.stemmer.hunspell;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/** A compound flag sequence with optional and repeated elements. */
final class HunspellCompoundRule {

/** Interprets a flag according to the affix file's FLAG setting. */
@FunctionalInterface
interface FlagReader {
/**
* Interprets one flag.
*
* @param text The encoded flag.
* @param line The source line.
* @return The flag value.
* @throws IOException If the flag is malformed.
*/
int read(String text, int line) throws IOException;
}

/** Maximum elements in one compound pattern. */
private static final int MAX_ELEMENTS = 4096;

private static final String INVALID_PATTERN = "invalid COMPOUNDRULE at line ";

private final int[] flags;
private final char[] repetition;

/**
* Initializes a parsed compound pattern.
*
* @param flags The required flag at each position.
* @param repetition The position's repetition operator or a space.
*/
private HunspellCompoundRule(int[] flags, char[] repetition) {
this.flags = flags;
this.repetition = repetition;
}

/**
* Parses a compound rule without using a regular-expression engine.
*
* @param pattern The rule text.
* @param line The source line.
* @param reader The flag decoder.
* @return The parsed rule.
* @throws IOException If the pattern or a flag is malformed.
*/
static HunspellCompoundRule parse(String pattern, int line, FlagReader reader) throws IOException {
final List<Integer> flags = new ArrayList<>();
final StringBuilder repetitions = new StringBuilder();
for (int at = 0; at < pattern.length();) {
final String flag;
if (pattern.charAt(at) == '(') {
final int end = pattern.indexOf(')', at + 1);
if (end <= at + 1) {
throw new IOException(INVALID_PATTERN + line);
}
flag = pattern.substring(at + 1, end);
at = end + 1;
} else {
final int point = pattern.codePointAt(at);
if (point == '*' || point == '?' || point == ')') {
throw new IOException(INVALID_PATTERN + line);
}
final int end = at + Character.charCount(point);
flag = pattern.substring(at, end);
at = end;
}
flags.add(reader.read(flag, line));
if (flags.size() > MAX_ELEMENTS) {
throw new IOException("COMPOUNDRULE exceeds " + MAX_ELEMENTS + " elements at line " + line);
}
if (at < pattern.length() && (pattern.charAt(at) == '*' || pattern.charAt(at) == '?')) {
repetitions.append(pattern.charAt(at++));
} else {
repetitions.append(' ');
}
}
if (flags.isEmpty()) {
throw new IOException("empty COMPOUNDRULE at line " + line);
}
final int[] values = new int[flags.size()];
final char[] repetition = new char[flags.size()];
for (int i = 0; i < values.length; i++) {
values[i] = flags.get(i);
repetition[i] = repetitions.charAt(i);
}
return new HunspellCompoundRule(values, repetition);
}

/**
* Tests a sequence of selected homonyms. Each part consumes one flag position.
*
* @param parts The flags of one selected entry per compound part.
* @param complete Whether the sequence must complete the rule.
* @return Whether the sequence is permitted by this rule.
*/
boolean matches(List<int[]> parts, boolean complete) {
boolean[] states = new boolean[flags.length + 1];
states[0] = true;
skipOptional(states);
for (int[] part : parts) {
final boolean[] next = new boolean[states.length];
for (int i = 0; i < flags.length; i++) {
if (states[i] && HunspellDictionary.contains(part, flags[i])) {
next[repetition[i] == '*' ? i : i + 1] = true;
}
}
skipOptional(next);
states = next;
}
if (complete) {
return states[flags.length];
}
for (boolean state : states) {
if (state) {
return true;
}
}
return false;
}

/**
* Advances states through optional pattern elements.
*
* @param states The active pattern positions.
*/
private void skipOptional(boolean[] states) {
for (int i = 0; i < flags.length; i++) {
if (states[i] && repetition[i] != ' ') {
states[i + 1] = true;
}
}
}

}
Loading
Loading