1//! [`AsCharacter`] / [`AsCharacterVec`]: a label argument read the way R's
2//! `as.character()` reads it, from any atomic vector, a factor by its labels.
3//! The reading rules are documented on [`AsCharacterVec`].
45use crate::expression::{RCall, RSymbol};
6use crate::from_r::{
7SexpError, SexpLengthError, SexpTypeError, TryFromSexp, impl_option_try_from_sexp,
8};
9use crate::gc_protect::OwnedProtect;
10use crate::into_r::IntoR;
11use crate::{SEXP, SEXPTYPE, SexpExt};
1213// region: Marker types
1415/// A character scalar read like R's `as.character()`: from an atomic vector
16/// of length 1 of any type, a factor by its label. `NA` of any type is `None`.
17///
18/// It follows the same reading rules as [`AsCharacterVec`], and also requires
19/// length 1 ([`SexpError::Length`] otherwise), both of the argument and of what
20/// a class's `as.character()` method returns for it.
21///
22/// # Example
23///
24/// ```ignore
25/// use miniextendr_api::{miniextendr, AsCharacter};
26///
27/// #[miniextendr]
28/// fn subject_label(id: AsCharacter) -> String {
29/// format!("subject {}", id.0.as_deref().unwrap_or("unknown"))
30/// }
31/// // R: subject_label("S-01") → "subject S-01"
32/// // subject_label(101L) → "subject 101"
33/// // subject_label(factor("S-02")) → "subject S-02" (the label, not the code)
34/// // subject_label(NA) → "subject unknown"
35/// // subject_label(c(1, 2)) → error: 'id' must have length 1
36/// ```
37#[derive(#[automatically_derived]
impl ::core::fmt::Debug for AsCharacter {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "AsCharacter",
&&self.0)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for AsCharacter {
#[inline]
fn clone(&self) -> Self {
AsCharacter(::core::clone::Clone::clone(&self.0))
}
}Clone, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for AsCharacter { }
#[automatically_derived]
impl ::core::cmp::PartialEq for AsCharacter {
#[inline]
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for AsCharacter {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<Option<String>>;
}
}Eq)]
38pub struct AsCharacter(pub Option<String>);
3940/// A character vector read like R's `as.character()`: from an atomic vector of
41/// any type, a factor by its labels. `NA` of any type is `None`.
42///
43/// Identifiers in R data often arrive as numbers or factors: subject IDs,
44/// visit numbers, or grouping columns read by `read.csv()`. A `Vec<String>`
45/// parameter accepts character input only, so a function that treats such
46/// values as labels would need an R wrapper calling `as.character()` first.
47/// This marker does that conversion itself, and the strings are exactly the
48/// ones R produces elsewhere, because R makes them.
49///
50/// # Reading rules
51///
52/// | R input | Result |
53/// |---------|--------|
54/// | character | as is; `NA_character_` → `None`. `"NA"` and `""` stay values |
55/// | integer, double | R's own formatting (doubles to 15 significant digits): `0.1 + 0.2` → `"0.3"`, `1e6` → `"1e+06"`, `100` → `"100"`, `NaN` → `"NaN"`, `Inf` → `"Inf"`; `NA` → `None` |
56/// | logical | `"TRUE"` / `"FALSE"`; `NA` → `None` |
57/// | complex | `"1+2i"`; `NA` → `None` |
58/// | raw | two hex digits: `as.raw(255)` → `"ff"` |
59/// | classed object (factor, `Date`, `POSIXct`, …) | its `as.character()` method: factor labels (never the codes), `"2024-01-15"` for a `Date`, `"2024-01-15 10:30:00"` for a `POSIXct` |
60/// | list, data frame, `NULL`, … | [`SexpError::Type`] |
61///
62/// A plain atomic vector is converted with R's `coerceVector()` (what the
63/// `as.character()` primitive does for it); the text never comes from Rust
64/// formatting, which would give `"0.30000000000000004"` for `0.1 + 0.2`. A
65/// vector with a class attribute (`is.object(x)`) is passed to
66/// `as.character(x)`, evaluated as if at top level: base R's `as.character`
67/// (a user binding of that name does not interfere), with S3 and S4 methods
68/// registered by packages or defined in the global environment. A `Date` is a
69/// double underneath, and `coerceVector()` alone would give its day count
70/// (`"19737"`). An error in the method becomes the conversion error, as does a
71/// method that returns something other than a character vector.
72///
73/// Names and `dim` are dropped, as `as.character()` drops them: a matrix is
74/// read in column-major order. Zero-length input gives an empty vector.
75///
76/// Unlike [`AsNumericVec`](crate::convert::AsNumericVec), which reads the
77/// token `"NA"` and blank strings as missing, only `NA` itself is `None`
78/// here: `as.character()` keeps `"NA"` and `""` as strings. `NaN` becomes
79/// `"NaN"` (`no_na` refuses it in R, as `anyNA()` counts it). `no_na` also
80/// checks the converted value after the call, so it refuses an `NA` only the
81/// conversion produces: a factor `NA` level (`factor(x, exclude = NULL)`,
82/// whose code is not `NA`) or a class's `as.character()` method returning
83/// `NA`. A type alias or a `#[derive(TryFromSexp)]` newtype of the marker is
84/// checked the same way.
85///
86/// `Option<AsCharacterVec>` (and `Option<AsCharacter>`) also accept `NULL` as
87/// `None`.
88///
89/// # `IntoR`
90///
91/// The markers convert to R as their inner `Vec<Option<String>>` /
92/// `Option<String>`: a character vector with `NA_character_` for `None`,
93/// which the marker reads back unchanged (`"NA"` and `""` stay strings), and
94/// `Option<marker>` gives `NULL` for `None`. That is what lets a
95/// `#[miniextendr]` trait method take a marker parameter: the trait's View
96/// converts each argument with `IntoR` and the implementing side reads it
97/// back with `TryFromSexp`. A `#[miniextendr]` function returning a marker
98/// gives the same R value as returning `.0`; returning `.0` stays the idiom.
99///
100/// # Example
101///
102/// ```ignore
103/// use miniextendr_api::{miniextendr, AsCharacterVec};
104/// use std::collections::HashSet;
105///
106/// #[miniextendr]
107/// fn n_subjects(ids: AsCharacterVec) -> i32 {
108/// let distinct: HashSet<String> = ids.0.into_iter().flatten().collect();
109/// i32::try_from(distinct.len()).expect("fewer than 2^31 subjects")
110/// }
111/// // R: n_subjects(c(101L, 102L, 101L)) → 2
112/// // n_subjects(factor(c("S1", "S2", NA))) → 2 (labels, not codes)
113/// // n_subjects(c(0.1 + 0.2, 0.3)) → 1 (both read as "0.3")
114/// // n_subjects(list(1)) → error: 'ids' must be atomic
115/// ```
116#[derive(#[automatically_derived]
impl ::core::fmt::Debug for AsCharacterVec {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "AsCharacterVec",
&&self.0)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for AsCharacterVec {
#[inline]
fn clone(&self) -> Self {
AsCharacterVec(::core::clone::Clone::clone(&self.0))
}
}Clone, #[automatically_derived]
impl ::core::default::Default for AsCharacterVec {
#[inline]
fn default() -> Self {
AsCharacterVec(::core::default::Default::default())
}
}Default, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for AsCharacterVec { }
#[automatically_derived]
impl ::core::cmp::PartialEq for AsCharacterVec {
#[inline]
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for AsCharacterVec {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<Vec<Option<String>>>;
}
}Eq)]
117pub struct AsCharacterVec(pub Vec<Option<String>>);
118119impl TryFromSexpfor AsCharacterVec {
120type Error = SexpError;
121#[inline]
122fn __mx_has_na(&self) -> bool {
123self.0.iter().any(Option::is_none)
124 }
125126fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
127read_character(sexp).map(AsCharacterVec)
128 }
129}
130131impl TryFromSexpfor AsCharacter {
132type Error = SexpError;
133#[inline]
134fn __mx_has_na(&self) -> bool {
135self.0.is_none()
136 }
137138fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
139// The type is checked before the length, so a length-1 list reports
140 // its type rather than passing the length check.
141check_type(sexp.type_of())?;
142 check_length_one(sexp.len())?;
143let mut values = read_character(sexp)?;
144// A class's `as.character()` method may return another length.
145check_length_one(values.len())?;
146Ok(AsCharacter(values.swap_remove(0)))
147 }
148}
149150impl crate::from_r::TryFromSexp for Option<AsCharacter> {
const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
<AsCharacter as crate::from_r::TryFromSexp>::NATIVE_BORROW;
const CHARACTER_ONLY: bool =
<AsCharacter as crate::from_r::TryFromSexp>::CHARACTER_ONLY;
type Error = crate::from_r::SexpError;
#[inline]
fn __mx_has_na(&self) -> bool {
self.as_ref().is_some_and(<AsCharacter as
crate::from_r::TryFromSexp>::__mx_has_na)
}
fn try_from_sexp(sexp: crate::SEXP) -> Result<Self, Self::Error> {
use crate::{SEXPTYPE, SexpExt};
if sexp.type_of() == SEXPTYPE::NILSXP { return Ok(None); }
<AsCharacter as
crate::from_r::TryFromSexp>::try_from_sexp(sexp).map(Some)
}
unsafe fn try_from_sexp_unchecked(sexp: crate::SEXP)
-> Result<Self, Self::Error> {
use crate::{SEXPTYPE, SexpExt};
if sexp.type_of() == SEXPTYPE::NILSXP { return Ok(None); }
unsafe {
<AsCharacter as
crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
}
}
}impl_option_try_from_sexp!(AsCharacter);
151impl crate::from_r::TryFromSexp for Option<AsCharacterVec> {
const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
<AsCharacterVec as crate::from_r::TryFromSexp>::NATIVE_BORROW;
const CHARACTER_ONLY: bool =
<AsCharacterVec as crate::from_r::TryFromSexp>::CHARACTER_ONLY;
type Error = crate::from_r::SexpError;
#[inline]
fn __mx_has_na(&self) -> bool {
self.as_ref().is_some_and(<AsCharacterVec as
crate::from_r::TryFromSexp>::__mx_has_na)
}
fn try_from_sexp(sexp: crate::SEXP) -> Result<Self, Self::Error> {
use crate::{SEXPTYPE, SexpExt};
if sexp.type_of() == SEXPTYPE::NILSXP { return Ok(None); }
<AsCharacterVec as
crate::from_r::TryFromSexp>::try_from_sexp(sexp).map(Some)
}
unsafe fn try_from_sexp_unchecked(sexp: crate::SEXP)
-> Result<Self, Self::Error> {
use crate::{SEXPTYPE, SexpExt};
if sexp.type_of() == SEXPTYPE::NILSXP { return Ok(None); }
unsafe {
<AsCharacterVec as
crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
}
}
}impl_option_try_from_sexp!(AsCharacterVec);
152// endregion
153154// region: IntoR (the inner value)
155156// A marker converts to R as its inner value, which the marker reads back
157// unchanged: `None` is `NA_character_`, and `"NA"` / `""` stay strings. A
158// trait's View passes a marker argument to the implementing method this way.
159160impl IntoRfor AsCharacter {
161type Error = <Option<String> as IntoR>::Error;
162#[inline]
163fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
164self.0.try_into_sexp()
165 }
166#[inline]
167unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
168unsafe { self.0.try_into_sexp_unchecked() }
169 }
170#[inline]
171fn into_sexp(self) -> SEXP {
172self.0.into_sexp()
173 }
174#[inline]
175unsafe fn into_sexp_unchecked(self) -> SEXP {
176unsafe { self.0.into_sexp_unchecked() }
177 }
178}
179180impl IntoRfor AsCharacterVec {
181type Error = <Vec<Option<String>> as IntoR>::Error;
182#[inline]
183fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
184self.0.try_into_sexp()
185 }
186#[inline]
187unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
188unsafe { self.0.try_into_sexp_unchecked() }
189 }
190#[inline]
191fn into_sexp(self) -> SEXP {
192self.0.into_sexp()
193 }
194#[inline]
195unsafe fn into_sexp_unchecked(self) -> SEXP {
196unsafe { self.0.into_sexp_unchecked() }
197 }
198}
199200impl crate::into_r::IntoR for Option<AsCharacter> {
type Error = <AsCharacter as crate::into_r::IntoR>::Error;
#[inline]
fn try_into_sexp(self) -> Result<crate::SEXP, Self::Error> {
match self {
Some(v) => v.try_into_sexp(),
None => Ok(crate::SEXP::nil()),
}
}
#[inline]
unsafe fn try_into_sexp_unchecked(self)
-> Result<crate::SEXP, Self::Error> {
match self {
Some(v) => unsafe { v.try_into_sexp_unchecked() },
None => Ok(crate::SEXP::nil()),
}
}
#[inline]
fn into_sexp(self) -> crate::SEXP {
match self { Some(v) => v.into_sexp(), None => crate::SEXP::nil(), }
}
#[inline]
unsafe fn into_sexp_unchecked(self) -> crate::SEXP {
match self {
Some(v) => unsafe { v.into_sexp_unchecked() },
None => crate::SEXP::nil(),
}
}
}impl_option_into_r_null!(AsCharacter);
201impl crate::into_r::IntoR for Option<AsCharacterVec> {
type Error = <AsCharacterVec as crate::into_r::IntoR>::Error;
#[inline]
fn try_into_sexp(self) -> Result<crate::SEXP, Self::Error> {
match self {
Some(v) => v.try_into_sexp(),
None => Ok(crate::SEXP::nil()),
}
}
#[inline]
unsafe fn try_into_sexp_unchecked(self)
-> Result<crate::SEXP, Self::Error> {
match self {
Some(v) => unsafe { v.try_into_sexp_unchecked() },
None => Ok(crate::SEXP::nil()),
}
}
#[inline]
fn into_sexp(self) -> crate::SEXP {
match self { Some(v) => v.into_sexp(), None => crate::SEXP::nil(), }
}
#[inline]
unsafe fn into_sexp_unchecked(self) -> crate::SEXP {
match self {
Some(v) => unsafe { v.into_sexp_unchecked() },
None => crate::SEXP::nil(),
}
}
}impl_option_into_r_null!(AsCharacterVec);
202// endregion
203204// region: SEXP dispatch
205206/// Refuse every SEXPTYPE that is not an atomic vector. `STRSXP` is named as
207/// the expected type because that is what the value becomes.
208fn check_type(actual: SEXPTYPE) -> Result<(), SexpError> {
209match actual {
210 SEXPTYPE::LGLSXP211 | SEXPTYPE::INTSXP212 | SEXPTYPE::REALSXP213 | SEXPTYPE::CPLXSXP214 | SEXPTYPE::STRSXP215 | SEXPTYPE::RAWSXP => Ok(()),
216_ => Err(SexpTypeError {
217 expected: SEXPTYPE::STRSXP,
218actual,
219 }
220 .into()),
221 }
222}
223224fn check_length_one(actual: usize) -> Result<(), SexpError> {
225if actual == 1 {
226Ok(())
227 } else {
228Err(SexpLengthError {
229 expected: 1,
230actual,
231 }
232 .into())
233 }
234}
235236/// Read any atomic vector into `Vec<Option<String>>` through R's own
237/// conversion to character. Also labels double group keys
238/// (`dataframe::group`).
239pub(crate) fn read_character(sexp: SEXP) -> Result<Vec<Option<String>>, SexpError> {
240 check_type(sexp.type_of())?;
241let strings = if sexp.is_object() {
242 dispatch_as_character(sexp)?
243} else {
244// `coerceVector()` returns a character vector as is. For an integer or
245 // double vector without attributes it returns a deferred-string ALTREP
246 // whose elements are allocated as they are read, so the result stays
247 // protected until every element has been copied out.
248 // SAFETY: argument conversion runs on R's main thread.
249unsafe { OwnedProtect::new(sexp.coerce(SEXPTYPE::STRSXP)) }
250 };
251Vec::<Option<String>>::try_from_sexp(strings.get())
252}
253254/// Evaluate `as.character(x)` for a classed `x`, so its method decides the
255/// text (factor labels, formatted dates, ...).
256///
257/// The call's head is the function bound in the base environment, and it is
258/// evaluated in the global environment: R finds the methods a top-level
259/// `as.character(x)` would find, and a global binding named `as.character`
260/// does not replace the function. [`RCall::eval`] catches an R error as
261/// `Err`, so a failing method never unwinds through Rust frames; its message
262/// (R's own, without the `Error in` prefix) goes into the conversion error.
263fn dispatch_as_character(sexp: SEXP) -> Result<OwnedProtect, SexpError> {
264// SAFETY: argument conversion runs on R's main thread, and `as.character`
265 // is always bound in the base environment, so `Rf_findFun` cannot fail.
266unsafe {
267let fun = crate::sys::Rf_findFun(
268RSymbol::from_cstr(c"as.character").as_sexp(),
269crate::sys::R_BaseEnv,
270 );
271let call = RCall::from_sexp(fun).arg(sexp);
272let result = call
273 .eval(crate::sys::R_GlobalEnv)
274 .map_err(|e| SexpError::InvalidValue(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("as.character() failed: {0}", e))
})format!("as.character() failed: {e}")))?;
275// `eval` returns the value unprotected: root it before anything else
276 // allocates.
277let result = OwnedProtect::new(result);
278let actual = result.get().type_of();
279if actual != SEXPTYPE::STRSXP {
280return Err(SexpError::InvalidValue(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("as.character() returned {0}, not a character vector",
crate::typed_list::sexptype_name(actual)))
})format!(
281"as.character() returned {}, not a character vector",
282crate::typed_list::sexptype_name(actual)
283 )));
284 }
285Ok(result)
286 }
287}
288// endregion
289290#[cfg(test)]
291mod tests {
292use super::*;
293294/// `no_na` refuses what the markers read as `None`; the strings `"NA"`,
295 /// `""` and `"NaN"` are values here.
296#[test]
297fn no_na_refuses_none_only() {
298assert!(!AsCharacter(Some("NA".to_string())).__mx_has_na());
299assert!(!AsCharacter(Some(String::new())).__mx_has_na());
300assert!(AsCharacter(None).__mx_has_na());
301302let values =
303 |v: &[Option<&str>]| AsCharacterVec(v.iter().map(|s| s.map(str::to_string)).collect());
304assert!(!values(&[]).__mx_has_na());
305assert!(!values(&[Some("a"), Some("NaN"), Some("")]).__mx_has_na());
306assert!(values(&[Some("a"), None]).__mx_has_na());
307308assert!(!None::<AsCharacter>.__mx_has_na());
309assert!(Some(AsCharacter(None)).__mx_has_na());
310assert!(!crate::Missing::<AsCharacterVec>::Absent.__mx_has_na());
311assert!(crate::Missing::Present(values(&[None])).__mx_has_na());
312 }
313}