Skip to main content

miniextendr_api/convert/
as_numeric.rs

1//! [`AsNumeric`] / [`AsNumericVec`]: a number argument read the way R's
2//! `as.numeric()` reads it, from numbers, text, or factor labels. The reading
3//! rules are documented on [`AsNumericVec`].
4
5use crate::from_r::{
6    BatchedErrors, SexpError, SexpLengthError, SexpTypeError, TryFromSexp, charsxp_to_str,
7    impl_option_try_from_sexp, map_strsxp_with,
8};
9use crate::into_r::IntoR;
10use crate::{IsNA, RLogical, SEXP, SEXPTYPE, SexpExt};
11
12// region: Marker types
13
14/// A numeric scalar read like R's `as.numeric()`: from a double, integer,
15/// logical, character, or factor (by its labels) of length 1. `NA` of any
16/// type is `None`.
17///
18/// It follows the same reading rules as [`AsNumericVec`], and also requires
19/// length 1 ([`SexpError::Length`] otherwise). A value that is not a number
20/// fails with `non-numeric value(s): "n/a" (element 1)`. Under
21/// `#[miniextendr(no_na)]` it refuses the same values as the vector marker
22/// (see [`AsNumericVec`], "`no_na`"), with `'x' must not be NA`.
23///
24/// # Example
25///
26/// ```ignore
27/// use miniextendr_api::{miniextendr, AsNumeric};
28///
29/// #[miniextendr]
30/// fn half(x: AsNumeric) -> Option<f64> {
31///     x.0.map(|v| v / 2.0)
32/// }
33/// // R: half(3L)            → 1.5
34/// //    half(" 1e3 ")       → 500
35/// //    half(factor("10"))  → 5   (the label, not the code)
36/// //    half(NA_character_) → NA
37/// //    half("n/a")         → error: non-numeric value(s): "n/a" (element 1)
38/// ```
39#[derive(#[automatically_derived]
impl ::core::fmt::Debug for AsNumeric {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_tuple_field1_finish(f, "AsNumeric",
            &&self.0)
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for AsNumeric { }
#[automatically_derived]
impl ::core::clone::Clone for AsNumeric {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<Option<f64>>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for AsNumeric { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for AsNumeric { }
#[automatically_derived]
impl ::core::cmp::PartialEq for AsNumeric {
    #[inline]
    fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}PartialEq)]
40pub struct AsNumeric(pub Option<f64>);
41
42/// A numeric vector read like R's `as.numeric()`: from a double, integer,
43/// logical, character, or factor (by its labels) vector. `NA` of any type is
44/// `None`.
45///
46/// Data read from CSV files or spreadsheets often arrives as character, with
47/// numbers mixed with tokens such as `"n/a"` or `"<0.1"`, or as a factor whose
48/// labels are the numbers. This marker takes any of those, maps every kind of
49/// `NA` to `None`, and names the values that are not numbers in one error.
50///
51/// # Reading rules
52///
53/// | R input | Result |
54/// |---------|--------|
55/// | double | as is; `NA_real_` → `None`, `NaN` stays `Some(NaN)` |
56/// | integer, logical | widened to `f64`; `NA` → `None` |
57/// | character | parsed as `as.numeric()` does (see below) |
58/// | factor | each *label* (`levels(x)[x]`) parsed as character, never the codes; code `NA` → `None` |
59/// | anything else (list, raw, complex, …) | [`SexpError::Type`] |
60///
61/// Character parsing follows R's `String2Real` (`RealFromString` in
62/// `src/main/coerce.c`):
63///
64/// - `NA_character_` → `None`.
65/// - A blank string (empty, or whitespace only) → `None`, as `as.numeric("")`
66///   gives `NA` without a warning.
67/// - The token `"NA"`, with blanks around it at most, → `None`. This follows
68///   `scan()` / `type.convert()`, which read `"NA"` as missing;
69///   `as.numeric("NA")` also gives `NA`, but with a coercion warning.
70/// - Otherwise R's own `R_strtod` reads a number, and the rest of the string
71///   must be blank. That accepts surrounding spaces, `"Inf"`, `"-inf"`,
72///   `"Infinity"`, `"NaN"`, `"1e3"`, `"+.5"`, `"5."`, and hex such as `"0x1A"`
73///   or `"0x1p3"`, exactly as the running R does.
74/// - Anything else fails. All failures are collected and reported together:
75///   `non-numeric value(s): "n/a", "<0.1" (elements 2, 5)`, with 1-based
76///   element numbers and at most 10 values listed (the rest as `"; and N more"`).
77///
78/// # `no_na`
79///
80/// `#[miniextendr(no_na)]` refuses everything this marker reads as missing,
81/// not only what R's `anyNA()` sees: the R guard refuses `NA` (and a numeric
82/// `NaN`) before the call, and the C wrapper checks the converted value
83/// after it, refusing `None` (the token `"NA"`, blank strings, factor labels
84/// among those) and `NaN` (from the text `"NaN"` too). Both raise the same
85/// `'x' must not contain NA` argument error. `NULL` given to an
86/// `Option<AsNumericVec>` is still `None`, as not given. A type alias or a
87/// `#[derive(TryFromSexp)]` newtype of the marker is checked the same way.
88///
89/// `Option<AsNumericVec>` (and `Option<AsNumeric>`) also accept `NULL` as
90/// `None`.
91///
92/// # `IntoR`
93///
94/// The markers convert to R as their inner `Vec<Option<f64>>` /
95/// `Option<f64>`: a double vector with `NA` for `None`, which the marker
96/// reads back unchanged (`NaN` stays `Some(NaN)`, though a NaN carrying R's
97/// NA payload reads back as `None`, as `R_IsNA` says), and `Option<marker>`
98/// gives `NULL` for `None`. That is what lets a `#[miniextendr]` trait method
99/// take a marker parameter: the trait's View converts each argument with
100/// `IntoR` and the implementing side reads it back with `TryFromSexp`. A
101/// `#[miniextendr]` function returning a marker gives the same R value as
102/// returning `.0`; returning `.0` stays the idiom.
103///
104/// # Example
105///
106/// ```ignore
107/// use miniextendr_api::{miniextendr, AsNumericVec};
108///
109/// #[miniextendr]
110/// fn total(x: AsNumericVec) -> f64 {
111///     x.0.into_iter().flatten().sum()
112/// }
113/// // R: total(c(" 1.5 ", "NA", "", "0x10")) → 17.5
114/// //    total(factor(c("10", "2")))         → 12  (labels, not codes)
115/// //    total(c("1", "n/a", "3", "4", "<0.1"))
116/// //    → error: non-numeric value(s): "n/a", "<0.1" (elements 2, 5)
117/// ```
118#[derive(#[automatically_derived]
impl ::core::fmt::Debug for AsNumericVec {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_tuple_field1_finish(f, "AsNumericVec",
            &&self.0)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for AsNumericVec {
    #[inline]
    fn clone(&self) -> Self {
        AsNumericVec(::core::clone::Clone::clone(&self.0))
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for AsNumericVec {
    #[inline]
    fn default() -> Self { AsNumericVec(::core::default::Default::default()) }
}Default, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for AsNumericVec { }
#[automatically_derived]
impl ::core::cmp::PartialEq for AsNumericVec {
    #[inline]
    fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}PartialEq)]
119pub struct AsNumericVec(pub Vec<Option<f64>>);
120
121impl TryFromSexp for AsNumericVec {
122    type Error = SexpError;
123    #[inline]
124    fn __mx_has_na(&self) -> bool {
125        self.0.iter().copied().any(refused_by_no_na)
126    }
127
128    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
129        read_numeric(sexp).map(AsNumericVec)
130    }
131}
132
133impl TryFromSexp for AsNumeric {
134    type Error = SexpError;
135    #[inline]
136    fn __mx_has_na(&self) -> bool {
137        refused_by_no_na(self.0)
138    }
139
140    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
141        // The type is checked before the length, so a length-1 list reports
142        // its type rather than passing the length check.
143        check_type(sexp.type_of())?;
144        let len = sexp.len();
145        if len != 1 {
146            return Err(SexpLengthError {
147                expected: 1,
148                actual: len,
149            }
150            .into());
151        }
152        let values = read_numeric(sexp)?;
153        Ok(AsNumeric(values[0]))
154    }
155}
156
157impl crate::from_r::TryFromSexp for Option<AsNumeric> {
    const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
        <AsNumeric as crate::from_r::TryFromSexp>::NATIVE_BORROW;
    const CHARACTER_ONLY: bool =
        <AsNumeric 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(<AsNumeric 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); }
        <AsNumeric 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 {
            <AsNumeric as
                        crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
        }
    }
}impl_option_try_from_sexp!(AsNumeric);
158impl crate::from_r::TryFromSexp for Option<AsNumericVec> {
    const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
        <AsNumericVec as crate::from_r::TryFromSexp>::NATIVE_BORROW;
    const CHARACTER_ONLY: bool =
        <AsNumericVec 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(<AsNumericVec 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); }
        <AsNumericVec 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 {
            <AsNumericVec as
                        crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
        }
    }
}impl_option_try_from_sexp!(AsNumericVec);
159
160/// Whether `#[miniextendr(no_na)]` refuses a value the markers read: `None`
161/// (every kind of `NA`, the text `"NA"`, a blank string) and `NaN` (also read
162/// from the text `"NaN"`), as R's `anyNA()` refuses a numeric `NaN`.
163fn refused_by_no_na(value: Option<f64>) -> bool {
164    value.is_none_or(f64::is_nan)
165}
166// endregion
167
168// region: IntoR (the inner value)
169
170// A marker converts to R as its inner value, which the marker reads back
171// unchanged: `None` is `NA_real_` and `Some(NaN)` stays `NaN` (a NaN carrying
172// R's NA payload, as from a plain `f64` argument given `NA_real_`, reads back
173// as `None`). A trait's View passes a marker argument to the implementing
174// method this way.
175
176impl IntoR for AsNumeric {
177    type Error = <Option<f64> as IntoR>::Error;
178    #[inline]
179    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
180        self.0.try_into_sexp()
181    }
182    #[inline]
183    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
184        unsafe { self.0.try_into_sexp_unchecked() }
185    }
186    #[inline]
187    fn into_sexp(self) -> SEXP {
188        self.0.into_sexp()
189    }
190    #[inline]
191    unsafe fn into_sexp_unchecked(self) -> SEXP {
192        unsafe { self.0.into_sexp_unchecked() }
193    }
194}
195
196impl IntoR for AsNumericVec {
197    type Error = <Vec<Option<f64>> as IntoR>::Error;
198    #[inline]
199    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
200        self.0.try_into_sexp()
201    }
202    #[inline]
203    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
204        unsafe { self.0.try_into_sexp_unchecked() }
205    }
206    #[inline]
207    fn into_sexp(self) -> SEXP {
208        self.0.into_sexp()
209    }
210    #[inline]
211    unsafe fn into_sexp_unchecked(self) -> SEXP {
212        unsafe { self.0.into_sexp_unchecked() }
213    }
214}
215
216impl crate::into_r::IntoR for Option<AsNumeric> {
    type Error = <AsNumeric 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!(AsNumeric);
217impl crate::into_r::IntoR for Option<AsNumericVec> {
    type Error = <AsNumericVec 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!(AsNumericVec);
218// endregion
219
220// region: SEXP dispatch
221
222/// A value that `as.numeric()` would turn into `NA` with a coercion warning.
223struct NotNumeric;
224
225/// The rejected-element accumulator: 0-based index and the offending text.
226type Rejected = BatchedErrors;
227
228/// Refuse every SEXPTYPE the markers do not read. `REALSXP` is named as the
229/// expected type because that is the storage the value ends up in.
230fn check_type(actual: SEXPTYPE) -> Result<(), SexpError> {
231    match actual {
232        SEXPTYPE::REALSXP | SEXPTYPE::INTSXP | SEXPTYPE::LGLSXP | SEXPTYPE::STRSXP => Ok(()),
233        _ => Err(SexpTypeError {
234            expected: SEXPTYPE::REALSXP,
235            actual,
236        }
237        .into()),
238    }
239}
240
241/// Read any accepted input into `Vec<Option<f64>>`, collecting every
242/// non-numeric element into one error.
243fn read_numeric(sexp: SEXP) -> Result<Vec<Option<f64>>, SexpError> {
244    let actual = sexp.type_of();
245    check_type(actual)?;
246    match actual {
247        SEXPTYPE::REALSXP => {
248            let values: &[f64] = unsafe { sexp.as_slice() };
249            Ok(values.iter().map(|&v| (!v.is_na()).then_some(v)).collect())
250        }
251        SEXPTYPE::INTSXP if sexp.is_factor() => read_factor_labels(sexp),
252        SEXPTYPE::INTSXP => {
253            let values: &[i32] = unsafe { sexp.as_slice() };
254            Ok(values
255                .iter()
256                .map(|&v| (!v.is_na()).then(|| f64::from(v)))
257                .collect())
258        }
259        SEXPTYPE::LGLSXP => {
260            let values: &[RLogical] = unsafe { sexp.as_slice() };
261            Ok(values
262                .iter()
263                .map(|v| (!v.is_na()).then(|| f64::from(v.to_i32())))
264                .collect())
265        }
266        // STRSXP: `check_type` admitted nothing else.
267        _ => {
268            let mut rejected = Rejected::default();
269            let values = map_strsxp_with(sexp, |charsxp, i| {
270                Ok(parse_charsxp(charsxp).unwrap_or_else(|NotNumeric| {
271                    rejected.push(i, || unsafe { charsxp_to_str(charsxp) }.to_owned());
272                    None
273                }))
274            })?;
275            finish(values, rejected)
276        }
277    }
278}
279
280/// Read a factor by its labels: each level is parsed once, then every code
281/// looks its level up. Code `NA` is `None`; a code outside the levels is a
282/// malformed factor (R's `as.character()` refuses it too).
283fn read_factor_labels(sexp: SEXP) -> Result<Vec<Option<f64>>, SexpError> {
284    let levels = sexp.get_levels();
285    if levels.type_of() != SEXPTYPE::STRSXP {
286        return Err(SexpError::InvalidValue(
287            "malformed factor: levels are not a character vector".to_string(),
288        ));
289    }
290    let parsed: Vec<Result<Option<f64>, NotNumeric>> =
291        map_strsxp_with(levels, |charsxp, _| Ok(parse_charsxp(charsxp)))?;
292
293    let codes: &[i32] = unsafe { sexp.as_slice() };
294    let mut values = Vec::with_capacity(codes.len());
295    let mut rejected = Rejected::default();
296    for (i, &code) in codes.iter().enumerate() {
297        if code.is_na() {
298            values.push(None);
299            continue;
300        }
301        let level = usize::try_from(code)
302            .ok()
303            .and_then(|c| c.checked_sub(1))
304            .filter(|&k| k < parsed.len());
305        let Some(k) = level else {
306            return Err(SexpError::InvalidValue(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("malformed factor: code {2} at element {0} is outside levels 1..={1}",
                i + 1, parsed.len(), code))
    })format!(
307                "malformed factor: code {code} at element {} is outside levels 1..={}",
308                i + 1,
309                parsed.len()
310            )));
311        };
312        match parsed[k] {
313            Ok(v) => values.push(v),
314            Err(NotNumeric) => {
315                rejected.push(i, || level_label(levels, k));
316                values.push(None);
317            }
318        }
319    }
320    finish(values, rejected)
321}
322
323/// The text of level `k` (0-based) of a factor's `levels` STRSXP.
324fn level_label(levels: SEXP, k: usize) -> String {
325    let charsxp = levels.string_elt(isize::try_from(k).expect("level index fits in R_xlen_t"));
326    // A rejected label is never `NA_character_` (that parses as `None`).
327    unsafe { charsxp_to_str(charsxp) }.to_owned()
328}
329
330/// Return the values, or the one batched error naming every rejected element.
331fn finish(values: Vec<Option<f64>>, rejected: Rejected) -> Result<Vec<Option<f64>>, SexpError> {
332    if rejected.is_empty() {
333        Ok(values)
334    } else {
335        Err(rejected.into_value_error("non-numeric"))
336    }
337}
338// endregion
339
340// region: String parsing (R's `String2Real`)
341
342/// Parse one CHARSXP the way `as.numeric()` does.
343fn parse_charsxp(charsxp: SEXP) -> Result<Option<f64>, NotNumeric> {
344    if charsxp == SEXP::na_string() {
345        return Ok(None);
346    }
347    // SAFETY: a non-NA CHARSXP; its data is NUL-terminated, which is what
348    // `parse_nul_terminated` needs.
349    unsafe { parse_nul_terminated(charsxp_to_str(charsxp)) }
350}
351
352/// Parse `s` as R's `RealFromString` does, plus the `"NA"` token.
353///
354/// # Safety
355///
356/// The byte after `s` must be NUL (R's `CHAR()` data is), because `R_strtod`
357/// reads a C string.
358unsafe fn parse_nul_terminated(s: &str) -> Result<Option<f64>, NotNumeric> {
359    // Also covers `""`, whose `as_ptr()` would not point at R's data.
360    if is_blank(s) || is_na_token(s) {
361        return Ok(None);
362    }
363    let start = s.as_ptr().cast::<std::os::raw::c_char>();
364    let mut end: *mut std::os::raw::c_char = std::ptr::null_mut();
365    // SAFETY: `start` is a NUL-terminated C string (caller contract).
366    // `R_strtod` allocates nothing and never signals an R condition.
367    let value = unsafe { crate::sys::R_strtod(start, &mut end) };
368    // SAFETY: `R_strtod` leaves `end` inside `s` (it stops at or before the NUL).
369    let consumed = unsafe { end.cast_const().offset_from(start) };
370    let rest_blank = usize::try_from(consumed)
371        .ok()
372        .and_then(|n| s.as_bytes().get(n..))
373        .is_some_and(rest_is_blank);
374    if rest_blank {
375        Ok(Some(value))
376    } else {
377        Err(NotNumeric)
378    }
379}
380
381/// Whether `c` counts as blank for R's `isBlankString`, which uses `iswspace`
382/// in a UTF-8 locale: Unicode whitespace except NEL (U+0085) and the no-break
383/// spaces U+00A0, U+2007 and U+202F, which neither glibc nor macOS classes as
384/// space.
385fn is_blank_char(c: char) -> bool {
386    c.is_whitespace() && !#[allow(non_exhaustive_omitted_patterns)] match c {
    '\u{0085}' | '\u{00A0}' | '\u{2007}' | '\u{202F}' => true,
    _ => false,
}matches!(c, '\u{0085}' | '\u{00A0}' | '\u{2007}' | '\u{202F}')
387}
388
389/// R's `isBlankString`: empty, or only blank characters.
390fn is_blank(s: &str) -> bool {
391    s.chars().all(is_blank_char)
392}
393
394/// The missing-value token `"NA"`, with blanks around it at most.
395fn is_na_token(s: &str) -> bool {
396    s.trim_matches(is_blank_char) == "NA"
397}
398
399/// Whether the bytes `R_strtod` left unread are blank. They follow an ASCII
400/// prefix of a valid UTF-8 string, so a failed UTF-8 check only happens for a
401/// split multi-byte character, which is not blank either.
402fn rest_is_blank(rest: &[u8]) -> bool {
403    std::str::from_utf8(rest).is_ok_and(is_blank)
404}
405// endregion
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn blank_strings_match_r_is_blank_string() {
413        for s in [
414            "",
415            " ",
416            "   ",
417            "\t\n",
418            "\u{0B}\u{0C}\r",
419            "\u{3000}",
420            "\u{2028}",
421            "\u{2003}",
422        ] {
423            assert!(is_blank(s), "{s:?} should be blank");
424        }
425        // Neither glibc nor macOS `iswspace` accepts NEL or the no-break spaces.
426        for s in [
427            "\u{0085}", "\u{00A0}", "\u{2007}", "\u{202F}", "x", " 1 ", "\0",
428        ] {
429            assert!(!is_blank(s), "{s:?} should not be blank");
430        }
431    }
432
433    #[test]
434    fn na_token_allows_surrounding_blanks_only() {
435        for s in ["NA", " NA", "NA ", "\tNA\n"] {
436            assert!(is_na_token(s), "{s:?} should be the NA token");
437        }
438        for s in ["na", "N A", "NAN", "NA1", "<NA>", "\u{00A0}NA"] {
439            assert!(!is_na_token(s), "{s:?} should not be the NA token");
440        }
441    }
442
443    /// `no_na` refuses what the markers read as missing, and `NaN`.
444    #[test]
445    fn no_na_refuses_none_and_nan() {
446        assert!(!AsNumeric(Some(2.5)).__mx_has_na());
447        assert!(!AsNumeric(Some(f64::INFINITY)).__mx_has_na());
448        assert!(AsNumeric(None).__mx_has_na());
449        assert!(AsNumeric(Some(f64::NAN)).__mx_has_na());
450
451        assert!(!AsNumericVec(vec![]).__mx_has_na());
452        assert!(!AsNumericVec(vec![Some(1.0), Some(-0.0)]).__mx_has_na());
453        assert!(AsNumericVec(vec![Some(1.0), None]).__mx_has_na());
454        assert!(AsNumericVec(vec![Some(f64::NAN), Some(1.0)]).__mx_has_na());
455    }
456
457    /// The layers a marker comes in forward the check: `NULL` (`None`) and an
458    /// omitted argument (`Absent`) pass, a present marker is asked. A type
459    /// that is not a marker keeps the default `false`.
460    #[test]
461    fn no_na_is_forwarded_through_the_layers() {
462        use crate::Missing;
463        use std::collections::{BTreeMap, HashMap};
464
465        assert!(!None::<AsNumeric>.__mx_has_na());
466        assert!(!Some(AsNumeric(Some(1.0))).__mx_has_na());
467        assert!(Some(AsNumeric(None)).__mx_has_na());
468        assert!(Some(AsNumericVec(vec![None])).__mx_has_na());
469
470        assert!(!Missing::<AsNumeric>::Absent.__mx_has_na());
471        assert!(!Missing::Present(AsNumeric(Some(1.0))).__mx_has_na());
472        assert!(Missing::Present(AsNumeric(None)).__mx_has_na());
473        assert!(!Missing::<Option<AsNumeric>>::Present(None).__mx_has_na());
474        assert!(Missing::Present(Some(AsNumeric(None))).__mx_has_na());
475
476        assert!(!Err::<AsNumeric, ()>(()).__mx_has_na());
477        assert!(!Ok::<AsNumeric, ()>(AsNumeric(Some(1.0))).__mx_has_na());
478        assert!(Ok::<AsNumeric, ()>(AsNumeric(None)).__mx_has_na());
479
480        let mut map = HashMap::new();
481        map.insert("a".to_string(), AsNumeric(Some(1.0)));
482        assert!(!map.__mx_has_na());
483        map.insert("b".to_string(), AsNumeric(None));
484        assert!(map.__mx_has_na());
485        assert!(Some(map).__mx_has_na());
486        assert!(!None::<HashMap<String, AsNumeric>>.__mx_has_na());
487
488        let tree: BTreeMap<String, AsNumericVec> =
489            [("a".to_string(), AsNumericVec(vec![Some(1.0), None]))].into();
490        assert!(tree.__mx_has_na());
491
492        assert!(!Some(f64::NAN).__mx_has_na());
493        assert!(!None::<f64>.__mx_has_na());
494        assert!(!Missing::<f64>::Absent.__mx_has_na());
495        assert!(!vec![None::<f64>].__mx_has_na());
496    }
497
498    #[test]
499    fn unread_rest_must_be_blank_utf8() {
500        assert!(rest_is_blank(b""));
501        assert!(rest_is_blank(b"  \t"));
502        assert!(rest_is_blank("\u{3000}".as_bytes()));
503        assert!(!rest_is_blank(b" x"));
504        assert!(!rest_is_blank(b"\xa0"), "split multi-byte character");
505    }
506}