diff --git a/Cargo.lock b/Cargo.lock index 01dd8eb..c4822e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,7 +388,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libhaystack" -version = "4.1.0" +version = "4.1.1" dependencies = [ "chrono", "chrono-tz", diff --git a/Cargo.toml b/Cargo.toml index 72a7fc7..6e00c2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libhaystack" -version = "4.1.0" +version = "4.1.1" description = "Rust implementation of the Haystack 4 data types, defs, filter, units, and encodings" authors = ["J2 Innovations", "Radu Racariu "] edition = "2024" diff --git a/src/haystack/encoding/json/decode.rs b/src/haystack/encoding/json/decode.rs index 2f7247a..1f46222 100644 --- a/src/haystack/encoding/json/decode.rs +++ b/src/haystack/encoding/json/decode.rs @@ -263,22 +263,32 @@ impl<'de> Visitor<'de> for JsonValueDecoderVisitor { } fn parse_number(dict: &Dict) -> Result { - match dict.get_num("val") { - Some(val) => match dict.get_str("unit") { - Some(unit) => { - if let Some(unit) = get_unit(unit.as_str()) { - Ok(Number { - value: val.value, - unit: Some(unit), - } - .into()) - } else { - Err(JsonErr::custom(format!("Unit not found: {unit:?}"))) + let val = match dict.get_str("val") { + Some(val) => match val.as_str() { + "INF" => Number::make(f64::INFINITY), + "-INF" => Number::make(f64::NEG_INFINITY), + "NaN" => Number::make(f64::NAN), + _ => return Err(JsonErr::custom("Invalid 'val' string")), + }, + None => match dict.get_num("val") { + Some(val) => *val, + None => return Err(JsonErr::custom("Missing or invalid 'val'")), + }, + }; + + match dict.get_str("unit") { + Some(unit) => { + if let Some(unit) = get_unit(unit.as_str()) { + Ok(Number { + value: val.value, + unit: Some(unit), } + .into()) + } else { + Err(JsonErr::custom(format!("Unit not found: {unit:?}"))) } - None => Ok((*val).into()), - }, - None => Err(JsonErr::custom("Missing or invalid 'val'")), + } + None => Ok(val.into()), } } diff --git a/src/haystack/encoding/json/encode.rs b/src/haystack/encoding/json/encode.rs index e052f19..da6cc95 100644 --- a/src/haystack/encoding/json/encode.rs +++ b/src/haystack/encoding/json/encode.rs @@ -86,13 +86,34 @@ impl Serialize for Symbol { impl Serialize for Number { fn serialize(&self, serializer: S) -> Result { - if let Some(unit) = self.unit { - let mut map = serializer.serialize_map(Some(3))?; + // JSON has no representation for these values, Hayson encodes them as strings + let non_finite = if self.value.is_nan() { + Some("NaN") + } else if self.value == f64::INFINITY { + Some("INF") + } else if self.value == f64::NEG_INFINITY { + Some("-INF") + } else { + None + }; + + if self.unit.is_some() || non_finite.is_some() { + let len = if self.unit.is_some() { 3 } else { 2 }; + let mut map = serializer.serialize_map(Some(len))?; map.serialize_entry("_kind", "number")?; - map.serialize_entry("val", &self.value)?; - map.serialize_entry("unit", unit.symbol())?; + match non_finite { + Some(val) => map.serialize_entry("val", val)?, + None => map.serialize_entry("val", &self.value)?, + } + if let Some(unit) = self.unit { + map.serialize_entry("unit", unit.symbol())?; + } map.end() - } else if self.value.fract() == 0.0 { + } else if self.value.fract() == 0.0 + // `i64::MAX as f64` rounds up to 2^63, which doesn't fit in an i64, hence the `<` + && self.value >= i64::MIN as f64 + && self.value < i64::MAX as f64 + { serializer.serialize_i64(self.value as i64) } else { serializer.serialize_f64(self.value) diff --git a/tests/json/test_number.rs b/tests/json/test_number.rs index ed348cb..8769f77 100644 --- a/tests/json/test_number.rs +++ b/tests/json/test_number.rs @@ -25,6 +25,79 @@ fn test_json_serialize_num() { ); } +#[test] +fn test_json_serialize_num_outside_i64_range() { + // 2^63 is the first whole value above the i64 range + for num in [1e300, -1e300, 1e19, -1e19, 9223372036854775808.0, f64::MAX] { + let json = serde_json::to_string(&Value::make_number(num)).unwrap(); + let decoded: Value = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded, Value::make_number(num), "encoded as {json}"); + } +} + +#[test] +fn test_json_serialize_num_at_i64_bounds() { + // -2^63 is exactly i64::MIN + let mut json = serde_json::to_string(&Value::make_number(i64::MIN as f64)); + assert_eq!(json.unwrap(), i64::MIN.to_string()); + + // The largest f64 below 2^63 + json = serde_json::to_string(&Value::make_number(9223372036854774784.0)); + assert_eq!(json.unwrap(), "9223372036854774784"); +} + +#[test] +fn test_json_serialize_non_finite_num() { + for (num, val) in [ + (f64::INFINITY, "INF"), + (f64::NEG_INFINITY, "-INF"), + (f64::NAN, "NaN"), + ] { + let json = serde_json::to_string(&Value::make_number(num)).unwrap(); + assert_eq!(json, format!(r#"{{"_kind":"number","val":"{val}"}}"#)); + + let value = Value::make_number_unit(num, get_unit_or_default("m/s")); + let json = serde_json::to_string(&value).unwrap(); + assert_eq!( + json, + format!(r#"{{"_kind":"number","val":"{val}","unit":"m/s"}}"#) + ); + } +} + +#[test] +fn test_json_decode_non_finite_num() { + let mut value: Value = serde_json::from_str(r#"{"_kind":"number","val":"INF"}"#).unwrap(); + assert_eq!(value, Value::make_number(f64::INFINITY)); + + value = serde_json::from_str(r#"{"_kind":"number","val":"-INF"}"#).unwrap(); + assert_eq!(value, Value::make_number(f64::NEG_INFINITY)); + + value = serde_json::from_str(r#"{"_kind":"number","val":"NaN"}"#).unwrap(); + assert!(Number::try_from(&value).unwrap().value.is_nan()); + + value = serde_json::from_str(r#"{"_kind":"number","val":"INF","unit":"m/s"}"#).unwrap(); + assert_eq!( + value, + Value::make_number_unit(f64::INFINITY, get_unit_or_default("m/s")) + ); + + value = serde_json::from_str(r#"{"_kind":"number","val":"NaN","unit":"m/s"}"#).unwrap(); + let num = Number::try_from(&value).unwrap(); + assert!(num.value.is_nan()); + assert_eq!(num.unit, Some(get_unit_or_default("m/s"))); + + // A plain string stays a string + value = serde_json::from_str(r#""INF""#).unwrap(); + assert_eq!(value, Value::make_str("INF")); +} + +#[test] +fn test_json_decode_invalid_number_string() { + assert!(serde_json::from_str::(r#"{"_kind":"number","val":"foo"}"#).is_err()); + assert!(serde_json::from_str::(r#"{"_kind":"number","val":"inf"}"#).is_err()); +} + #[test] fn test_json_decode_number() { let mut value: Value = serde_json::from_str(r#"22.44"#).unwrap();