use bacnet_types::enums::{resolve_bits, resolve_enum, PropertyIdentifier, ResolvedBits};
use bacnet_types::primitives::{Date, PropertyValue, Time};
use super::Floats;
use crate::tui::app::objects::{kebab, object_label};
const DISPLAY_DIGITS: usize = 6;
pub(super) fn raw(data: &[u8]) -> String {
format!("[raw {}]", hex(data, " "))
}
fn hex(data: &[u8], separator: &str) -> String {
data.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(separator)
}
pub(super) fn display_float(value: f64) -> String {
if !value.is_finite() {
return value.to_string();
}
if value == 0.0 {
return "0".into();
}
let scientific = format!("{value:.prec$e}", prec = DISPLAY_DIGITS - 1);
let (mantissa, exponent) = scientific
.split_once('e')
.expect("an exponent form always has an e");
let exponent: i32 = exponent.parse().expect("a decimal exponent");
if (-4..DISPLAY_DIGITS as i32).contains(&exponent) {
let decimals = (DISPLAY_DIGITS as i32 - 1 - exponent).max(0) as usize;
trim(format!("{value:.decimals$}"))
} else {
format!("{}e{exponent}", trim(mantissa.to_string()))
}
}
fn trim(text: String) -> String {
if !text.contains('.') {
return text;
}
text.trim_end_matches('0').trim_end_matches('.').to_string()
}
fn float(value: f64, exact: String, floats: Floats) -> String {
match floats {
Floats::Display => display_float(value),
Floats::Exact => exact,
}
}
pub(super) fn text(property: PropertyIdentifier, value: &PropertyValue, floats: Floats) -> String {
match value {
PropertyValue::Null => "null".into(),
PropertyValue::Boolean(b) => b.to_string(),
PropertyValue::Unsigned(n) => n.to_string(),
PropertyValue::Signed(n) => n.to_string(),
PropertyValue::Real(v) => float(f64::from(*v), v.to_string(), floats),
PropertyValue::Double(v) => float(*v, v.to_string(), floats),
PropertyValue::OctetString(bytes) if bytes.is_empty() => "(no octets)".into(),
PropertyValue::OctetString(bytes) => format!("0x{}", hex(bytes, "")),
PropertyValue::CharacterString(s) => format!("\"{s}\""),
PropertyValue::BitString { .. } => bits(property, value),
PropertyValue::Enumerated(raw) => match resolve_enum(property, value) {
Some(bacnet_types::enums::ResolvedEnum::Unknown(_)) | None => raw.to_string(),
Some(named) => kebab(named),
},
PropertyValue::Date(date) => date_text(date),
PropertyValue::Time(time) => time_text(time),
PropertyValue::ObjectIdentifier(object) => object_label(*object),
PropertyValue::List(items) => format!(
"[{}]",
items
.iter()
.map(|item| text(property, item, floats))
.collect::<Vec<_>>()
.join(", ")
),
PropertyValue::ApplicationData(bytes) => raw(bytes),
}
}
fn bits(property: PropertyIdentifier, value: &PropertyValue) -> String {
match resolve_bits(property, value) {
Some(ResolvedBits::Unknown { .. }) | None => plain_bits(value),
Some(_) if !any_bit_set(value) => "none".into(),
Some(named) => kebab(named),
}
}
fn any_bit_set(value: &PropertyValue) -> bool {
matches!(value, PropertyValue::BitString { data, .. } if data.iter().any(|b| *b != 0))
}
fn plain_bits(value: &PropertyValue) -> String {
let PropertyValue::BitString { unused_bits, data } = value else {
return String::new();
};
let total = (data.len() * 8).saturating_sub(usize::from(*unused_bits));
let bits: String = (0..total)
.map(|i| {
if data[i / 8] & (0x80 >> (i % 8)) != 0 {
'1'
} else {
'0'
}
})
.collect();
format!("bits({bits})")
}
fn any_date(date: &Date) -> bool {
[date.year, date.month, date.day, date.day_of_week] == [Date::UNSPECIFIED; 4]
}
fn any_time(time: &Time) -> bool {
[time.hour, time.minute, time.second, time.hundredths] == [Time::UNSPECIFIED; 4]
}
const DAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
pub(super) fn weekday(day: u8) -> Option<&'static str> {
DAYS.get(usize::from(day).checked_sub(1)?).copied()
}
pub(super) fn month_text(month: u8) -> String {
match month {
Date::UNSPECIFIED => "*".into(),
13 => "odd-months".into(),
14 => "even-months".into(),
month => format!("{month:02}"),
}
}
fn day_text(day: u8) -> String {
match day {
Date::UNSPECIFIED => "*".into(),
32 => "last-day".into(),
33 => "odd-days".into(),
34 => "even-days".into(),
day => format!("{day:02}"),
}
}
pub(super) fn date_text(date: &Date) -> String {
if any_date(date) {
return "any date".into();
}
let year = if date.year == Date::UNSPECIFIED {
"*".to_string()
} else {
(u16::from(date.year) + 1900).to_string()
};
let mut text = format!("{year}-{}-{}", month_text(date.month), day_text(date.day));
if let Some(day) = weekday(date.day_of_week) {
text.push(' ');
text.push_str(day);
}
text
}
pub(super) fn time_text(time: &Time) -> String {
if any_time(time) {
return "any time".into();
}
let field = |value: u8| {
if value == Time::UNSPECIFIED {
"*".to_string()
} else {
format!("{value:02}")
}
};
let mut text = format!("{}:{}", field(time.hour), field(time.minute));
if time.second != 0 || time.hundredths != 0 {
text.push(':');
text.push_str(&field(time.second));
}
if time.hundredths != 0 {
text.push('.');
text.push_str(&field(time.hundredths));
}
text
}
pub(super) fn date_time(date: &Date, time: &Time) -> String {
match (any_date(date), any_time(time)) {
(true, true) => "unspecified".into(),
(false, true) => date_text(date),
_ => format!("{} {}", date_text(date), time_text(time)),
}
}
pub(super) fn date_range(start: &Date, end: &Date) -> String {
match (any_date(start), any_date(end)) {
(true, true) => "any date".into(),
(false, true) => format!("from {}", date_text(start)),
(true, false) => format!("until {}", date_text(end)),
(false, false) => format!("{} to {}", date_text(start), date_text(end)),
}
}