enumerated_latin 1.0.0

Encodes short strings as numeric values
Documentation
// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

use crate::EncodingTarget;

/// Problem that can occur when using
/// [`enumerated_latin_encode`][EnumeratedLatinEncode::enumerated_latin_encode]
///  on invalid input.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncodingError {
	/// The given string lenght exceeds the MAX_ENUMERATED_LATIN_LEN of the target type.
	InputIsTooLong,

	/// The given string conatins a letter that isn't in the 26-letter Latin alphabet.
	ContainsNonLatinLetter,
}

/// Trait for implementing the letters to number part of enumerated latin.
///
/// This trait is implemented on [str], [String] and `[u8]` (assumes ascii compatible encoding) to allow converting the string to a number according to the enumerated latin rules.
pub trait EnumeratedLatinEncode<T: EncodingTarget> {
	/// Encodes `self` into a number.
	fn enumerated_latin_encode(&self) -> Result<T, EncodingError>;
}

impl<T: EncodingTarget> EnumeratedLatinEncode<T> for str {
	fn enumerated_latin_encode(&self) -> Result<T, EncodingError> {
		self.as_bytes().enumerated_latin_encode()
	}
}

impl<T: EncodingTarget> EnumeratedLatinEncode<T> for String {
	fn enumerated_latin_encode(&self) -> Result<T, EncodingError> {
		self.as_bytes().enumerated_latin_encode()
	}
}

impl<T: EncodingTarget> EnumeratedLatinEncode<T> for [u8] {
	fn enumerated_latin_encode(&self) -> Result<T, EncodingError> {
		if self.len() > T::MAX_ENUMERATED_LATIN_LEN {
			return Err(EncodingError::InputIsTooLong);
		}
		let mut acc: T = 1.into(); //Initalize with end-cap
		for c in self {
			let n: T = ascii_letter_to_number(*c)
				.ok_or(EncodingError::ContainsNonLatinLetter)?
				.into();
			acc = (acc * 26.into()) + n;
		}
		Ok(acc)
	}
}

fn ascii_letter_to_number(to_encode: u8) -> Option<u8> {
	match to_encode {
		b'a'..=b'z' => Some(to_encode - b'a'),
		b'A'..=b'Z' => Some(to_encode - b'A'),
		_ => None,
	}
}

#[cfg(test)]
mod test {
	use super::*;

	use std::fmt::Debug;

	#[test]
	fn encode_test() {
		assert_eq!("a".enumerated_latin_encode(), Ok(26 as i128));
		assert_eq!("z".enumerated_latin_encode(), Ok(51 as i128));
		assert_eq!("aa".enumerated_latin_encode(), Ok(676 as i128));
		assert_eq!("zz".enumerated_latin_encode(), Ok(1351 as i128));
		assert_eq!("Blubber".enumerated_latin_encode(), Ok(8480578981 as u64));
	}

	#[test]
	fn encode_with_invalid_input() {
		assert_eq!(
			"blubber!".enumerated_latin_encode(),
			Result::<u64, _>::Err(EncodingError::ContainsNonLatinLetter)
		);
		assert_eq!(
			"hello world".enumerated_latin_encode(),
			Result::<u64, _>::Err(EncodingError::ContainsNonLatinLetter)
		);
	}

	fn expect_too_long<T: EncodingTarget + Debug>(text: &str) {
		assert_eq!(
			text.enumerated_latin_encode(),
			Result::<T, _>::Err(EncodingError::InputIsTooLong),
		);
	}

	#[test]
	fn encode_too_long() {
		expect_too_long::<u8>("aa");
		expect_too_long::<i16>("aaa");
		expect_too_long::<u16>("aaaa");
		expect_too_long::<i32>("aaaaabb");
		expect_too_long::<u32>("aaaaabb");
		expect_too_long::<i64>("aaaaabbbbbcccc");
		expect_too_long::<u64>("aaaaabbbbbcccc");
		expect_too_long::<i128>("aaaaabbbbbcccccdddddeeeeeff");
		expect_too_long::<u128>("aaaaabbbbbcccccdddddeeeeefff");
	}

	fn test_at_max_len<T: EncodingTarget + Debug + PartialEq>(text: &str, outcome: T) {
		if text.len() != T::MAX_ENUMERATED_LATIN_LEN {
			panic!(
				"Test text isn't at expected maximum length of {}",
				T::MAX_ENUMERATED_LATIN_LEN
			);
		}
		assert_eq!(text.enumerated_latin_encode(), Ok(outcome));
	}

	#[test]
	fn encode_at_max_len() {
		test_at_max_len("z", 51 as u8);
		test_at_max_len("zz", 1351 as i16);
		test_at_max_len("zzz", 35151 as u16);
		test_at_max_len("zzzzzz", 617831551 as i32);
		test_at_max_len("zzzzzz", 617831551 as u32);
		test_at_max_len("zzzzzzzzzzzzz", 4962305746407473151 as i64);
		test_at_max_len("zzzzzzzzzzzzz", 4962305746407473151 as u64);
		test_at_max_len(
			"zzzzzzzzzzzzzzzzzzzzzzzzzz",
			12312239160414314621593348576800407551 as i128,
		);
		test_at_max_len(
			"zzzzzzzzzzzzzzzzzzzzzzzzzzz",
			320118218170772180161427062996810596351 as u128,
		);
	}

	#[test]
	fn encode_empty() {
		assert_eq!("".enumerated_latin_encode(), Ok(1 as u8))
	}
}