use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatParseError {
pub input: String,
pub message: String,
}
impl fmt::Display for FormatParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid format '{}': {}", self.input, self.message)
}
}
impl std::error::Error for FormatParseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u16)]
pub enum Justification {
Left = 0,
#[default]
Right = 1,
}
impl Justification {
#[must_use]
pub fn from_nfj(value: i16) -> Self {
if value == 0 { Self::Left } else { Self::Right }
}
#[must_use]
pub fn as_nfj(self) -> i16 {
self as i16
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Format {
name: String,
length: u16,
decimals: u16,
justification: Justification,
is_character: bool,
}
impl Default for Format {
fn default() -> Self {
Self {
name: String::new(),
length: 8,
decimals: 0,
justification: Justification::Right,
is_character: false,
}
}
}
impl Format {
#[must_use]
pub fn new(
name: impl Into<String>,
length: u16,
decimals: u16,
justification: Justification,
) -> Self {
let name = name.into();
let is_character = name.starts_with('$') || name.eq_ignore_ascii_case("CHAR");
Self {
name,
length,
decimals,
justification,
is_character,
}
}
#[must_use]
pub fn numeric(length: u16, decimals: u16) -> Self {
Self {
name: String::new(),
length,
decimals,
justification: Justification::Right,
is_character: false,
}
}
#[must_use]
pub fn character(length: u16) -> Self {
Self {
name: String::from("$CHAR"),
length,
decimals: 0,
justification: Justification::Left,
is_character: true,
}
}
#[must_use]
pub fn from_namestr(name: &str, length: i16, decimals: i16, justification: i16) -> Self {
let is_character = name.starts_with('$') || name.eq_ignore_ascii_case("CHAR");
Self {
name: name.to_string(),
length: length.max(0) as u16,
decimals: decimals.max(0) as u16,
justification: Justification::from_nfj(justification),
is_character,
}
}
pub fn parse(s: &str) -> Result<Self, FormatParseError> {
let s = s.trim();
if s.is_empty() {
return Err(FormatParseError {
input: s.to_string(),
message: "empty format string".to_string(),
});
}
if !s.contains('.') {
return Err(FormatParseError {
input: s.to_string(),
message: "format must contain a '.' separator".to_string(),
});
}
let (is_char, rest) = if let Some(stripped) = s.strip_prefix('$') {
(true, stripped)
} else {
(false, s)
};
let dot_pos = rest.rfind('.').unwrap(); let before_dot = &rest[..dot_pos];
let after_dot = &rest[dot_pos + 1..];
let decimals: u16 = if after_dot.is_empty() {
0
} else {
after_dot.parse().map_err(|_| FormatParseError {
input: s.to_string(),
message: format!("invalid decimals: '{}'", after_dot),
})?
};
let (name, length) = parse_name_and_width(before_dot, s)?;
let full_name = if is_char { format!("${}", name) } else { name };
Ok(Self {
name: full_name.clone(),
length,
decimals,
justification: if is_char {
Justification::Left
} else {
Justification::Right
},
is_character: is_char,
})
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn name_without_prefix(&self) -> &str {
self.name.strip_prefix('$').unwrap_or(&self.name)
}
#[must_use]
pub fn length(&self) -> u16 {
self.length
}
#[must_use]
pub fn decimals(&self) -> u16 {
self.decimals
}
#[must_use]
pub fn justification(&self) -> Justification {
self.justification
}
#[must_use]
pub fn is_character(&self) -> bool {
self.is_character
}
#[must_use]
pub fn with_justification(mut self, justification: Justification) -> Self {
self.justification = justification;
self
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.name.is_empty() {
if self.decimals > 0 {
write!(f, "{}.{}", self.length, self.decimals)
} else {
write!(f, "{}.", self.length)
}
} else {
if self.decimals > 0 {
write!(f, "{}{}.{}", self.name, self.length, self.decimals)
} else {
write!(f, "{}{}.", self.name, self.length)
}
}
}
}
impl FromStr for Format {
type Err = FormatParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Format {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Format {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Format::parse(&s).map_err(serde::de::Error::custom)
}
}
fn parse_name_and_width(
before_dot: &str,
original: &str,
) -> Result<(String, u16), FormatParseError> {
if before_dot.is_empty() {
return Err(FormatParseError {
input: original.to_string(),
message: "missing width before '.'".to_string(),
});
}
let digit_start = before_dot
.char_indices()
.rev()
.take_while(|(_, c)| c.is_ascii_digit())
.last()
.map(|(i, _)| i)
.unwrap_or(before_dot.len());
let name_part = &before_dot[..digit_start];
let width_part = &before_dot[digit_start..];
if width_part.is_empty() {
return Err(FormatParseError {
input: original.to_string(),
message: "missing width in format".to_string(),
});
}
let width: u16 = width_part.parse().map_err(|_| FormatParseError {
input: original.to_string(),
message: format!("invalid width: '{}'", width_part),
})?;
Ok((name_part.to_uppercase(), width))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_date_format() {
let fmt = Format::parse("DATE9.").unwrap();
assert_eq!(fmt.name(), "DATE");
assert_eq!(fmt.length(), 9);
assert_eq!(fmt.decimals(), 0);
assert!(!fmt.is_character());
assert_eq!(fmt.to_string(), "DATE9.");
}
#[test]
fn test_parse_bare_numeric() {
let fmt = Format::parse("8.2").unwrap();
assert_eq!(fmt.name(), "");
assert_eq!(fmt.length(), 8);
assert_eq!(fmt.decimals(), 2);
assert!(!fmt.is_character());
assert_eq!(fmt.to_string(), "8.2");
}
#[test]
fn test_parse_bare_numeric_no_decimals() {
let fmt = Format::parse("8.").unwrap();
assert_eq!(fmt.name(), "");
assert_eq!(fmt.length(), 8);
assert_eq!(fmt.decimals(), 0);
assert_eq!(fmt.to_string(), "8.");
}
#[test]
fn test_parse_best_format() {
let fmt = Format::parse("BEST12.").unwrap();
assert_eq!(fmt.name(), "BEST");
assert_eq!(fmt.length(), 12);
assert_eq!(fmt.decimals(), 0);
assert_eq!(fmt.to_string(), "BEST12.");
}
#[test]
fn test_parse_character_format() {
let fmt = Format::parse("$CHAR200.").unwrap();
assert_eq!(fmt.name(), "$CHAR");
assert_eq!(fmt.name_without_prefix(), "CHAR");
assert_eq!(fmt.length(), 200);
assert_eq!(fmt.decimals(), 0);
assert!(fmt.is_character());
assert_eq!(fmt.justification(), Justification::Left);
assert_eq!(fmt.to_string(), "$CHAR200.");
}
#[test]
fn test_parse_lowercase_normalized() {
let fmt = Format::parse("date9.").unwrap();
assert_eq!(fmt.name(), "DATE");
}
#[test]
fn test_parse_errors() {
assert!(Format::parse("").is_err());
assert!(Format::parse("DATE").is_err()); assert!(Format::parse(".2").is_err()); assert!(Format::parse("DATE.").is_err()); }
#[test]
fn test_numeric_constructor() {
let fmt = Format::numeric(8, 2);
assert_eq!(fmt.length(), 8);
assert_eq!(fmt.decimals(), 2);
assert_eq!(fmt.to_string(), "8.2");
}
#[test]
fn test_character_constructor() {
let fmt = Format::character(200);
assert_eq!(fmt.length(), 200);
assert!(fmt.is_character());
assert_eq!(fmt.to_string(), "$CHAR200.");
}
#[test]
fn test_from_namestr() {
let fmt = Format::from_namestr("DATE", 9, 0, 1);
assert_eq!(fmt.name(), "DATE");
assert_eq!(fmt.length(), 9);
assert_eq!(fmt.decimals(), 0);
assert_eq!(fmt.justification(), Justification::Right);
}
#[test]
fn test_from_namestr_character() {
let fmt = Format::from_namestr("$CHAR", 200, 0, 0);
assert!(fmt.is_character());
assert_eq!(fmt.justification(), Justification::Left);
}
#[test]
fn test_justification() {
assert_eq!(Justification::from_nfj(0), Justification::Left);
assert_eq!(Justification::from_nfj(1), Justification::Right);
assert_eq!(Justification::from_nfj(99), Justification::Right);
assert_eq!(Justification::Left.as_nfj(), 0);
assert_eq!(Justification::Right.as_nfj(), 1);
}
#[test]
fn test_from_str() {
let fmt: Format = "DATE9.".parse().unwrap();
assert_eq!(fmt.name(), "DATE");
}
}