Skip to main content

miniextendr_api/from_r/
collections.rs

1//! Collection conversions (HashMap, BTreeMap, HashSet, BTreeSet).
2//!
3//! Named R lists convert to `HashMap<String, V>` / `BTreeMap<String, V>`.
4//! Unnamed R vectors convert to `HashSet<T>` / `BTreeSet<T>` for native types.
5//! Nested lists convert to `Vec<HashMap<String, V>>` etc.
6//!
7//! # Tradeoff
8//!
9//! These impls live on [`TryFromSexp`], so the
10//! shape (named-vs-unnamed) is strictly enforced and element conversion
11//! delegates to the `V: TryFromSexp` bound. Failure mode: feeding an unnamed
12//! list into a `HashMap<String, V>` parameter yields a `SexpError`, not
13//! silently empty keys.
14//!
15//! Outbound counterpart: `crate::into_r::collections`.
16
17use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
18
19use crate::from_r::{SexpError, SexpTypeError, TryFromSexp, charsxp_to_str, map_vecsxp_with};
20use crate::{RLogical, SEXP, SEXPTYPE, SexpExt};
21
22macro_rules! impl_map_try_from_sexp {
23    ($(#[$meta:meta])* $map_ty:ident, $create:expr) => {
24        $(#[$meta])*
25        impl<V: TryFromSexp> TryFromSexp for $map_ty<String, V>
26        where
27            V::Error: Into<SexpError>,
28        {
29            const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> = crate::from_r::NativeBorrow::in_list(V::NATIVE_BORROW);
30
31            type Error = SexpError;
32            // Each value is a top-level element of the named list, read by its
33            // own type: `no_na` stays top-level on a map.
34            #[inline]
35            fn __mx_has_na(&self) -> bool {
36                self.values().any(V::__mx_has_na)
37            }
38
39            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
40                named_list_to_map(sexp, $create)
41            }
42        }
43    };
44}
45
46#[doc = r" Convert R named list (VECSXP) to HashMap<String, V>."]
#[doc = r""]
#[doc =
r" See `named_list_to_map` for NA/empty name handling (elements with NA/empty"]
#[doc = r#" names map to key `""` and may silently overwrite each other)."#]
impl<V: TryFromSexp> TryFromSexp for HashMap<String, V> where
    V::Error: Into<SexpError> {
    const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
        crate::from_r::NativeBorrow::in_list(V::NATIVE_BORROW);
    type Error = SexpError;
    #[inline]
    fn __mx_has_na(&self) -> bool { self.values().any(V::__mx_has_na) }
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        named_list_to_map(sexp, HashMap::with_capacity)
    }
}impl_map_try_from_sexp!(
47    /// Convert R named list (VECSXP) to HashMap<String, V>.
48    ///
49    /// See `named_list_to_map` for NA/empty name handling (elements with NA/empty
50    /// names map to key `""` and may silently overwrite each other).
51    HashMap, HashMap::with_capacity
52);
53#[doc = r" Convert R named list (VECSXP) to BTreeMap<String, V>."]
#[doc = r""]
#[doc =
r" See `named_list_to_map` for NA/empty name handling (elements with NA/empty"]
#[doc = r#" names map to key `""` and may silently overwrite each other)."#]
impl<V: TryFromSexp> TryFromSexp for BTreeMap<String, V> where
    V::Error: Into<SexpError> {
    const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
        crate::from_r::NativeBorrow::in_list(V::NATIVE_BORROW);
    type Error = SexpError;
    #[inline]
    fn __mx_has_na(&self) -> bool { self.values().any(V::__mx_has_na) }
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        named_list_to_map(sexp, |_| BTreeMap::new())
    }
}impl_map_try_from_sexp!(
54    /// Convert R named list (VECSXP) to BTreeMap<String, V>.
55    ///
56    /// See `named_list_to_map` for NA/empty name handling (elements with NA/empty
57    /// names map to key `""` and may silently overwrite each other).
58    BTreeMap, |_| BTreeMap::new()
59);
60
61/// Helper to convert R named list to a map type.
62///
63/// Returns an error if the list has duplicate non-empty, non-NA names.
64///
65/// # NA and Empty Name Handling
66///
67/// **Warning:** Elements with NA or empty names are converted with key `""`:
68/// - `NA` names become empty string key `""`
69/// - Empty string names `""` stay as `""`
70/// - If multiple elements have NA/empty names, later ones **silently overwrite** earlier ones
71///
72/// This means data loss can occur without error if your list has multiple
73/// unnamed or NA-named elements.
74///
75/// **Example of silent data loss:**
76/// ```r
77/// # In R:
78/// x <- list(a = 1, 2, 3)  # Elements 2 and 3 have empty names
79/// # After conversion, only one of them survives under key ""
80/// ```
81///
82/// If you need all elements regardless of names, use `Vec<(String, V)>` instead,
83/// or convert the list to a vector first.
84fn named_list_to_map<V, M, F>(sexp: SEXP, create_map: F) -> Result<M, SexpError>
85where
86    V: TryFromSexp,
87    V::Error: Into<SexpError>,
88    M: Extend<(String, V)>,
89    F: FnOnce(usize) -> M,
90{
91    let actual = sexp.type_of();
92    if actual != SEXPTYPE::VECSXP {
93        return Err(SexpTypeError {
94            expected: SEXPTYPE::VECSXP,
95            actual,
96        }
97        .into());
98    }
99
100    let len = sexp.len();
101    let mut map = create_map(len);
102
103    // Get names attribute
104    let names = sexp.get_names();
105    let has_names = names.type_of() == SEXPTYPE::STRSXP && names.len() == len;
106
107    // Single-pass: check duplicates AND convert in one loop
108    let mut seen = HashSet::with_capacity(len);
109
110    for i in 0..len {
111        let key = if has_names {
112            let charsxp = names.string_elt(i as crate::R_xlen_t);
113            if charsxp == SEXP::na_string() {
114                String::new()
115            } else {
116                unsafe { charsxp_to_str(charsxp) }.to_owned()
117            }
118        } else {
119            // Use index as key if no names
120            i.to_string()
121        };
122
123        // Check duplicate for non-empty keys
124        if !key.is_empty() && !seen.insert(key.clone()) {
125            return Err(SexpError::DuplicateName(key));
126        }
127
128        let elem = sexp.vector_elt(i as crate::R_xlen_t);
129        let value = V::try_from_sexp(elem).map_err(|e| e.into())?;
130        map.extend(std::iter::once((key, value)));
131    }
132
133    Ok(map)
134}
135
136macro_rules! impl_vec_map_try_from_sexp {
137    ($(#[$meta:meta])* $map_ty:ident) => {
138        $(#[$meta])*
139        impl<V: TryFromSexp> TryFromSexp for Vec<$map_ty<String, V>>
140        where
141            V::Error: Into<SexpError>,
142        {
143            const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> = crate::from_r::NativeBorrow::in_list(<$map_ty<String, V> as TryFromSexp>::NATIVE_BORROW);
144
145    type Error = SexpError;
146
147            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
148                list_to_vec_of_maps::<$map_ty<String, V>>(sexp)
149            }
150        }
151    };
152}
153
154#[doc = r" Convert R list of named lists to `Vec<HashMap<String, V>>`."]
impl<V: TryFromSexp> TryFromSexp for Vec<HashMap<String, V>> where
    V::Error: Into<SexpError> {
    const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
        crate::from_r::NativeBorrow::in_list(<HashMap<String, V> as
                TryFromSexp>::NATIVE_BORROW);
    type Error = SexpError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        list_to_vec_of_maps::<HashMap<String, V>>(sexp)
    }
}impl_vec_map_try_from_sexp!(
155    /// Convert R list of named lists to `Vec<HashMap<String, V>>`.
156    HashMap
157);
158#[doc = r" Convert R list of named lists to `Vec<BTreeMap<String, V>>`."]
impl<V: TryFromSexp> TryFromSexp for Vec<BTreeMap<String, V>> where
    V::Error: Into<SexpError> {
    const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
        crate::from_r::NativeBorrow::in_list(<BTreeMap<String, V> as
                TryFromSexp>::NATIVE_BORROW);
    type Error = SexpError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        list_to_vec_of_maps::<BTreeMap<String, V>>(sexp)
    }
}impl_vec_map_try_from_sexp!(
159    /// Convert R list of named lists to `Vec<BTreeMap<String, V>>`.
160    BTreeMap
161);
162
163/// Helper to convert R list (VECSXP) to `Vec<M>` where each element is
164/// converted via `M: TryFromSexp`.
165fn list_to_vec_of_maps<M>(sexp: SEXP) -> Result<Vec<M>, SexpError>
166where
167    M: TryFromSexp,
168    M::Error: Into<SexpError>,
169{
170    map_vecsxp_with(sexp, |_i, elem| M::try_from_sexp(elem).map_err(Into::into))
171}
172
173macro_rules! impl_set_try_from_sexp_native {
174    ($set:ident<$t:ty>) => {
175        impl TryFromSexp for $set<$t> {
176            type Error = SexpTypeError;
177
178            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
179                let slice: &[$t] = TryFromSexp::try_from_sexp(sexp)?;
180                Ok(slice.iter().copied().collect())
181            }
182        }
183    };
184}
185
186impl TryFromSexp for HashSet<i32> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[i32] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.iter().copied().collect())
    }
}impl_set_try_from_sexp_native!(HashSet<i32>);
187impl TryFromSexp for HashSet<u8> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[u8] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.iter().copied().collect())
    }
}impl_set_try_from_sexp_native!(HashSet<u8>);
188impl TryFromSexp for HashSet<RLogical> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[RLogical] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.iter().copied().collect())
    }
}impl_set_try_from_sexp_native!(HashSet<RLogical>);
189impl TryFromSexp for BTreeSet<i32> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[i32] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.iter().copied().collect())
    }
}impl_set_try_from_sexp_native!(BTreeSet<i32>);
190impl TryFromSexp for BTreeSet<u8> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[u8] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.iter().copied().collect())
    }
}impl_set_try_from_sexp_native!(BTreeSet<u8>);
191
192macro_rules! impl_vec_try_from_sexp_native {
193    ($t:ty) => {
194        impl TryFromSexp for Vec<$t> {
195            type Error = SexpTypeError;
196
197            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
198                let slice: &[$t] = TryFromSexp::try_from_sexp(sexp)?;
199                Ok(slice.to_vec())
200            }
201        }
202    };
203}
204
205impl TryFromSexp for Vec<i32> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[i32] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.to_vec())
    }
}impl_vec_try_from_sexp_native!(i32);
206impl TryFromSexp for Vec<f64> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[f64] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.to_vec())
    }
}impl_vec_try_from_sexp_native!(f64);
207impl TryFromSexp for Vec<u8> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[u8] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.to_vec())
    }
}impl_vec_try_from_sexp_native!(u8);
208impl TryFromSexp for Vec<RLogical> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[RLogical] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.to_vec())
    }
}impl_vec_try_from_sexp_native!(RLogical);
209impl TryFromSexp for Vec<crate::Rcomplex> {
    type Error = SexpTypeError;
    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
        let slice: &[crate::Rcomplex] = TryFromSexp::try_from_sexp(sexp)?;
        Ok(slice.to_vec())
    }
}impl_vec_try_from_sexp_native!(crate::Rcomplex);
210// endregion