Skip to main content

miniextendr_api/list/
named.rs

1//! `NamedList` — O(1) name-indexed access to R lists.
2//!
3//! Wraps a [`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::from_r::{SexpError, TryFromSexp};
10use crate::into_r::IntoR;
11use crate::{SEXP, SexpExt};
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. The
72    /// conversion error is discarded, so `T`'s `TryFromSexp::Error` is
73    /// unconstrained; use [`get_raw`](Self::get_raw) when you need the error.
74    #[inline]
75    pub fn get<T>(&self, name: &str) -> Option<T>
76    where
77        T: TryFromSexp,
78    {
79        let &idx = self.index.get(name)?;
80        let idx_isize: isize = idx.try_into().ok()?;
81        let elem = self.list.as_sexp().vector_elt(idx_isize);
82        T::try_from_sexp(elem).ok()
83    }
84
85    /// Get a raw SEXP element by name with O(1) lookup.
86    #[inline]
87    pub fn get_raw(&self, name: &str) -> Option<SEXP> {
88        let &idx = self.index.get(name)?;
89        let idx_isize: isize = idx.try_into().ok()?;
90        Some(self.list.as_sexp().vector_elt(idx_isize))
91    }
92
93    /// Get element at 0-based index and convert to type `T`.
94    ///
95    /// Falls through to [`List::get_index`] — no name lookup involved.
96    #[inline]
97    pub fn get_index<T>(&self, idx: isize) -> Option<T>
98    where
99        T: TryFromSexp,
100    {
101        self.list.get_index(idx)
102    }
103
104    /// Check if a name exists in the index.
105    #[inline]
106    pub fn contains(&self, name: &str) -> bool {
107        self.index.contains_key(name)
108    }
109
110    /// Number of elements in the list (including unnamed ones).
111    #[inline]
112    pub fn len(&self) -> isize {
113        self.list.len()
114    }
115
116    /// Returns `true` if the list is empty.
117    #[inline]
118    pub fn is_empty(&self) -> bool {
119        self.list.is_empty()
120    }
121
122    /// Number of indexed (named) elements.
123    #[inline]
124    pub fn named_len(&self) -> usize {
125        self.index.len()
126    }
127
128    /// Get the underlying `List`.
129    #[inline]
130    pub fn as_list(&self) -> List {
131        self.list
132    }
133
134    /// Consume and return the underlying `List`.
135    #[inline]
136    pub fn into_list(self) -> List {
137        self.list
138    }
139
140    /// Iterate over indexed names (unordered).
141    pub fn names(&self) -> impl Iterator<Item = &str> {
142        self.index.keys().map(|s| s.as_str())
143    }
144
145    /// Iterate over `(name, position)` pairs (unordered).
146    pub fn entries(&self) -> impl Iterator<Item = (&str, usize)> {
147        self.index.iter().map(|(k, &v)| (k.as_str(), v))
148    }
149}
150
151impl IntoR for NamedList {
152    type Error = std::convert::Infallible;
153    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
154        Ok(self.into_sexp())
155    }
156    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
157        self.try_into_sexp()
158    }
159    #[inline]
160    fn into_sexp(self) -> SEXP {
161        self.list.into_sexp()
162    }
163}
164
165impl TryFromSexp for NamedList {
166    type Error = SexpError;
167
168    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
169        let list = List::try_from_sexp(sexp).map_err(|e| SexpError::InvalidValue(e.to_string()))?;
170        NamedList::new(list)
171            .ok_or_else(|| SexpError::InvalidValue("list has no names attribute".into()))
172    }
173}
174
175impl TryFromSexp for Option<NamedList> {
176    type Error = SexpError;
177
178    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
179        if sexp == SEXP::nil() {
180            return Ok(None);
181        }
182        let named = NamedList::try_from_sexp(sexp)?;
183        Ok(Some(named))
184    }
185}
186
187// IntoList and TryFromList traits are defined in the parent list.rs module.