use std::{
borrow::Cow,
fs, io,
ops::{Deref, Index},
path::{Path, PathBuf},
slice,
sync::LazyLock,
};
use thiserror::Error;
use crate::{metadata::WordListMetadata, newline_delimited_words};
pub(crate) type Word = String;
pub(crate) type WordSource = Box<[Word]>;
#[derive(Debug)]
pub struct WordList {
words: EagerOrLazy<WordSource>,
pub metadata: WordListMetadata,
}
impl WordList {
#[doc = include_str!("../data/aosp/en_Latn.toml")]
#[allow(clippy::result_large_err)]
pub fn load(
path: impl AsRef<Path>,
metadata_path: impl AsRef<Path>,
) -> Result<Self, WordListError> {
let mut word_list = WordList::load_without_metadata(path)?;
word_list.metadata = WordListMetadata::load(metadata_path)?;
Ok(word_list)
}
#[allow(clippy::result_large_err)]
pub fn load_without_metadata(
path: impl AsRef<Path>,
) -> Result<Self, WordListError> {
let path = path.as_ref();
let file_content = fs::read_to_string(path).map_err(|io_err| {
WordListError::FailedToRead(path.to_owned(), io_err)
})?;
let name = path
.file_stem()
.ok_or_else(|| {
WordListError::FailedToRead(
path.to_owned(),
io::Error::new(
io::ErrorKind::InvalidData,
"file name is empty",
),
)
})?
.to_string_lossy()
.replace("/", "_");
Ok(WordList {
metadata: WordListMetadata::new_from_name(name),
words: newline_delimited_words(file_content).into(),
})
}
#[must_use]
pub fn define(
name_or_metadata: impl Into<WordListMetadata>,
words: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
WordList {
metadata: name_or_metadata.into(),
words: words.into_iter().map(Into::into).collect::<Vec<_>>().into(),
}
}
#[must_use]
pub(crate) const fn new_lazy(
metadata: WordListMetadata,
words: LazyLock<WordSource>,
) -> Self {
WordList {
words: EagerOrLazy::Lazy(words),
metadata,
}
}
#[allow(dead_code)]
pub(crate) const fn stub() -> Self {
WordList {
metadata: WordListMetadata {
name: Cow::Borrowed("stub"),
script: None,
language: None,
},
words: EagerOrLazy::Lazy(LazyLock::new(|| unreachable!())),
}
}
#[inline]
#[must_use]
pub fn name(&self) -> &str {
self.metadata.name()
}
#[inline]
#[must_use]
pub fn script(&self) -> Option<&str> {
self.metadata.script()
}
#[inline]
#[must_use]
pub fn language(&self) -> Option<&str> {
self.metadata.language()
}
#[must_use]
pub fn iter(&self) -> WordListIter<'_> {
WordListIter(self.words.iter())
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.words.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.words.is_empty()
}
pub fn filter<F>(&self, mut predicate: F) -> Self
where
F: FnMut(&str) -> bool,
{
let reduced_words = self
.words
.iter()
.filter(|word| predicate(word))
.cloned()
.collect::<Vec<_>>();
let reduced_words =
EagerOrLazy::Eager(reduced_words.into_boxed_slice());
Self {
metadata: self.metadata.clone(),
words: reduced_words,
}
}
}
impl Clone for WordList {
fn clone(&self) -> Self {
Self {
metadata: self.metadata.clone(),
words: EagerOrLazy::Eager(self.words.deref().clone()),
}
}
}
impl Index<usize> for WordList {
type Output = str;
fn index(&self, index: usize) -> &Self::Output {
self.words.index(index).deref()
}
}
#[derive(Debug)]
enum EagerOrLazy<T> {
Eager(T),
Lazy(LazyLock<T>),
}
impl<T> From<T> for EagerOrLazy<T> {
fn from(value: T) -> Self {
EagerOrLazy::Eager(value)
}
}
impl<T> Deref for EagerOrLazy<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
EagerOrLazy::Eager(e) => e,
EagerOrLazy::Lazy(l) => l,
}
}
}
impl From<Vec<String>> for EagerOrLazy<WordSource> {
fn from(value: Vec<String>) -> Self {
Self::Eager(value.into_boxed_slice())
}
}
#[derive(Debug)]
pub struct WordListIter<'a>(slice::Iter<'a, String>);
impl<'a> Iterator for WordListIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(String::as_ref)
}
}
impl ExactSizeIterator for WordListIter<'_> {
fn len(&self) -> usize {
self.0.len()
}
}
impl DoubleEndedIterator for WordListIter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(String::as_ref)
}
}
#[derive(Debug, Error)]
pub enum WordListError {
#[error("failed to read from {}: {}", .0.display(), .1)]
FailedToRead(PathBuf, io::Error),
#[error("failed to parse metadata from {}: {}", .0.display(), .1)]
MetadataError(PathBuf, toml::de::Error),
}
#[cfg(feature = "rayon")]
pub(crate) mod rayon {
use rayon::iter::{
IndexedParallelIterator, ParallelIterator,
plumbing::{
Consumer, Producer, ProducerCallback, UnindexedConsumer, bridge,
},
};
use super::{WordList, WordListIter};
#[derive(Debug)]
pub struct ParWordListIter<'a>(&'a [String]);
impl<'a> ParallelIterator for ParWordListIter<'a> {
type Item = &'a str;
fn drive_unindexed<C>(self, consumer: C) -> C::Result
where
C: UnindexedConsumer<Self::Item>,
{
bridge(self, consumer)
}
fn opt_len(&self) -> Option<usize> {
Some(self.0.len())
}
}
impl<'a> Producer for ParWordListIter<'a> {
type IntoIter = WordListIter<'a>;
type Item = &'a str;
fn into_iter(self) -> Self::IntoIter {
WordListIter(self.0.iter())
}
fn split_at(self, index: usize) -> (Self, Self) {
let (left, right) = self.0.split_at(index);
(ParWordListIter(left), ParWordListIter(right))
}
}
impl IndexedParallelIterator for ParWordListIter<'_> {
fn len(&self) -> usize {
self.0.len()
}
fn drive<C: Consumer<Self::Item>>(self, consumer: C) -> C::Result {
bridge(self, consumer)
}
fn with_producer<CB>(self, callback: CB) -> CB::Output
where
CB: ProducerCallback<Self::Item>,
{
callback.callback(self)
}
}
impl<'a> rayon::iter::IntoParallelIterator for &'a WordList {
type Item = &'a str;
type Iter = ParWordListIter<'a>;
fn into_par_iter(self) -> Self::Iter {
ParWordListIter(&self.words)
}
}
impl WordList {
#[must_use]
pub fn par_iter(&self) -> ParWordListIter<'_> {
ParWordListIter(&self.words)
}
}
}