snowstem 0.1.0

Snowball stemming for 21 languages, generated from the current Snowball release, with no required dependencies and no_std support
Documentation
//! Snowball stemming for 21 languages.
//!
//! Each algorithm module is generated from the current Snowball release
//! and checked against the official test vocabularies word for word.
//! The crate has no required dependencies, builds without `std`, and
//! stems a word with at most one allocation. Serde support for
//! [`Algorithm`] is an opt-in `serde` feature.
//!
//! ```
//! use snowstem::{Algorithm, Stemmer};
//!
//! let stemmer = Stemmer::create(Algorithm::English);
//! assert_eq!(stemmer.stem("international"), "internat");
//! assert_eq!(stemmer.stem("internal"), "internal");
//! ```
//!
//! Input is expected in lower case. Snowball defines stemming over
//! lowercase words, so casefold and tokenize before calling.
//!
//! # Reusing one buffer
//!
//! [`Stemmer::stem_into`] writes the stem into a `String` you own, so a
//! loop over many words allocates only when the buffer has to grow:
//!
//! ```
//! use snowstem::{Algorithm, Stemmer};
//!
//! let stemmer = Stemmer::create(Algorithm::German);
//! let mut stem = String::new();
//! for word in ["häuser", "gelaufen"] {
//!     stemmer.stem_into(word, &mut stem);
//!     assert!(!stem.is_empty());
//! }
//! ```
//!
//! # Dutch
//!
//! Snowball 3.0 replaced its Dutch algorithm. [`Algorithm::Dutch`] is the
//! current one. [`Algorithm::DutchPorter`] is the earlier algorithm, kept
//! for indexes built with it, since the two produce different stems for
//! about half of the Dutch test vocabulary.

#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]

extern crate alloc;

mod snowball;

use alloc::borrow::Cow;
use alloc::string::String;
use core::fmt;
use core::str::FromStr;

use snowball::algorithms;
use snowball::SnowballEnv;

/// A stemming algorithm, named after the language it handles.
///
/// Parse one from its lowercase name with [`FromStr`].
/// [`Display`](core::fmt::Display) formats it back to that name.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Algorithm {
    /// Arabic.
    Arabic,
    /// Czech.
    Czech,
    /// Danish.
    Danish,
    /// Dutch, the algorithm introduced in Snowball 3.0.
    Dutch,
    /// Dutch, the algorithm Snowball shipped before 3.0.
    DutchPorter,
    /// English.
    English,
    /// Finnish.
    Finnish,
    /// French.
    French,
    /// German.
    German,
    /// Greek.
    Greek,
    /// Hungarian.
    Hungarian,
    /// Italian.
    Italian,
    /// Norwegian.
    Norwegian,
    /// Polish.
    Polish,
    /// Portuguese.
    Portuguese,
    /// Romanian.
    Romanian,
    /// Russian.
    Russian,
    /// Spanish.
    Spanish,
    /// Swedish.
    Swedish,
    /// Tamil.
    Tamil,
    /// Turkish.
    Turkish,
}

impl Algorithm {
    /// Every algorithm, in name order. Drives [`FromStr`].
    const ALL: [Algorithm; 21] = [
        Algorithm::Arabic,
        Algorithm::Czech,
        Algorithm::Danish,
        Algorithm::Dutch,
        Algorithm::DutchPorter,
        Algorithm::English,
        Algorithm::Finnish,
        Algorithm::French,
        Algorithm::German,
        Algorithm::Greek,
        Algorithm::Hungarian,
        Algorithm::Italian,
        Algorithm::Norwegian,
        Algorithm::Polish,
        Algorithm::Portuguese,
        Algorithm::Romanian,
        Algorithm::Russian,
        Algorithm::Spanish,
        Algorithm::Swedish,
        Algorithm::Tamil,
        Algorithm::Turkish,
    ];

