#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
fn non_empty_text(value: impl AsRef<str>) -> Result<String, RockTextError> {
let original = value.as_ref();
if original.trim().is_empty() {
Err(RockTextError::Empty)
} else {
Ok(original.to_string())
}
}
fn normalized_token(value: &str) -> String {
let mut normalized = String::with_capacity(value.len());
let mut previous_separator = false;
for character in value.trim().chars() {
if character.is_ascii_alphanumeric() {
normalized.push(character.to_ascii_lowercase());
previous_separator = false;
} else if (character.is_whitespace() || character == '-' || character == '_')
&& !previous_separator
&& !normalized.is_empty()
{
normalized.push('-');
previous_separator = true;
}
}
if normalized.ends_with('-') {
let _ = normalized.pop();
}
normalized
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RockTextError {
Empty,
}
impl fmt::Display for RockTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("rock text cannot be empty"),
}
}
}
impl Error for RockTextError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RockParseError {
Empty,
}
impl fmt::Display for RockParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("rock vocabulary cannot be empty"),
}
}
}
impl Error for RockParseError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RockCompositionError {
EmptyLabel,
EmptyMineralName,
NoMineralNames,
}
impl fmt::Display for RockCompositionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyLabel => formatter.write_str("rock composition label cannot be empty"),
Self::EmptyMineralName => {
formatter.write_str("rock composition mineral names cannot be empty")
},
Self::NoMineralNames => {
formatter.write_str("rock composition requires at least one mineral name")
},
}
}
}
impl Error for RockCompositionError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RockName(String);
impl RockName {
pub fn new(value: impl AsRef<str>) -> Result<Self, RockTextError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for RockName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RockName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for RockName {
type Err = RockTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RockKind {
Igneous,
Sedimentary,
Metamorphic,
Unknown,
Custom(String),
}
impl fmt::Display for RockKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Igneous => formatter.write_str("igneous"),
Self::Sedimentary => formatter.write_str("sedimentary"),
Self::Metamorphic => formatter.write_str("metamorphic"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(value) => formatter.write_str(value),
}
}
}
impl FromStr for RockKind {
type Err = RockParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(RockParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"igneous" => Ok(Self::Igneous),
"sedimentary" => Ok(Self::Sedimentary),
"metamorphic" => Ok(Self::Metamorphic),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RockTexture {
Clastic,
Crystalline,
Glassy,
Vesicular,
Foliated,
NonFoliated,
Porphyritic,
FineGrained,
CoarseGrained,
Unknown,
Custom(String),
}
impl fmt::Display for RockTexture {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Clastic => formatter.write_str("clastic"),
Self::Crystalline => formatter.write_str("crystalline"),
Self::Glassy => formatter.write_str("glassy"),
Self::Vesicular => formatter.write_str("vesicular"),
Self::Foliated => formatter.write_str("foliated"),
Self::NonFoliated => formatter.write_str("non-foliated"),
Self::Porphyritic => formatter.write_str("porphyritic"),
Self::FineGrained => formatter.write_str("fine-grained"),
Self::CoarseGrained => formatter.write_str("coarse-grained"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(value) => formatter.write_str(value),
}
}
}
impl FromStr for RockTexture {
type Err = RockParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(RockParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"clastic" => Ok(Self::Clastic),
"crystalline" => Ok(Self::Crystalline),
"glassy" => Ok(Self::Glassy),
"vesicular" => Ok(Self::Vesicular),
"foliated" => Ok(Self::Foliated),
"non-foliated" => Ok(Self::NonFoliated),
"porphyritic" => Ok(Self::Porphyritic),
"fine-grained" => Ok(Self::FineGrained),
"coarse-grained" => Ok(Self::CoarseGrained),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RockComposition {
label: Option<String>,
mineral_names: Vec<String>,
}
impl RockComposition {
pub fn with_label(label: impl AsRef<str>) -> Result<Self, RockTextError> {
Ok(Self {
label: Some(non_empty_text(label)?),
mineral_names: Vec::new(),
})
}
pub fn with_mineral_names<I, S>(mineral_names: I) -> Result<Self, RockCompositionError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mineral_names = collect_mineral_names(mineral_names)?;
Ok(Self {
label: None,
mineral_names,
})
}
pub fn describe<I, S>(
label: impl AsRef<str>,
mineral_names: I,
) -> Result<Self, RockCompositionError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let label = label.as_ref();
if label.trim().is_empty() {
return Err(RockCompositionError::EmptyLabel);
}
Ok(Self {
label: Some(label.to_string()),
mineral_names: collect_mineral_names(mineral_names)?,
})
}
#[must_use]
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
#[must_use]
pub fn mineral_names(&self) -> &[String] {
&self.mineral_names
}
}
impl fmt::Display for RockComposition {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.label.as_deref(), self.mineral_names.is_empty()) {
(Some(label), true) => formatter.write_str(label),
(Some(label), false) => {
let mineral_names = self.mineral_names.join(", ");
write!(formatter, "{label} [{mineral_names}]")
},
(None, false) => formatter.write_str(&self.mineral_names.join(", ")),
(None, true) => formatter.write_str("unspecified"),
}
}
}
fn collect_mineral_names<I, S>(mineral_names: I) -> Result<Vec<String>, RockCompositionError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mineral_names = mineral_names
.into_iter()
.map(|value| {
let original = value.as_ref();
if original.trim().is_empty() {
Err(RockCompositionError::EmptyMineralName)
} else {
Ok(original.to_string())
}
})
.collect::<Result<Vec<_>, _>>()?;
if mineral_names.is_empty() {
Err(RockCompositionError::NoMineralNames)
} else {
Ok(mineral_names)
}
}
#[cfg(test)]
mod tests {
use super::{
RockComposition, RockCompositionError, RockKind, RockName, RockParseError, RockTextError,
RockTexture,
};
#[test]
fn valid_rock_name() -> Result<(), RockTextError> {
let name = RockName::new("Basalt")?;
assert_eq!(name.as_str(), "Basalt");
Ok(())
}
#[test]
fn empty_rock_name_rejected() {
assert_eq!(RockName::new("\t"), Err(RockTextError::Empty));
}
#[test]
fn rock_kind_display_parse() -> Result<(), RockParseError> {
assert_eq!(RockKind::Igneous.to_string(), "igneous");
assert_eq!("metamorphic".parse::<RockKind>()?, RockKind::Metamorphic);
Ok(())
}
#[test]
fn rock_texture_display_parse() -> Result<(), RockParseError> {
assert_eq!(RockTexture::FineGrained.to_string(), "fine-grained");
assert_eq!(
"non foliated".parse::<RockTexture>()?,
RockTexture::NonFoliated
);
Ok(())
}
#[test]
fn custom_rock_kind() -> Result<(), RockParseError> {
assert_eq!(
"volcaniclastic".parse::<RockKind>()?,
RockKind::Custom("volcaniclastic".to_string())
);
Ok(())
}
#[test]
fn rock_composition_construction() -> Result<(), RockCompositionError> {
let composition = RockComposition::describe("felsic", ["Quartz", "Feldspar"])?;
assert_eq!(composition.label(), Some("felsic"));
assert_eq!(composition.mineral_names(), ["Quartz", "Feldspar"]);
assert_eq!(composition.to_string(), "felsic [Quartz, Feldspar]");
Ok(())
}
}