From 81a9ac109ea8fe1af588577ac9bc8ddfa9094490 Mon Sep 17 00:00:00 2001 From: mehbark Date: Tue, 7 Jul 2026 14:29:40 -0400 Subject: [PATCH] add `letterset` --- .gitignore | 1 + Cargo.lock | 7 ++ Cargo.toml | 6 ++ src/letterset.rs | 184 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 ++ 5 files changed, 203 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/letterset.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ac3ad34 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "dynamic-pangramming" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f19b8bd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "dynamic-pangramming" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/letterset.rs b/src/letterset.rs new file mode 100644 index 0000000..3b51fe5 --- /dev/null +++ b/src/letterset.rs @@ -0,0 +1,184 @@ +use std::{fmt, ops::Deref}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] +pub struct LetterSet(u32); + +/// Immutable set of English letters +impl LetterSet { + pub const EMPTY: Self = Self(0); + pub const FULL: Self = Self((1 << 26) - 1); + + pub const fn has(self, letter: u8) -> bool { + if let Some(index) = Self::letter_index(letter) { + ((self.0 >> index) & 1) == 1 + } else { + false + } + } + + pub const fn from_slice(letters: &[u8]) -> Self { + let mut out = Self::EMPTY; + let mut i = 0; + while i < letters.len() { + out = out.add(letters[i]); + i += 1; + } + out + } + + pub const fn add(self, letter: u8) -> Self { + if let Some(index) = Self::letter_index(letter) { + Self(self.0 | (1 << index)) + } else { + self + } + } + + // TODO: impl IntoIterator + pub fn items(self) -> impl Iterator { + (b'a'..=b'z').filter(move |l| self.has(*l)) + } + + pub const fn intersection(self, other: Self) -> Self { + Self(self.0 & other.0) + } + + pub const fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } + + pub const fn difference(self, subtrahend: Self) -> Self { + Self(self.0 ^ (self.0 & subtrahend.0)) + } + + pub const fn negation(self) -> Self { + Self::FULL.difference(self) + } + + #[allow(clippy::cast_possible_truncation, reason = "len is at most 26")] + pub const fn len(self) -> u8 { + self.0.count_ones() as u8 + } + + const fn letter_index(letter: u8) -> Option { + if letter.is_ascii_alphabetic() { + let lower = letter.to_ascii_lowercase(); + let index = lower - b'a'; + Some(index) + } else { + None + } + } +} + +impl fmt::Display for LetterSet { + #[allow(clippy::cast_possible_truncation, reason = "it can't")] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{{")?; + for (i, l) in self.items().enumerate() { + write!(f, "{}", char::from(l))?; + if ((i + 1) as u8) < self.len() { + write!(f, ", ")?; + } + } + write!(f, "}}") + } +} + +impl FromIterator for LetterSet +where + B: Deref, +{ + fn from_iter>(iter: T) -> Self { + let mut out = Self::EMPTY; + for l in iter { + out = out.add(*l); + } + out + } +} + +#[cfg(test)] +mod test { + use super::*; + + const A: LetterSet = LetterSet::from_slice(b"a"); + const B: LetterSet = LetterSet::from_slice(b"b"); + const C: LetterSet = LetterSet::from_slice(b"c"); + const AB: LetterSet = LetterSet::from_slice(b"ab"); + const AC: LetterSet = LetterSet::from_slice(b"ac"); + const BC: LetterSet = LetterSet::from_slice(b"bc"); + const ABC: LetterSet = LetterSet::from_slice(b"abc"); + + #[test] + fn empty() { + assert_eq!(LetterSet::EMPTY.to_string(), "{}"); + } + + #[test] + fn full() { + assert_eq!( + LetterSet::FULL.to_string(), + "{a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z}" + ); + } + + #[test] + fn intersection() { + assert_eq!(A.intersection(B), LetterSet::EMPTY); + assert_eq!(B.intersection(A), LetterSet::EMPTY); + + assert_eq!(A.intersection(AB), A); + assert_eq!(B.intersection(AB), B); + assert_eq!(AB.intersection(A), A); + assert_eq!(AB.intersection(B), B); + } + + #[test] + fn union() { + assert_eq!(A.union(A), A); + assert_eq!(A.union(B), AB); + assert_eq!(A.union(C), AC); + assert_eq!(A.union(AB), AB); + assert_eq!(B.union(AB), AB); + assert_eq!(AB.union(C), ABC); + assert_eq!(C.union(AB), ABC); + } + + #[test] + fn difference() { + assert_eq!(AB.difference(B), A); + assert_eq!(AB.difference(A), B); + assert_eq!(ABC.difference(A), BC); + assert_eq!(A.difference(A), LetterSet::EMPTY); + assert_eq!(ABC.difference(ABC), LetterSet::EMPTY); + assert_eq!( + LetterSet::FULL.difference(LetterSet::FULL), + LetterSet::EMPTY + ); + } + + #[test] + fn negation() { + assert_eq!(LetterSet::EMPTY.negation(), LetterSet::FULL); + assert_eq!(LetterSet::FULL.negation(), LetterSet::EMPTY); + assert_eq!(ABC.negation().negation(), ABC); + } + + #[test] + fn len() { + assert_eq!(LetterSet::FULL.len(), 26); + assert_eq!(LetterSet::EMPTY.len(), 0); + assert_eq!(A.len(), 1); + assert_eq!(B.len(), 1); + assert_eq!(AB.len(), 2); + assert_eq!(ABC.len(), 3); + assert_eq!(ABC.negation().len(), 26 - 3); + } + + #[test] + fn add() { + assert_eq!(A.add(b'b'), AB); + assert_eq!(AB.add(b'c'), ABC); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b27f6f6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +mod letterset; + +fn main() { + println!("Hello, world!"); +}