use super::{
window_around, Candidate, CompiledRules, ContextBuf, EnclosureSlot, Judge, Judgment,
PartialState, PendingCandidate, PendingEnclosure, TerminatorKind, WINDOW_CHARS,
};
use crate::domain::types::DepthVec;
pub(crate) fn scan_chunk(text: &str, rules: &CompiledRules) -> PartialState {
let mut state = PartialState::identity();
state.deltas.resize(rules.asym_type_count(), 0);
state.chunk_len = text.len();
state.head_ctx = ContextBuf::head_of(text);
state.tail_ctx = ContextBuf::tail_of(text);
let total_chars = text.chars().count();
let mut depths: DepthVec = state.deltas.clone();
let mut parity: u32 = 0;
for (char_idx, (i, ch)) in text.char_indices().enumerate() {
let before = char_idx;
let after = total_chars - char_idx;
let class = rules.classify(ch);
if let Some(enc) = class.enclosure {
if !enc.suppressible {
apply_slot(enc.slot, &mut depths, &mut parity);
} else if before >= WINDOW_CHARS && after >= WINDOW_CHARS {
let (window, pos) = window_around(text, i, WINDOW_CHARS);
if !rules.suppress_enclosure(window, pos, ch) {
apply_slot(enc.slot, &mut depths, &mut parity);
}
} else {
state.pending_enc.push(PendingEnclosure {
local_offset: i,
ch,
slot: enc.slot,
});
}
}
if class.terminator {
let offset = i + ch.len_utf8();
let kind = TerminatorKind::Char(ch);
if before + 1 >= WINDOW_CHARS && after > WINDOW_CHARS {
let (window, pos) = window_around(text, offset, WINDOW_CHARS);
if let Judgment::Boundary(flags) = rules.judge(window, pos, kind) {
state.boundaries.push(Candidate {
local_offset: offset,
local_depths: depths.clone(),
local_parity: parity,
flags,
});
}
} else {
state.pending.push(PendingCandidate {
local_offset: offset,
local_depths: depths.clone(),
local_parity: parity,
kind,
});
}
}
}
state.deltas = depths;
state.parity = parity;
state
}
fn apply_slot(slot: EnclosureSlot, depths: &mut DepthVec, parity: &mut u32) {
match slot {
EnclosureSlot::Asym { index, delta } => {
let i = index as usize;
debug_assert!(i < depths.len(), "slot index within compiled type count");
depths[i] += i32::from(delta);
}
EnclosureSlot::Sym { bit } => *parity ^= 1 << bit,
}
}
#[cfg(test)]
mod tests {
use super::super::tests::{char_boundary_cuts, segments};
use super::*;
use proptest::prelude::*;
use std::sync::LazyLock;
static EN: LazyLock<CompiledRules> =
LazyLock::new(|| CompiledRules::from_code("en").expect("en config compiles"));
static JA: LazyLock<CompiledRules> =
LazyLock::new(|| CompiledRules::from_code("ja").expect("ja config compiles"));
fn v2_boundaries(state: &PartialState) -> Vec<usize> {
state
.boundaries
.iter()
.filter(|c| c.local_parity == 0 && c.local_depths.iter().all(|&d| d <= 0))
.map(|c| c.local_offset)
.collect()
}
#[test]
fn single_chunk_output_is_pinned() {
let en_cases: [(&str, &[usize]); 7] = [
(
"Dr. Smith went to Washington. He arrived at 3.5 p.m. and left.",
&[29, 62],
),
(
"She said \"Hello world.\" Then (after a pause) she left! Really?!",
&[54, 63],
),
(
"Wait... what happened? The U.S. economy grew. That's John's book.",
&[22, 45, 65],
),
(
"Don't stop. \"It's fine,\" he said. Lists use 1) markers.",
&[11, 33, 55],
),
("Short. Even shorter! End", &[6, 20]),
("He waited.... Then he left.", &[13, 27]),
("Wait... what?", &[13]),
];
for (text, expected) in en_cases {
let state = scan_chunk(text, &EN).resolve_edges(&*EN);
assert_eq!(v2_boundaries(&state), expected, "en mismatch for {text:?}");
}
let ja_cases: [(&str, &[usize]); 5] = [
(
"彼は「こんにちは」と言った。今日は晴れ!明日は?",
&[42, 58, 68],
),
("彼は『引用「入れ子」だ』と言った。終わり。", &[51, 63]),
("これはテストです。値は3.5です。すごい!?", &[27, 45, 56]),
("「囲まれた文。」の外。", &[33]),
("彼は待った……そして言った。終わり。", &[21, 42, 54]),
];
for (text, expected) in ja_cases {
let state = scan_chunk(text, &JA).resolve_edges(&*JA);
assert_eq!(v2_boundaries(&state), expected, "ja mismatch for {text:?}");
}
}
fn en_soup() -> impl Strategy<Value = String> {
let token = prop::sample::select(vec![
"the", "He", "said", "Dr.", "U.S.", "Mr.", "3.5", "approx", "Smith", "don't", ". ",
"! ", "? ", "... ", "?! ", "(", ")", "\"", "'", ", ", " ", "\n", "word.", "A",
]);
proptest::collection::vec(token, 0..60).prop_map(|v| v.concat())
}
fn ja_soup() -> impl Strategy<Value = String> {
let token = prop::sample::select(vec![
"彼は",
"言った",
"。",
"!",
"?",
"!?",
"「",
"」",
"『",
"』",
"(",
")",
"…",
"今日",
"3.5",
" ",
"\n",
"。」",
"です",
]);
proptest::collection::vec(token, 0..60).prop_map(|v| v.concat())
}
proptest! {
#[test]
fn en_chunked_equals_single_chunk(
text in en_soup(),
ixs in proptest::collection::vec(any::<prop::sample::Index>(), 0..5),
) {
let whole = scan_chunk(&text, &EN).resolve_edges(&*EN);
let cuts = char_boundary_cuts(&text, &ixs);
let mut acc = PartialState::identity();
for seg in segments(&text, &cuts) {
acc = acc.combine_with(&scan_chunk(seg, &EN), &*EN);
}
let chunked = acc.resolve_edges(&*EN);
prop_assert_eq!(&whole.boundaries, &chunked.boundaries);
prop_assert_eq!(&whole.deltas, &chunked.deltas);
prop_assert_eq!(whole.parity, chunked.parity);
}
#[test]
fn ja_chunked_equals_single_chunk(
text in ja_soup(),
ixs in proptest::collection::vec(any::<prop::sample::Index>(), 0..5),
) {
let whole = scan_chunk(&text, &JA).resolve_edges(&*JA);
let cuts = char_boundary_cuts(&text, &ixs);
let mut acc = PartialState::identity();
for seg in segments(&text, &cuts) {
acc = acc.combine_with(&scan_chunk(seg, &JA), &*JA);
}
let chunked = acc.resolve_edges(&*JA);
prop_assert_eq!(&whole.boundaries, &chunked.boundaries);
prop_assert_eq!(&whole.deltas, &chunked.deltas);
prop_assert_eq!(whole.parity, chunked.parity);
}
}
}