Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <radur@j2inn.com>"]
edition = "2024"
Expand Down
38 changes: 24 additions & 14 deletions src/haystack/encoding/json/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,22 +263,32 @@ impl<'de> Visitor<'de> for JsonValueDecoderVisitor {
}

fn parse_number(dict: &Dict) -> Result<HVal, JsonErr> {
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()),
}
}

Expand Down
31 changes: 26 additions & 5 deletions src/haystack/encoding/json/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,34 @@ impl Serialize for Symbol {

impl Serialize for Number {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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)
Expand Down
73 changes: 73 additions & 0 deletions tests/json/test_number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Value>(r#"{"_kind":"number","val":"foo"}"#).is_err());
assert!(serde_json::from_str::<Value>(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();
Expand Down
Loading