    /// The lowercase name, as accepted by [`FromStr`].
    const fn name(self) -> &'static str {
        match self {
            Algorithm::Arabic => "arabic",
            Algorithm::Czech => "czech",
            Algorithm::Danish => "danish",
            Algorithm::Dutch => "dutch",
            Algorithm::DutchPorter => "dutch_porter",
            Algorithm::English => "english",
            Algorithm::Finnish => "finnish",
            Algorithm::French => "french",
            Algorithm::German => "german",
            Algorithm::Greek => "greek",
            Algorithm::Hungarian => "hungarian",
            Algorithm::Italian => "italian",
            Algorithm::Norwegian => "norwegian",
            Algorithm::Polish => "polish",
            Algorithm::Portuguese => "portuguese",
            Algorithm::Romanian => "romanian",
            Algorithm::Russian => "russian",
            Algorithm::Spanish => "spanish",
            Algorithm::Swedish => "swedish",
            Algorithm::Tamil => "tamil",
            Algorithm::Turkish => "turkish",
        }
    }

    fn program(self) -> fn(&mut SnowballEnv) -> bool {
        match self {
            Algorithm::Arabic => algorithms::arabic::stem,
            Algorithm::Czech => algorithms::czech::stem,
            Algorithm::Danish => algorithms::danish::stem,
            Algorithm::Dutch => algorithms::dutch::stem,
            Algorithm::DutchPorter => algorithms::dutch_porter::stem,
            Algorithm::English => algorithms::english::stem,
            Algorithm::Finnish => algorithms::finnish::stem,
            Algorithm::French => algorithms::french::stem,
            Algorithm::German => algorithms::german::stem,
            Algorithm::Greek => algorithms::greek::stem,
            Algorithm::Hungarian => algorithms::hungarian::stem,
            Algorithm::Italian => algorithms::italian::stem,
            Algorithm::Norwegian => algorithms::norwegian::stem,
            Algorithm::Polish => algorithms::polish::stem,
            Algorithm::Portuguese => algorithms::portuguese::stem,
            Algorithm::Romanian => algorithms::romanian::stem,
            Algorithm::Russian => algorithms::russian::stem,
            Algorithm::Spanish => algorithms::spanish::stem,
            Algorithm::Swedish => algorithms::swedish::stem,
            Algorithm::Tamil => algorithms::tamil::stem,
            Algorithm::Turkish => algorithms::turkish::stem,
        }
    }
}

impl fmt::Display for Algorithm {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.name())
    }
}

/// The error from parsing a name no [`Algorithm`] carries.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownAlgorithm {
    _private: (),
}

impl fmt::Display for UnknownAlgorithm {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("unknown stemming algorithm")
    }
}

impl FromStr for Algorithm {
    type Err = UnknownAlgorithm;

    /// Parse a lowercase algorithm name such as `"english"` or
    /// `"dutch_porter"`.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Algorithm::ALL
            .iter()
            .copied()
            .find(|a| a.name() == s)
            .ok_or(UnknownAlgorithm { _private: () })
    }
}

/// A stemmer for one [`Algorithm`].
///
/// Construction wires up a table reference and nothing else, so creating
/// one per call costs nothing. A `Stemmer` is `Send` and `Sync`: all
/// per-call state lives on the stack and in the output.
#[derive(Clone, Copy)]
pub struct Stemmer {
    algorithm: Algorithm,
    program: fn(&mut SnowballEnv) -> bool,
}

impl Stemmer {
    /// A stemmer for `algorithm`.
    pub fn create(algorithm: Algorithm) -> Stemmer {
        Stemmer {
            algorithm,
            program: algorithm.program(),
        }
    }

    /// Stem one lowercase word.
    ///
    /// Returns `Cow::Borrowed` when the stem equals the input. Words up to
    /// 64 bytes are edited on the stack, so an unchanged word costs no
    /// allocation and a changed one costs exactly one, sized to the stem.
    /// Longer words allocate once on their first edit.
    pub fn stem<'a>(&self, input: &'a str) -> Cow<'a, str> {
        SnowballEnv::run(self.program, input)
    }

    /// Stem one lowercase word into `out`, replacing its contents.
    ///
    /// The only allocation is growth of `out`, so a loop that reuses one
    /// buffer stops allocating once the buffer fits the longest word.
    pub fn stem_into(&self, input: &str, out: &mut String) {
        SnowballEnv::run_into(self.program, input, out)
    }
}

impl fmt::Debug for Stemmer {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Stemmer")
            .field("algorithm", &self.algorithm)
            .finish()
    }
}

#[cfg(feature = "serde")]
mod serde_impl {
    use super::Algorithm;
    use core::fmt;
    use serde::de::{Deserialize, Deserializer, Error, Unexpected, Visitor};
    use serde::ser::{Serialize, Serializer};

    /// Serializes as the lowercase name, for example `"english"`.
    impl Serialize for Algorithm {
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
            serializer.serialize_str(self.name())
        }
    }

    struct NameVisitor;

    impl<'de> Visitor<'de> for NameVisitor {
        type Value = Algorithm;

        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.write_str("a lowercase stemming algorithm name")
        }

        fn visit_str<E: Error>(self, v: &str) -> Result<Algorithm, E> {
            v.parse()
                .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
        }
    }

    /// Deserializes from the lowercase name, for example `"english"`.
    impl<'de> Deserialize<'de> for Algorithm {
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
            deserializer.deserialize_str(NameVisitor)
        }
    }
}