Skip to main content

miniextendr_api/from_r/
strings.rs

1//! String conversions — STRSXP requires special handling via `STRING_ELT`.
2//!
3//! R stores strings as STRSXP (vector of CHARSXP). Each element requires
4//! `STRING_ELT` + `R_CHAR` to extract, unlike numeric vectors which expose
5//! a contiguous data pointer.
6//!
7//! Covers: `&str`, `String`, `char`, `Option<&str>`, `Option<String>`,
8//! `Vec<String>`, `Vec<&str>`, `Box<[String]>`.
9//!
10//! # Tradeoff
11//!
12//! Prefer borrowed `&str` / `Vec<&str>` over owned `String` / `Vec<String>`
13//! when the data only needs to live for the `.Call` — borrowed strings point
14//! straight into R's CHARSXP pool with no allocation. Use `Option<String>`
15//! / `Vec<Option<String>>` when callers may pass `NA_character_`; the plain
16//! `String` impl rejects NA with [`SexpNaError`](crate::from_r::SexpNaError).
17//! Failure mode of binding plain `String` to a column that contains `NA`:
18//! every NA element surfaces as a hard error instead of `None`.
19//!
20//! UTF-8 validity is guaranteed by `miniextendr_assert_utf8_locale()` at
21//! package init — these impls skip per-string validation. Outbound
22//! counterparts: `String` / `&str` impls in [`crate::into_r`].
23
24use crate::from_r::{
25    SexpError, TryFromSexp, charsxp_to_str, charsxp_to_str_unchecked, scalar_charsxp,
26    scalar_charsxp_unchecked,
27};
28use crate::{SEXP, SEXPTYPE, SexpExt};
29
30/// Convert R character vector (STRSXP) to Rust &str.
31///
32/// Extracts the first element of the character vector and returns it as a UTF-8 string.
33/// The returned string has static lifetime because it points to R's internal string pool.
34///
35/// # NA Handling
36///
37/// **Warning:** `NA_character_` is converted to empty string `""`. This is lossy!
38/// If you need to distinguish between NA and empty strings, use `Option<String>` instead:
39///
40/// ```ignore
41/// let maybe_str: Option<String> = sexp.try_into()?;
42/// ```
43///
44/// # Safety
45/// The returned &str is only valid as long as R doesn't garbage collect the CHARSXP.
46/// In practice, this is safe within a single .Call invocation.
47impl TryFromSexp for &'static str {
48    type Error = SexpError;
49
50    #[inline]
51    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
52        let charsxp = scalar_charsxp(sexp)?;
53
54        // Check for NA_STRING or R_BlankString
55        if charsxp == SEXP::na_string() {
56            return Ok("");
57        }
58        if charsxp == SEXP::blank_string() {
59            return Ok("");
60        }
61
62        // Use LENGTH-based conversion (O(1)) instead of CStr::from_ptr (O(n) strlen)
63        Ok(unsafe { charsxp_to_str(charsxp) })
64    }
65
66    #[inline]
67    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
68        let charsxp = unsafe { scalar_charsxp_unchecked(sexp)? };
69
70        // Check for NA_STRING or R_BlankString
71        if charsxp == SEXP::na_string() {
72            return Ok("");
73        }
74        if charsxp == SEXP::blank_string() {
75            return Ok("");
76        }
77
78        // Use LENGTH-based conversion (O(1)) instead of CStr::from_ptr (O(n) strlen)
79        Ok(unsafe { charsxp_to_str_unchecked(charsxp) })
80    }
81}
82
83impl TryFromSexp for Option<&'static str> {
84    type Error = SexpError;
85
86    #[inline]
87    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
88        if sexp.type_of() == SEXPTYPE::NILSXP {
89            return Ok(None);
90        }
91
92        let charsxp = scalar_charsxp(sexp)?;
93        if charsxp == SEXP::na_string() {
94            return Ok(None);
95        }
96        if charsxp == SEXP::blank_string() {
97            return Ok(Some(""));
98        }
99
100        Ok(Some(unsafe { charsxp_to_str(charsxp) }))
101    }
102
103    #[inline]
104    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
105        if sexp.type_of() == SEXPTYPE::NILSXP {
106            return Ok(None);
107        }
108
109        let charsxp = unsafe { scalar_charsxp_unchecked(sexp)? };
110        if charsxp == SEXP::na_string() {
111            return Ok(None);
112        }
113        if charsxp == SEXP::blank_string() {
114            return Ok(Some(""));
115        }
116
117        Ok(Some(unsafe { charsxp_to_str_unchecked(charsxp) }))
118    }
119}
120
121/// Convert R character vector (STRSXP) to Rust char.
122///
123/// Extracts the first character of the first element of the character vector.
124/// Returns an error if the string is empty, NA, or has more than one character.
125impl TryFromSexp for char {
126    type Error = SexpError;
127
128    #[inline]
129    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
130        let s: &str = TryFromSexp::try_from_sexp(sexp)?;
131        let mut chars = s.chars();
132        match (chars.next(), chars.next()) {
133            (Some(c), None) => Ok(c),
134            (None, _) => Err(SexpError::InvalidValue(
135                "empty string cannot be converted to char".to_string(),
136            )),
137            (Some(_), Some(_)) => Err(SexpError::InvalidValue(
138                "string has more than one character".to_string(),
139            )),
140        }
141    }
142
143    #[inline]
144    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
145        let s: &str = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
146        let mut chars = s.chars();
147        match (chars.next(), chars.next()) {
148            (Some(c), None) => Ok(c),
149            (None, _) => Err(SexpError::InvalidValue(
150                "empty string cannot be converted to char".to_string(),
151            )),
152            (Some(_), Some(_)) => Err(SexpError::InvalidValue(
153                "string has more than one character".to_string(),
154            )),
155        }
156    }
157}
158
159/// Convert R character vector (STRSXP) to owned Rust String.
160///
161/// Extracts the first element and creates an owned copy.
162///
163/// # NA Handling
164///
165/// **Warning:** `NA_character_` is converted to empty string `""`. This is lossy!
166/// If you need to distinguish between NA and empty strings, use `Option<String>` instead:
167///
168/// ```ignore
169/// let maybe_str: Option<String> = sexp.try_into()?;
170/// ```
171impl TryFromSexp for String {
172    type Error = SexpError;
173
174    #[inline]
175    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
176        let charsxp = scalar_charsxp(sexp)?;
177
178        if charsxp == SEXP::na_string() {
179            return Ok(String::new());
180        }
181
182        Ok(unsafe { charsxp_to_str(charsxp) }.to_owned())
183    }
184
185    #[inline]
186    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
187        let charsxp = unsafe { scalar_charsxp_unchecked(sexp)? };
188
189        if charsxp == SEXP::na_string() {
190            return Ok(String::new());
191        }
192
193        Ok(unsafe { charsxp_to_str_unchecked(charsxp) }.to_owned())
194    }
195}
196
197/// NA-aware string conversion: returns `None` for `NA_character_`.
198///
199/// Use this when you need to distinguish between NA and empty strings:
200/// ```ignore
201/// let maybe_str: Option<String> = sexp.try_into()?;
202/// match maybe_str {
203///     Some(s) => println!("Got string: {}", s),
204///     None => println!("Got NA"),
205/// }
206/// ```
207impl TryFromSexp for Option<String> {
208    type Error = SexpError;
209
210    #[inline]
211    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
212        // NULL -> None
213        if sexp.type_of() == SEXPTYPE::NILSXP {
214            return Ok(None);
215        }
216
217        let charsxp = scalar_charsxp(sexp)?;
218
219        // Return None for NA_STRING
220        if charsxp == SEXP::na_string() {
221            return Ok(None);
222        }
223
224        Ok(Some(unsafe { charsxp_to_str(charsxp) }.to_owned()))
225    }
226
227    #[inline]
228    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
229        // For Option<String>, unchecked is same as checked (NA check is semantic, not safety)
230        Self::try_from_sexp(sexp)
231    }
232}
233// endregion