use core::num::NonZeroU64;
use smol_str::SmolStr;
use crate::{core::AlignmentUnit, types::Lang};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum OovKind {
Symbol(char),
BoundaryPunct,
InternalPunct(char),
NotInspected,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OovEvent {
kind: OovKind,
char_index: usize,
word_index: usize,
language: Lang,
}
impl OovEvent {
#[must_use]
pub(crate) const fn new(
kind: OovKind,
char_index: usize,
word_index: usize,
language: Lang,
) -> Self {
Self {
kind,
char_index,
word_index,
language,
}
}
#[must_use]
pub const fn kind(&self) -> &OovKind {
&self.kind
}
#[must_use]
pub const fn char_index(&self) -> usize {
self.char_index
}
#[must_use]
pub const fn word_index(&self) -> usize {
self.word_index
}
#[must_use]
pub const fn language(&self) -> &Lang {
&self.language
}
pub(crate) fn set_language(&mut self, language: Lang) {
self.language = language;
}
#[must_use]
pub fn char(&self) -> Option<char> {
match self.kind {
OovKind::Symbol(c) | OovKind::InternalPunct(c) => Some(c),
OovKind::BoundaryPunct | OovKind::NotInspected => None,
}
}
#[must_use]
pub fn matches_position(&self, other: &OovEvent) -> bool {
self.kind == other.kind
&& self.char_index == other.char_index
&& self.word_index == other.word_index
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OovDecision {
Wildcard,
FailClosed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedOov {
event: OovEvent,
decision: OovDecision,
}
impl ResolvedOov {
#[must_use]
pub(crate) const fn new(event: OovEvent, decision: OovDecision) -> Self {
Self { event, decision }
}
#[must_use]
pub const fn event(&self) -> &OovEvent {
&self.event
}
#[must_use]
pub const fn decision(&self) -> OovDecision {
self.decision
}
}
#[derive(Debug)]
enum Binding {
Text { reader: NonZeroU64, text: SmolStr },
Job {
reader: NonZeroU64,
ticket: NonZeroU64,
},
Unit { reader: Option<NonZeroU64> },
}
#[derive(Debug)]
#[must_use = "a detection does nothing until it is decided"]
pub struct OovDetection {
unit: AlignmentUnit,
language: Lang,
events: Vec<OovEvent>,
binding: Binding,
}
impl OovDetection {
pub(crate) fn of_text(
text: &str,
language: Lang,
events: Vec<OovEvent>,
reader: NonZeroU64,
) -> Self {
Self {
unit: AlignmentUnit::Whole,
language,
events,
binding: Binding::Text {
reader,
text: SmolStr::new(text),
},
}
}
pub(crate) fn of_job(
job: &crate::core::UnitJob,
events: Vec<OovEvent>,
reader: NonZeroU64,
) -> Self {
Self {
unit: job.unit(),
language: job.language().clone(),
events,
binding: Binding::Job {
reader,
ticket: job.ticket(),
},
}
}
pub(crate) const fn of_unit(
unit: AlignmentUnit,
language: Lang,
events: Vec<OovEvent>,
reader: Option<NonZeroU64>,
) -> Self {
Self {
unit,
language,
events,
binding: Binding::Unit { reader },
}
}
#[must_use]
pub const fn unit(&self) -> AlignmentUnit {
self.unit
}
#[must_use]
pub const fn language(&self) -> &Lang {
&self.language
}
#[must_use]
pub fn events(&self) -> &[OovEvent] {
&self.events
}
pub fn decide(self, mut policy: impl FnMut(&OovEvent) -> OovDecision) -> OovResolution {
let resolved = self
.events
.into_iter()
.map(|event| {
let decision = policy(&event);
ResolvedOov { event, decision }
})
.collect();
OovResolution {
unit: self.unit,
language: self.language,
resolved,
binding: self.binding,
}
}
}
#[derive(Debug)]
#[must_use = "a resolution does nothing until alignment applies it"]
pub struct OovResolution {
unit: AlignmentUnit,
language: Lang,
resolved: Vec<ResolvedOov>,
binding: Binding,
}
impl OovResolution {
#[must_use]
pub const fn unit(&self) -> AlignmentUnit {
self.unit
}
#[must_use]
pub const fn language(&self) -> &Lang {
&self.language
}
#[must_use]
pub fn resolved(&self) -> &[ResolvedOov] {
&self.resolved
}
pub(crate) fn for_text(&self, text: &str, reader: NonZeroU64) -> Option<&[ResolvedOov]> {
match &self.binding {
Binding::Text {
reader: read_by,
text: read,
} if *read_by == reader && read == text => Some(&self.resolved),
_ => None,
}
}
pub(crate) fn for_job(
&self,
job: &crate::core::UnitJob,
reader: NonZeroU64,
) -> Option<&[ResolvedOov]> {
match self.binding {
Binding::Job {
reader: read_by,
ticket,
} if read_by == reader
&& ticket == job.ticket()
&& self.unit == job.unit()
&& self.language == *job.language() =>
{
Some(&self.resolved)
}
_ => None,
}
}
pub(crate) fn read_by(&self, reader: NonZeroU64) -> Option<&[ResolvedOov]> {
match self.binding {
Binding::Unit {
reader: Some(read_by),
} if read_by == reader => Some(&self.resolved),
_ => None,
}
}
pub(crate) fn unread_decision(&self) -> Option<OovDecision> {
match (&self.binding, self.resolved.as_slice()) {
(Binding::Unit { reader: None }, [only]) if only.event.kind == OovKind::NotInspected => {
Some(only.decision)
}
_ => None,
}
}
}
#[must_use]
pub fn default_oov_policy(event: &OovEvent) -> OovDecision {
match &event.kind {
OovKind::Symbol(c) => {
if c.is_alphanumeric() || *c == '\'' || *c == '\u{2019}' {
OovDecision::Wildcard
} else {
OovDecision::FailClosed
}
}
OovKind::BoundaryPunct | OovKind::InternalPunct(_) => OovDecision::Wildcard,
OovKind::NotInspected => OovDecision::Wildcard,
}
}
#[must_use]
pub fn wildcard_all_policy(_event: &OovEvent) -> OovDecision {
OovDecision::Wildcard
}
#[must_use]
pub fn fail_closed_all_policy(_event: &OovEvent) -> OovDecision {
OovDecision::FailClosed
}
#[cfg(any(test, feature = "bench-internals"))]
#[doc(hidden)]
#[must_use]
pub fn resolve_events(
events: &[OovEvent],
mut policy: impl FnMut(&OovEvent) -> OovDecision,
) -> Vec<ResolvedOov> {
events
.iter()
.map(|event| ResolvedOov::new(event.clone(), policy(event)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(c: char) -> OovEvent {
OovEvent::new(OovKind::Symbol(c), 0, 0, Lang::En)
}
fn boundary_ev() -> OovEvent {
OovEvent::new(OovKind::BoundaryPunct, 0, 0, Lang::En)
}
fn internal_ev(c: char) -> OovEvent {
OovEvent::new(OovKind::InternalPunct(c), 0, 0, Lang::En)
}
fn decisions(events: &[OovEvent], policy: fn(&OovEvent) -> OovDecision) -> Vec<OovDecision> {
events.iter().map(policy).collect()
}
const READER: NonZeroU64 = NonZeroU64::MIN;
#[test]
fn default_wildcards_alphanumeric() {
let events = vec![ev('4'), ev('a'), ev('Z')];
assert_eq!(
decisions(&events, default_oov_policy),
vec![
OovDecision::Wildcard,
OovDecision::Wildcard,
OovDecision::Wildcard,
]
);
}
#[test]
fn default_wildcards_apostrophes() {
let events = vec![ev('\''), ev('\u{2019}')];
assert_eq!(
decisions(&events, default_oov_policy),
vec![OovDecision::Wildcard, OovDecision::Wildcard]
);
}
#[test]
fn default_fails_closed_on_pronounced_symbols() {
let events = vec![ev('&'), ev('@'), ev('%'), ev(',')];
assert_eq!(
decisions(&events, default_oov_policy),
vec![
OovDecision::FailClosed,
OovDecision::FailClosed,
OovDecision::FailClosed,
OovDecision::FailClosed,
]
);
}
#[test]
fn wildcard_all_does_what_it_says() {
let events = vec![ev('a'), ev('&'), ev(',')];
assert_eq!(
decisions(&events, wildcard_all_policy),
vec![
OovDecision::Wildcard,
OovDecision::Wildcard,
OovDecision::Wildcard,
]
);
}
#[test]
fn fail_closed_all_does_what_it_says() {
let events = vec![ev('a'), ev('&'), ev(',')];
assert_eq!(
decisions(&events, fail_closed_all_policy),
vec![
OovDecision::FailClosed,
OovDecision::FailClosed,
OovDecision::FailClosed,
]
);
}
#[test]
fn default_wildcards_structural_kinds() {
let events = vec![boundary_ev(), internal_ev('.')];
assert_eq!(
decisions(&events, default_oov_policy),
vec![OovDecision::Wildcard, OovDecision::Wildcard],
);
}
#[test]
fn fail_closed_all_includes_structural_wildcards() {
let events = vec![ev('a'), boundary_ev(), internal_ev('.')];
assert_eq!(
decisions(&events, fail_closed_all_policy),
vec![
OovDecision::FailClosed,
OovDecision::FailClosed,
OovDecision::FailClosed,
],
);
}
#[test]
fn every_policy_decides_a_unit_nothing_inspected() {
let not_inspected = OovEvent::new(OovKind::NotInspected, 0, 0, Lang::Ko);
assert_eq!(not_inspected.char(), None);
let events = [not_inspected];
assert_eq!(
decisions(&events, fail_closed_all_policy),
vec![OovDecision::FailClosed]
);
assert_eq!(
decisions(&events, default_oov_policy),
vec![OovDecision::Wildcard]
);
assert_eq!(
decisions(&events, wildcard_all_policy),
vec![OovDecision::Wildcard]
);
}
#[test]
fn deciding_a_detection_pairs_every_event_with_its_decision() {
let events = vec![ev('4'), ev('&'), boundary_ev()];
let mut seen = Vec::new();
let resolution = OovDetection::of_text("4 &", Lang::En, events.clone(), READER).decide(|e| {
seen.push(e.clone());
default_oov_policy(e)
});
assert_eq!(seen, events, "the policy sees every event once, in order");
let paired: Vec<(OovEvent, OovDecision)> = resolution
.resolved()
.iter()
.map(|r| (r.event().clone(), r.decision()))
.collect();
assert_eq!(
paired,
vec![
(ev('4'), OovDecision::Wildcard),
(ev('&'), OovDecision::FailClosed),
(boundary_ev(), OovDecision::Wildcard),
]
);
assert_eq!(resolution.unit(), AlignmentUnit::Whole);
assert!(resolution.for_text("4 &", READER).is_some());
assert!(
resolution.for_text("4 & ", READER).is_none(),
"another text"
);
let other = NonZeroU64::new(2).expect("2 != 0");
assert!(
resolution.for_text("4 &", other).is_none(),
"another aligner"
);
assert!(
resolution.read_by(READER).is_none(),
"a direct resolution is no job unit"
);
assert_eq!(resolution.unread_decision(), None);
}
#[test]
fn a_job_unit_answers_only_to_its_reader() {
let other = NonZeroU64::new(2).expect("2 != 0");
let read = OovDetection::of_unit(AlignmentUnit::Run(1), Lang::En, vec![ev('4')], Some(READER))
.decide(fail_closed_all_policy);
assert_eq!(read.unit(), AlignmentUnit::Run(1));
assert!(read.read_by(READER).is_some());
assert!(read.read_by(other).is_none());
assert!(
read.for_text("4", READER).is_none(),
"a job unit is no text"
);
assert_eq!(read.unread_decision(), None);
let unread = OovDetection::of_unit(
AlignmentUnit::Whole,
Lang::Ko,
vec![OovEvent::new(OovKind::NotInspected, 0, 0, Lang::Ko)],
None,
)
.decide(fail_closed_all_policy);
assert_eq!(unread.unread_decision(), Some(OovDecision::FailClosed));
assert!(unread.read_by(READER).is_none());
}
#[test]
fn no_events_decide_to_no_decisions() {
let resolution = OovDetection::of_text("hello", Lang::En, Vec::new(), READER)
.decide(|_| unreachable!("no event to decide"));
assert!(resolution.resolved().is_empty());
assert!(resolve_events(&[], default_oov_policy).is_empty());
}
}