diff --git a/Cargo.lock b/Cargo.lock index cf7e467..946d4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "this_algorithm" +version = "0.1.0" +dependencies = [ + "s2", + "thiserror", + "words", +] + [[package]] name = "this_algoritm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d115f55..d4499ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" members = [ ".", "./words", + "./this_algorithm", ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/this_algorithm/Cargo.toml b/this_algorithm/Cargo.toml new file mode 100644 index 0000000..fcc7083 --- /dev/null +++ b/this_algorithm/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "this_algorithm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +s2 = "0.0.12" +thiserror = "1.0.38" +words={path="../words"} diff --git a/this_algorithm/src/lib.rs b/this_algorithm/src/lib.rs new file mode 100644 index 0000000..0a4b93e --- /dev/null +++ b/this_algorithm/src/lib.rs @@ -0,0 +1,172 @@ +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +#![allow(clippy::cast_possible_truncation, clippy::multiple_crate_versions)] +#![allow(dead_code, unused_imports)] + +use std::{ + ascii::AsciiExt, + fmt::Display, + ops::{Add, AddAssign}, + str::FromStr, +}; +use thiserror::Error; + +use s2::cellid::CellID; +use words::Word; + +pub type Number = u32; +pub type Version = u8; + +const V0_MAX_NUMBER: u32 = 1024; + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum Error { + #[error("Word does not exist")] + WordDoesNotExist(String), + #[error("Invalid version")] + InvalidVersion(Version), + #[error("Unimplemented version")] + UnimplementedVersion(Version), + #[error("Number out of range")] + NumberOutOfRange(Number), + #[error("Invalid encoding")] + InvalidEncoding, + #[error("Wrong number of components")] + WrongComponentCount, + #[error("No number component")] + NoNumberComponent, + #[error("Empty string given")] + Empty, +} + +/// An encoded this_algorithm address +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Address<'a> { + number: Number, + words: [&'a Word<'a>; 3], +} + +impl FromStr for Address<'_> { + type Err = Error; + + /// Try to parse `&str` as an `Address`. The format can be one of: + /// + /// * `0000 WORD0 WORD1 WORD2` (regular) + /// * `WORD2 WORD1 WORD0 0000` (reversed) + fn from_str(s: &str) -> Result { + if !s.is_ascii() { + return Err(Error::InvalidEncoding); + } + + let s = s.trim().to_ascii_uppercase(); + let components = s.split_ascii_whitespace().collect::>(); + + // Make sure our indexing can't fail + if components.is_empty() { + return Err(Error::Empty); + } + + // Check if either the beginning or end is a number + let (reverse, number) = if let Ok(number) = components.first().unwrap().parse::() { + // The number is the first component + (false, number) + } else if let Ok(number) = components.last().unwrap().parse::() { + // The number is the last component + (true, number) + } else { + return Err(Error::NoNumberComponent); + }; + + // A vec of the non-number component + let other_components: Vec<&str> = if reverse { + components.into_iter().rev().skip(1).collect::>() + } else { + components.into_iter().skip(1).collect::>() + }; + + match extract_version(number) { + 0 => Self::parse_v0(number, other_components), + ver => Err(Error::InvalidVersion(ver)), + } + } +} + +impl Address<'_> { + fn parse_v0(number: Number, word_components: Vec<&str>) -> Result { + if number > V0_MAX_NUMBER { + return Err(Error::NumberOutOfRange(number)); + } + + if word_components.len() != 3 { + return Err(Error::WrongComponentCount); + } + + // Convert each word component into a word, returning an error if any do not match + let words = TryInto::<[&'static Word; 3]>::try_into( + word_components + .iter() + .map(|w| words::get_word(w).ok_or_else(|| Error::WordDoesNotExist(w.to_string()))) + .collect::, Error>>()?, + ) + // This unwrap is okay because we just checked to make sure the number of word components was 3 + .unwrap(); + + Ok(Self { number, words }) + } +} + +fn extract_version(number: Number) -> Version { + ((number >> 10) & 0b11) as Version +} + +impl Display for Address<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} {} {} {}", + self.number, self.words[0], self.words[1], self.words[2] + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! w { + ($word:tt) => { + words::get_word($word).unwrap() + }; + } + + macro_rules! addr { + ($number:tt, $word0:tt, $word1:tt, $word2:tt) => { + Address { + number: $number, + words: [w!($word0), w!($word1), w!($word2)], + } + }; + } + + #[test] + fn test_extract_version() { + assert_eq!(extract_version(0b0000_0000_0000), 0b00); + assert_eq!(extract_version(0b1100_0000_0000), 0b11); + assert_eq!(extract_version(0b0100_0000_0000), 0b01); + assert_eq!(extract_version(0b1000_0000_0000), 0b10); + assert_eq!(extract_version(0b11 << 10), 0b11); + assert_eq!(extract_version(0b00 << 10), 0b00); + assert_eq!(extract_version(0b10 << 10), 0b10); + assert_eq!(extract_version(0b01 << 10), 0b01); + } + + #[test] + fn test_parse_v0() { + assert_eq!( + Address::parse_v0(1000, vec!["apple", "orange", "grape"]), + Ok(addr![1000, "apple", "orange", "grape"]) + ); + + assert!(Address::parse_v0(1000, vec!["ASDF", "orange", "grape"]).is_err()); + assert!(Address::parse_v0(10_000, vec!["apple", "orange", "grape"]).is_err()); + } +} diff --git a/this_algorithm/tests/common/mod.rs b/this_algorithm/tests/common/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/this_algorithm/tests/common/mod.rs @@ -0,0 +1 @@ + diff --git a/this_algorithm/tests/display.rs b/this_algorithm/tests/display.rs new file mode 100644 index 0000000..435d379 --- /dev/null +++ b/this_algorithm/tests/display.rs @@ -0,0 +1,18 @@ +use words::Word; +mod common; + +// #[test] +// fn test_address() { +// let addr = Address { +// number: 1234, + +// }; +// assert_eq!( +// (Word { +// word: "asdf", +// number: 0, +// }) +// .to_string(), +// "asdf" +// ); +// } diff --git a/words/src/lib.rs b/words/src/lib.rs index 9e01733..17b8a8f 100644 --- a/words/src/lib.rs +++ b/words/src/lib.rs @@ -1,7 +1,8 @@ -use thiserror::Error; +use std::fmt::Display; + include!(concat!(env!("OUT_DIR"), "/codegen.rs")); -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] /// A word struct pub struct Word<'a> { /// The word itself @@ -13,10 +14,10 @@ pub struct Word<'a> { pub number: u16, } -#[derive(Debug, Clone, PartialEq)] -pub struct Address<'a> { - number: u32, - words: [Word<'a>; 3], +impl Display for Word<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } } /// Helper function that gets the mapped number from a word @@ -24,10 +25,10 @@ pub struct Address<'a> { /// ```rust /// use words::get_number; /// -/// assert!(get_number("ThE").is_ok()); -/// assert!(get_number("AsDf").is_err()); +/// assert!(get_number("ThE").is_some()); +/// assert!(get_number("AsDf").is_none()); /// ``` -pub fn get_number(maybe_word: S) -> Result +pub fn get_number(maybe_word: S) -> Option where S: AsRef, { @@ -39,25 +40,16 @@ where /// ```rust /// use words::get_word; /// -/// assert!(get_word("THE").is_ok()); -/// assert!(get_word("the").is_ok()); -/// assert!(get_word("tHe").is_ok()); -/// assert!(get_word("ASDFASDF").is_err()); +/// assert!(get_word("THE").is_some()); +/// assert!(get_word("the").is_some()); +/// assert!(get_word("tHe").is_some()); +/// assert!(get_word("ASDFASDF").is_none()); /// ``` -pub fn get_word(maybe_word: S) -> Result<&'static Word<'static>, Error> +pub fn get_word(maybe_word: S) -> Option<&'static Word<'static>> where S: AsRef, { WORD_MAP .get(&maybe_word.as_ref().trim().to_ascii_uppercase()) .copied() - .ok_or(Error::WordNotFound) -} - -#[derive(Error, Debug)] -pub enum Error { - #[error("Word not found")] - WordNotFound, - #[error("The requested number is out of bounds")] - NumberOutOfBounds, }