use crate::value::HoconValue;
pub(crate) fn numeric_object_to_array(value: &HoconValue) -> Option<Vec<HoconValue>> {
let map = match value {
HoconValue::Object(m) => m,
_ => return None,
};
if map.is_empty() {
return None;
}
let mut eligible: Vec<(i32, HoconValue)> = map
.iter()
.filter_map(|(k, v)| parse_eligible_key(k).map(|n| (n, v.clone())))
.collect();
if eligible.is_empty() {
return None;
}
eligible.sort_by_key(|(n, _)| *n);
Some(eligible.into_iter().map(|(_, v)| v).collect())
}
fn parse_eligible_key(s: &str) -> Option<i32> {
if s.is_empty() {
return None;
}
let mut chars = s.chars();
let first = chars.next()?;
match first {
'0' => {
if chars.next().is_some() {
return None; }
}
'1'..='9' => {
for c in chars {
if !c.is_ascii_digit() {
return None;
}
}
}
_ => return None, }
s.parse::<i32>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::{HoconValue, ScalarValue};
use indexmap::IndexMap;
fn sv(s: &str) -> HoconValue {
HoconValue::Scalar(ScalarValue::string(s.to_string()))
}
fn make_obj(pairs: &[(&str, &str)]) -> HoconValue {
let mut map = IndexMap::new();
for (k, v) in pairs {
map.insert(k.to_string(), sv(v));
}
HoconValue::Object(map)
}
#[test]
fn eligible_zero() {
assert_eq!(parse_eligible_key("0"), Some(0));
}
#[test]
fn eligible_positive() {
assert_eq!(parse_eligible_key("1"), Some(1));
assert_eq!(parse_eligible_key("42"), Some(42));
assert_eq!(parse_eligible_key("100"), Some(100));
}
#[test]
fn eligible_i32_max() {
assert_eq!(parse_eligible_key("2147483647"), Some(2_147_483_647_i32));
}
#[test]
fn rejected_leading_zero() {
assert_eq!(parse_eligible_key("00"), None);
assert_eq!(parse_eligible_key("01"), None);
assert_eq!(parse_eligible_key("007"), None);
}
#[test]
fn rejected_plus_sign() {
assert_eq!(parse_eligible_key("+1"), None);
assert_eq!(parse_eligible_key("+0"), None);
}
#[test]
fn rejected_minus_sign() {
assert_eq!(parse_eligible_key("-1"), None);
assert_eq!(parse_eligible_key("-0"), None);
}
#[test]
fn rejected_whitespace() {
assert_eq!(parse_eligible_key(" 1"), None);
assert_eq!(parse_eligible_key("1 "), None);
}
#[test]
fn rejected_empty() {
assert_eq!(parse_eligible_key(""), None);
}
#[test]
fn rejected_overflow() {
assert_eq!(parse_eligible_key("2147483648"), None);
assert_eq!(parse_eligible_key("99999999999"), None);
}
#[test]
fn rejected_decimal() {
assert_eq!(parse_eligible_key("1.0"), None);
assert_eq!(parse_eligible_key("1e2"), None);
}
#[test]
fn rejected_hex() {
assert_eq!(parse_eligible_key("0x1"), None);
assert_eq!(parse_eligible_key("0b10"), None);
}
#[test]
fn rejected_alpha() {
assert_eq!(parse_eligible_key("foo"), None);
assert_eq!(parse_eligible_key("bar"), None);
}
#[test]
fn not_an_object_returns_none() {
let v = sv("hello");
assert!(numeric_object_to_array(&v).is_none());
}
#[test]
fn empty_object_returns_none() {
let v = HoconValue::Object(IndexMap::new());
assert!(numeric_object_to_array(&v).is_none());
}
#[test]
fn no_eligible_keys_returns_none() {
let v = make_obj(&[("foo", "a"), ("bar", "b")]);
assert!(numeric_object_to_array(&v).is_none());
}
#[test]
fn basic_conversion() {
let v = make_obj(&[("0", "a"), ("1", "b")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 2);
assert_eq!(extract_raw(&arr[0]), "a");
assert_eq!(extract_raw(&arr[1]), "b");
}
#[test]
fn non_int_keys_ignored() {
let v = make_obj(&[("0", "a"), ("foo", "b"), ("1", "c")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 2);
assert_eq!(extract_raw(&arr[0]), "a");
assert_eq!(extract_raw(&arr[1]), "c");
}
#[test]
fn gaps_compacted() {
let v = make_obj(&[("0", "a"), ("2", "c")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 2);
assert_eq!(extract_raw(&arr[0]), "a");
assert_eq!(extract_raw(&arr[1]), "c");
}
#[test]
fn sorted_by_key() {
let v = make_obj(&[("1", "b"), ("0", "a")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 2);
assert_eq!(extract_raw(&arr[0]), "a");
assert_eq!(extract_raw(&arr[1]), "b");
}
#[test]
fn leading_zero_rejected_only_canonical_zero_eligible() {
let v = make_obj(&[("00", "a"), ("0", "b")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 1);
assert_eq!(extract_raw(&arr[0]), "b");
}
#[test]
fn negative_key_rejected() {
let v = make_obj(&[("-1", "a"), ("0", "b")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 1);
assert_eq!(extract_raw(&arr[0]), "b");
}
#[test]
fn plus_sign_rejected() {
let v = make_obj(&[("+1", "a"), ("0", "b")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 1);
assert_eq!(extract_raw(&arr[0]), "b");
}
#[test]
fn minus_zero_rejected() {
let v = make_obj(&[("-0", "a"), ("0", "b")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 1);
assert_eq!(extract_raw(&arr[0]), "b");
}
#[test]
fn overflow_rejected() {
let v = make_obj(&[("99999999999", "a"), ("0", "b")]);
let arr = numeric_object_to_array(&v).expect("should convert");
assert_eq!(arr.len(), 1);
assert_eq!(extract_raw(&arr[0]), "b");
}
fn extract_raw(v: &HoconValue) -> &str {
match v {
HoconValue::Scalar(sv) => &sv.raw,
_ => panic!("expected scalar, got {:?}", v),
}
}
}