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`, empty-string and non-UTF-8 names are excluded from the index
32/// - If a name appears more than once, the **first** occurrence wins, as with
33///   R's `x[["name"]]` and [`List::get_named`]; check
34///   [`List::first_duplicate_name`] to refuse such a list instead
35/// - Positional access via [`get_index`](Self::get_index) is always available
36pub struct NamedList {
37    list: List,
38    index: HashMap<String, usize>,
39}
40
41impl NamedList {
42    /// Build a `NamedList` from a `List`, indexing all non-empty, non-NA names.
43    /// A repeated name indexes its first element.
44    ///
45    /// Returns `None` if the list has no `names` attribute.
46    pub fn new(list: List) -> Option<Self> {
47        // No `names` attribute, no `NamedList`.
48        list.names()?;
49        let n: usize = list
50            .len()
51            .try_into()
52            .expect("list length must be non-negative");
53        let mut index = HashMap::with_capacity(n);
54
55        for (i, s) in list.text_names() {
56            if !s.is_empty() && !index.contains_key(s) {
57                let i: usize = i.try_into().expect("list position is non-negative");
58                index.insert(s.to_owned(), i);
59            }
60        }
61
62        Some(NamedList { list, index })
63    }
64
65    /// Get an element by name with O(1) lookup, converting to type `T`.
66    ///
67    /// Returns `None` if the name is not found or conversion fails. The
68    /// conversion error is discarded, so `T`'s `TryFromSexp::Error` is
69    /// unconstrained; use [`get_raw`](Self::get_raw) when you need the error.
70    #[inline]
71    pub fn get<T>(&self, name: &str) -> Option<T>
72    where
73        T: TryFromSexp,
74    {
75        let &idx = self.index.get(name)?;
76        let idx_isize: isize = idx.try_into().ok()?;
77        let elem = self.list.as_sexp().vector_elt(idx_isize);
78        T::try_from_sexp(elem).ok()
79    }
80
81    /// Get a raw SEXP element by name with O(1) lookup.
82    #[inline]
83    pub fn get_raw(&self, name: &str) -> Option<SEXP> {
84        let &idx = self.index.get(name)?;
85        let idx_isize: isize = idx.try_into().ok()?;
86        Some(self.list.as_sexp().vector_elt(idx_isize))
87    }
88
89    /// Get element at 0-based index and convert to type `T`.
90    ///
91    /// Falls through to [`List::get_index`] — no name lookup involved.
92    #[inline]
93    pub fn get_index<T>(&self, idx: isize) -> Option<T>
94    where
95        T: TryFromSexp,
96    {
97        self.list.get_index(idx)
98    }
99
100    /// Check if a name exists in the index.
101    #[inline]
102    pub fn contains(&self, name: &str) -> bool {
103        self.index.contains_key(name)
104    }
105
106    /// Number of elements in the list (including unnamed ones).
107    #[inline]
108    pub fn len(&self) -> isize {
109        self.list.len()
110    }
111
112    /// Returns `true` if the list is empty.
113    #[inline]
114    pub fn is_empty(&self) -> bool {
115        self.list.is_empty()
116    }
117
118    /// Number of indexed (named) elements.
119    #[inline]
120    pub fn named_len(&self) -> usize {
121        self.index.len()
122    }
123
124    /// Get the underlying `List`.
125    #[inline]
126    pub fn as_list(&self) -> List {
127        self.list
128    }
129
130    /// Consume and return the underlying `List`.
131    #[inline]
132    pub fn into_list(self) -> List {
133        self.list
134    }
135
136    /// Iterate over indexed names (unordered).
137    pub fn names(&self) -> impl Iterator<Item = &str> {
138        self.index.keys().map(|s| s.as_str())
139    }
140
141    /// Iterate over `(name, position)` pairs (unordered).
142    pub fn entries(&self) -> impl Iterator<Item = (&str, usize)> {
143        self.index.iter().map(|(k, &v)| (k.as_str(), v))
144    }
145}
146
147impl IntoR for NamedList {
148    type Error = std::convert::Infallible;
149    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
150        Ok(self.into_sexp())
151    }
152    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
153        self.try_into_sexp()
154    }
155    #[inline]
156    fn into_sexp(self) -> SEXP {
157        self.list.into_sexp()
158    }
159}
160
161impl TryFromSexp for NamedList {
162    type Error = SexpError;
163
164    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
165        let list = List::try_from_sexp(sexp)?;
166        NamedList::new(list)
167            .ok_or_else(|| SexpError::InvalidValue("list has no names attribute".into()))
168    }
169}
170
171impl TryFromSexp for Option<NamedList> {
172    type Error = SexpError;
173
174    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
175        if sexp == SEXP::nil() {
176            return Ok(None);
177        }
178        let named = NamedList::try_from_sexp(sexp)?;
179        Ok(Some(named))
180    }
181}
182
183// IntoList and TryFromList traits are defined in the parent list.rs module.