use indexmap::IndexMap;
use std::fmt::Write;
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Bool(bool),
SInt(i8),
Int(i16),
DInt(i32),
LInt(i64),
USInt(u8),
UInt(u16),
UDInt(u32),
ULInt(u64),
Real(f32),
LReal(f64),
String(String),
Bytes(Vec<u8>),
Array(Vec<Value>),
Struct(IndexMap<String, Value>),
}
impl Value {
pub fn as_i128(&self) -> Option<i128> {
Some(match *self {
Value::SInt(v) => v.into(),
Value::Int(v) => v.into(),
Value::DInt(v) => v.into(),
Value::LInt(v) => v.into(),
Value::USInt(v) => v.into(),
Value::UInt(v) => v.into(),
Value::UDInt(v) => v.into(),
Value::ULInt(v) => v.into(),
_ => return None,
})
}
pub fn as_f64(&self) -> Option<f64> {
match *self {
Value::Real(v) => Some(v.into()),
Value::LReal(v) => Some(v),
_ => self.as_i128().map(|v| v as f64),
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => match self.as_i128() {
Some(0) => Some(false),
Some(1) => Some(true),
_ => None,
},
}
}
pub fn py_repr(&self) -> String {
let mut out = String::new();
self.write_repr(&mut out);
out
}
fn write_repr(&self, out: &mut String) {
match self {
Value::Bool(true) => out.push_str("True"),
Value::Bool(false) => out.push_str("False"),
Value::Real(v) => out.push_str(&py_float_repr(f64::from(*v))),
Value::LReal(v) => out.push_str(&py_float_repr(*v)),
Value::String(s) => out.push_str(&py_str_repr(s)),
Value::Bytes(b) => out.push_str(&py_bytes_repr(b)),
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
item.write_repr(out);
}
out.push(']');
}
Value::Struct(map) => {
out.push('{');
for (i, (k, v)) in map.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&py_str_repr(k));
out.push_str(": ");
v.write_repr(out);
}
out.push('}');
}
other => {
let _ = write!(out, "{}", other.as_i128().unwrap_or_default());
}
}
}
}
pub(crate) fn py_float_repr(x: f64) -> String {
if x.is_nan() {
return "nan".into();
}
if x.is_infinite() {
return if x > 0.0 { "inf".into() } else { "-inf".into() };
}
if x == 0.0 {
return if x.is_sign_negative() {
"-0.0".into()
} else {
"0.0".into()
};
}
let sci = format!("{:e}", x.abs());
let (mantissa, exp) = sci.split_once('e').unwrap_or((&sci, "0"));
let exp: i32 = exp.parse().unwrap_or(0);
let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
let sign = if x < 0.0 { "-" } else { "" };
if !(-4..16).contains(&exp) {
let (first, rest) = digits.split_at(1);
let rest = if rest.is_empty() {
String::new()
} else {
format!(".{rest}")
};
let esign = if exp < 0 { '-' } else { '+' };
return format!("{sign}{first}{rest}e{esign}{:02}", exp.abs());
}
let point = exp + 1; let body = if point <= 0 {
format!("0.{}{}", "0".repeat((-point) as usize), digits)
} else if point as usize >= digits.len() {
format!("{}{}.0", digits, "0".repeat(point as usize - digits.len()))
} else {
let (a, b) = digits.split_at(point as usize);
format!("{a}.{b}")
};
format!("{sign}{body}")
}
pub(crate) fn py_str_repr(s: &str) -> String {
let quote = if s.contains('\'') && !s.contains('"') {
'"'
} else {
'\''
};
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c == quote => {
out.push('\\');
out.push(c);
}
c if (c as u32) < 0x20 || c as u32 == 0x7f => {
let _ = write!(out, "\\x{:02x}", c as u32);
}
c => out.push(c),
}
}
out.push(quote);
out
}
pub(crate) fn py_bytes_repr(b: &[u8]) -> String {
let quote = if b.contains(&b'\'') && !b.contains(&b'"') {
b'"'
} else {
b'\''
};
let mut out = String::with_capacity(b.len() + 3);
out.push('b');
out.push(quote as char);
for &c in b {
match c {
b'\\' => out.push_str("\\\\"),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
c if c == quote => {
out.push('\\');
out.push(c as char);
}
0x20..=0x7e => out.push(c as char),
c => {
let _ = write!(out, "\\x{c:02x}");
}
}
}
out.push(quote as char);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn float_repr_matches_python() {
let cases = [
(1.5, "1.5"),
(1e39, "1e+39"),
(-1e39, "-1e+39"),
(1.0, "1.0"),
(0.5, "0.5"),
(1e16, "1e+16"),
(1e15, "1000000000000000.0"),
(0.0001, "0.0001"),
(0.00001, "1e-05"),
(123456789.0, "123456789.0"),
(3.4028234663852886e38, "3.4028234663852886e+38"),
(0.1, "0.1"),
(1.401298464324817e-45, "1.401298464324817e-45"),
];
for (x, want) in cases {
assert_eq!(py_float_repr(x), want);
}
}
#[test]
fn str_and_bytes_repr_match_python() {
assert_eq!(py_str_repr("it's"), "\"it's\"");
assert_eq!(py_str_repr("a\nb"), "'a\\nb'");
assert_eq!(py_bytes_repr(b"\x00ab'"), "b\"\\x00ab'\"");
assert_eq!(py_bytes_repr(b"\xff"), "b'\\xff'");
}
#[test]
fn bool_class_accepts_only_bool_or_zero_and_one() {
assert_eq!(Value::LInt(1).as_bool(), Some(true));
assert_eq!(Value::LInt(2).as_bool(), None);
assert_eq!(Value::Real(1.0).as_bool(), None);
assert_eq!(Value::Bool(true).as_i128(), None);
}
}