use std::borrow::Cow;
pub(crate) fn sanitize_open_meteo_floats(bytes: &[u8]) -> Cow<'_, [u8]> {
let mut output = None::<Vec<u8>>;
let mut index = 0;
let mut in_string = false;
let mut escaped = false;
while index < bytes.len() {
let byte = bytes[index];
if in_string {
push_if_replacing(&mut output, byte);
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == b'"' {
in_string = false;
}
index += 1;
continue;
}
if byte == b'"' {
in_string = true;
push_if_replacing(&mut output, byte);
index += 1;
continue;
}
if bytes[index..].starts_with(b"nan") && is_json_token_boundary(bytes.get(index + 3)) {
output
.get_or_insert_with(|| {
let mut output = Vec::with_capacity(bytes.len());
output.extend_from_slice(&bytes[..index]);
output
})
.extend_from_slice(b"null");
index += 3;
continue;
}
push_if_replacing(&mut output, byte);
index += 1;
}
match output {
Some(output) => Cow::Owned(output),
None => Cow::Borrowed(bytes),
}
}
fn push_if_replacing(output: &mut Option<Vec<u8>>, byte: u8) {
if let Some(output) = output {
output.push(byte);
}
}
fn is_json_token_boundary(byte: Option<&u8>) -> bool {
matches!(
byte,
None | Some(b' ' | b'\n' | b'\r' | b'\t' | b',' | b']' | b'}')
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replaces_bare_nan_tokens_with_null() {
let sanitized = sanitize_open_meteo_floats(br#"{"elevation":[0.0,nan,0.0]}"#);
assert_eq!(sanitized.as_ref(), br#"{"elevation":[0.0,null,0.0]}"#);
}
#[test]
fn does_not_replace_nan_inside_json_strings() {
let sanitized = sanitize_open_meteo_floats(br#"{"note":"nan","elevation":[nan]}"#);
assert_eq!(sanitized.as_ref(), br#"{"note":"nan","elevation":[null]}"#);
}
#[test]
fn borrows_original_bytes_when_no_nan_token_is_present() {
let input = br#"{"elevation":[38.0,409.0]}"#;
assert!(matches!(
sanitize_open_meteo_floats(input),
Cow::Borrowed(_)
));
}
}