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    /// Render unresolved source faithfully and report handled fallback diagnostics.
43    Fallback,
44}
45
46/// Explicit interpretation of a whole original-source span.
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum HintKind {
50    /// An exact integer, including valid grouping and explicit zero-padding intent.
51    Cardinal,
52    /// ASCII digits, optional initial plus, and space/dot/slash/hyphen/parentheses.
53    Digits,
54    /// A valid numeric Gregorian date.
55    Date,
56    /// A valid 24-hour clock.
57    Time,
58    /// Explicit ordinal intent on a numeric period or ordinal suffix expression.
59    Ordinal,
60    /// Canonical uppercase Roman numeral; period establishes ordinal intent.
61    Roman,
62    /// Exact bounded numeric endpoints with optional quantity context.
63    Range,
64    /// Turkish national or +90 telephone reading.
65    Telephone,
66    /// Supported whole ASCII address or cued/hinted bare domain.
67    Electronic,
68}
69
70/// Caller intent at original grapheme-safe byte coordinates.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct Hint {
73    pub(crate) range: SourceRange,
74    pub(crate) kind: HintKind,
75}
76
77impl Hint {
78    /// Create a hint; normalization validates coordinates, overlap, and content.
79    pub const fn new(range: SourceRange, kind: HintKind) -> Self {
80        Self { range, kind }
81    }
82    /// Original-source range.
83    pub const fn range(self) -> SourceRange {
84        self.range
85    }
86    /// Requested interpretation.
87    pub const fn kind(self) -> HintKind {
88        self.kind
89    }
90}
91
92/// Per-call options. Source-faithful fallback is opt-in.
93#[derive(Clone, Debug, Default)]
94pub struct NormalizeOptions {
95    /// Preserve by default; explicitly reject or apply source-faithful fallback.
96    pub ambiguity_policy: AmbiguityPolicy,
97    /// Non-overlapping, whole-expression hints in original-source coordinates.
98    pub hints: Vec<Hint>,
99}
100
101/// Semantic kind of a result segment.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize))]
104pub enum SegmentKind {
105    /// Unchanged ordinary words, whitespace, or punctuation.
106    Verbatim,
107    /// Preserved unresolved linguistic work.
108    Unresolved,
109    /// Exact integer.
110    Cardinal,
111    /// Integer ordinal.
112    Ordinal,
113    /// Exact comma decimal.
114    Decimal,
115    /// Individual digits.
116    Digits,
117    /// Prefix percentage.
118    Percent,
119    /// Exact approved currency major/minor amount (TRY/USD/EUR/GBP).
120    Money,
121    /// Approved unit quantity or bounded rate.
122    Unit,
123    /// Gregorian date.
124    Date,
125    /// 24-hour digital clock.
126    Time,
127    /// Approved abbreviation.
128    Abbreviation,
129    /// Contextual or explicitly hinted numerical range.
130    Range,
131    /// Grouped Turkish telephone expression.
132    Telephone,
133    /// Full checksum-valid Turkish IBAN.
134    Iban,
135    /// Canonical uppercase Roman numeral with explicit/contextual intent.
136    Roman,
137    /// Supported email or web address.
138    Electronic,
139    /// Approved prose hashtag or ampersand.
140    Symbol,
141    /// Source-faithful fallback, not certification of the source value.
142    Fallback,
143}
144
145/// Source family attempted before fallback rendering.
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize))]
148pub enum FallbackClass {
149    /// Exact numeric notation.
150    Number,
151    /// Written date components, which may not form a valid calendar date.
152    Date,
153    /// Written clock components.
154    Time,
155    /// Percentage notation.
156    Percent,
157    /// Currency, measurement or rate notation.
158    Quantity,
159    /// Unapproved abbreviation.
160    Abbreviation,
161    /// Identifier-like source, including phone/account forms.
162    Identifier,
163    /// Roman-looking letters without established numeral intent.
164    Roman,
165    /// Address-like source.
166    Electronic,
167    /// Other structured notation, including ambiguous operators.
168    Expression,
169    /// Otherwise-unhandled symbolic graphemes.
170    Symbol,
171}
172
173/// Why a primary reading was unavailable; contains no copied source values.
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize))]
176pub enum FallbackReason {
177    /// Insufficient source reading intent or context.
178    MissingIntent,
179    /// Significant leading zeroes require a faithful digit reading.
180    LeadingZeroes,
181    /// A supported family rejected source grammar or logical value.
182    InvalidForm,
183    /// An identifier has no approved normal reading.
184    ProtectedIdentifier,
185    /// A source form is outside the normal grammar.
186    UnsupportedForm,
187    /// No approved abbreviation expansion exists.
188    UnapprovedAbbreviation,
189    /// A grapheme otherwise would remain unhandled.
190    UnhandledSymbol,
191}
192
193/// Applied source-faithful rendering strategy.
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize))]
196pub enum FallbackStrategy {
197    /// A validated number or neutral sentence-final numeric period.
198    PreferredNumber,
199    /// A validated date, adopting its written date format without an explicit cue.
200    PreferredDate,
201    /// A validated clock, adopting its written clock format without an explicit cue.
202    PreferredTime,
203    /// Date-shaped source components without calendar certification.
204    SurfaceDate,
205    /// Clock-shaped source components without clock certification.
206    SurfaceTime,
207    /// Ordered literal words, letters, digits and symbol names.
208    Literal,
209    /// One or more unnamed scalars spoken as conventional hexadecimal U+ codes.
210    UnicodeCodePoint,
211}
212
213/// Immutable provenance for one handled original-source fallback span.
214#[derive(Clone, Debug, Eq, PartialEq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize))]
216pub struct FallbackDiagnostic {
217    pub(crate) range: SourceRange,
218    pub(crate) attempted_class: FallbackClass,
219    pub(crate) reason: FallbackReason,
220    pub(crate) original_category: Option<IssueCategory>,
221    pub(crate) strategy: FallbackStrategy,
222}
223
224impl FallbackDiagnostic {
225    /// Original-source half-open UTF-8 range.
226    pub const fn range(&self) -> SourceRange {
227        self.range
228    }
229    /// Source family, not a claim that its value was valid.
230    pub const fn attempted_class(&self) -> FallbackClass {
231        self.attempted_class
232    }
233    /// Primary-reading reason or uncovered-symbol reason.
234    pub const fn reason(&self) -> FallbackReason {
235        self.reason
236    }
237    /// Original primary issue category, absent for newly covered symbols.
238    pub const fn original_category(&self) -> Option<IssueCategory> {
239        self.original_category
240    }
241    /// Strategy which completed the reading.
242    pub const fn strategy(&self) -> FallbackStrategy {
243        self.strategy
244    }
245}
246
247/// Machine-readable reason for preserved linguistic work.
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize))]
250pub enum IssueCategory {
251    /// Multiple or insufficiently cued readings.
252    Ambiguous,
253    /// Invalid supported grammar, value, or suffix allomorph.
254    InvalidExpression,
255    /// Structured identifier that must not be rewritten in fragments.
256    ProtectedIdentifier,
257    /// Expression outside the normalizer's bounded coverage.
258    Unsupported,
259    /// Unapproved uppercase abbreviation.
260    UnknownAbbreviation,
261}
262
263/// Non-sensitive diagnostic for an unresolved original-source range.
264#[derive(Clone, Debug, Eq, PartialEq)]
265#[cfg_attr(feature = "serde", derive(serde::Serialize))]
266pub struct Issue {
267    pub(crate) range: SourceRange,
268    pub(crate) category: IssueCategory,
269    pub(crate) explanation: &'static str,
270}
271
272impl Issue {
273    /// Original-source range.
274    pub const fn range(&self) -> SourceRange {
275        self.range
276    }
277    /// Machine-readable category.
278    pub const fn category(&self) -> IssueCategory {
279        self.category
280    }
281    /// Static explanation; contains no copied input values.
282    pub const fn explanation(&self) -> &'static str {
283        self.explanation
284    }
285}
286
287/// One member of an ordered, contiguous original-source partition.
288#[derive(Clone, Debug, Eq, PartialEq)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize))]
290pub struct Segment {
291    pub(crate) range: SourceRange,
292    pub(crate) kind: SegmentKind,
293    pub(crate) text: String,
294    pub(crate) rule_id: &'static str,
295}
296
297impl Segment {
298    /// Original-source range.
299    pub const fn range(&self) -> SourceRange {
300        self.range
301    }
302    /// Semantic kind.
303    pub const fn kind(&self) -> SegmentKind {
304        self.kind
305    }
306    /// Emitted text.
307    pub fn text(&self) -> &str {
308        &self.text
309    }
310    /// Diagnostic identifier of the reader that emitted this segment.
311    pub const fn rule_id(&self) -> &'static str {
312        self.rule_id
313    }
314}
315
316/// Owned immutable result. Completeness concerns TN work, not voice quality.
317#[derive(Clone, Debug, Eq, PartialEq)]
318pub struct NormalizeResult {
319    pub(crate) normalized_text: String,
320    pub(crate) locale: &'static str,
321    pub(crate) normalizer_id: &'static str,
322    pub(crate) complete: bool,
323    pub(crate) segments: Vec<Segment>,
324    pub(crate) issues: Vec<Issue>,
325    pub(crate) fallbacks: Vec<FallbackDiagnostic>,
326}
327
328impl NormalizeResult {
329    /// Concatenation of all emitted segment text.
330    pub fn normalized_text(&self) -> &str {
331        &self.normalized_text
332    }
333    /// Selected locale.
334    pub const fn locale(&self) -> &'static str {
335        self.locale
336    }
337    /// Diagnostic identity of the normalizer build.
338    pub const fn normalizer_id(&self) -> &'static str {
339        self.normalizer_id
340    }
341    /// Whether there are no unresolved TN issues.
342    pub fn complete(&self) -> bool {
343        self.complete
344    }
345    /// Ordered original-source partition.
346    pub fn segments(&self) -> &[Segment] {
347        &self.segments
348    }
349    /// Ordered unresolved diagnostics.
350    pub fn issues(&self) -> &[Issue] {
351        &self.issues
352    }
353    /// Handled source-reading assumptions, separate from unresolved issues.
354    pub fn fallbacks(&self) -> &[FallbackDiagnostic] {
355        &self.fallbacks
356    }
357    /// Whether a fallback reading was used; derived from the diagnostic collection.
358    pub fn fallback_used(&self) -> bool {
359        !self.fallbacks.is_empty()
360    }
361}
362
363#[cfg(feature = "serde")]
364impl serde::Serialize for NormalizeResult {
365    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
366    where
367        S: serde::Serializer,
368    {
369        use serde::ser::SerializeStruct;
370        let mut result = serializer.serialize_struct("NormalizeResult", 8)?;
371        result.serialize_field("normalized_text", &self.normalized_text)?;
372        result.serialize_field("locale", &self.locale)?;
373        result.serialize_field("normalizer_id", &self.normalizer_id)?;
374        result.serialize_field("complete", &self.complete)?;
375        result.serialize_field("segments", &self.segments)?;
376        result.serialize_field("issues", &self.issues)?;
377        result.serialize_field("fallbacks", &self.fallbacks)?;
378        result.serialize_field("fallback_used", &self.fallback_used())?;
379        result.end()
380    }
381}
382
383/// Resource limit which was exceeded.
384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
385pub enum LimitKind {
386    /// Original UTF-8 input length.
387    Input,
388    /// Hint count.
389    Hints,
390    /// Candidate work records.
391    Candidates,
392    /// Logical owned result allocation.
393    Result,
394}
395
396/// Explicit input, policy, resource, control, or engine failure.
397#[derive(Clone, Debug, Eq, PartialEq)]
398pub enum NormalizeError {
399    /// Empty/all-whitespace input or unsupported control/Bidi_Control characters.
400    InvalidInput,
401    /// Invalid hint range, content, overlap, or protected-expression boundary.
402    InvalidHint,
403    /// Bundled assets or project-owned patterns are inconsistent.
404    InvalidConfiguration,
405    /// A documented resource limit was exceeded.
406    LimitExceeded(LimitKind),
407    /// Cooperative cancellation or monotonic deadline.
408    Cancelled,
409    /// Strict-mode unresolved linguistic diagnostics.
410    Unresolved(Vec<Issue>),
411    /// Unexpected violated internal invariant.
412    Internal,
413}
414
415impl fmt::Display for NormalizeError {
416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        let message = match self {
418            Self::InvalidInput => "input is empty, whitespace-only, or contains forbidden controls",
419            Self::InvalidHint => "hint is not a valid whole-expression original-source range",
420            Self::InvalidConfiguration => "built-in normalizer configuration is invalid",
421            Self::LimitExceeded(_) => "normalization resource limit exceeded",
422            Self::Cancelled => "normalization was cancelled or its deadline expired",
423            Self::Unresolved(_) => "strict normalization contains unresolved linguistic work",
424            Self::Internal => "normalization invariant failed",
425        };
426        f.write_str(message)
427    }
428}
429
430impl std::error::Error for NormalizeError {}
431
432/// Runtime-neutral cooperative control. Clones share the cancellation signal.
433#[derive(Clone, Debug, Default)]
434pub struct WorkControl {
435    cancelled: Arc<AtomicBool>,
436    deadline: Option<Instant>,
437}
438
439impl WorkControl {
440    /// Construct control with an optional monotonic deadline.
441    pub fn new(deadline: Option<Instant>) -> Self {
442        Self {
443            deadline,
444            ..Self::default()
445        }
446    }
447    /// Cancel this control and all its clones.
448    pub fn cancel(&self) {
449        self.cancelled.store(true, Ordering::Relaxed);
450    }
451    pub(crate) fn check(&self) -> Result<(), NormalizeError> {
452        if self.cancelled.load(Ordering::Relaxed)
453            || self
454                .deadline
455                .is_some_and(|deadline| Instant::now() >= deadline)
456        {
457            Err(NormalizeError::Cancelled)
458        } else {
459            Ok(())
460        }
461    }
462}