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}