msime-engine 0.1.1

Input method engine for Chinese and other languages: quanpin, shuangpin, nine-key, wubi, zhuyin, Cantonese, Japanese, Korean and more, ranked over a word lattice with typo correction and learning from what the user picks. The engine of 水杉输入法 (Metasequoia IME).
Documentation
use super::*;

const ALL: &[Language] = &[Language::Chinese, Language::English, Language::Japanese];

fn edge(start: usize, end: usize, text: &str, language: Language) -> Lexeme {
    Lexeme {
        input: start..end,
        text: text.to_owned(),
        language,
        rank: 0,
    }
}

fn text(path: &Conversion) -> String {
    path.segments
        .iter()
        .map(|segment| segment.text.as_str())
        .collect()
}

#[test]
fn converts_a_continuous_input_across_two_language_changes() {
    let input = "alphazhongwenohayou";
    let path = decode(
        input,
        &[
            edge(0, 5, "alpha", Language::English),
            edge(5, 10, "中", Language::Chinese),
            edge(10, 13, "文", Language::Chinese),
            edge(13, 19, "おはよう", Language::Japanese),
        ],
        ALL,
    )
    .expect("完整的三语言路径必须能够转换");
    assert_eq!(text(&path), "alpha中文おはよう");
    assert!(!path.chinese_complete);
    assert_eq!(path.segments.len(), 3);
    assert_eq!(path.segments[1].input, 5..13);
    assert_eq!(path.segments[1].language, Language::Chinese);
}

#[test]
fn backtracks_when_the_longest_local_prefix_cannot_cover_the_tail() {
    let path = decode(
        "abcde",
        &[
            edge(0, 3, "abc", Language::English),
            edge(0, 2, "甲", Language::Chinese),
            edge(2, 5, "おはよう", Language::Japanese),
        ],
        ALL,
    )
    .expect("不能因最长前缀无后续路径而丢掉完整备选");
    assert_eq!(text(&path), "甲おはよう");
}

#[test]
fn excludes_unselected_languages_and_keeps_an_available_complete_alternative() {
    let edges = [
        edge(0, 2, "甲", Language::Chinese),
        edge(2, 5, "おはよう", Language::Japanese),
        edge(2, 5, "def", Language::English),
    ];
    let path = decode("abcde", &edges, &[Language::Chinese, Language::English])
        .expect("未选语言不能挤掉已选语言的路径");
    assert_eq!(text(&path), "甲def");
    assert!(path
        .segments
        .iter()
        .all(|segment| segment.language != Language::Japanese));
}

#[test]
fn does_not_offer_a_partial_or_single_language_conversion_as_mixed() {
    assert!(decode("abcd", &[edge(0, 3, "甲", Language::Chinese)], ALL).is_none());
    assert!(decode("abc", &[edge(0, 3, "甲", Language::Chinese)], ALL).is_none());
    assert!(decode("", &[], ALL).is_none());
}

#[test]
fn rejects_broken_utf8_ranges_and_empty_output_without_losing_a_valid_path() {
    let path = decode(
        "aéz",
        &[
            edge(0, 2, "错误", Language::Chinese),
            edge(0, 99, "错误", Language::Japanese),
            edge(1, 1, "错误", Language::Japanese),
            edge(1, 3, "", Language::Japanese),
            edge(0, 1, "a", Language::English),
            edge(1, 3, "甲", Language::Chinese),
            edge(3, 4, "z", Language::English),
        ],
        ALL,
    )
    .expect("损坏的提供者边不能污染另一条完整路径");
    assert_eq!(text(&path), "a甲z");
}

#[test]
fn preserves_a_whole_word_instead_of_forcing_a_language_change() {
    assert!(decode(
        "women",
        &[
            edge(0, 5, "我们", Language::Chinese),
            edge(0, 2, "我", Language::Chinese),
            edge(2, 5, "men", Language::English),
        ],
        ALL
    )
    .is_none());
}

#[test]
fn keeps_a_mixed_alternative_for_an_equally_short_chinese_sentence() {
    let path = decode(
        "abcd",
        &[
            edge(0, 2, "甲", Language::Chinese),
            edge(2, 4, "乙", Language::Chinese),
            edge(2, 4, "cd", Language::English),
        ],
        ALL,
    )
    .expect("同样完整的中文解释不应删除合理的混合备选");
    assert_eq!(text(&path), "甲cd");
    assert!(path.chinese_complete);
}

#[test]
fn a_shorter_complete_mixed_path_can_beat_a_fragmented_single_language_path() {
    let path = decode(
        "abcdef",
        &[
            edge(0, 2, "甲", Language::Chinese),
            edge(2, 4, "乙", Language::Chinese),
            edge(4, 6, "丙", Language::Chinese),
            edge(0, 4, "abcd", Language::English),
        ],
        ALL,
    )
    .expect("更少词条的完整混合路径仍可用");
    assert_eq!(text(&path), "abcd丙");
    assert!(path.chinese_complete);
}

#[test]
fn accepts_the_input_budget_boundary_and_rejects_one_more_byte() {
    let input = "a".repeat(MAX_INPUT_BYTES);
    let edges = [
        edge(0, MAX_INPUT_BYTES - 1, "alpha", Language::English),
        edge(
            MAX_INPUT_BYTES - 1,
            MAX_INPUT_BYTES,
            "甲",
            Language::Chinese,
        ),
    ];
    assert!(decode(&input, &edges, ALL).is_some());
    assert!(decode(&(input + "a"), &edges, ALL).is_none());
}

#[test]
fn accepts_the_lexeme_budget_boundary_and_counts_invalid_edges_towards_it() {
    let mut edges = vec![edge(0, 0, "", Language::Chinese); MAX_LEXEMES - 2];
    edges.push(edge(0, 1, "a", Language::English));
    edges.push(edge(1, 2, "甲", Language::Chinese));
    assert!(decode("ab", &edges, ALL).is_some());
    edges.push(edge(0, 0, "", Language::Chinese));
    assert!(decode("ab", &edges, ALL).is_none());
}

#[test]
fn disabling_all_or_all_but_one_language_does_not_create_a_mixed_path() {
    let edges = [
        edge(0, 1, "a", Language::English),
        edge(1, 2, "甲", Language::Chinese),
    ];
    assert!(decode("ab", &edges, &[]).is_none());
    for language in ALL {
        assert!(decode("ab", &edges, &[*language]).is_none());
    }
}

#[test]
fn uses_provider_rank_and_stays_deterministic_when_enumeration_order_changes() {
    let mut preferred = edge(0, 2, "甲", Language::Chinese);
    preferred.rank = 0;
    let mut second = edge(0, 2, "乙", Language::Chinese);
    second.rank = 1;
    let mut edges = vec![second, preferred, edge(2, 5, "abc", Language::English)];
    let forward = decode("abcde", &edges, ALL).expect("正向枚举有完整路径");
    edges.reverse();
    let reversed = decode("abcde", &edges, ALL).expect("反向枚举有完整路径");
    assert_eq!(forward, reversed);
    assert_eq!(text(&forward), "甲abc");
}