detritus: Initial commit
This commit is contained in:
commit
a94398d31f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
16
.rustfmt.toml
Normal file
16
.rustfmt.toml
Normal file
@ -0,0 +1,16 @@
|
||||
unstable_features = true
|
||||
max_width = 100
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
struct_lit_width = 100
|
||||
|
||||
imports_granularity = "Crate"
|
||||
# Allow structs to fill an entire line
|
||||
# use_small_heuristics = "Max"
|
||||
# Allow small functions on single line
|
||||
# fn_single_line = true
|
||||
|
||||
# Alignment
|
||||
enum_discrim_align_threshold = 12
|
||||
#struct_field_align_threshold = 12
|
||||
where_single_line = true
|
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "detritus"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
repline = { version = "0.0.8", registry = "soft-fish" }
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 soft.fish
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
9
README.md
Normal file
9
README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# detritus
|
||||
|
||||
A prefix tree on T9 dial codes. Perfect for indulging in the benthic zone.
|
||||
|
||||
<img src=res/passthe.jpg/>
|
||||
|
||||
## Why is it named detritus?
|
||||
|
||||
I put in a word, and detritus popped out.
|
BIN
res/passthe.jpg
Normal file
BIN
res/passthe.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
180
src/lib.rs
Normal file
180
src/lib.rs
Normal file
@ -0,0 +1,180 @@
|
||||
//! Implements a trie on T9 numpads
|
||||
//!
|
||||
//! # T9:
|
||||
//! 1: 2:abc 3:def
|
||||
//! 4:ghi 5:jkl 6:mno
|
||||
//! 7:pqrs 8:tuv 9:wxyz
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
ops::{Index, IndexMut},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Trie9 {
|
||||
pub link: Option<Box<[Trie9; 8]>>,
|
||||
pub leaf: Vec<String>,
|
||||
}
|
||||
|
||||
impl Trie9 {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, string: &str) -> bool {
|
||||
let Some(trie) = self.at_mut(string.chars()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
trie.leaf.push(string.into());
|
||||
true
|
||||
}
|
||||
|
||||
pub fn get(&self, index: T9) -> Option<&Trie9> {
|
||||
self.link.as_ref()?.get(index as usize)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, index: T9) -> Option<&mut Trie9> {
|
||||
self.link.as_mut()?.get_mut(index as usize)
|
||||
}
|
||||
|
||||
pub fn get_or_insert(&mut self, index: T9) -> &mut Trie9 {
|
||||
self.link
|
||||
.get_or_insert_default()
|
||||
.get_mut(index as usize)
|
||||
.expect("link exists at all T9 indices")
|
||||
}
|
||||
|
||||
pub fn at<Iter, T>(&self, code: Iter) -> Option<&Self>
|
||||
where
|
||||
Iter: IntoIterator<Item = T>,
|
||||
T: TryInto<T9>,
|
||||
{
|
||||
let mut code = code.into_iter();
|
||||
match code.next() {
|
||||
None => Some(self),
|
||||
Some(link) => self.get(link.try_into().ok()?)?.at(code),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn at_mut<Iter, T>(&mut self, code: Iter) -> Option<&mut Self>
|
||||
where
|
||||
Iter: IntoIterator<Item = T>,
|
||||
T: TryInto<T9>,
|
||||
{
|
||||
let mut code = code.into_iter();
|
||||
match code.next() {
|
||||
None => Some(self),
|
||||
Some(link) => self.get_or_insert(link.try_into().ok()?).at_mut(code),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter<'t>(&'t self) -> Trie9Iter<'t> {
|
||||
Trie9Iter::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Trie9Iter<'t9> {
|
||||
queue: VecDeque<&'t9 Trie9>,
|
||||
items: std::slice::Iter<'t9, String>,
|
||||
}
|
||||
|
||||
impl<'t> Trie9Iter<'t> {
|
||||
pub fn new(trie: &'t Trie9) -> Self {
|
||||
Self { queue: VecDeque::from([trie]), items: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t> Iterator for Trie9Iter<'t> {
|
||||
type Item = &'t str;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let Self { queue, items } = self;
|
||||
|
||||
if let Some(item) = items.next() {
|
||||
return Some(item.as_str());
|
||||
}
|
||||
|
||||
let Trie9 { link, leaf } = queue.pop_front()?;
|
||||
*items = leaf.iter();
|
||||
queue.extend(link.iter().flat_map(|v| v.iter()));
|
||||
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t> IntoIterator for &'t Trie9 {
|
||||
type IntoIter = Trie9Iter<'t>;
|
||||
type Item = &'t str;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Self::IntoIter::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum T9 {
|
||||
T2,
|
||||
T3,
|
||||
T4,
|
||||
T5,
|
||||
T6,
|
||||
T7,
|
||||
T8,
|
||||
T9,
|
||||
}
|
||||
|
||||
impl From<T9> for u8 {
|
||||
fn from(value: T9) -> Self {
|
||||
value as u8 + 2
|
||||
}
|
||||
}
|
||||
|
||||
impl From<T9> for char {
|
||||
fn from(value: T9) -> Self {
|
||||
match value {
|
||||
T9::T2 => '2',
|
||||
T9::T3 => '3',
|
||||
T9::T4 => '4',
|
||||
T9::T5 => '5',
|
||||
T9::T6 => '6',
|
||||
T9::T7 => '7',
|
||||
T9::T8 => '8',
|
||||
T9::T9 => '9',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for T9 {
|
||||
type Error = char;
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
'2' | 'A'..='C' | 'a'..='c' | 'À'..='Æ' | 'à'..='æ' | 'Ç' | 'ç' => Self::T2,
|
||||
'3' | 'D'..='F' | 'd'..='f' | 'È'..='Ë' | 'è'..='ë' => Self::T3,
|
||||
'4' | 'G'..='I' | 'g'..='i' | 'Ì'..='Ï' | 'ì'..='ï' => Self::T4,
|
||||
'5' | 'J'..='L' | 'j'..='l' | '1' => Self::T5,
|
||||
'6' | 'M'..='O' | 'm'..='o' | 'Ò'..='Ø' | 'ò'..='ø' | '0' => Self::T6,
|
||||
'7' | 'P'..='S' | 'p'..='s' => Self::T7,
|
||||
'8' | 'T'..='V' | 't'..='v' | 'Ù'..='Ü' | 'ù'..='ü' => Self::T8,
|
||||
'9' | 'W'..='Z' | 'w'..='z' => Self::T9,
|
||||
_ => Err(value)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<T9> for Trie9 {
|
||||
type Output = Trie9;
|
||||
fn index(&self, index: T9) -> &Self::Output {
|
||||
let Some(link) = self.get(index) else {
|
||||
panic!("No Trie9 at index {index:?}");
|
||||
};
|
||||
link
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<T9> for Trie9 {
|
||||
fn index_mut(&mut self, index: T9) -> &mut Self::Output {
|
||||
self.get_or_insert(index)
|
||||
}
|
||||
}
|
70
src/main.rs
Normal file
70
src/main.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! Solves a fun little puzzle invented by a coworker
|
||||
|
||||
use detritus::{T9, Trie9};
|
||||
use repline::prebaked::*;
|
||||
use std::{
|
||||
error::Error,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, IsTerminal, Write},
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut trie = Trie9::new();
|
||||
|
||||
// "Parse" CLI arg
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let path = match args.as_slice() {
|
||||
[] => "/usr/share/dict/words",
|
||||
[path] => path,
|
||||
_ => Err("Usage: detritus [dict]")?,
|
||||
};
|
||||
|
||||
// Read in the database and construct the Trie9
|
||||
for line in BufReader::new(File::open(path)?).lines() {
|
||||
trie.insert(&line?);
|
||||
}
|
||||
|
||||
if std::io::stdin().is_terminal() {
|
||||
yestty(trie)
|
||||
} else {
|
||||
notty(trie)
|
||||
}
|
||||
}
|
||||
|
||||
fn yestty(trie: Trie9) -> Result<(), Box<dyn Error>> {
|
||||
repline::read_and("\x1b[36m", " t>", " ?>", |line| {
|
||||
let key: Vec<_> = match line.trim().chars().map(T9::try_from).collect() {
|
||||
Ok(t9) => t9,
|
||||
Err(e) => {
|
||||
println!("Unexpected character: {e}");
|
||||
return Ok(Response::Deny);
|
||||
}
|
||||
};
|
||||
|
||||
key.iter().for_each(|&c| print!("{}", char::from(c)));
|
||||
println!();
|
||||
|
||||
let Some(trie) = trie.at(key.iter().copied()) else {
|
||||
return Ok(Response::Accept);
|
||||
};
|
||||
for item in trie {
|
||||
println!("{item}")
|
||||
}
|
||||
|
||||
Ok(Response::Accept)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notty(trie: Trie9) -> Result<(), Box<dyn Error>> {
|
||||
let stdin = std::io::stdin().lock();
|
||||
let key = std::io::read_to_string(stdin)?;
|
||||
if let Some(trie) = trie.at(key.trim().chars()) {
|
||||
for word in trie.iter() {
|
||||
if writeln!(std::io::stdout(), "{word}").is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user