use bacnet_client::log_reader::LogCursor;
use bacnet_services::read_range::RangeSpec;
use bacnet_types::calendar::SpecificDate;
use bacnet_types::primitives::{Date, Time};
const DEFAULT_COUNT: i16 = 100;
#[derive(Debug, Clone, Default, PartialEq, Eq, clap::Args)]
pub struct RangeOptions {
#[arg(long, conflicts_with_all = ["sequence", "time"])]
pub position: Option<u64>,
#[arg(long, conflicts_with = "time")]
pub sequence: Option<u64>,
#[arg(long, value_parser = parse_reference_time)]
pub time: Option<(Date, Time)>,
#[arg(long, allow_negative_numbers = true)]
pub count: Option<i16>,
#[arg(long)]
pub all: bool,
#[arg(long, conflicts_with = "all")]
pub strict: bool,
}
impl RangeOptions {
fn count(&self) -> Result<i16, String> {
match self.count.unwrap_or(DEFAULT_COUNT) {
0 => Err("--count may not be zero".into()),
count => Ok(count),
}
}
pub fn spec(&self) -> Result<Option<RangeSpec>, String> {
let count = || self.count().map(i32::from);
Ok(match (self.position, self.sequence, self.time) {
(Some(reference_index), _, _) => Some(RangeSpec::ByPosition {
reference_index,
count: count()?,
}),
(_, Some(reference_seq), _) => Some(RangeSpec::BySequenceNumber {
reference_seq,
count: count()?,
}),
(_, _, Some(reference_time)) => Some(RangeSpec::ByTime {
reference_time,
count: count()?,
}),
(None, None, None) if self.count.is_some() => {
return Err("--count needs --position, --sequence, --time or --all".into())
}
(None, None, None) => None,
})
}
pub fn pages(&self) -> Result<(LogCursor, u16), String> {
let cursor = match (self.position, self.sequence, self.time) {
(Some(position), _, _) => LogCursor::Position(position),
(_, Some(sequence), _) => LogCursor::Sequence(sequence),
(_, _, Some((date, time))) => LogCursor::Time(date, time),
(None, None, None) => LogCursor::Oldest,
};
let page_size = u16::try_from(self.count()?)
.map_err(|_| "--all reads forward: --count must be positive".to_string())?;
Ok((cursor, page_size))
}
}
fn number<T: std::str::FromStr>(text: &str, what: &str) -> Result<T, String> {
text.parse()
.map_err(|_| format!("invalid {what} '{text}' in --time"))
}
pub fn parse_reference_time(text: &str) -> Result<(Date, Time), String> {
let usage = || format!("--time must be YYYY-MM-DDTHH:MM[:SS[.hh]], got '{text}'");
let (date, time) = text.split_once(['T', ' ']).ok_or_else(usage)?;
let [year, month, day] =
<[&str; 3]>::try_from(date.split('-').collect::<Vec<_>>()).map_err(|_| usage())?;
let day = SpecificDate::new(
number(year, "year")?,
number(month, "month")?,
number(day, "day")?,
)
.ok_or_else(|| format!("--time date '{date}' is not a day in 1900..=2154"))?;
let (clock, hundredths) = match time.split_once('.') {
Some((clock, fraction)) if fraction.len() == 2 => (clock, number(fraction, "hundredths")?),
Some(_) => return Err(usage()),
None => (time, 0),
};
let parts: Vec<&str> = clock.split(':').collect();
let (hour, minute, second) = match parts.as_slice() {
[hour, minute] => (number(hour, "hour")?, number(minute, "minute")?, 0),
[hour, minute, second] => (
number(hour, "hour")?,
number(minute, "minute")?,
number(second, "second")?,
),
_ => return Err(usage()),
};
let time = Time {
hour,
minute,
second,
hundredths,
};
if !time.is_specific() {
return Err(format!("--time '{text}' is not a time of day"));
}
Ok((day.to_date(), time))
}
#[cfg(test)]
#[path = "read_range_options_tests.rs"]
mod tests;