Skip to main content

miniextendr_api/convert/
as_character.rs

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`].
4
5use crate::expression::{RCall, RSymbol};
6use crate::from_r::{
7    SexpError, SexpLengthError, SexpTypeError, TryFromSexp, impl_option_try_from_sexp,
8};
9use crate::gc_protect::OwnedProtect;
10use crate::into_r::IntoR;
11use crate::{SEXP, SEXPTYPE, SexpExt};
12
13// region: Marker types
14
15/// 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>);
39
40/// 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>>);
118
119impl TryFromSexp for AsCharacterVec {
120    type Error = SexpError;
121    #[inline]
122    fn __mx_has_na(&self) -> bool {
123        self.0.iter().any(Option::is_none)
124    }
125
126    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
127        read_character(sexp).map(AsCharacterVec)
128    }
129}
130
131impl TryFromSexp for AsCharacter {
132    type Error = SexpError;
133    #[inline]
134    fn __mx_has_na(&self) -> bool {
135        self.0.is_none()
136    }
137
138    fn 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.
141        check_type(sexp.type_of())?;
142        check_length_one(sexp.len())?;
143        let mut values = read_character(sexp)?;
144        // A class's `as.character()` method may return another length.
145        check_length_one(values.len())?;
146        Ok(AsCharacter(values.swap_remove(0)))
147    }
148}
149
150impl 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
153
154// region: IntoR (the inner value)
155
156// 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.
159
160impl IntoR for AsCharacter {
161    type Error = <Option<String> as IntoR>::Error;
162    #[inline]
163    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
164        self.0.try_into_sexp()
165    }
166    #[inline]
167    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
168        unsafe { self.0.try_into_sexp_unchecked() }
169    }
170    #[inline]
171    fn into_sexp(self) -> SEXP {
172        self.0.into_sexp()
173    }
174    #[inline]
175    unsafe fn into_sexp_unchecked(self) -> SEXP {
176        unsafe { self.0.into_sexp_unchecked() }
177    }
178}
179
180impl IntoR for AsCharacterVec {
181    type Error = <Vec<Option<String>> as IntoR>::Error;
182    #[inline]
183    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
184        self.0.try_into_sexp()
185    }
186    #[inline]
187    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
188        unsafe { self.0.try_into_sexp_unchecked() }
189    }
190    #[inline]
191    fn into_sexp(self) -> SEXP {
192        self.0.into_sexp()
193    }
194    #[inline]
195    unsafe fn into_sexp_unchecked(self) -> SEXP {
196        unsafe { self.0.into_sexp_unchecked() }
197    }
198}
199
200impl 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
203
204// region: SEXP dispatch
205
206/// 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> {
209    match actual {
210        SEXPTYPE::LGLSXP
211        | SEXPTYPE::INTSXP
212        | SEXPTYPE::REALSXP
213        | SEXPTYPE::CPLXSXP
214        | SEXPTYPE::STRSXP
215        | SEXPTYPE::RAWSXP => Ok(()),
216        _ => Err(SexpTypeError {
217            expected: SEXPTYPE::STRSXP,
218            actual,
219        }
220        .into()),
221    }
222}
223
224fn check_length_one(actual: usize) -> Result<(), SexpError> {
225    if actual == 1 {
226        Ok(())
227    } else {
228        Err(SexpLengthError {
229            expected: 1,
230            actual,
231        }
232        .into())
233    }
234}
235
236/// 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())?;
241    let 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.
249        unsafe { OwnedProtect::new(sexp.coerce(SEXPTYPE::STRSXP)) }
250    };
251    Vec::<Option<String>>::try_from_sexp(strings.get())
252}
253
254/// 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.
266    unsafe {
267        let fun = crate::sys::Rf_findFun(
268            RSymbol::from_cstr(c"as.character").as_sexp(),
269            crate::sys::R_BaseEnv,
270        );
271        let call = RCall::from_sexp(fun).arg(sexp);
272        let 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.
277        let result = OwnedProtect::new(result);
278        let actual = result.get().type_of();
279        if actual != SEXPTYPE::STRSXP {
280            return 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",
282                crate::typed_list::sexptype_name(actual)
283            )));
284        }
285        Ok(result)
286    }
287}
288// endregion
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    /// `no_na` refuses what the markers read as `None`; the strings `"NA"`,
295    /// `""` and `"NaN"` are values here.
296    #[test]
297    fn no_na_refuses_none_only() {
298        assert!(!AsCharacter(Some("NA".to_string())).__mx_has_na());
299        assert!(!AsCharacter(Some(String::new())).__mx_has_na());
300        assert!(AsCharacter(None).__mx_has_na());
301
302        let values =
303            |v: &[Option<&str>]| AsCharacterVec(v.iter().map(|s| s.map(str::to_string)).collect());
304        assert!(!values(&[]).__mx_has_na());
305        assert!(!values(&[Some("a"), Some("NaN"), Some("")]).__mx_has_na());
306        assert!(values(&[Some("a"), None]).__mx_has_na());
307
308        assert!(!None::<AsCharacter>.__mx_has_na());
309        assert!(Some(AsCharacter(None)).__mx_has_na());
310        assert!(!crate::Missing::<AsCharacterVec>::Absent.__mx_has_na());
311        assert!(crate::Missing::Present(values(&[None])).__mx_has_na());
312    }
313}