Skip to main content

bubbles/value/
types.rs

1//! The [`Value`] type representing all runtime values.
2
3use core::fmt;
4
5/// All possible runtime values used in expressions and variables.
6#[derive(Debug, Clone, PartialEq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum Value {
9    /// A floating-point number.
10    Number(f64),
11    /// A UTF-8 string.
12    Text(String),
13    /// A boolean.
14    Bool(bool),
15}
16
17impl Value {
18    /// Returns whether the value is truthy under permissive coercion.
19    #[must_use]
20    pub fn is_truthy(&self) -> bool {
21        match self {
22            Self::Bool(v) => *v,
23            Self::Number(v) => *v != 0.0,
24            Self::Text(v) => !v.is_empty(),
25        }
26    }
27}
28
29impl fmt::Display for Value {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Number(v) => {
33                // Omit ".0" suffix so `{$n}` renders as "2" not "2.0".
34                #[allow(clippy::cast_possible_truncation)]
35                if v.fract() == 0.0 && v.abs() < 1e15 {
36                    write!(f, "{}", *v as i64)
37                } else {
38                    write!(f, "{v}")
39                }
40            }
41            Self::Text(v) => f.write_str(v),
42            Self::Bool(v) => write!(f, "{v}"),
43        }
44    }
45}
46
47impl From<f64> for Value {
48    fn from(v: f64) -> Self {
49        Self::Number(v)
50    }
51}
52
53impl From<bool> for Value {
54    fn from(v: bool) -> Self {
55        Self::Bool(v)
56    }
57}
58
59impl From<String> for Value {
60    fn from(v: String) -> Self {
61        Self::Text(v)
62    }
63}
64
65impl From<&str> for Value {
66    fn from(v: &str) -> Self {
67        Self::Text(v.to_owned())
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn display_integer_number() {
77        assert_eq!(Value::Number(3.0).to_string(), "3");
78    }
79
80    #[test]
81    fn display_fractional_number() {
82        assert_eq!(Value::Number(3.5).to_string(), "3.5");
83    }
84
85    #[test]
86    fn truthy_coercion() {
87        assert!(Value::Bool(true).is_truthy());
88        assert!(!Value::Bool(false).is_truthy());
89        assert!(Value::Number(1.0).is_truthy());
90        assert!(!Value::Number(0.0).is_truthy());
91        assert!(Value::Text("hi".into()).is_truthy());
92        assert!(!Value::Text(String::new()).is_truthy());
93    }
94
95    #[test]
96    fn display_very_large_integer_uses_float_formatting() {
97        // Above the `1e15` fast-path threshold in `Display`.
98        let s = Value::Number(1e16).to_string();
99        assert!(s.contains('e') || s.len() >= 15, "got {s}");
100    }
101
102    #[test]
103    fn display_bool() {
104        assert_eq!(Value::Bool(true).to_string(), "true");
105    }
106
107    #[test]
108    fn from_conversions() {
109        assert_eq!(Value::from(3.5_f64), Value::Number(3.5));
110        assert_eq!(Value::from(true), Value::Bool(true));
111        assert_eq!(Value::from("x".to_owned()), Value::Text("x".into()));
112        assert_eq!(Value::from("y"), Value::Text("y".into()));
113    }
114
115    #[test]
116    fn negative_number_is_truthy() {
117        assert!(Value::Number(-1.0).is_truthy());
118    }
119}