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