use reddb_types::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellType {
Text,
Integer,
Real,
}
impl CellType {
pub fn from_char(c: char) -> Option<Self> {
match c {
'T' => Some(CellType::Text),
'I' => Some(CellType::Integer),
'R' => Some(CellType::Real),
_ => None,
}
}
pub fn to_char(self) -> char {
match self {
CellType::Text => 'T',
CellType::Integer => 'I',
CellType::Real => 'R',
}
}
}
pub fn render_cell(value: &Value, ty: CellType) -> String {
if matches!(value, Value::Null) {
return "NULL".to_string();
}
match ty {
CellType::Text => render_text(value),
CellType::Integer => match coerce_integer(value) {
Some(i) => i.to_string(),
None => render_text(value),
},
CellType::Real => match coerce_real(value) {
Some(f) => format!("{f:.3}"),
None => render_text(value),
},
}
}
fn render_text(value: &Value) -> String {
let raw = match value {
Value::Text(s) => s.to_string(),
Value::Integer(i) => i.to_string(),
Value::UnsignedInteger(u) => u.to_string(),
Value::Boolean(b) => (if *b { 1 } else { 0 }).to_string(),
Value::Float(f) => format!("{f:.3}"),
other => format!("{other:?}"),
};
if raw.is_empty() {
return "(empty)".to_string();
}
raw.chars()
.map(|c| {
if ('\u{20}'..='\u{7e}').contains(&c) {
c
} else {
'@'
}
})
.collect()
}
fn coerce_integer(value: &Value) -> Option<i64> {
match value {
Value::Integer(i) => Some(*i),
Value::UnsignedInteger(u) => Some(*u as i64),
Value::Boolean(b) => Some(if *b { 1 } else { 0 }),
Value::Float(f) => Some(*f as i64),
_ => None,
}
}
fn coerce_real(value: &Value) -> Option<f64> {
match value {
Value::Float(f) => Some(*f),
Value::Integer(i) => Some(*i as f64),
Value::UnsignedInteger(u) => Some(*u as f64),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_renders_as_keyword_for_every_type() {
for ty in [CellType::Text, CellType::Integer, CellType::Real] {
assert_eq!(render_cell(&Value::Null, ty), "NULL");
}
}
#[test]
fn text_empty_string_is_marked() {
assert_eq!(
render_cell(&Value::Text("".into()), CellType::Text),
"(empty)"
);
}
#[test]
fn text_passes_printable_through_and_scrubs_the_rest() {
assert_eq!(
render_cell(&Value::Text("Alice".into()), CellType::Text),
"Alice"
);
assert_eq!(
render_cell(&Value::Text("a\tb".into()), CellType::Text),
"a@b"
);
}
#[test]
fn integer_coerces_and_prints_decimal() {
assert_eq!(render_cell(&Value::Integer(42), CellType::Integer), "42");
assert_eq!(
render_cell(&Value::UnsignedInteger(7), CellType::Integer),
"7"
);
assert_eq!(render_cell(&Value::Boolean(true), CellType::Integer), "1");
}
#[test]
fn real_prints_three_decimals() {
assert_eq!(render_cell(&Value::Float(1.5), CellType::Real), "1.500");
assert_eq!(render_cell(&Value::Integer(2), CellType::Real), "2.000");
}
#[test]
fn cell_type_round_trips_through_char() {
for ty in [CellType::Text, CellType::Integer, CellType::Real] {
assert_eq!(CellType::from_char(ty.to_char()), Some(ty));
}
assert_eq!(CellType::from_char('X'), None);
}
}