use rust_decimal::Decimal;
use time::OffsetDateTime;
use crate::error::FieldError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
tag: u32,
value: Vec<u8>,
}
impl Field {
pub fn new(tag: u32, value: impl Into<Vec<u8>>) -> Self {
Self {
tag,
value: value.into(),
}
}
pub fn string(tag: u32, value: &str) -> Self {
Self::new(tag, value.as_bytes().to_vec())
}
pub fn int(tag: u32, value: i64) -> Self {
Self::new(tag, value.to_string().into_bytes())
}
pub fn utc_timestamp(tag: u32, value: OffsetDateTime) -> Self {
Self::string(tag, &format_utc_timestamp_millis(value))
}
pub fn bytes(tag: u32, value: &[u8]) -> Self {
Self::new(tag, value.to_vec())
}
pub fn as_bytes(&self) -> &[u8] {
&self.value
}
pub fn utc_date_only(tag: u32, value: time::Date) -> Self {
Self::string(tag, &format_utc_date_only(value))
}
pub fn as_utc_date_only(&self) -> Result<time::Date, FieldError> {
let s = self.as_str()?;
parse_utc_date_only(s).ok_or_else(|| FieldError::NotDateOnly {
tag: self.tag,
value: s.to_owned(),
})
}
pub fn utc_time_only(tag: u32, value: time::Time) -> Self {
Self::string(tag, &format_utc_time_only_millis(value))
}
pub fn as_utc_time_only(&self) -> Result<time::Time, FieldError> {
let s = self.as_str()?;
parse_utc_time_only(s).ok_or_else(|| FieldError::NotTimeOnly {
tag: self.tag,
value: s.to_owned(),
})
}
pub fn tag(&self) -> u32 {
self.tag
}
pub fn value_bytes(&self) -> &[u8] {
&self.value
}
pub fn as_str(&self) -> Result<&str, FieldError> {
core::str::from_utf8(&self.value).map_err(|_| FieldError::NotUtf8 { tag: self.tag })
}
pub fn as_int(&self) -> Result<i64, FieldError> {
let s = self.as_str()?;
s.parse().map_err(|_| FieldError::NotInt {
tag: self.tag,
value: s.to_owned(),
})
}
pub fn as_decimal(&self) -> Result<Decimal, FieldError> {
let s = self.as_str()?;
s.parse().map_err(|_| FieldError::NotDecimal {
tag: self.tag,
value: s.to_owned(),
})
}
pub fn as_char(&self) -> Result<char, FieldError> {
let s = self.as_str()?;
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(c),
_ => Err(FieldError::NotChar {
tag: self.tag,
value: s.to_owned(),
}),
}
}
pub fn as_bool(&self) -> Result<bool, FieldError> {
match self.value.as_slice() {
b"Y" => Ok(true),
b"N" => Ok(false),
_ => Err(FieldError::NotBool {
tag: self.tag,
value: match core::str::from_utf8(&self.value) {
Ok(s) => s.to_owned(),
Err(_) => format!("0x{}", hex_encode(&self.value)),
},
}),
}
}
pub fn as_utc_timestamp(&self) -> Result<OffsetDateTime, FieldError> {
let s = self.as_str()?;
parse_utc_timestamp(s).ok_or_else(|| FieldError::NotTimestamp {
tag: self.tag,
value: s.to_owned(),
})
}
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn format_utc_timestamp_millis(dt: OffsetDateTime) -> String {
format!(
"{:04}{:02}{:02}-{:02}:{:02}:{:02}.{:03}",
dt.year(),
u8::from(dt.month()),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
dt.millisecond()
)
}
fn parse_utc_timestamp(s: &str) -> Option<OffsetDateTime> {
let (date, time_part) = s.split_once('-')?;
if date.len() != 8 {
return None;
}
let year: i32 = date.get(0..4)?.parse().ok()?;
let month: u8 = date.get(4..6)?.parse().ok()?;
let day: u8 = date.get(6..8)?.parse().ok()?;
let (hms, frac) = match time_part.split_once('.') {
Some((h, f)) => (h, Some(f)),
None => (time_part, None),
};
let mut parts = hms.split(':');
let hour: u8 = parts.next()?.parse().ok()?;
let min: u8 = parts.next()?.parse().ok()?;
let sec: u8 = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
let nanos: u32 = match frac {
None => 0,
Some(f) if matches!(f.len(), 3 | 6 | 9) && f.bytes().all(|b| b.is_ascii_digit()) => {
let mut buf = [b'0'; 9];
for (slot, digit) in buf.iter_mut().zip(f.bytes()) {
*slot = digit;
}
core::str::from_utf8(&buf).ok()?.parse().ok()?
}
Some(_) => return None,
};
let month = time::Month::try_from(month).ok()?;
let date = time::Date::from_calendar_date(year, month, day).ok()?;
let (sec, nanos) = if sec == 60 {
(59, 999_999_999)
} else {
(sec, nanos)
};
let time = time::Time::from_hms_nano(hour, min, sec, nanos).ok()?;
Some(time::PrimitiveDateTime::new(date, time).assume_utc())
}
fn format_utc_date_only(date: time::Date) -> String {
format!(
"{:04}{:02}{:02}",
date.year(),
u8::from(date.month()),
date.day()
)
}
fn parse_utc_date_only(s: &str) -> Option<time::Date> {
if s.len() != 8 || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let year: i32 = s.get(0..4)?.parse().ok()?;
let month: u8 = s.get(4..6)?.parse().ok()?;
let day: u8 = s.get(6..8)?.parse().ok()?;
let month = time::Month::try_from(month).ok()?;
time::Date::from_calendar_date(year, month, day).ok()
}
fn format_utc_time_only_millis(t: time::Time) -> String {
format!(
"{:02}:{:02}:{:02}.{:03}",
t.hour(),
t.minute(),
t.second(),
t.millisecond()
)
}
fn parse_utc_time_only(s: &str) -> Option<time::Time> {
let (hms, frac) = match s.split_once('.') {
Some((h, f)) => (h, Some(f)),
None => (s, None),
};
let mut parts = hms.split(':');
let hour: u8 = parts.next()?.parse().ok()?;
let min: u8 = parts.next()?.parse().ok()?;
let sec: u8 = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
let nanos: u32 = match frac {
None => 0,
Some(f) if matches!(f.len(), 3 | 6 | 9) && f.bytes().all(|b| b.is_ascii_digit()) => {
let mut buf = [b'0'; 9];
for (slot, digit) in buf.iter_mut().zip(f.bytes()) {
*slot = digit;
}
core::str::from_utf8(&buf).ok()?.parse().ok()?
}
Some(_) => return None,
};
let (sec, nanos) = if sec == 60 {
(59, 999_999_999)
} else {
(sec, nanos)
};
time::Time::from_hms_nano(hour, min, sec, nanos).ok()
}