Skip to main content

icu_segmenter/
sentence.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use alloc::vec::Vec;
6use icu_locale_core::LanguageIdentifier;
7use icu_provider::prelude::*;
8
9use crate::indices::*;
10use crate::provider::*;
11use crate::scaffold::*;
12
13/// Options to tailor sentence breaking behavior.
14#[non_exhaustive]
15#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
16pub struct SentenceBreakOptions<'a> {
17    /// Content locale for sentence segmenter.
18    ///
19    /// If you know the language of the text being segmented, provide it here in order to produce
20    /// higher quality breakpoints.
21    ///
22    /// # Examples
23    ///
24    /// Normally, a semicolon character ';' is not a sentence separator:
25    ///
26    /// ```rust
27    /// use icu::segmenter::SentenceSegmenter;
28    ///
29    /// let segmenter = SentenceSegmenter::new(Default::default());
30    ///
31    /// let breakpoints: Vec<usize> =
32    ///     segmenter.segment_str("hello; world").collect();
33    /// assert_eq!(&breakpoints, &[0, 12]);
34    /// ```
35    ///
36    /// But not in Greek, where it is used as a question mark:
37    ///
38    /// ```rust
39    /// use icu::locale::langid;
40    /// use icu::segmenter::SentenceSegmenter;
41    /// use icu::segmenter::options::SentenceBreakOptions;
42    ///
43    /// let mut options = SentenceBreakOptions::default();
44    /// let langid = &langid!("el");
45    /// options.content_locale = Some(langid);
46    /// let segmenter = SentenceSegmenter::try_new(options).unwrap();
47    ///
48    /// let breakpoints: Vec<usize> = segmenter
49    ///     .as_borrowed()
50    ///     .segment_str("hello; world")
51    ///     .collect();
52    /// assert_eq!(&breakpoints, &[0, 7, 12]);
53    /// ```
54    pub content_locale: Option<&'a LanguageIdentifier>,
55    /// Options independent of the locale
56    pub invariant_options: SentenceBreakInvariantOptions,
57}
58
59/// Locale-independent options to tailor sentence breaking behavior
60///
61/// Currently empty but may grow in the future
62#[non_exhaustive]
63#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
64pub struct SentenceBreakInvariantOptions {}
65
66/// Implements the [`Iterator`] trait over the sentence boundaries of the given string.
67///
68/// Lifetimes:
69///
70/// - `'data` = lifetime of the segmenter object from which this iterator was created
71/// - `'s` = lifetime of the string being segmented
72///
73/// The [`Iterator::Item`] is an [`usize`] representing index of a code unit
74/// _after_ the boundary (for a boundary at the end of text, this index is the length
75/// of the [`str`] or array of code units).
76///
77/// For examples of use, see [`SentenceSegmenter`].
78#[derive(Debug)]
79pub struct SentenceBreakIterator<'data, 's, Y: RuleBreakType>(
80    SentenceBreakIteratorInner<'data, 's, Y>,
81);
82
83#[derive(Debug)]
84enum SentenceBreakIteratorInner<'data, 's, Y: RuleBreakType> {
85    V1(crate::rule_segmenter_v1::RuleBreakIterator<'data, 's, Y>),
86    #[cfg(feature = "unstable")]
87    V2(crate::rule_segmenter_v2::RuleBreakIterator<'data, 's, Y>),
88}
89
90impl<Y: RuleBreakType> Iterator for SentenceBreakIterator<'_, '_, Y> {
91    type Item = usize;
92    #[inline]
93    fn next(&mut self) -> Option<Self::Item> {
94        match &mut self.0 {
95            SentenceBreakIteratorInner::V1(iter) => iter.next(),
96            #[cfg(feature = "unstable")]
97            SentenceBreakIteratorInner::V2(iter) => iter.next(),
98        }
99    }
100}
101
102/// Supports loading sentence break data, and creating sentence break iterators for different string
103/// encodings.
104///
105/// Most segmentation methods live on [`SentenceSegmenterBorrowed`], which can be obtained via
106/// [`SentenceSegmenter::new()`] or [`SentenceSegmenter::as_borrowed()`].
107///
108/// Sentence segmenter is currently compatible with [Unicode Standard Annex #29][UAX29] (Version 17.0.0).
109///
110/// [UAX29]: https://www.unicode.org/reports/tr29/tr29-47.html
111///
112/// # Content Locale
113///
114/// You can optionally provide a _content locale_ to the [`SentenceSegmenter`] constructor. If you
115/// have information on the language of the text being segmented, providing this hint can
116/// produce higher-quality results.
117///
118/// If you have a content locale, use [`SentenceBreakOptions`] and a constructor begining with `new`.
119/// If you do not have a content locale use [`SentenceBreakInvariantOptions`] and a constructor
120/// beginning with `try_new`.
121///
122/// # Examples
123///
124/// Segment a string:
125///
126/// ```rust
127/// use icu::segmenter::SentenceSegmenter;
128///
129/// let segmenter = SentenceSegmenter::new(Default::default());
130///
131/// let breakpoints: Vec<usize> =
132///     segmenter.segment_str("Hello World").collect();
133/// assert_eq!(&breakpoints, &[0, 11]);
134/// ```
135///
136/// Segment a Latin1 byte string with a content locale:
137///
138/// ```rust
139/// use icu::locale::langid;
140/// use icu::segmenter::SentenceSegmenter;
141/// use icu::segmenter::options::SentenceBreakOptions;
142///
143/// let mut options = SentenceBreakOptions::default();
144/// let langid = &langid!("en");
145/// options.content_locale = Some(langid);
146/// let segmenter = SentenceSegmenter::try_new(options).unwrap();
147///
148/// let breakpoints: Vec<usize> = segmenter
149///     .as_borrowed()
150///     .segment_latin1(b"Hello World")
151///     .collect();
152/// assert_eq!(&breakpoints, &[0, 11]);
153/// ```
154///
155/// Successive boundaries can be used to retrieve the sentences.
156/// In particular, the first boundary is always 0, and the last one is the
157/// length of the segmented text in code units.
158///
159/// ```rust
160/// # use icu::segmenter::{SentenceSegmenter, options::SentenceBreakInvariantOptions};
161/// # let segmenter = SentenceSegmenter::new(SentenceBreakInvariantOptions::default());
162/// use itertools::Itertools;
163/// let text = "Ceci tuera cela. Le livre tuera l’édifice.";
164/// let sentences: Vec<&str> = segmenter
165///     .segment_str(text)
166///     .tuple_windows()
167///     .map(|(i, j)| &text[i..j])
168///     .collect();
169/// assert_eq!(
170///     &sentences,
171///     &["Ceci tuera cela. ", "Le livre tuera l’édifice."]
172/// );
173/// ```
174#[derive(Debug)]
175pub struct SentenceSegmenter(SentenceSegmenterInner);
176
177#[derive(Debug)]
178enum SentenceSegmenterInner {
179    V1 {
180        payload: DataPayload<SegmenterBreakSentenceV1>,
181        payload_locale_override: Option<DataPayload<SegmenterBreakSentenceOverrideV1>>,
182    },
183    #[cfg(feature = "unstable")]
184    V2 {
185        payload: DataPayload<SegmenterBreakSentenceV2>,
186        tailoring: Option<DataPayload<SegmenterBreakSentenceOverrideV2>>,
187    },
188}
189
190/// Segments a string into sentences (borrowed version).
191///
192/// See [`SentenceSegmenter`] for examples.
193#[derive(Clone, Debug, Copy)]
194pub struct SentenceSegmenterBorrowed<'data>(SentenceSegmenterBorrowedInner<'data>);
195
196#[derive(Clone, Debug, Copy)]
197enum SentenceSegmenterBorrowedInner<'data> {
198    V1 {
199        data: &'data RuleBreakData<'data>,
200        locale_override: Option<&'data RuleBreakDataOverride<'data>>,
201    },
202    #[cfg(feature = "unstable")]
203    V2 {
204        data: &'data SegmenterStateMachine<'data>,
205        tailoring: Option<&'data SegmenterStateMachineOverride<'data>>,
206    },
207}
208
209impl SentenceSegmenter {
210    /// Constructs a [`SentenceSegmenterBorrowed`] with an invariant locale and compiled data.
211    ///
212    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
213    ///
214    /// [📚 Help choosing a constructor](icu_provider::constructors)
215    #[cfg(feature = "compiled_data")]
216    #[expect(clippy::new_ret_no_self)]
217    pub const fn new(
218        _options: SentenceBreakInvariantOptions,
219    ) -> SentenceSegmenterBorrowed<'static> {
220        SentenceSegmenterBorrowed(SentenceSegmenterBorrowedInner::V1 {
221            data: Baked::SINGLETON_SEGMENTER_BREAK_SENTENCE_V1,
222            locale_override: None,
223        })
224    }
225
226    icu_provider::gen_buffer_data_constructors!(
227        (options: SentenceBreakOptions) -> error: DataError,
228        /// Constructs a [`SentenceSegmenter`] for a given options and using compiled data.
229        functions: [
230            try_new,
231            try_new_with_buffer_provider,
232            try_new_unstable,
233            Self
234        ]
235    );
236
237    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
238    pub fn try_new_unstable<D>(
239        provider: &D,
240        options: SentenceBreakOptions,
241    ) -> Result<Self, DataError>
242    where
243        D: DataProvider<SegmenterBreakSentenceV1>
244            + DataProvider<SegmenterBreakSentenceOverrideV1>
245            + ?Sized,
246    {
247        let payload = provider.load(Default::default())?.payload;
248        let payload_locale_override = if let Some(locale) = options.content_locale {
249            let locale = DataLocale::from(locale);
250            let req = DataRequest {
251                id: DataIdentifierBorrowed::for_locale(&locale),
252                metadata: {
253                    let mut metadata = DataRequestMetadata::default();
254                    metadata.silent = true;
255                    metadata
256                },
257            };
258            provider
259                .load(req)
260                .allow_identifier_not_found()?
261                .map(|r| r.payload)
262        } else {
263            None
264        };
265
266        Ok(Self(SentenceSegmenterInner::V1 {
267            payload,
268            payload_locale_override,
269        }))
270    }
271
272    /// Constructs a [`SentenceSegmenterBorrowed`] with an invariant locale and compiled data.
273    ///
274    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
275    ///
276    /// [📚 Help choosing a constructor](icu_provider::constructors)
277    #[cfg(feature = "compiled_data")]
278    #[cfg(feature = "unstable")]
279    pub const fn new_neo(
280        _options: SentenceBreakInvariantOptions,
281    ) -> SentenceSegmenterBorrowed<'static> {
282        SentenceSegmenterBorrowed(SentenceSegmenterBorrowedInner::V2 {
283            data: Baked::SINGLETON_SEGMENTER_BREAK_SENTENCE_V2,
284            tailoring: None,
285        })
286    }
287
288    /// Constructs a [`SentenceSegmenter`] for a given options and using compiled data.
289    ///
290    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
291    ///
292    /// [📚 Help choosing a constructor](icu_provider::constructors)
293    #[cfg(feature = "compiled_data")]
294    #[cfg(feature = "unstable")]
295    pub fn try_new_neo(options: SentenceBreakOptions) -> Result<Self, DataError> {
296        Self::try_new_neo_unstable(&Baked, options)
297    }
298
299    #[cfg(feature = "unstable")]
300    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new_neo)]
301    pub fn try_new_neo_unstable<D>(
302        provider: &D,
303        options: SentenceBreakOptions,
304    ) -> Result<Self, DataError>
305    where
306        D: DataProvider<SegmenterBreakSentenceV2>
307            + DataProvider<SegmenterBreakSentenceOverrideV2>
308            + ?Sized,
309    {
310        let payload = provider.load(Default::default())?.payload;
311        let tailoring = if let Some(locale) = options.content_locale {
312            provider
313                .load(DataRequest {
314                    id: DataIdentifierBorrowed::for_locale(&DataLocale::from(locale)),
315                    metadata: {
316                        let mut metadata = DataRequestMetadata::default();
317                        metadata.silent = true;
318                        metadata
319                    },
320                })
321                .allow_identifier_not_found()?
322                .map(|r| r.payload)
323        } else {
324            None
325        };
326
327        Ok(Self(SentenceSegmenterInner::V2 { payload, tailoring }))
328    }
329
330    /// Constructs a borrowed version of this type for more efficient querying.
331    ///
332    /// Most useful methods for segmentation are on this type.
333    pub fn as_borrowed(&self) -> SentenceSegmenterBorrowed<'_> {
334        match &self.0 {
335            SentenceSegmenterInner::V1 {
336                payload,
337                payload_locale_override,
338            } => SentenceSegmenterBorrowed(SentenceSegmenterBorrowedInner::V1 {
339                data: payload.get(),
340                locale_override: payload_locale_override.as_ref().map(|p| p.get()),
341            }),
342            #[cfg(feature = "unstable")]
343            SentenceSegmenterInner::V2 { payload, tailoring } => {
344                SentenceSegmenterBorrowed(SentenceSegmenterBorrowedInner::V2 {
345                    data: payload.get(),
346                    tailoring: tailoring.as_ref().map(|p| p.get()),
347                })
348            }
349        }
350    }
351}
352
353impl<'data> SentenceSegmenterBorrowed<'data> {
354    fn segment<'s, Y: RuleBreakType>(
355        self,
356        iter: Y::IterAttr<'s>,
357        len: usize,
358    ) -> SentenceBreakIterator<'data, 's, Y> {
359        match self.0 {
360            SentenceSegmenterBorrowedInner::V1 {
361                data,
362                locale_override,
363            } => SentenceBreakIterator(SentenceBreakIteratorInner::V1(
364                crate::rule_segmenter_v1::RuleBreakIterator {
365                    iter,
366                    len,
367                    current_pos_data: None,
368                    result_cache: Vec::new(),
369                    data,
370                    complex: None,
371                    boundary_property: 0,
372                    locale_override,
373                    handle_complex: crate::rule_segmenter_v1::empty_handle_complex,
374                },
375            )),
376            #[cfg(feature = "unstable")]
377            SentenceSegmenterBorrowedInner::V2 { data, tailoring } => {
378                SentenceBreakIterator(SentenceBreakIteratorInner::V2(
379                    crate::rule_segmenter_v2::RuleBreakIterator::new(iter, data, tailoring, None),
380                ))
381            }
382        }
383    }
384
385    /// Creates a sentence break iterator for an `str` (a UTF-8 string).
386    ///
387    /// There are always breakpoints at 0 and the string length, or only at 0 for the empty string.
388    pub fn segment_str<'s>(self, input: &'s str) -> SentenceBreakIterator<'data, 's, Utf8> {
389        self.segment(input.char_indices(), input.len())
390    }
391    /// Creates a sentence break iterator for a potentially ill-formed UTF8 string
392    ///
393    /// Invalid characters are treated as REPLACEMENT CHARACTER
394    ///
395    /// There are always breakpoints at 0 and the string length, or only at 0 for the empty string.
396    pub fn segment_utf8<'s>(
397        self,
398        input: &'s [u8],
399    ) -> SentenceBreakIterator<'data, 's, PotentiallyIllFormedUtf8> {
400        self.segment(Utf8CharIndices::new(input), input.len())
401    }
402    /// Creates a sentence break iterator for a Latin-1 (8-bit) string.
403    ///
404    /// There are always breakpoints at 0 and the string length, or only at 0 for the empty string.
405    pub fn segment_latin1<'s>(self, input: &'s [u8]) -> SentenceBreakIterator<'data, 's, Latin1> {
406        self.segment(Latin1Indices::new(input), input.len())
407    }
408
409    /// Creates a sentence break iterator for a UTF-16 string.
410    ///
411    /// There are always breakpoints at 0 and the string length, or only at 0 for the empty string.
412    pub fn segment_utf16<'s>(self, input: &'s [u16]) -> SentenceBreakIterator<'data, 's, Utf16> {
413        self.segment(Utf16Indices::new(input), input.len())
414    }
415}
416
417impl SentenceSegmenterBorrowed<'static> {
418    /// Cheaply converts a [`SentenceSegmenterBorrowed<'static>`] into a [`SentenceSegmenter`].
419    ///
420    /// Note: Due to branching and indirection, using [`SentenceSegmenter`] might inhibit some
421    /// compile-time optimizations that are possible with [`SentenceSegmenterBorrowed`].
422    pub const fn static_to_owned(self) -> SentenceSegmenter {
423        SentenceSegmenter(match self.0 {
424            SentenceSegmenterBorrowedInner::V1 {
425                data,
426                locale_override,
427            } => SentenceSegmenterInner::V1 {
428                payload: DataPayload::from_static_ref(data),
429                payload_locale_override: if let Some(d) = locale_override {
430                    Some(DataPayload::from_static_ref(d))
431                } else {
432                    None
433                },
434            },
435            #[cfg(feature = "unstable")]
436            SentenceSegmenterBorrowedInner::V2 { data, tailoring } => SentenceSegmenterInner::V2 {
437                payload: DataPayload::from_static_ref(data),
438                tailoring: if let Some(d) = tailoring {
439                    Some(DataPayload::from_static_ref(d))
440                } else {
441                    None
442                },
443            },
444        })
445    }
446}
447
448#[test]
449fn empty_string() {
450    let segmenter = SentenceSegmenter::new(Default::default());
451    let breaks: Vec<usize> = segmenter.segment_str("").collect();
452    assert_eq!(breaks, [0]);
453}
454
455#[test]
456fn empty_string_neo() {
457    let segmenter = SentenceSegmenter::new_neo(Default::default());
458    let breaks: Vec<usize> = segmenter.segment_str("").collect();
459    assert_eq!(breaks, [0]);
460}