#![warn(missing_docs)]
use std::error::Error;
use std::fmt;
#[cfg(feature = "cli")]
pub mod cli;
mod conllu;
mod document;
mod lang;
mod layout;
mod sentences;
mod tokens;
use sentences::Split;
use tokens::Exceptions;
pub use conllu::{ConlluError, read_conllu};
pub use document::{Column, Document, DocumentError, Line, LineBreak, Paragraph, Sentence};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn slice(self, text: &str) -> &str {
&text[self.start..self.end]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Language {
#[cfg(feature = "ru")]
Russian,
}
impl Language {
pub const ALL: &[Language] = &[
#[cfg(feature = "ru")]
Language::Russian,
];
pub fn code(self) -> &'static str {
match self {
#[cfg(feature = "ru")]
Language::Russian => "ru",
}
}
pub fn from_code(code: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|language| language.code() == code)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownLanguage(String);
impl UnknownLanguage {
pub fn code(&self) -> &str {
&self.0
}
}
impl fmt::Display for UnknownLanguage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let codes: Vec<&str> = Language::ALL
.iter()
.map(|language| language.code())
.collect();
write!(
f,
"unsupported language {:?}, expected one of: {}",
self.0,
codes.join(", ")
)
}
}
impl Error for UnknownLanguage {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidEntry {
Abbreviation(String),
Token(String),
}
impl fmt::Display for InvalidEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidEntry::Abbreviation(word) => write!(
f,
"abbreviation {word:?} is not a word of letters with an optional final period"
),
InvalidEntry::Token(token) => write!(
f,
"token {token:?} is empty, has whitespace or control characters or starts or ends with an invisible separator"
),
}
}
}
impl Error for InvalidEntry {}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Segmenter {
language: Language,
exceptions: Exceptions,
}
impl Segmenter {
pub fn new(code: &str) -> Result<Self, UnknownLanguage> {
Language::from_code(code)
.map(|language| Segmenter {
language,
exceptions: Exceptions::default(),
})
.ok_or_else(|| UnknownLanguage(code.to_owned()))
}
pub fn with_abbreviations<I>(mut self, words: I) -> Result<Self, InvalidEntry>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let tables = lang::tables(self.language);
for word in words {
let word = word.as_ref();
let letters = word.strip_suffix('.').unwrap_or(word);
if !tokens::is_letters(letters) {
return Err(InvalidEntry::Abbreviation(word.to_owned()));
}
self.exceptions.abbreviations.insert(tables.key(letters));
}
Ok(self)
}
pub fn with_tokens<I>(mut self, strings: I) -> Result<Self, InvalidEntry>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
for string in strings {
let string = string.as_ref();
if string.is_empty()
|| string.chars().any(|c| c.is_whitespace() || c.is_control())
|| string.starts_with(layout::is_separator)
|| string.ends_with(layout::is_separator)
{
return Err(InvalidEntry::Token(string.to_owned()));
}
self.exceptions.add_token(string);
}
Ok(self)
}
pub fn language(&self) -> Language {
self.language
}
pub fn abbreviations(&self) -> impl ExactSizeIterator<Item = &str> {
self.exceptions.abbreviations.iter().map(String::as_str)
}
pub fn tokens(&self) -> impl ExactSizeIterator<Item = &str> {
self.exceptions.tokens.iter().map(String::as_str)
}
pub fn tokenize(&self, text: &str) -> Vec<Span> {
self.split(text, false).tokens
}
pub fn sentenize(&self, text: &str) -> Vec<Span> {
let split = self.split(text, false);
let tokens = &split.tokens;
(0..split.starts.len())
.map(|index| {
let end = split.starts.get(index + 1).map_or(tokens.len(), |&end| end);
Span {
start: tokens[split.starts[index]].start,
end: tokens[end - 1].end,
}
})
.collect()
}
pub fn segment(&self, text: &str) -> Document {
self.segment_string(text.to_owned())
}
pub(crate) fn segment_string(&self, text: String) -> Document {
let split = self.split(&text, true);
Document::segmented(
text,
split.tokens,
split.starts,
split.paragraphs,
split.lines.unwrap_or_default(),
)
}
fn split(&self, text: &str, lines: bool) -> Split {
let tables = lang::tables(self.language);
let mut split = Split {
tokens: Vec::with_capacity(text.len() / 8),
lines: lines.then(Vec::new),
..Split::default()
};
let mut paragraph = Vec::new();
for paragraph_lines in layout::paragraphs(text) {
let span = Span {
start: paragraph_lines[0].start,
end: paragraph_lines[paragraph_lines.len() - 1].end,
};
paragraph.clear();
tokens::tokenize(text, span, tables, &self.exceptions, &mut paragraph);
sentences::split(
text,
¶graph_lines,
¶graph,
tables,
&self.exceptions,
&mut split,
);
}
split
}
}
#[cfg(all(test, feature = "ru"))]
mod tests {
use super::*;
fn pieces<'a>(text: &'a str, spans: &[Span]) -> Vec<&'a str> {
spans.iter().map(|span| span.slice(text)).collect()
}
#[test]
fn language_codes() {
assert_eq!(Language::from_code("ru"), Some(Language::Russian));
assert_eq!(Language::Russian.code(), "ru");
assert_eq!(Language::from_code("xx"), None);
assert!(Language::ALL.contains(&Language::Russian));
}
#[test]
fn unknown_language() {
let error = Segmenter::new("xx").unwrap_err();
assert_eq!(error, UnknownLanguage("xx".to_owned()));
assert_eq!(error.code(), "xx");
assert_eq!(
error.to_string(),
"unsupported language \"xx\", expected one of: ru"
);
assert_eq!(Segmenter::new("ru").unwrap().language(), Language::Russian);
}
#[test]
fn tokens_are_byte_spans() {
let text = " Ёж\u{a0}идёт\t😀\r\n";
let spans = Segmenter::new("ru").unwrap().tokenize(text);
assert_eq!(
spans,
[
Span { start: 1, end: 5 },
Span { start: 7, end: 15 },
Span { start: 16, end: 20 },
]
);
assert_eq!(pieces(text, &spans), ["Ёж", "идёт", "😀"]);
}
#[test]
fn blank_text() {
let segmenter = Segmenter::new("ru").unwrap();
for text in ["", " ", "\n\u{2028}\u{3000}"] {
assert!(segmenter.tokenize(text).is_empty());
assert!(segmenter.sentenize(text).is_empty());
}
}
#[test]
fn sentences_inside_paragraphs() {
let text = "\n Мама мыла раму.\nРама чистая.\u{a0}\n \n\u{feff}Утро";
let segmenter = Segmenter::new("ru").unwrap();
let spans = segmenter.sentenize(text);
assert_eq!(
pieces(text, &spans),
["Мама мыла раму.", "Рама чистая.", "Утро"]
);
let doc = segmenter.segment(text);
assert_eq!(doc.tokens(), segmenter.tokenize(text));
let sentences: Vec<Span> = (0..doc.sentence_count())
.map(|index| doc.sentence(index).span)
.collect();
assert_eq!(sentences, spans);
assert_eq!((doc.paragraph_count(), doc.lines().len()), (2, 3));
assert_eq!(doc.paragraph(0).sentences, 0..2);
}
#[test]
fn lists_of_the_user() {
let text = "На C++, C++11 и aC++ сущ. и глаг. т.н.\n\nТаков т.н.";
let default = Segmenter::new("ru").unwrap();
assert_eq!(
pieces(text, &default.tokenize(text)),
[
"На",
"C",
"++",
",",
"C",
"++",
"11",
"и",
"aC",
"++",
"сущ",
".",
"и",
"глаг",
".",
"т.",
"н",
".",
"Таков",
"т.",
"н",
"."
]
);
let segmenter = default
.with_abbreviations(["Сущ.", "глаг", "сущ"])
.unwrap()
.with_tokens(["C+", "C++", "т.н."])
.unwrap();
assert_eq!(
pieces(text, &segmenter.tokenize(text)),
[
"На",
"C++",
",",
"C+",
"+",
"11",
"и",
"aC",
"++",
"сущ.",
"и",
"глаг.",
"т.н.",
"Таков",
"т.н."
]
);
assert_eq!(
segmenter.abbreviations().collect::<Vec<_>>(),
["глаг", "сущ"]
);
assert_eq!(
segmenter.tokens().collect::<Vec<_>>(),
["C+", "C++", "т.н."]
);
assert_ne!(segmenter, Segmenter::new("ru").unwrap());
}
#[test]
fn invalid_entries() {
let segmenter = Segmenter::new("ru").unwrap();
for word in ["", ".", "т.н", "г-н", "2-й", "с "] {
assert_eq!(
segmenter.clone().with_abbreviations([word]),
Err(InvalidEntry::Abbreviation(word.to_owned()))
);
}
for token in ["", "a b", " a", "\u{ad}a", "a\u{200b}", "a\u{1f}b"] {
assert_eq!(
segmenter.clone().with_tokens([token]),
Err(InvalidEntry::Token(token.to_owned()))
);
}
assert_eq!(
InvalidEntry::Abbreviation("т.н".to_owned()).to_string(),
"abbreviation \"т.н\" is not a word of letters with an optional final period"
);
}
#[test]
fn documents_keep_only_what_they_hold() {
let doc = Segmenter::new("ru")
.unwrap()
.segment(&"Ёж идёт домой. Кот спит.\n".repeat(1000));
assert_eq!(doc.tokens.capacity(), doc.tokens.len());
assert_eq!(doc.sentence_tokens.capacity(), doc.sentence_tokens.len());
assert_eq!(
doc.paragraph_sentences.capacity(),
doc.paragraph_sentences.len()
);
assert_eq!(doc.lines.capacity(), doc.lines.len());
}
#[test]
fn many_tokens_of_the_user() {
let strings: Vec<String> = (0..50_000).map(|number| format!("x{number}+")).collect();
let segmenter = Segmenter::new("ru").unwrap().with_tokens(&strings).unwrap();
let text = "x1+ x49999+ x ".repeat(10_000);
assert_eq!(segmenter.tokenize(&text).len(), 30_000);
}
}