Skip to main content

miniextendr_api/list/
named.rs

1//! `NamedList` — O(1) name-indexed access to R lists.
2//!
3//! Wraps a [`List`](super::List) and builds a `HashMap<String, usize>` index
4//! on construction. Use when accessing multiple elements by name from the
5//! same list — each lookup is O(1) instead of O(n).
6
7use std::collections::HashMap;
8
9use crate::ffi::{SEXP, SexpExt};
10use crate::from_r::{SexpError, TryFromSexp};
11use crate::into_r::IntoR;
12
13use super::List;
14
15/// A named list with O(1) name-based element lookup.
16///
17/// Wraps a [`List`] and builds a `HashMap<String, usize>` index of element names
18/// on construction. Use this when you need to access multiple elements by name
19/// from the same list — each lookup is O(1) instead of O(n).
20///
21/// # When to Use
22///
23/// | Pattern | Type |
24/// |---------|------|
25/// | Single named lookup | [`List::get_named`] is fine |
26/// | Multiple named lookups | `NamedList` (O(n) build + O(1) per lookup) |
27/// | Positional access only | [`List`] — no indexing overhead |
28///
29/// # Name Handling
30///
31/// - `NA` and empty-string names are excluded from the index
32/// - If duplicate names exist, the **last** occurrence wins
33/// - Positional access via [`get_index`](Self::get_index) is always available
34pub struct NamedList {
35    list: List,
36    index: HashMap<String, usize>,
37}
38
39impl NamedList {
40    /// Build a `NamedList` from a `List`, indexing all non-empty, non-NA names.
41    ///
42    /// Returns `None` if the list has no `names` attribute.
43    pub fn new(list: List) -> Option<Self> {
44        let names_sexp = list.names()?;
45        let n: usize = list
46            .len()
47            .try_into()
48            .expect("list length must be non-negative");
49        let mut index = HashMap::with_capacity(n);
50
51        for i in 0..n {
52            let idx: isize = i.try_into().expect("index exceeds isize::MAX");
53            let name_sexp = names_sexp.string_elt(idx);
54            if name_sexp == SEXP::na_string() {
55                continue;
56            }
57            let name_ptr = name_sexp.r_char();
58            let name_cstr = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
59            if let Ok(s) = name_cstr.to_str() {
60                if !s.is_empty() {
61                    index.insert(s.to_owned(), i);
62                }
63            }
64        }
65
66        Some(NamedList { list, index })
67    }
68
69    /// Get an element by name with O(1) lookup, converting to type `T`.
70    ///
71    /// Returns `None` if the name is not found or conversion fails.
72    #[inline]
73    pub fn get<T>(&self, name: &str) -> Option<T>
74    where
75        T: TryFromSexp<Error = SexpError>,
76    {
77        let &idx = self.index.get(name)?;
78        let idx_isize: isize = idx.try_into().ok()?;
79        let elem = self.list.as_sexp().vector_elt(idx_isize);
80        T::try_from_sexp(elem).ok()
81    }
82
83    /// Get a raw SEXP element by name with O(1) lookup.
84    #[inline]
85    pub fn get_raw(&self, name: &str) -> Option<SEXP> {
86        let &idx = self.index.get(name)?;
87        let idx_isize: isize = idx.try_into().ok()?;
88        Some(self.list.as_sexp().vector_elt(idx_isize))
89    }
90
91    /// Get element at 0-based index and convert to type `T`.
92    ///
93    /// Falls through to [`List::get_index`] — no name lookup involved.
94    #[inline]
95    pub fn get_index<T>(&self, idx: isize) -> Option<T>
96    where
97        T: TryFromSexp<Error = SexpError>,
98    {
99        self.list.get_index(idx)
100    }
101
102    /// Check if a name exists in the index.
103    #[inline]
104    pub fn contains(&self, name: &str) -> bool {
105        self.index.contains_key(name)
106    }
107
108    /// Number of elements in the list (including unnamed ones).
109    #[inline]
110    pub fn len(&self) -> isize {
111        self.list.len()
112    }
113
114    /// Returns `true` if the list is empty.
115    #[inline]
116    pub fn is_empty(&self) -> bool {
117        self.list.is_empty()
118    }
119
120    /// Number of indexed (named) elements.
121    #[inline]
122    pub fn named_len(&self) -> usize {
123        self.index.len()
124    }
125
126    /// Get the underlying `List`.
127    #[inline]
128    pub fn as_list(&self) -> List {
129        self.list
130    }
131
132    /// Consume and return the underlying `List`.
133    #[inline]
134    pub fn into_list(self) -> List {
135        self.list
136    }
137
138    /// Iterate over indexed names (unordered).
139    pub fn names(&self) -> impl Iterator<Item = &str> {
140        self.index.keys().map(|s| s.as_str())
141    }
142
143    /// Iterate over `(name, position)` pairs (unordered).
144    pub fn entries(&self) -> impl Iterator<Item = (&str, usize)> {
145        self.index.iter().map(|(k, &v)| (k.as_str(), v))
146    }
147}
148
149impl IntoR for NamedList {
150    type Error = std::convert::Infallible;
151    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
152        Ok(self.into_sexp())
153    }
154    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
155        self.try_into_sexp()
156    }
157    #[inline]
158    fn into_sexp(self) -> SEXP {
159        self.list.into_sexp()
160    }
161}
162
163impl TryFromSexp for NamedList {
164    type Error = SexpError;
165
166    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
167        let list = List::try_from_sexp(sexp).map_err(|e| SexpError::InvalidValue(e.to_string()))?;
168        NamedList::new(list)
169            .ok_or_else(|| SexpError::InvalidValue("list has no names attribute".into()))
170    }
171}
172
173impl TryFromSexp for Option<NamedList> {
174    type Error = SexpError;
175
176    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
177        if sexp == SEXP::nil() {
178            return Ok(None);
179        }
180        let named = NamedList::try_from_sexp(sexp)?;
181        Ok(Some(named))
182    }
183}
184
185// IntoList and TryFromList traits are defined in the parent list.rs module.