use crate::pinyin::active_helpcode::{detect_active_helpcode_length, strip_active_helpcodes};
use crate::pinyin::segment::{cut_pinyin_by_mode, join_segments, CutMode};
use crate::shuangpin::query::{apply_segmentation_cases_into, remove_manual_delimiters};
use crate::types::{QueryRequest, SchemeKey, SchemeType};
fn replace_string(target: &mut String, source: String) {
if target.capacity() == 0 {
*target = source;
} else {
target.clone_from(&source);
}
}
#[derive(Debug, Clone, Default)]
pub struct QuanpinScheme {
raw: String,
}
impl QuanpinScheme {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.raw.clear();
}
pub fn handle_key(&mut self, key: SchemeKey) -> bool {
match key {
SchemeKey::Letter(letter) if letter.is_ascii_alphabetic() => {
self.raw.push(char::from(letter));
true
}
SchemeKey::Apostrophe => {
if !self.raw.ends_with('\'') {
self.raw.push('\'');
true
} else {
false
}
}
SchemeKey::Backspace => self.raw.pop().is_some(),
SchemeKey::Letter(_)
| SchemeKey::Semicolon
| SchemeKey::Minus
| SchemeKey::Symbol(_)
| SchemeKey::Requery => false,
}
}
pub fn build_request(&self) -> QueryRequest {
let mut request = QueryRequest::default();
self.build_request_into(&mut request);
request
}
pub fn build_request_into(&self, request: &mut QueryRequest) {
Self::build_request_from_raw_into(&self.raw, &self.raw, request);
}
pub fn build_request_from_raw_into(
raw: &str,
raw_with_cases: &str,
request: &mut QueryRequest,
) {
request.scheme = SchemeType::Quanpin;
request.raw_input.clear();
request.raw_input.extend(
raw.bytes()
.map(|byte| char::from(byte.to_ascii_lowercase())),
);
let raw_with_cases = if raw_with_cases.is_empty() {
raw
} else {
raw_with_cases
};
request.raw_input_with_cases.clear();
request.raw_input_with_cases.push_str(raw_with_cases);
Self::apply_segmentation(request, CutMode::Correction);
}
pub fn preedit(&self) -> String {
self.raw.clone()
}
pub fn set_raw_input(&mut self, raw: &str, raw_with_cases: &str) {
let source = if raw_with_cases.is_empty() {
raw
} else {
raw_with_cases
};
self.raw.clear();
self.raw.reserve(source.len());
self.raw.push_str(source);
}
pub fn apply_segmentation(request: &mut QueryRequest, mode: CutMode) {
let helpcode_length = if request.enable_quanpin_helpcode {
detect_active_helpcode_length(&request.raw_input, &request.raw_input_with_cases)
} else {
0
};
let normalized_source = if helpcode_length > 0 {
strip_active_helpcodes(&request.raw_input, &request.raw_input_with_cases)
} else {
request.raw_input.clone()
};
let normalized_input = remove_manual_delimiters(&normalized_source);
let normalized_input_is_empty = normalized_input.is_empty();
replace_string(&mut request.normalized_input, normalized_input);
let normalized_segmentation = if normalized_input_is_empty {
String::new()
} else {
cut_pinyin_by_mode(&normalized_source, mode)
.first()
.map(|segments| join_segments(segments))
.unwrap_or_default()
};
if normalized_segmentation.is_empty() {
replace_string(&mut request.normalized_segmentation, normalized_source);
} else {
replace_string(
&mut request.normalized_segmentation,
normalized_segmentation,
);
}
let cased = &request.raw_input_with_cases;
let cased_source = if helpcode_length > 0 && cased.len() >= helpcode_length {
&cased[..cased.len() - helpcode_length]
} else {
cased.as_str()
};
apply_segmentation_cases_into(
&request.normalized_segmentation,
cased_source,
&mut request.raw_segmentation,
);
let raw_segmentation = &mut request.raw_segmentation;
if cased_source.ends_with('\'') && !raw_segmentation.ends_with('\'') {
raw_segmentation.push('\'');
}
if helpcode_length > 0 {
raw_segmentation.push('\'');
raw_segmentation.push_str(&cased[cased.len() - helpcode_length..]);
}
request
.segmentation
.clone_from(&request.normalized_segmentation);
request.valid = !normalized_input_is_empty;
}
pub fn apply_literal_segmentation(request: &mut QueryRequest) {
Self::apply_segmentation(request, CutMode::Greedy)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn typed(text: &str) -> QuanpinScheme {
let mut scheme = QuanpinScheme::new();
for byte in text.bytes() {
scheme.handle_key(if byte == b'\'' {
SchemeKey::Apostrophe
} else {
SchemeKey::Letter(byte)
});
}
scheme
}
#[test]
fn refreshing_a_request_reuses_the_case_output_buffer() {
let scheme = typed("nihaoAB");
let mut request = QueryRequest {
enable_quanpin_helpcode: true,
..QueryRequest::default()
};
scheme.build_request_into(&mut request);
let expected = request.clone();
let pointer = request.raw_segmentation.as_ptr();
let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
scheme.build_request_into(&mut request);
});
assert_eq!(request, expected);
assert_eq!(request.raw_segmentation, "ni'hao'AB");
assert_eq!(request.raw_segmentation.as_ptr(), pointer);
assert_eq!(allocations, 16);
}
#[test]
fn keys_edit_the_raw_letters() {
let mut scheme = typed("Ni''");
assert_eq!(scheme.preedit(), "Ni'", "a doubled apostrophe is dropped");
scheme.handle_key(SchemeKey::Semicolon);
scheme.handle_key(SchemeKey::Minus);
scheme.handle_key(SchemeKey::Requery);
scheme.handle_key(SchemeKey::Letter(b'1'));
scheme.handle_key(SchemeKey::Symbol(b'-'));
scheme.handle_key(SchemeKey::Symbol(b' '));
assert_eq!(scheme.preedit(), "Ni'");
scheme.handle_key(SchemeKey::Backspace);
assert_eq!(scheme.preedit(), "Ni");
scheme.reset();
assert_eq!(scheme.preedit(), "");
let mut scheme = typed("ab");
scheme.reset();
assert_eq!(scheme.preedit(), "");
scheme.handle_key(SchemeKey::Backspace);
assert_eq!(scheme.preedit(), "");
}
#[test]
fn host_raw_input_prefers_the_cased_form() {
let mut scheme = QuanpinScheme::new();
scheme.set_raw_input("nihao", "NiHao");
assert_eq!(scheme.preedit(), "NiHao");
scheme.set_raw_input("nihao", "");
assert_eq!(scheme.preedit(), "nihao");
}
#[test]
fn host_raw_input_reuses_existing_storage() {
let mut scheme = QuanpinScheme::new();
scheme.set_raw_input("abcdefghijklmnopqrstuvwxyz", "");
let capacity = scheme.raw.capacity();
scheme.set_raw_input("ni", "");
assert_eq!(scheme.preedit(), "ni");
assert!(scheme.raw.capacity() >= capacity);
}
#[test]
fn request_is_cut_in_correction_mode() {
let request = typed("nihao").build_request();
assert_eq!(request.scheme, SchemeType::Quanpin);
assert_eq!(request.raw_input, "nihao");
assert_eq!(request.normalized_input, "nihao");
assert_eq!(request.normalized_segmentation, "ni'hao");
assert_eq!(request.segmentation, "ni'hao");
assert_eq!(request.raw_segmentation, "ni'hao");
assert!(request.valid);
let aliased = typed("sahng").build_request();
assert_eq!(aliased.segmentation, "shang", "the alias layer always runs");
let mut literal = aliased.clone();
QuanpinScheme::apply_literal_segmentation(&mut literal);
assert_ne!(literal.segmentation, "shang");
}
#[test]
fn trailing_apostrophe_and_case_survive_in_the_raw_segmentation() {
let request = typed("NiHao'").build_request();
assert_eq!(request.raw_input, "nihao'");
assert_eq!(request.segmentation, "ni'hao");
assert_eq!(request.raw_segmentation, "Ni'Hao'");
}
#[test]
fn active_helpcode_letters_are_split_off() {
let mut request = QueryRequest {
enable_quanpin_helpcode: true,
..QueryRequest::default()
};
typed("nihaoAB").build_request_into(&mut request);
assert_eq!(request.normalized_input, "nihao");
assert_eq!(
request.normalized_input.capacity(),
request.normalized_input.len()
);
assert_eq!(request.segmentation, "ni'hao");
assert_eq!(request.raw_segmentation, "ni'hao'AB");
}
#[test]
fn disabled_helpcode_keeps_uppercase_letters_in_pinyin_segmentation() {
let request = typed("maO").build_request();
assert_eq!(request.raw_input, "mao");
assert_eq!(request.normalized_input, "mao");
assert_eq!(request.raw_segmentation, "maO");
}
#[test]
fn empty_input_is_invalid() {
let request = typed("'").build_request();
assert!(!request.valid);
assert!(!QuanpinScheme::new().build_request().valid);
}
}