///|
pub(all) struct SpellResult {
accepted : Bool
stem : String?
prefix : String?
suffix : String?
}
///|
fn flag_tokens(flag_mode : String, flags : String) -> Array[String] {
let result : Array[String] = []
if flag_mode == "num" {
for token in flags.split(",") {
let value = token.to_owned()
if !value.is_empty() {
result.push(value)
}
}
} else if flag_mode == "long" {
let chars = flags.to_array()
let mut i = 0
while i + 1 < chars.length() {
result.push(chars[i].to_string() + chars[i + 1].to_string())
i += 2
}
} else {
for c in flags {
result.push(c.to_string())
}
}
result
}
///|
fn expand_flag_vector(config : AffixConfig, flags : String) -> String {
match parse_decimal_count(flags) {
Some(index) =>
if index > 0 && index <= config.flag_aliases.length() {
config.flag_aliases[index - 1]
} else {
flags
}
None => flags
}
}
///|
fn flags_contain(config : AffixConfig, flags : String, flag : String) -> Bool {
if flag.is_empty() {
false
} else {
flag_tokens(config.flag_mode, expand_flag_vector(config, flags)).contains(
flag,
)
}
}
///|
fn entry_is_forbidden(config : AffixConfig, entry : DicEntry) -> Bool {
match config.forbidden_word {
Some(flag) => flags_contain(config, entry.flags, flag)
None => false
}
}
///|
fn entry_needs_affix(config : AffixConfig, entry : DicEntry) -> Bool {
match config.need_affix {
Some(flag) => flags_contain(config, entry.flags, flag)
None => false
}
}
///|
fn entry_keeps_case(config : AffixConfig, entry : DicEntry) -> Bool {
match config.keep_case {
Some(flag) => flags_contain(config, entry.flags, flag)
None => false
}
}
///|
fn entry_is_only_in_compound(config : AffixConfig, entry : DicEntry) -> Bool {
match config.only_in_compound {
Some(flag) => flags_contain(config, entry.flags, flag)
None => false
}
}
///|
fn entry_can_be_exact(config : AffixConfig, entry : DicEntry) -> Bool {
!entry_is_forbidden(config, entry) &&
!entry_needs_affix(config, entry) &&
!entry_is_only_in_compound(config, entry)
}
///|
fn entry_can_be_affixed(config : AffixConfig, entry : DicEntry) -> Bool {
!entry_is_forbidden(config, entry) &&
!entry_is_only_in_compound(config, entry)
}
///|
fn rule_has_circumfix(config : AffixConfig, rule : AffixRule) -> Bool {
match config.circumfix {
Some(flag) => flags_contain(config, rule.continuation, flag)
None => false
}
}
///|
fn rule_needs_affix(config : AffixConfig, rule : AffixRule) -> Bool {
match config.need_affix {
Some(flag) => flags_contain(config, rule.continuation, flag)
None => false
}
}
///|
fn rule_is_only_in_compound(config : AffixConfig, rule : AffixRule) -> Bool {
match config.only_in_compound {
Some(flag) => flags_contain(config, rule.continuation, flag)
None => false
}
}
///|
fn entry_can_be_compound(config : AffixConfig, entry : DicEntry) -> Bool {
!entry_is_forbidden(config, entry)
}
///|
fn condition_width(pattern : String) -> Int {
let chars = pattern.to_array()
let mut width = 0
let mut i = 0
while i < chars.length() {
if chars[i] == '[' {
let mut j = i + 1
while j < chars.length() && chars[j] != ']' {
j += 1
}
if j >= chars.length() {
return -1
}
width += 1
i = j + 1
} else {
width += 1
i += 1
}
}
width
}
///|
fn condition_matches_at(
pattern : String,
text : Array[Char],
start : Int,
) -> Bool {
let pattern_chars = pattern.to_array()
let mut pattern_index = 0
let mut text_index = start
while pattern_index < pattern_chars.length() {
if pattern_chars[pattern_index] == '[' {
let mut end = pattern_index + 1
while end < pattern_chars.length() && pattern_chars[end] != ']' {
end += 1
}
if end >= pattern_chars.length() || text_index >= text.length() {
return false
}
let negate = pattern_index + 1 < end &&
pattern_chars[pattern_index + 1] == '^'
let char_start = if negate {
pattern_index + 2
} else {
pattern_index + 1
}
let mut found = false
for i in char_start..= text.length() {
return false
}
pattern_index += 1
text_index += 1
} else {
if text_index >= text.length() ||
pattern_chars[pattern_index] != text[text_index] {
return false
}
pattern_index += 1
text_index += 1
}
}
true
}
///|
fn condition_matches(rule : AffixRule, stem : String) -> Bool {
let condition = rule.condition
if condition.is_empty() || condition == "." {
return true
}
let width = condition_width(condition)
let chars = stem.to_array()
if width < 0 || width > chars.length() {
return false
}
let start = match rule.kind {
Prefix => 0
Suffix => chars.length() - width
}
condition_matches_at(condition, chars, start)
}
///|
fn ends_with(text : StringView, suffix : String) -> Bool {
let text_len = text.length()
let suffix_len = suffix.length()
if suffix_len > text_len {
return false
}
text.view(start_offset=text_len - suffix_len).to_owned() == suffix
}
///|
pub fn Dictionary::find_entries(
self : Dictionary,
word : String,
) -> Array[DicEntry] {
match self.index.get(word) {
Some(entries) => entries.copy()
None => []
}
}
///|
fn exact_result(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> SpellResult? {
for entry in dictionary.find_entries(word) {
if entry_can_be_exact(config, entry) {
return Some({
accepted: true,
stem: Some(word),
prefix: None,
suffix: None,
})
}
}
None
}
///|
fn prefix_result(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
rule : AffixRule,
) -> SpellResult? {
let add = apply_ignore(config, rule.add)
let strip = apply_ignore(config, rule.strip)
if !add.is_empty() && word.find(add) != Some(0) {
return None
}
let remainder = word.view(start_offset=add.length())
let stem = strip + remainder.to_owned()
if !condition_matches(rule, stem) {
return None
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, rule.flag) &&
entry_can_be_affixed(config, entry) &&
!rule_is_only_in_compound(config, rule) &&
!rule_needs_affix(config, rule) &&
!rule_has_circumfix(config, rule) {
return Some({
accepted: true,
stem: Some(stem),
prefix: Some(add),
suffix: None,
})
}
}
None
}
///|
fn suffix_result(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
rule : AffixRule,
) -> SpellResult? {
if !rule.add.is_empty() && !ends_with(word, rule.add) {
return None
}
let stem = word.view(end_offset=word.length() - rule.add.length()).to_owned() +
rule.strip
if !condition_matches(rule, stem) {
return None
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, rule.flag) &&
entry_can_be_affixed(config, entry) &&
!rule_is_only_in_compound(config, rule) &&
!rule_needs_affix(config, rule) &&
!rule_has_circumfix(config, rule) {
return Some({
accepted: true,
stem: Some(stem),
prefix: None,
suffix: Some(rule.add),
})
}
}
None
}
///|
fn twofold_suffix_result(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
outer : AffixRule,
inner : AffixRule,
) -> SpellResult? {
if outer.add.is_empty() || inner.add.is_empty() {
return None
}
if !ends_with(word, outer.add) {
return None
}
let intermediate = word
.view(end_offset=word.length() - outer.add.length())
.to_owned()
if !condition_matches(outer, intermediate) {
return None
}
if !flags_contain(config, inner.continuation, outer.flag) {
return None
}
if !ends_with(intermediate.view(), inner.add) {
return None
}
let stem = intermediate
.view(end_offset=intermediate.length() - inner.add.length())
.to_owned() +
inner.strip
if !condition_matches(inner, stem) {
return None
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, inner.flag) &&
entry_can_be_affixed(config, entry) &&
!rule_is_only_in_compound(config, inner) &&
!rule_is_only_in_compound(config, outer) &&
!rule_needs_affix(config, outer) {
return Some({
accepted: true,
stem: Some(stem),
prefix: None,
suffix: Some(outer.add),
})
}
}
None
}
///|
fn twofold_prefix_result(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
outer : AffixRule,
inner : AffixRule,
) -> SpellResult? {
if outer.add.is_empty() ||
inner.add.is_empty() ||
word.find(outer.add) != Some(0) {
return None
}
let intermediate = word.view(start_offset=outer.add.length()).to_owned()
if !condition_matches(outer, intermediate) ||
!flags_contain(config, inner.continuation, outer.flag) ||
intermediate.find(inner.add) != Some(0) {
return None
}
let stem = inner.strip +
intermediate.view(start_offset=inner.add.length()).to_owned()
if !condition_matches(inner, stem) {
return None
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, inner.flag) &&
entry_can_be_affixed(config, entry) {
return Some({
accepted: true,
stem: Some(stem),
prefix: Some(outer.add),
suffix: None,
})
}
}
None
}
///|
fn cross_result(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
prefix : AffixRule,
suffix : AffixRule,
) -> SpellResult? {
if prefix.add.is_empty() || suffix.add.is_empty() {
return None
}
if !prefix.cross_product || !suffix.cross_product {
return None
}
if word.find(prefix.add) != Some(0) || !ends_with(word, suffix.add) {
return None
}
if prefix.add.length() + suffix.add.length() >= word.length() {
return None
}
let inner = word.view(
start_offset=prefix.add.length(),
end_offset=word.length() - suffix.add.length(),
)
let stem = prefix.strip + inner.to_owned() + suffix.strip
if !condition_matches(prefix, stem) || !condition_matches(suffix, stem) {
return None
}
for entry in dictionary.find_entries(stem) {
let prefix_flag_ok = flags_contain(config, entry.flags, prefix.flag) ||
flags_contain(config, suffix.continuation, prefix.flag)
let suffix_flag_ok = flags_contain(config, entry.flags, suffix.flag) ||
flags_contain(config, prefix.continuation, suffix.flag)
if prefix_flag_ok &&
suffix_flag_ok &&
entry_can_be_affixed(config, entry) &&
!(rule_needs_affix(config, prefix) && rule_needs_affix(config, suffix)) &&
(
!rule_has_circumfix(config, prefix) ||
rule_has_circumfix(config, suffix)
) &&
(
!rule_has_circumfix(config, suffix) ||
rule_has_circumfix(config, prefix)
) {
return Some({
accepted: true,
stem: Some(stem),
prefix: Some(prefix.add),
suffix: Some(suffix.add),
})
}
}
None
}
///|
priv struct CompoundPartMatch {
flags : String
prefix : Bool
suffix : Bool
prefix_permit : Bool
suffix_permit : Bool
forbid : Bool
}
///|
fn affix_compound_permit(config : AffixConfig, rule : AffixRule) -> Bool {
match config.compound_permit {
Some(flag) => flags_contain(config, rule.continuation, flag)
None => false
}
}
///|
fn affix_compound_forbid(config : AffixConfig, rule : AffixRule) -> Bool {
match config.compound_forbid {
Some(flag) => flags_contain(config, rule.continuation, flag)
None => false
}
}
///|
fn entry_has_compound_forbid(config : AffixConfig, entry : DicEntry) -> Bool {
match config.compound_forbid {
Some(flag) => flags_contain(config, entry.flags, flag)
None => false
}
}
///|
fn exact_part_matches(
dictionary : Dictionary,
config : AffixConfig,
part : String,
) -> Array[CompoundPartMatch] {
let result : Array[CompoundPartMatch] = []
for entry in dictionary.find_entries(part) {
if entry_can_be_compound(config, entry) && !entry_needs_affix(config, entry) {
result.push({
flags: entry.flags,
prefix: false,
suffix: false,
prefix_permit: false,
suffix_permit: false,
forbid: entry_has_compound_forbid(config, entry),
})
}
}
result
}
///|
fn prefix_part_matches(
dictionary : Dictionary,
config : AffixConfig,
part : StringView,
rule : AffixRule,
) -> Array[CompoundPartMatch] {
let result : Array[CompoundPartMatch] = []
if rule.add.is_empty() || part.find(rule.add) != Some(0) {
return result
}
let stem = rule.strip + part.view(start_offset=rule.add.length()).to_owned()
if !condition_matches(rule, stem) {
return result
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, rule.flag) &&
entry_can_be_compound(config, entry) {
result.push({
flags: entry.flags + rule.continuation,
prefix: true,
suffix: false,
prefix_permit: affix_compound_permit(config, rule),
suffix_permit: false,
forbid: affix_compound_forbid(config, rule),
})
}
}
result
}
///|
fn suffix_part_matches(
dictionary : Dictionary,
config : AffixConfig,
part : StringView,
rule : AffixRule,
) -> Array[CompoundPartMatch] {
let result : Array[CompoundPartMatch] = []
if rule.add.is_empty() || !ends_with(part, rule.add) {
return result
}
let stem = part.view(end_offset=part.length() - rule.add.length()).to_owned() +
rule.strip
if !condition_matches(rule, stem) {
return result
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, rule.flag) &&
entry_can_be_compound(config, entry) {
result.push({
flags: entry.flags + rule.continuation,
prefix: false,
suffix: true,
prefix_permit: false,
suffix_permit: affix_compound_permit(config, rule),
forbid: affix_compound_forbid(config, rule),
})
}
}
result
}
///|
fn cross_part_matches(
dictionary : Dictionary,
config : AffixConfig,
part : StringView,
prefix : AffixRule,
suffix : AffixRule,
) -> Array[CompoundPartMatch] {
let result : Array[CompoundPartMatch] = []
if prefix.add.is_empty() ||
suffix.add.is_empty() ||
!prefix.cross_product ||
!suffix.cross_product ||
part.find(prefix.add) != Some(0) ||
!ends_with(part, suffix.add) ||
prefix.add.length() + suffix.add.length() >= part.length() {
return result
}
let inner = part.view(
start_offset=prefix.add.length(),
end_offset=part.length() - suffix.add.length(),
)
let stem = prefix.strip + inner.to_owned() + suffix.strip
if !condition_matches(prefix, stem) || !condition_matches(suffix, stem) {
return result
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, prefix.flag) &&
flags_contain(config, entry.flags, suffix.flag) &&
entry_can_be_compound(config, entry) {
result.push({
flags: entry.flags + prefix.continuation + suffix.continuation,
prefix: true,
suffix: true,
prefix_permit: affix_compound_permit(config, prefix),
suffix_permit: affix_compound_permit(config, suffix),
forbid: affix_compound_forbid(config, prefix) ||
affix_compound_forbid(config, suffix),
})
}
}
result
}
///|
fn twofold_suffix_part_matches(
dictionary : Dictionary,
config : AffixConfig,
part : StringView,
outer : AffixRule,
inner : AffixRule,
) -> Array[CompoundPartMatch] {
let result : Array[CompoundPartMatch] = []
if outer.add.is_empty() || inner.add.is_empty() || !ends_with(part, outer.add) {
return result
}
let intermediate = part
.view(end_offset=part.length() - outer.add.length())
.to_owned()
if !condition_matches(outer, intermediate) ||
!flags_contain(config, inner.continuation, outer.flag) ||
!ends_with(intermediate.view(), inner.add) {
return result
}
let stem = intermediate
.view(end_offset=intermediate.length() - inner.add.length())
.to_owned() +
inner.strip
if !condition_matches(inner, stem) {
return result
}
for entry in dictionary.find_entries(stem) {
if flags_contain(config, entry.flags, inner.flag) &&
entry_can_be_compound(config, entry) {
result.push({
flags: entry.flags + inner.continuation + outer.continuation,
prefix: false,
suffix: true,
prefix_permit: false,
suffix_permit: affix_compound_permit(config, outer),
forbid: affix_compound_forbid(config, outer),
})
}
}
result
}
///|
fn compound_part_matches(
dictionary : Dictionary,
config : AffixConfig,
part : StringView,
) -> Array[CompoundPartMatch] {
let result = exact_part_matches(dictionary, config, part.to_owned())
for rule in config.prefixes {
for item in prefix_part_matches(dictionary, config, part, rule) {
result.push(item)
}
}
for rule in config.suffixes {
for item in suffix_part_matches(dictionary, config, part, rule) {
result.push(item)
}
}
for outer in config.suffixes {
for inner in config.suffixes {
for
item in twofold_suffix_part_matches(
dictionary, config, part, outer, inner,
) {
result.push(item)
}
}
}
for prefix in config.prefixes {
for suffix in config.suffixes {
for item in cross_part_matches(dictionary, config, part, prefix, suffix) {
result.push(item)
}
}
}
result
}
///|
fn compound_boundary_has_triple(word : StringView, position : Int) -> Bool {
if position <= 0 || position >= word.length() {
return false
}
let left = word.unsafe_get(position - 1)
let right = word.unsafe_get(position)
if left != right {
return false
}
(position > 1 && word.unsafe_get(position - 2) == left) ||
(position + 1 < word.length() && word.unsafe_get(position + 1) == right)
}
///|
fn compound_boundary_has_bad_case(word : StringView, position : Int) -> Bool {
if position <= 0 || position >= word.length() {
return false
}
let left_code = word.unsafe_get(position - 1)
let right_code = word.unsafe_get(position)
if left_code.to_int() == 45 || right_code.to_int() == 45 {
return false
}
match (left_code.to_char(), right_code.to_char()) {
(Some(left), Some(right)) =>
is_uppercase_char(left) || is_uppercase_char(right)
_ => false
}
}
///|
fn compound_role_matches(
config : AffixConfig,
flags : String,
is_first : Bool,
is_last : Bool,
) -> Bool {
match config.compound_flag {
Some(flag) => if flags_contain(config, flags, flag) { return true }
None => ()
}
if is_first {
match config.compound_begin {
Some(flag) => flags_contain(config, flags, flag)
None => false
}
} else if is_last {
match config.compound_end {
Some(flag) => flags_contain(config, flags, flag)
None => false
}
} else {
match config.compound_middle {
Some(flag) => flags_contain(config, flags, flag)
None => false
}
}
}
///|
///|
///|
fn part_forbidden_by_zero_pattern(
config : AffixConfig,
part : CompoundPartMatch,
) -> Bool {
for pattern in config.compound_patterns {
if pattern.end_zero {
match pattern.end_flag {
Some(flag) => if flags_contain(config, part.flags, flag) { return true }
None => ()
}
}
}
false
}
///|
fn compound_pattern_matches(
config : AffixConfig,
pattern : CompoundPattern,
left_part : String,
left : CompoundPartMatch,
right_part : StringView,
right : CompoundPartMatch,
) -> Bool {
if pattern.end_zero && (left.prefix || left.suffix) {
return false
}
if !pattern.end_zero &&
!pattern.end_chars.is_empty() &&
!ends_with(left_part.view(), pattern.end_chars) {
return false
}
match pattern.end_flag {
Some(flag) => if !flags_contain(config, left.flags, flag) { return false }
None => ()
}
if !pattern.begin_chars.is_empty() &&
right_part.find(pattern.begin_chars) != Some(0) {
return false
}
match pattern.begin_flag {
Some(flag) => if !flags_contain(config, right.flags, flag) { return false }
None => ()
}
true
}
///|
fn compound_boundary_forbidden_by_pattern(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
left_part : String,
left : CompoundPartMatch,
boundary : Int,
) -> Bool {
let mut end = boundary + 1
while end <= word.length() {
let right_view = word.view(start_offset=boundary, end_offset=end)
if dictionary.find_entries(left_part + " " + right_view.to_owned()).length() >
0 {
return true
}
for right in compound_part_matches(dictionary, config, right_view) {
for pattern in config.compound_patterns {
if compound_pattern_matches(
config, pattern, left_part, left, right_view, right,
) {
return true
}
}
}
end += 1
}
false
}
///|
fn compound_part_length_ok(part : StringView, config : AffixConfig) -> Bool {
part.length() >= config.compound_min ||
(part.length() == 1 && part.unsafe_get(0).to_int() == 45)
}
///|
fn compound_boundary_has_triple_chars(
chars : Array[Char],
position : Int,
) -> Bool {
if position <= 0 || position >= chars.length() {
return false
}
let left = chars[position - 1]
let right = chars[position]
if left != right {
return false
}
(position > 1 && chars[position - 2] == left) ||
(position + 1 < chars.length() && chars[position + 1] == right)
}
///|
fn compound_boundary_has_bad_case_chars(
chars : Array[Char],
position : Int,
) -> Bool {
if position <= 0 || position >= chars.length() {
return false
}
let left = chars[position - 1]
let right = chars[position]
if left.to_int() == 45 || right.to_int() == 45 {
return false
}
is_uppercase_char(left) || is_uppercase_char(right)
}
///|
fn compound_part_length_ok_chars(length : Int, config : AffixConfig) -> Bool {
length >= config.compound_min
}
///|
fn compound_search_chars(
dictionary : Dictionary,
config : AffixConfig,
chars : Array[Char],
start : Int,
part_count : Int,
previous_part : String,
max_parts : Int,
) -> Bool {
if start >= chars.length() || part_count >= max_parts {
return false
}
let mut end = start + 1
while end <= chars.length() {
let is_first = start == 0
let is_last = end == chars.length()
if !compound_part_length_ok_chars(end - start, config) {
end += 1
continue
}
let part = String::from_array(chars.exact_view(start~, end~))
let part_view = part.view()
let part_matches = compound_part_matches(dictionary, config, part_view)
let mut part_forbidden = false
if !is_last {
for candidate in part_matches {
if candidate.forbid {
part_forbidden = true
break
}
}
}
if part_forbidden {
end += 1
continue
}
for item in part_matches {
if !compound_role_matches(config, item.flags, is_first, is_last) {
continue
}
if !is_first && item.prefix && !item.prefix_permit {
continue
}
if !is_last && item.suffix && !item.suffix_permit {
continue
}
if is_last && item.suffix && part_forbidden_by_zero_pattern(config, item) {
continue
}
if !is_last {
let mut right_end = end + 1
let mut forbidden = false
while right_end <= chars.length() {
let right = String::from_array(
chars.exact_view(start=end, end=right_end),
)
if dictionary.find_entries(part + " " + right).length() > 0 {
forbidden = true
break
}
for
right_match in compound_part_matches(
dictionary,
config,
right.view(),
) {
for pattern in config.compound_patterns {
if compound_pattern_matches(
config,
pattern,
part,
item,
right.view(),
right_match,
) {
forbidden = true
break
}
}
if forbidden {
break
}
}
if forbidden {
break
}
right_end += 1
}
if forbidden {
continue
}
}
if config.check_compound_dup && is_last && previous_part == part {
continue
}
if config.check_compound_triple &&
compound_boundary_has_triple_chars(chars, end) {
continue
}
if config.check_compound_case &&
compound_boundary_has_bad_case_chars(chars, end) {
continue
}
if is_last {
if part_count >= 1 {
return true
}
} else if compound_search_chars(
dictionary,
config,
chars,
end,
part_count + 1,
part,
max_parts,
) {
return true
}
}
end += 1
}
false
}
///|
fn compound_search(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
start : Int,
part_count : Int,
previous_part : String,
max_parts : Int,
) -> Bool {
if start >= word.length() || part_count >= max_parts {
return false
}
let mut end = start + 1
while end <= word.length() {
let is_first = start == 0
let is_last = end == word.length()
let part_view = word.view(start_offset=start, end_offset=end)
if !compound_part_length_ok(part_view, config) {
end += 1
continue
}
let part = part_view.to_owned()
let part_matches = compound_part_matches(dictionary, config, part_view)
let mut part_forbidden = false
if !is_last {
for candidate in part_matches {
if candidate.forbid {
part_forbidden = true
break
}
}
}
if part_forbidden {
end += 1
continue
}
for item in part_matches {
let role_ok = compound_role_matches(config, item.flags, is_first, is_last)
if !role_ok {
continue
}
if !is_first && item.prefix && !item.prefix_permit {
continue
}
if !is_last && item.suffix && !item.suffix_permit {
continue
}
if is_last && item.suffix && part_forbidden_by_zero_pattern(config, item) {
continue
}
if !is_last &&
compound_boundary_forbidden_by_pattern(
dictionary, config, word, part, item, end,
) {
continue
}
if config.check_compound_dup && is_last && previous_part == part {
continue
}
if config.check_compound_triple && compound_boundary_has_triple(word, end) {
continue
}
if config.check_compound_case && compound_boundary_has_bad_case(word, end) {
continue
}
if is_last {
if part_count >= 1 {
return true
}
} else if compound_search(
dictionary,
config,
word,
end,
part_count + 1,
part,
max_parts,
) {
return true
}
}
end += 1
}
false
}
///|
priv struct CompoundRuleToken {
flag : String
quantifier : String
}
///|
fn parse_compound_rule(
pattern : String,
flag_mode : String,
) -> Array[CompoundRuleToken] {
let chars = pattern.to_array()
let result : Array[CompoundRuleToken] = []
let mut index = 0
while index < chars.length() {
let flag = if chars[index] == '(' {
let mut close = index + 1
while close < chars.length() && chars[close] != ')' {
close += 1
}
if close >= chars.length() {
return []
}
let value = pattern
.view(start_offset=index + 1, end_offset=close)
.to_owned()
index = close + 1
value
} else if flag_mode == "long" && index + 1 < chars.length() {
let value = chars[index].to_string() + chars[index + 1].to_string()
index += 2
value
} else {
let value = chars[index].to_string()
index += 1
value
}
let quantifier = if index < chars.length() &&
(chars[index] == '*' || chars[index] == '?') {
let value = chars[index].to_string()
index += 1
value
} else {
""
}
result.push({ flag, quantifier, })
}
result
}
///|
fn compound_rule_match_at(
tokens : Array[CompoundRuleToken],
flags : Array[String],
token_index : Int,
part_index : Int,
config : AffixConfig,
) -> Bool {
if token_index >= tokens.length() {
return part_index >= flags.length()
}
let token = tokens[token_index]
let can_consume = part_index < flags.length() &&
flags_contain(config, flags[part_index], token.flag)
match token.quantifier {
"" =>
can_consume &&
compound_rule_match_at(
tokens,
flags,
token_index + 1,
part_index + 1,
config,
)
"?" =>
compound_rule_match_at(tokens, flags, token_index + 1, part_index, config) ||
(
can_consume &&
compound_rule_match_at(
tokens,
flags,
token_index + 1,
part_index + 1,
config,
)
)
"*" =>
compound_rule_match_at(tokens, flags, token_index + 1, part_index, config) ||
(
can_consume &&
compound_rule_match_at(
tokens,
flags,
token_index,
part_index + 1,
config,
)
)
_ => false
}
}
///|
fn compound_rule_matches(
pattern : String,
flags : Array[String],
config : AffixConfig,
) -> Bool {
if flags.length() == 0 {
return false
}
if flags.length() == 1 {
match config.only_in_compound {
Some(flag) => if flags_contain(config, flags[0], flag) { return false }
None => ()
}
}
let tokens = parse_compound_rule(pattern, config.flag_mode)
if tokens.length() == 0 {
return false
}
compound_rule_match_at(tokens, flags, 0, 0, config)
}
///|
fn compound_rule_search(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
pattern : String,
start : Int,
flags : Array[String],
max_parts : Int,
) -> Bool {
if start >= word.length() || flags.length() >= max_parts {
return false
}
let mut end = start + 1
while end <= word.length() {
let is_last = end == word.length()
let part_view = word.view(start_offset=start, end_offset=end)
if compound_part_length_ok(part_view, config) {
for item in compound_part_matches(dictionary, config, part_view) {
if !is_last && (item.prefix || item.suffix) {
continue
}
let next_flags = flags.copy()
next_flags.push(item.flags)
if is_last {
if compound_rule_matches(pattern, next_flags, config) {
return true
}
} else if compound_rule_search(
dictionary, config, word, pattern, end, next_flags, max_parts,
) {
return true
}
}
}
end += 1
}
false
}
///|
fn compound_rule_result(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
if config.compound_rules.length() == 0 {
return false
}
for pattern in config.compound_rules {
if compound_rule_search(dictionary, config, word.view(), pattern, 0, [], 32) {
return true
}
}
false
}
///|
fn compound_rep_correctable(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
for rule in config.replacements {
match replacement_candidate(word, rule.from, rule.to) {
Some(candidate) => {
if dictionary.find_entries(candidate).length() > 0 {
return true
}
for entry in dictionary.entries {
if entry.word.length() >= config.compound_min &&
candidate.find(entry.word) is Some(_) &&
word.find(entry.word) is None {
return true
}
}
}
None => ()
}
}
false
}
///|
fn find_substring_from(text : String, needle : String, start : Int) -> Int? {
if start > text.length() || needle.is_empty() {
None
} else {
match text.view(start_offset=start).find(needle) {
Some(index) => Some(start + index)
None => None
}
}
}
///|
fn replace_range(
text : String,
start : Int,
length : Int,
replacement : String,
) -> String {
text.view(end_offset=start).to_owned() +
replacement +
text.view(start_offset=start + length).to_owned()
}
///|
fn two_part_compound_allows(
dictionary : Dictionary,
config : AffixConfig,
left : String,
right : String,
) -> Bool {
if left.char_length() < config.compound_min ||
right.char_length() < config.compound_min {
return false
}
for left_match in compound_part_matches(dictionary, config, left.view()) {
if !compound_role_matches(config, left_match.flags, true, false) {
continue
}
if left_match.suffix && !left_match.suffix_permit {
continue
}
for right_match in compound_part_matches(dictionary, config, right.view()) {
if !compound_role_matches(config, right_match.flags, false, true) {
continue
}
if right_match.prefix && !right_match.prefix_permit {
continue
}
return true
}
}
false
}
///|
fn compound_pattern_replacement_allows(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
for pattern in config.compound_patterns {
match pattern.replacement {
Some(replacement) => {
if replacement.is_empty() {
continue
}
let mut offset = 0
while offset <= word.length() {
match find_substring_from(word, replacement, offset) {
Some(position) => {
let boundary = position + pattern.end_chars.length()
let reconstructed = replace_range(
word,
position,
replacement.length(),
pattern.end_chars + pattern.begin_chars,
)
if boundary > 0 && boundary < reconstructed.length() {
let left = reconstructed.view(end_offset=boundary).to_owned()
let right = reconstructed.view(start_offset=boundary).to_owned()
for
left_match in compound_part_matches(
dictionary,
config,
left.view(),
) {
if !compound_role_matches(
config,
left_match.flags,
true,
false,
) {
continue
}
for
right_match in compound_part_matches(
dictionary,
config,
right.view(),
) {
if !compound_role_matches(
config,
right_match.flags,
false,
true,
) {
continue
}
if compound_pattern_matches(
config,
pattern,
left,
left_match,
right.view(),
right_match,
) {
return true
}
}
}
}
offset = position + 1
}
None => break
}
}
}
None => ()
}
}
false
}
///|
fn simplified_triple_allows(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
let chars = word.to_array()
let mut split = 1
while split < chars.length() {
let left = String::from_array(chars.exact_view(end=split))
let right = String::from_array(chars.exact_view(start=split))
if !left.is_empty() && !right.is_empty() {
let left_chars = left.to_array()
let boundary_char = left_chars[left_chars.length() - 1].to_string()
let reconstructed_right = boundary_char + right
if two_part_compound_allows(dictionary, config, left, reconstructed_right) {
return true
}
}
split += 1
}
false
}
///|
fn compound_force_search(
dictionary : Dictionary,
config : AffixConfig,
word : StringView,
start : Int,
part_count : Int,
previous_part : String,
max_parts : Int,
found_force : Bool,
) -> Bool {
if start >= word.length() || part_count >= max_parts {
return false
}
let mut end = start + 1
while end <= word.length() {
let is_first = start == 0
let is_last = end == word.length()
let part_view = word.view(start_offset=start, end_offset=end)
if compound_part_length_ok(part_view, config) {
let part = part_view.to_owned()
for item in compound_part_matches(dictionary, config, part_view) {
if !compound_role_matches(config, item.flags, is_first, is_last) {
continue
}
if !is_first && item.prefix && !item.prefix_permit {
continue
}
if !is_last && item.suffix && !item.suffix_permit {
continue
}
if !is_last &&
compound_boundary_forbidden_by_pattern(
dictionary, config, word, part, item, end,
) {
continue
}
if config.check_compound_dup && is_last && previous_part == part {
continue
}
if config.check_compound_triple &&
compound_boundary_has_triple(word, end) {
continue
}
if config.check_compound_case &&
compound_boundary_has_bad_case(word, end) {
continue
}
let item_force = if is_last {
match config.force_ucase {
Some(flag) => flags_contain(config, item.flags, flag)
None => false
}
} else {
false
}
let path_has_force = found_force || item_force
if is_last {
if part_count >= 1 && path_has_force {
return true
}
} else if compound_force_search(
dictionary,
config,
word,
end,
part_count + 1,
part,
max_parts,
path_has_force,
) {
return true
}
}
}
end += 1
}
false
}
///|
fn compound_force_ucase_violation(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
match config.force_ucase {
Some(_) => {
let max_parts = if config.compound_word_max > 0 {
config.compound_word_max
} else {
8
}
compound_force_search(
dictionary,
config,
word.view(),
0,
0,
"",
max_parts,
false,
)
}
None => false
}
}
///|
fn compound_has_three_exact_parts(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
let chars = word.to_array()
if chars.length() < config.compound_min * 3 {
return false
}
let mut first_end = config.compound_min
while first_end < chars.length() {
let first = String::from_array(chars.exact_view(end=first_end))
for first_match in exact_part_matches(dictionary, config, first) {
if !compound_role_matches(config, first_match.flags, true, false) {
continue
}
let mut second_end = first_end + config.compound_min
while second_end < chars.length() {
let second = String::from_array(
chars.exact_view(start=first_end, end=second_end),
)
let third = String::from_array(chars.exact_view(start=second_end))
for second_match in exact_part_matches(dictionary, config, second) {
if !compound_role_matches(config, second_match.flags, false, false) {
continue
}
for third_match in exact_part_matches(dictionary, config, third) {
if compound_role_matches(config, third_match.flags, false, true) {
return true
}
}
}
second_end += 1
}
}
first_end += 1
}
false
}
///|
fn compound_is_typo_of_dictionary_word(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
if !compound_has_three_exact_parts(dictionary, config, word) {
return false
}
for entry in dictionary.entries {
if !entry_is_forbidden(config, entry) &&
entry.word != word &&
levenshtein_distance(word, entry.word) == 1 {
return true
}
}
false
}
///|
fn compound_result(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
let max_parts = if config.compound_word_max > 0 {
config.compound_word_max
} else {
8
}
if word.length() < config.compound_min * 2 {
return false
}
if compound_pattern_replacement_allows(dictionary, config, word) {
return true
}
if config.simplified_triple &&
simplified_triple_allows(dictionary, config, word) {
return true
}
let matched = if word.char_length() == word.length() {
compound_search(dictionary, config, word.view(), 0, 0, "", max_parts)
} else {
compound_search_chars(
dictionary,
config,
word.to_array(),
0,
0,
"",
max_parts,
)
}
matched &&
!(config.check_compound_rep &&
compound_rep_correctable(dictionary, config, word)) &&
!compound_is_typo_of_dictionary_word(dictionary, config, word)
}
///|
fn reverse_suffix_rule(word : String, rule : AffixRule) -> String? {
if rule.add.is_empty() || !ends_with(word.view(), rule.add) {
None
} else {
let stem = word
.view(end_offset=word.length() - rule.add.length())
.to_owned() +
rule.strip
if condition_matches(rule, stem) {
Some(stem)
} else {
None
}
}
}
///|
fn reverse_prefix_rule(word : String, rule : AffixRule) -> String? {
if rule.add.is_empty() || word.find(rule.add) != Some(0) {
None
} else {
let stem = rule.strip + word.view(start_offset=rule.add.length()).to_owned()
if condition_matches(rule, stem) {
Some(stem)
} else {
None
}
}
}
///|
fn pending_after_rule(
config : AffixConfig,
pending : Array[String],
rule : AffixRule,
) -> Array[String] {
let result : Array[String] = []
for flag in pending {
if !flags_contain(config, rule.continuation, flag) {
remember_unique(result, flag)
}
}
remember_unique(result, rule.flag)
result
}
///|
fn pending_matches_entry(
config : AffixConfig,
pending : Array[String],
entry : DicEntry,
) -> Bool {
for flag in pending {
if !flags_contain(config, entry.flags, flag) {
return false
}
}
true
}
///|
fn affix_chain_search(
dictionary : Dictionary,
config : AffixConfig,
word : String,
depth : Int,
pending : Array[String],
has_prefix : Bool,
has_suffix : Bool,
all_cross : Bool,
needy_count : Int,
) -> Bool {
if depth > 4 {
return false
}
if depth > 0 && (needy_count == 0 || needy_count < depth) {
for entry in dictionary.find_entries(word) {
if entry_can_be_affixed(config, entry) &&
pending_matches_entry(config, pending, entry) {
return true
}
}
}
for rule in config.suffixes {
if rule_is_only_in_compound(config, rule) {
continue
}
match reverse_suffix_rule(word, rule) {
Some(stem) => {
let cross_ok = all_cross && rule.cross_product
if has_prefix && !cross_ok {
continue
}
let next_pending = pending_after_rule(config, pending, rule)
if affix_chain_search(
dictionary,
config,
stem,
depth + 1,
next_pending,
has_prefix,
true,
cross_ok,
needy_count + (if rule_needs_affix(config, rule) { 1 } else { 0 }),
) {
return true
}
}
None => ()
}
}
for rule in config.prefixes {
if rule_is_only_in_compound(config, rule) {
continue
}
match reverse_prefix_rule(word, rule) {
Some(stem) => {
let cross_ok = all_cross && rule.cross_product
if has_suffix && !cross_ok {
continue
}
let next_pending = pending_after_rule(config, pending, rule)
if affix_chain_search(
dictionary,
config,
stem,
depth + 1,
next_pending,
true,
has_suffix,
cross_ok,
needy_count + (if rule_needs_affix(config, rule) { 1 } else { 0 }),
) {
return true
}
}
None => ()
}
}
false
}
///|
fn deep_affix_result(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> SpellResult? {
if affix_chain_search(dictionary, config, word, 0, [], false, false, true, 0) {
Some({ accepted: true, stem: Some(word), prefix: None, suffix: None, })
} else {
None
}
}
///|
fn spell_core(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> SpellResult {
match exact_result(dictionary, config, word) {
Some(result) => return result
None => ()
}
let view = word.view()
for rule in config.prefixes {
match prefix_result(dictionary, config, view, rule) {
Some(result) => return result
None => ()
}
}
for rule in config.suffixes {
match suffix_result(dictionary, config, view, rule) {
Some(result) => return result
None => ()
}
}
if config.complex_prefixes {
for outer in config.prefixes {
for inner in config.prefixes {
match twofold_prefix_result(dictionary, config, view, outer, inner) {
Some(result) => return result
None => ()
}
}
}
}
for outer in config.suffixes {
for inner in config.suffixes {
match twofold_suffix_result(dictionary, config, view, outer, inner) {
Some(result) => return result
None => ()
}
}
}
for prefix in config.prefixes {
for suffix in config.suffixes {
match cross_result(dictionary, config, view, prefix, suffix) {
Some(result) => return result
None => ()
}
}
}
match deep_affix_result(dictionary, config, word) {
Some(result) => return result
None => ()
}
if compound_result(dictionary, config, word) {
return { accepted: true, stem: None, prefix: None, suffix: None, }
}
if compound_rule_result(dictionary, config, word) {
return { accepted: true, stem: None, prefix: None, suffix: None, }
}
{ accepted: false, stem: None, prefix: None, suffix: None, }
}
///|
fn ascii_lower(text : String) -> String {
let result = StringBuilder()
for c in text {
result.write_char(@unicode.to_lowercase(c))
}
result.to_string()
}
///|
fn language_lower(config : AffixConfig, text : String) -> String {
let result = StringBuilder()
for c in text {
if config.language == "tr" && c == 'I' {
result.write_char('ı')
} else if config.language == "tr" && c == 'İ' {
result.write_char('i')
} else {
result.write_char(@unicode.to_lowercase(c))
}
}
result.to_string()
}
///|
fn sharp_case_candidates(text : String) -> Array[String] {
let lower = ascii_lower(text)
let chars = lower.to_array()
let mut occurrences = 0
let mut i = 0
while i + 1 < chars.length() {
if chars[i] == 's' && chars[i + 1] == 's' {
occurrences += 1
i += 2
} else {
i += 1
}
}
if occurrences > 8 {
occurrences = 8
}
let result : Array[String] = []
let combinations = 1 << occurrences
for mask in 0..> bit) & 1) == 1 {
builder.write_char('ß')
} else {
builder.write_char('s')
builder.write_char('s')
}
index += 2
bit += 1
} else {
builder.write_char(chars[index])
index += 1
}
}
let variant = builder.to_string()
if !result.contains(variant) {
result.push(variant)
}
let capitalized = capitalize_ascii(variant)
if !result.contains(capitalized) {
result.push(capitalized)
}
}
result
}
///|
fn unicode_upper_char(c : Char) -> Char {
let code = c.to_int()
if code >= 97 && code <= 122 {
(code - 32).unsafe_to_char()
} else if code >= 0xE0 && code <= 0xFE && code != 0xF7 {
(code - 32).unsafe_to_char()
} else if code == 0xFF {
'\u{0178}'
} else if code == 0x0153 {
'\u{0152}'
} else if code >= 0x0101 && code <= 0x017F && code % 2 == 1 {
(code - 1).unsafe_to_char()
} else {
c
}
}
///|
fn ascii_upper_char(c : Char) -> Char {
if c >= 'a' && c <= 'z' {
(c.to_int() - 32).unsafe_to_char()
} else {
c
}
}
///|
fn capitalize_ascii(text : String) -> String {
let chars = text.to_array()
if chars.length() == 0 {
return text
}
let result = StringBuilder()
result.write_char(ascii_upper_char(chars[0]))
for i in 1.. Bool {
if (c >= 'A' && c <= 'Z') || c == '\u{0130}' {
true
} else {
let lowered = @unicode.to_lowercase(c)
lowered != c
}
}
///|
fn is_probably_lowercase_char(c : Char) -> Bool {
if c >= 'a' && c <= 'z' {
true
} else {
let lower = @unicode.to_lowercase(c)
lower == c &&
c.to_int() > 127 &&
c != '.' &&
c != '\'' &&
c != '-' &&
c != '_'
}
}
///|
fn is_all_upper(text : String) -> Bool {
let mut has_letter = false
let mut valid = true
for c in text {
if is_uppercase_char(c) {
has_letter = true
} else if is_probably_lowercase_char(c) {
valid = false
break
}
}
has_letter && valid
}
///|
fn is_capitalized(text : String) -> Bool {
let chars = text.to_array()
if chars.length() == 0 || !is_uppercase_char(chars[0]) {
return false
}
for i in 1.. String {
let mut end = text.length()
while end > 0 && text.unsafe_get(end - 1) == '.' {
end -= 1
}
if end == text.length() {
text
} else {
text.view(end_offset=end).to_owned()
}
}
///|
fn has_exact_forbidden(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
let mut found_forbidden = false
for entry in dictionary.find_entries(word) {
if entry_is_forbidden(config, entry) {
found_forbidden = true
} else {
return false
}
}
found_forbidden
}
///|
fn casefold_keepcase(config : AffixConfig, text : String) -> String {
language_lower(config, text).replace_all(old="\u{00DF}", new="ss")
}
///|
fn result_has_forbidden_homonym(
dictionary : Dictionary,
config : AffixConfig,
result : SpellResult,
) -> Bool {
match result.stem {
Some(stem) => {
let lower_stem = casefold_keepcase(config, stem)
for entry in dictionary.entries {
if casefold_keepcase(config, entry.word) == lower_stem &&
entry_is_forbidden(config, entry) {
return true
}
}
false
}
None => false
}
}
///|
fn result_is_keepcase_only(
dictionary : Dictionary,
config : AffixConfig,
result : SpellResult,
) -> Bool {
match result.stem {
Some(stem) => {
let mut has_match = false
let mut has_non_keepcase = false
let lower_stem = casefold_keepcase(config, stem)
for entry in dictionary.entries {
if casefold_keepcase(config, entry.word) == lower_stem {
has_match = true
if !entry_keeps_case(config, entry) {
has_non_keepcase = true
}
}
}
has_match && !has_non_keepcase
}
None => false
}
}
///|
fn is_numeric_word(config : AffixConfig, word : String) -> Bool {
if !config.word_chars.contains("0") && !config.word_chars.contains("1") {
return false
}
let chars = word.to_array()
if chars.length() == 0 {
return false
}
let mut digits = 0
let mut separators = 0
for i in 0..= '0' && ch <= '9' {
digits += 1
} else if ch == '.' || ch == ',' || ch == '-' {
if i == 0 || i + 1 >= chars.length() {
return false
}
let before = chars[i - 1]
let after = chars[i + 1]
if before < '0' || before > '9' || after < '0' || after > '9' {
return false
}
separators += 1
if separators > 1 {
return false
}
} else {
return false
}
}
digits > 0
}
///|
fn forbidden_prefix_extension(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
if config.forbidden_word is None {
return false
}
for entry in dictionary.entries {
if entry_is_forbidden(config, entry) &&
word.length() > entry.word.length() &&
word.find(entry.word) == Some(0) {
return true
}
}
false
}
///|
fn phrase_is_accepted(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
if !word.contains(" ") {
return false
}
let mut found = false
for part in word.split(" ") {
let token = part.to_owned()
if token.is_empty() {
return false
}
found = true
if !spell(dictionary, config, token).accepted {
return false
}
}
found
}
///|
fn apply_ignore(config : AffixConfig, word : String) -> String {
if config.ignore_chars.is_empty() {
return word
}
let ignored = config.ignore_chars.to_array()
let result = StringBuilder()
for c in word {
if !ignored.contains(c) {
result.write_char(c)
}
}
result.to_string()
}
///|
fn normalized_dictionary(
config : AffixConfig,
dictionary : Dictionary,
) -> Dictionary {
if config.ignore_chars.is_empty() {
return dictionary
}
let entries : Array[DicEntry] = []
let index : Map[String, Array[DicEntry]] = Map([])
for entry in dictionary.entries {
let normalized = apply_ignore(config, entry.word)
let item = if normalized == entry.word {
entry
} else {
{ word: normalized, flags: entry.flags, morph: entry.morph, }
}
entries.push(item)
match index.get(item.word) {
Some(bucket) => bucket.push(item)
None => index.set(item.word, [item])
}
}
{ declared_count: dictionary.declared_count, entries, index, }
}
///|
fn spell_breakable(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
if !config.break_enabled || config.break_patterns.length() == 0 {
return false
}
for position in 0.. matched_length && suffix.find(pattern) == Some(0) {
matched_length = pattern.length()
}
}
if matched_length > 0 {
let left = word.view(end_offset=position).to_owned()
let right = word.view(start_offset=position + matched_length).to_owned()
if config.break_explicit && (left.is_empty() || right.is_empty()) {
continue
}
let left_ok = left.is_empty() || spell(dictionary, config, left).accepted
let right_ok = right.is_empty() ||
spell(dictionary, config, right).accepted
if left_ok && right_ok && (!left.is_empty() || !right.is_empty()) {
return true
}
}
}
false
}
///|
fn apply_conversions(rules : Array[ReplacementRule], word : String) -> String {
let mut result = word
let mut iteration = 0
while iteration < 100 {
iteration += 1
let mut best_rule : Int = -1
let mut best_position = result.length() + 1
let mut best_length = 0
for i in 0.. {
let length = rules[i].from.length()
if position < best_position ||
(position == best_position && length > best_length) {
best_rule = i
best_position = position
best_length = length
}
}
None => ()
}
}
if best_rule < 0 {
break
}
let rule = rules[best_rule]
match replace_first(result, rule.from, rule.to) {
Some(value) => {
if value == result {
break
}
result = value
}
None => break
}
}
result
}
///|
fn apply_iconv(config : AffixConfig, word : String) -> String {
apply_conversions(config.iconv_rules, word)
}
///|
fn dotted_i_case_equal(left : String, right : String) -> Bool {
if !left.contains("\u{0130}") || !right.contains("\u{0130}") {
false
} else {
ascii_lower(left.replace_all(old="\u{0130}", new="i")) ==
ascii_lower(right.replace_all(old="\u{0130}", new="i"))
}
}
///|
fn contains_keepcase_part(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> Bool {
for entry in dictionary.entries {
if entry_keeps_case(config, entry) && word.contains(entry.word) {
return true
}
}
false
}
///|
fn case_variant_result(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> SpellResult? {
if is_all_upper(word) {
let lower_word = language_lower(config, word)
for candidate in generate_candidates(dictionary, config) {
if (
language_lower(config, candidate) == lower_word ||
dotted_i_case_equal(candidate, word)
) &&
!has_exact_forbidden(dictionary, config, candidate) {
let result = spell_core(dictionary, config, candidate)
if result.accepted &&
!result_is_keepcase_only(dictionary, config, result) {
return Some(result)
}
}
}
}
if config.check_sharps &&
(is_all_upper(word) || is_capitalized(word)) &&
!word.contains("ss") {
for candidate in sharp_case_candidates(word) {
if candidate != word {
let result = spell_core(dictionary, config, candidate)
if result.accepted {
return Some(result)
}
}
}
}
if is_all_upper(word) || is_capitalized(word) {
let lower = language_lower(config, word)
if lower != word {
let result = spell_core(dictionary, config, lower)
if result.accepted &&
!result_is_keepcase_only(dictionary, config, result) &&
!result_has_forbidden_homonym(dictionary, config, result) &&
!(result.stem is None &&
contains_keepcase_part(dictionary, config, lower)) {
return Some(result)
}
}
let capitalized = capitalize_ascii(lower)
if capitalized != word {
let result = spell_core(dictionary, config, capitalized)
if result.accepted &&
!result_is_keepcase_only(dictionary, config, result) &&
!result_has_forbidden_homonym(dictionary, config, result) {
return Some(result)
}
}
}
None
}
///|
pub fn spell(
dictionary : Dictionary,
config : AffixConfig,
word : String,
) -> SpellResult {
let word = apply_iconv(config, apply_ignore(config, word))
let dictionary = normalized_dictionary(config, dictionary)
if !is_capitalized(word) &&
!is_all_upper(word) &&
compound_force_ucase_violation(dictionary, config, word) {
return { accepted: false, stem: None, prefix: None, suffix: None, }
}
if is_numeric_word(config, word) {
return { accepted: true, stem: Some(word), prefix: None, suffix: None, }
}
if has_exact_forbidden(dictionary, config, word) {
return { accepted: false, stem: None, prefix: None, suffix: None, }
}
if forbidden_prefix_extension(dictionary, config, word) {
return { accepted: false, stem: None, prefix: None, suffix: None, }
}
if phrase_is_accepted(dictionary, config, word) {
return { accepted: true, stem: None, prefix: None, suffix: None, }
}
let direct = spell_core(dictionary, config, word)
if direct.accepted {
return direct
}
if spell_breakable(dictionary, config, word) {
return { accepted: true, stem: None, prefix: None, suffix: None, }
}
let without_periods = strip_trailing_periods(word)
if without_periods != word {
let bare = spell_core(dictionary, config, without_periods)
if bare.accepted {
return bare
}
match case_variant_result(dictionary, config, without_periods) {
Some(result) => return result
None => ()
}
}
match case_variant_result(dictionary, config, word) {
Some(result) => return result
None => ()
}
direct
}