use std::cmp::Ordering;
use std::fmt::Write as _;
use oino_ai::JsonValue;
pub(crate) fn len16(text: &str) -> usize {
text.encode_utf16().count()
}
pub(crate) fn prefix16(text: &str, units: usize) -> &str {
let mut used = 0;
for (index, ch) in text.char_indices() {
used += ch.len_utf16();
if used > units {
return &text[..index];
}
}
text
}
pub(crate) fn cmp16(a: &str, b: &str) -> Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
pub(crate) fn stringify(value: &JsonValue) -> String {
let mut out = String::new();
write_value(&mut out, value);
out
}
fn write_value(out: &mut String, value: &JsonValue) {
match value {
JsonValue::Null => out.push_str("null"),
JsonValue::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),
JsonValue::Number(number) => write_number(out, number),
JsonValue::String(text) => write_string(out, text),
JsonValue::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_value(out, item);
}
out.push(']');
}
JsonValue::Object(map) => {
out.push('{');
for (index, (key, item)) in map.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_string(out, key);
out.push(':');
write_value(out, item);
}
out.push('}');
}
}
}
fn write_number(out: &mut String, number: &serde_json::Number) {
if number.is_i64() || number.is_u64() {
let _ = write!(out, "{number}");
return;
}
let Some(float) = number.as_f64() else {
out.push_str("null");
return;
};
if !float.is_finite() {
out.push_str("null");
} else if float.fract() == 0.0 && float.abs() < 1e21 {
let _ = write!(out, "{float:.0}");
if out.ends_with("-0") {
out.truncate(out.len() - 2);
out.push('0');
}
} else {
let text = number.to_string();
match text.split_once('e') {
Some((mantissa, exponent)) if !exponent.starts_with(['-', '+']) => {
let _ = write!(out, "{mantissa}e+{exponent}");
}
_ => out.push_str(&text),
}
}
}
fn write_string(out: &mut String, text: &str) {
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if u32::from(ch) < 0x20 => {
let _ = write!(out, "\\u{:04x}", u32::from(ch));
}
ch => out.push(ch),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn utf16_helpers() {
assert_eq!(len16("aπΓ©"), 4);
assert_eq!(prefix16("aπb", 2), "a");
assert_eq!(prefix16("aπb", 3), "aπ");
assert_eq!(prefix16("ab", 9), "ab");
let mut list = vec!["b", "B", "a", "Γ©", "π", "\u{ffff}"];
list.sort_by(|a, b| cmp16(a, b));
assert_eq!(list, ["B", "a", "b", "Γ©", "π", "\u{ffff}"]);
}
#[test]
fn stringify_matches_js() {
let value = json!({"s": "q\"\n\u{1}ΓΌ", "n": 1.0, "f": 1.5, "i": -3, "z": null,
"a": [true, false], "big": 1e21, "small": 1e-7, "neg": -0.0});
assert_eq!(
stringify(&value),
r#"{"s":"q\"\n\u0001ΓΌ","n":1,"f":1.5,"i":-3,"z":null,"a":[true,false],"big":1e+21,"small":1e-7,"neg":0}"#
);
}
}