Skip to main content

normalizer_tr/
api.rs

1use std::{
2    fmt,
3    sync::{
4        Arc,
5        atomic::{AtomicBool, Ordering},
6    },
7    time::Instant,
8};
9
10/// Original-source half-open UTF-8 byte range.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize))]
13pub struct SourceRange {
14    pub(crate) start: usize,
15    pub(crate) end: usize,
16}
17
18impl SourceRange {
19    /// Construct coordinates. Validity against text is checked by normalization.
20    pub const fn new(start: usize, end: usize) -> Self {
21        Self { start, end }
22    }
23    /// Inclusive start byte offset.
24    pub const fn start(self) -> usize {
25        self.start
26    }
27    /// Exclusive end byte offset.
28    pub const fn end(self) -> usize {
29        self.end
30    }
31}
32
33/// How unresolved linguistic expressions are handled.
34#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum AmbiguityPolicy {
37    /// Preserve source spans and return structured issues.
38    #[default]
39    Preserve,
40    /// Return an error containing unresolved diagnostics, without partial output.
41    Reject,
42}
43
44/// Explicit interpretation of a whole original-source span.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub enum HintKind {
48    /// An exact integer, including valid grouping and explicit zero-padding intent.
49    Cardinal,
50    /// ASCII digits, optional initial plus, and space/dot/slash/hyphen/parentheses.
51    Digits,
52    /// A valid numeric Gregorian date.
53    Date,
54    /// A valid 24-hour clock.
55    Time,
56    /// Explicit ordinal intent on a numeric period or ordinal suffix expression.
57    Ordinal,
58    /// Canonical uppercase Roman numeral; period establishes ordinal intent.
59    Roman,
60    /// Exact bounded numeric endpoints with optional quantity context.
61    Range,
62    /// Turkish national or +90 telephone reading.
63    Telephone,
64    /// Supported whole ASCII address or cued/hinted bare domain.
65    Electronic,
66}
67
68/// Caller intent at original grapheme-safe byte coordinates.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct Hint {
71    pub(crate) range: SourceRange,
72    pub(crate) kind: HintKind,
73}
74
75impl Hint {
76    /// Create a hint; normalization validates coordinates, overlap, and content.
77    pub const fn new(range: SourceRange, kind: HintKind) -> Self {
78        Self { range, kind }
79    }
80    /// Original-source range.
81    pub const fn range(self) -> SourceRange {
82        self.range
83    }
84    /// Requested interpretation.
85    pub const fn kind(self) -> HintKind {
86        self.kind
87    }
88}
89
90/// Per-call options. No implicit guessing policy is provided.
91#[derive(Clone, Debug, Default)]
92pub struct NormalizeOptions {
93    /// Preserve unresolved spans by default, or reject them explicitly.
94    pub ambiguity_policy: AmbiguityPolicy,
95    /// Non-overlapping, whole-expression hints in original-source coordinates.
96    pub hints: Vec<Hint>,
97}
98
99/// Semantic kind of a result segment.
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize))]
102pub enum SegmentKind {
103    /// Unchanged ordinary words, whitespace, or punctuation.
104    Verbatim,
105    /// Preserved unresolved linguistic work.
106    Unresolved,
107    /// Exact integer.
108    Cardinal,
109    /// Integer ordinal.
110    Ordinal,
111    /// Exact comma decimal.
112    Decimal,
113    /// Individual digits.
114    Digits,
115    /// Prefix percentage.
116    Percent,
117    /// Exact approved currency major/minor amount (TRY/USD/EUR/GBP).
118    Money,
119    /// Approved unit quantity or bounded rate.
120    Unit,
121    /// Gregorian date.
122    Date,
123    /// 24-hour digital clock.
124    Time,
125    /// Approved abbreviation.
126    Abbreviation,
127    /// Contextual or explicitly hinted numerical range.
128    Range,
129    /// Grouped Turkish telephone expression.
130    Telephone,
131    /// Full checksum-valid Turkish IBAN.
132    Iban,
133    /// Canonical uppercase Roman numeral with explicit/contextual intent.
134    Roman,
135    /// Supported email or web address.
136    Electronic,
137    /// Approved prose hashtag or ampersand.
138    Symbol,
139}
140
141/// Machine-readable reason for preserved linguistic work.
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize))]
144pub enum IssueCategory {
145    /// Multiple or insufficiently cued readings.
146    Ambiguous,
147    /// Invalid supported grammar, value, or suffix allomorph.
148    InvalidExpression,
149    /// Structured identifier that must not be rewritten in fragments.
150    ProtectedIdentifier,
151    /// Expression outside the normalizer's bounded coverage.
152    Unsupported,
153    /// Unapproved uppercase abbreviation.
154    UnknownAbbreviation,
155}
156
157/// Non-sensitive diagnostic for an unresolved original-source range.
158#[derive(Clone, Debug, Eq, PartialEq)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize))]
160pub struct Issue {
161    pub(crate) range: SourceRange,
162    pub(crate) category: IssueCategory,
163    pub(crate) explanation: &'static str,
164}
165
166impl Issue {
167    /// Original-source range.
168    pub const fn range(&self) -> SourceRange {
169        self.range
170    }
171    /// Machine-readable category.
172    pub const fn category(&self) -> IssueCategory {
173        self.category
174    }
175    /// Static explanation; contains no copied input values.
176    pub const fn explanation(&self) -> &'static str {
177        self.explanation
178    }
179}
180
181/// One member of an ordered, contiguous original-source partition.
182#[derive(Clone, Debug, Eq, PartialEq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize))]
184pub struct Segment {
185    pub(crate) range: SourceRange,
186    pub(crate) kind: SegmentKind,
187    pub(crate) text: String,
188    pub(crate) rule_id: &'static str,
189}
190
191impl Segment {
192    /// Original-source range.
193    pub const fn range(&self) -> SourceRange {
194        self.range
195    }
196    /// Semantic kind.
197    pub const fn kind(&self) -> SegmentKind {
198        self.kind
199    }
200    /// Emitted text.
201    pub fn text(&self) -> &str {
202        &self.text
203    }
204    /// Diagnostic identifier of the reader that emitted this segment.
205    pub const fn rule_id(&self) -> &'static str {
206        self.rule_id
207    }
208}
209
210/// Owned immutable result. Completeness concerns TN work, not voice quality.
211#[derive(Clone, Debug, Eq, PartialEq)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize))]
213pub struct NormalizeResult {
214    pub(crate) normalized_text: String,
215    pub(crate) locale: &'static str,
216    pub(crate) normalizer_id: &'static str,
217    pub(crate) complete: bool,
218    pub(crate) segments: Vec<Segment>,
219    pub(crate) issues: Vec<Issue>,
220}
221
222impl NormalizeResult {
223    /// Concatenation of all emitted segment text.
224    pub fn normalized_text(&self) -> &str {
225        &self.normalized_text
226    }
227    /// Selected locale.
228    pub const fn locale(&self) -> &'static str {
229        self.locale
230    }
231    /// Diagnostic identity of the normalizer build.
232    pub const fn normalizer_id(&self) -> &'static str {
233        self.normalizer_id
234    }
235    /// Whether there are no unresolved TN issues.
236    pub fn complete(&self) -> bool {
237        self.complete
238    }
239    /// Ordered original-source partition.
240    pub fn segments(&self) -> &[Segment] {
241        &self.segments
242    }
243    /// Ordered unresolved diagnostics.
244    pub fn issues(&self) -> &[Issue] {
245        &self.issues
246    }
247}
248
249/// Resource limit which was exceeded.
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub enum LimitKind {
252    /// Original UTF-8 input length.
253    Input,
254    /// Hint count.
255    Hints,
256    /// Candidate work records.
257    Candidates,
258    /// Logical owned result allocation.
259    Result,
260}
261
262/// Explicit input, policy, resource, control, or engine failure.
263#[derive(Clone, Debug, Eq, PartialEq)]
264pub enum NormalizeError {
265    /// Empty/all-whitespace input or unsupported control/Bidi_Control characters.
266    InvalidInput,
267    /// Invalid hint range, content, overlap, or protected-expression boundary.
268    InvalidHint,
269    /// Bundled assets or project-owned patterns are inconsistent.
270    InvalidConfiguration,
271    /// A documented resource limit was exceeded.
272    LimitExceeded(LimitKind),
273    /// Cooperative cancellation or monotonic deadline.
274    Cancelled,
275    /// Strict-mode unresolved linguistic diagnostics.
276    Unresolved(Vec<Issue>),
277    /// Unexpected violated internal invariant.
278    Internal,
279}
280
281impl fmt::Display for NormalizeError {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        let message = match self {
284            Self::InvalidInput => "input is empty, whitespace-only, or contains forbidden controls",
285            Self::InvalidHint => "hint is not a valid whole-expression original-source range",
286            Self::InvalidConfiguration => "built-in normalizer configuration is invalid",
287            Self::LimitExceeded(_) => "normalization resource limit exceeded",
288            Self::Cancelled => "normalization was cancelled or its deadline expired",
289            Self::Unresolved(_) => "strict normalization contains unresolved linguistic work",
290            Self::Internal => "normalization invariant failed",
291        };
292        f.write_str(message)
293    }
294}
295
296impl std::error::Error for NormalizeError {}
297
298/// Runtime-neutral cooperative control. Clones share the cancellation signal.
299#[derive(Clone, Debug, Default)]
300pub struct WorkControl {
301    cancelled: Arc<AtomicBool>,
302    deadline: Option<Instant>,
303}
304
305impl WorkControl {
306    /// Construct control with an optional monotonic deadline.
307    pub fn new(deadline: Option<Instant>) -> Self {
308        Self {
309            deadline,
310            ..Self::default()
311        }
312    }
313    /// Cancel this control and all its clones.
314    pub fn cancel(&self) {
315        self.cancelled.store(true, Ordering::Relaxed);
316    }
317    pub(crate) fn check(&self) -> Result<(), NormalizeError> {
318        if self.cancelled.load(Ordering::Relaxed)
319            || self
320                .deadline
321                .is_some_and(|deadline| Instant::now() >= deadline)
322        {
323            Err(NormalizeError::Cancelled)
324        } else {
325            Ok(())
326        }
327    }
328}