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