Skip to main content

miniextendr_api/
factor.rs

1//! Factor support for enum ↔ R factor conversions.
2//!
3//! R factors are integer vectors with a `levels` attribute (character vector)
4//! and a `class` attribute set to `"factor"`. The integer payload uses 1-based
5//! indexing into the levels, with `NA_INTEGER` for missing values.
6//!
7//! # Usage
8//!
9//! ```ignore
10//! use miniextendr_api::RFactor;
11//!
12//! #[derive(Copy, Clone, RFactor)]
13//! enum Color { Red, Green, Blue }
14//!
15//! // Enum values convert to/from R factors automatically
16//! #[miniextendr]
17//! fn describe(c: Color) -> &'static str {
18//!     match c {
19//!         Color::Red => "red",
20//!         Color::Green => "green",
21//!         Color::Blue => "blue",
22//!     }
23//! }
24//! ```
25
26use std::marker::PhantomData;
27use std::ops::Deref;
28use std::sync::OnceLock;
29
30use crate::altrep_traits::NA_INTEGER;
31use crate::from_r::{
32    BatchedErrors, SexpError, SexpLengthError, SexpNaError, TryFromSexp, charsxp_to_str,
33};
34use crate::gc_protect::OwnedProtect;
35use crate::into_r::IntoR;
36use crate::sys::Rf_allocVector;
37use crate::{IsNA, SEXP, SEXPTYPE, SexpExt};
38
39// region: Cached "factor" class STRSXP
40
41static FACTOR_CLASS: OnceLock<SEXP> = OnceLock::new();
42
43pub(crate) fn factor_class_sexp() -> SEXP {
44    *FACTOR_CLASS.get_or_init(|| build_levels_sexp_cached(&["factor"]))
45}
46// endregion
47
48// region: RFactor trait
49
50/// Trait for mapping Rust enums to R factors.
51///
52/// Typically implemented via `#[derive(RFactor)]` for C-style enums.
53/// The derive macro also generates `IntoR` and `TryFromSexp` implementations.
54pub trait RFactor: crate::match_arg::MatchArg + Copy + 'static {
55    /// Convert variant to 1-based level index.
56    fn to_level_index(self) -> i32;
57
58    /// Convert 1-based level index to variant, or `None` if out of range.
59    fn from_level_index(idx: i32) -> Option<Self>;
60}
61// endregion
62
63// region: Core building functions
64
65/// Build a levels STRSXP of UTF-8 CHARSXPs.
66///
67/// Level names are not interned as R symbols, so names built from data stay
68/// collectable. The returned STRSXP is NOT protected - caller must protect or
69/// preserve it.
70///
71/// # Panics
72///
73/// If a level name contains a null byte.
74pub fn build_levels_sexp(levels: &[&str]) -> SEXP {
75    build_levels_sexp_protected(levels).get()
76}
77
78/// Keep the container rooted while each level's CHARSXP is allocated.
79fn build_levels_sexp_protected(levels: &[&str]) -> OwnedProtect {
80    // `Rf_mkCharLenCE` raises an R error on an embedded nul, which would
81    // longjmp over the container's protection.
82    if levels.iter().any(|level| level.as_bytes().contains(&0)) {
83        {
    ::core::panicking::panic_fmt(format_args!("level name contains null byte"));
};panic!("level name contains null byte");
84    }
85    let len = levels.len().try_into().expect("too many factor levels");
86    unsafe {
87        let sexp = OwnedProtect::new(Rf_allocVector(SEXPTYPE::STRSXP, len));
88        for (i, level) in (0..len).zip(levels) {
89            // A new CHARSXP is unrooted until it is stored in the protected
90            // container. R's CHARSXP cache shares repeated names.
91            sexp.set_string_elt(i, SEXP::charsxp(level));
92        }
93        sexp
94    }
95}
96
97/// Build a levels STRSXP and preserve it permanently (for caching).
98pub fn build_levels_sexp_cached(levels: &[&str]) -> SEXP {
99    unsafe {
100        let sexp = build_levels_sexp_protected(levels);
101        crate::sys::R_PreserveObject(sexp.get());
102        sexp.get()
103    }
104}
105
106/// Build a factor SEXP from indices and a levels STRSXP.
107///
108/// The caller must keep `levels` rooted across this call. The factor is rooted
109/// during construction; the returned SEXP must be protected or returned to R.
110pub fn build_factor(indices: &[i32], levels: SEXP) -> SEXP {
111    unsafe {
112        let (sexp, dst) = crate::into_r::alloc_r_vector::<i32>(indices.len());
113        // Attribute assignment and the cold class-cache initializer allocate.
114        let sexp = OwnedProtect::new(sexp);
115        dst.copy_from_slice(indices);
116        sexp.set_levels(levels);
117        sexp.set_class(factor_class_sexp());
118        sexp.get()
119    }
120}
121
122/// Build a factor SEXP from indices and level names in a single call.
123///
124/// Keeps the levels STRSXP rooted from allocation through [`build_factor`],
125/// so callers don't need to manage
126/// the levels protection themselves. The returned factor SEXP is **not**
127/// protected — caller must protect or return it.
128///
129/// This is the recommended path for one-shot factor construction; for
130/// repeated calls with the same levels prefer caching via
131/// [`build_levels_sexp_cached`] (no protection needed because the cached
132/// SEXP is on R's precious list).
133///
134/// The levels container roots its strings; the container and the factor
135/// payload are rooted while being built.
136pub fn build_factor_with_levels(indices: &[i32], level_names: &[&str]) -> SEXP {
137    let levels = build_levels_sexp_protected(level_names);
138    build_factor(indices, levels.get())
139}
140// endregion
141
142// region: Factor - view into an R factor's data
143
144/// A borrowed view into an R factor's integer indices.
145///
146/// Provides `Deref` to `&[i32]` for direct slice access to the factor's
147/// underlying integer data. The indices are 1-based (matching R's convention)
148/// with `NA_INTEGER` for missing values.
149///
150/// # Example
151///
152/// ```ignore
153/// use miniextendr_api::IsNA;
154/// let factor = Factor::try_new(sexp)?;
155/// for &idx in factor.iter() {
156///     if idx.is_na() {
157///         println!("NA");
158///     } else {
159///         println!("level index: {}", idx);
160///     }
161/// }
162/// ```
163pub struct Factor<'a> {
164    indices: &'a [i32],
165    levels_sexp: SEXP,
166    _marker: PhantomData<&'a ()>,
167}
168
169impl<'a> Factor<'a> {
170    /// Create a Factor from a factor SEXP.
171    ///
172    /// Returns an error if the SEXP is not a factor.
173    pub fn try_new(sexp: SEXP) -> Result<Self, SexpError> {
174        if !sexp.is_factor() {
175            return Err(SexpError::InvalidValue("expected a factor".into()));
176        }
177
178        let indices = unsafe { sexp.as_slice::<i32>() };
179        let levels_sexp = sexp.get_levels();
180
181        Ok(Self {
182            indices,
183            levels_sexp,
184            _marker: PhantomData,
185        })
186    }
187
188    /// Number of elements in the factor.
189    #[inline]
190    pub fn len(&self) -> usize {
191        self.indices.len()
192    }
193
194    /// Whether the factor is empty.
195    #[inline]
196    pub fn is_empty(&self) -> bool {
197        self.indices.is_empty()
198    }
199
200    /// The levels STRSXP.
201    #[inline]
202    pub fn levels_sexp(&self) -> SEXP {
203        self.levels_sexp
204    }
205
206    /// Number of levels.
207    #[inline]
208    pub fn n_levels(&self) -> usize {
209        self.levels_sexp.len()
210    }
211
212    /// Get level string at 0-based index.
213    #[inline]
214    pub fn level(&self, idx: usize) -> &'a str {
215        if !(idx < self.n_levels()) {
    {
        ::core::panicking::panic_fmt(format_args!("level index {1} out of bounds (n_levels = {0})",
                self.n_levels(), idx));
    }
};assert!(
216            idx < self.n_levels(),
217            "level index {idx} out of bounds (n_levels = {})",
218            self.n_levels()
219        );
220        let charsxp = self.levels_sexp.string_elt(idx as isize);
221        unsafe { charsxp_to_str(charsxp) }
222    }
223}
224
225impl Deref for Factor<'_> {
226    type Target = [i32];
227
228    #[inline]
229    fn deref(&self) -> &Self::Target {
230        self.indices
231    }
232}
233
234impl<'a> TryFromSexp for Factor<'a> {
235    type Error = SexpError;
236    const CHARACTER_ONLY: bool = true;
237
238    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
239        Self::try_new(sexp)
240    }
241}
242// endregion
243
244// region: FactorMut - mutable view into an R factor's data
245
246/// A mutable borrowed view into an R factor's integer indices.
247///
248/// Provides `DerefMut` to `&mut [i32]` for direct mutable slice access.
249/// The indices are 1-based (matching R's convention) with `NA_INTEGER` for NA.
250///
251/// # Example
252///
253/// ```ignore
254/// let mut factor_mut = FactorMut::try_new(sexp)?;
255/// // Set all values to level 1
256/// for idx in factor_mut.iter_mut() {
257///     *idx = 1;
258/// }
259/// ```
260pub struct FactorMut<'a> {
261    indices: &'a mut [i32],
262    levels_sexp: SEXP,
263    _marker: PhantomData<&'a mut ()>,
264}
265
266impl<'a> FactorMut<'a> {
267    /// Create a FactorMut from a factor SEXP.
268    ///
269    /// Returns an error if the SEXP is not a factor.
270    pub fn try_new(sexp: SEXP) -> Result<Self, SexpError> {
271        if !sexp.is_factor() {
272            return Err(SexpError::InvalidValue("expected a factor".into()));
273        }
274
275        let indices = unsafe { sexp.as_mut_slice::<i32>() };
276        let levels_sexp = sexp.get_levels();
277
278        Ok(Self {
279            indices,
280            levels_sexp,
281            _marker: PhantomData,
282        })
283    }
284
285    /// Number of elements in the factor.
286    #[inline]
287    pub fn len(&self) -> usize {
288        self.indices.len()
289    }
290
291    /// Whether the factor is empty.
292    #[inline]
293    pub fn is_empty(&self) -> bool {
294        self.indices.is_empty()
295    }
296
297    /// The levels STRSXP.
298    #[inline]
299    pub fn levels_sexp(&self) -> SEXP {
300        self.levels_sexp
301    }
302
303    /// Number of levels.
304    #[inline]
305    pub fn n_levels(&self) -> usize {
306        self.levels_sexp.len()
307    }
308
309    /// Get level string at 0-based index.
310    #[inline]
311    pub fn level(&self, idx: usize) -> &'a str {
312        if !(idx < self.n_levels()) {
    {
        ::core::panicking::panic_fmt(format_args!("level index {1} out of bounds (n_levels = {0})",
                self.n_levels(), idx));
    }
};assert!(
313            idx < self.n_levels(),
314            "level index {idx} out of bounds (n_levels = {})",
315            self.n_levels()
316        );
317        let charsxp = self.levels_sexp.string_elt(idx as isize);
318        unsafe { charsxp_to_str(charsxp) }
319    }
320}
321
322impl Deref for FactorMut<'_> {
323    type Target = [i32];
324
325    #[inline]
326    fn deref(&self) -> &Self::Target {
327        self.indices
328    }
329}
330
331impl std::ops::DerefMut for FactorMut<'_> {
332    #[inline]
333    fn deref_mut(&mut self) -> &mut Self::Target {
334        self.indices
335    }
336}
337// endregion
338
339// region: Validation helper
340
341/// Validate that a factor has the expected levels.
342pub(crate) fn validate_factor_levels(sexp: SEXP, expected: &[&str]) -> Result<(), SexpError> {
343    if !sexp.is_factor() {
344        return Err(SexpError::InvalidValue("expected a factor".into()));
345    }
346
347    let levels = sexp.get_levels();
348    if levels.type_of() != SEXPTYPE::STRSXP {
349        return Err(SexpError::InvalidValue("levels is not STRSXP".into()));
350    }
351
352    let n = levels.len();
353    if n != expected.len() {
354        return Err(SexpError::InvalidValue(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected {0} levels, got {1}",
                expected.len(), n))
    })format!(
355            "expected {} levels, got {}",
356            expected.len(),
357            n
358        )));
359    }
360
361    for (i, exp) in expected.iter().enumerate() {
362        let charsxp = levels.string_elt(i as isize);
363        let actual = unsafe { charsxp_to_str(charsxp) };
364        if actual != *exp {
365            return Err(SexpError::InvalidValue(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("level {0}: expected \'{1}\', got \'{2}\'",
                i + 1, exp, actual))
    })format!(
366                "level {}: expected '{}', got '{}'",
367                i + 1,
368                exp,
369                actual
370            )));
371        }
372    }
373
374    Ok(())
375}
376// endregion
377
378// region: Conversion helpers (used by derive macro)
379
380/// The reason a factor code does not name one of the levels (a malformed
381/// factor: [`validate_factor_levels`] has already checked the level set).
382fn code_not_a_level(code: i32) -> String {
383    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("code {0} is not a level of the factor",
                code))
    })format!("code {code} is not a level of the factor")
384}
385
386/// Read every code of a factor through `elt`, batching every failing element
387/// (its reason and 1-based position) into one error instead of stopping at
388/// the first.
389fn read_factor_codes<U>(
390    sexp: SEXP,
391    elt: impl Fn(i32) -> Result<U, String>,
392) -> Result<Vec<U>, SexpError> {
393    let len = sexp.len();
394    let mut result = Vec::with_capacity(len);
395    let mut errors = BatchedErrors::default();
396    for i in 0..len {
397        match elt(sexp.integer_elt(i as isize)) {
398            Ok(v) => result.push(v),
399            Err(reason) => errors.push(i, || reason),
400        }
401    }
402    if errors.is_empty() {
403        Ok(result)
404    } else {
405        Err(errors.into_element_error())
406    }
407}
408
409/// Convert an R factor SEXP to a single enum value.
410#[inline]
411pub fn factor_from_sexp<T: RFactor>(sexp: SEXP) -> Result<T, SexpError> {
412    validate_factor_levels(sexp, T::CHOICES)?;
413
414    let len = sexp.len();
415    if len != 1 {
416        return Err(SexpLengthError {
417            expected: 1,
418            actual: len,
419        }
420        .into());
421    }
422
423    let idx = sexp.integer_elt(0);
424    if idx.is_na() {
425        return Err(SexpNaError {
426            sexp_type: SEXPTYPE::INTSXP,
427        }
428        .into());
429    }
430
431    T::from_level_index(idx).ok_or_else(|| SexpError::InvalidValue(code_not_a_level(idx)))
432}
433
434/// Convert an R factor SEXP to a Vec of enum values. Every `NA` and every
435/// code outside the levels is reported, with its position.
436#[inline]
437pub(crate) fn factor_vec_from_sexp<T: UnitEnumFactor>(sexp: SEXP) -> Result<Vec<T>, SexpError> {
438    validate_factor_levels(sexp, T::FACTOR_LEVELS)?;
439    read_factor_codes(sexp, |idx| {
440        if idx.is_na() {
441            return Err("NA is not allowed".to_string());
442        }
443        T::from_factor_index(idx).ok_or_else(|| code_not_a_level(idx))
444    })
445}
446
447/// Convert an R factor SEXP to a Vec of Option enum values (NA → None).
448#[inline]
449pub(crate) fn factor_option_vec_from_sexp<T: RFactor>(
450    sexp: SEXP,
451) -> Result<Vec<Option<T>>, SexpError> {
452    validate_factor_levels(sexp, T::CHOICES)?;
453    read_factor_codes(sexp, |idx| {
454        if idx.is_na() {
455            return Ok(None);
456        }
457        T::from_level_index(idx)
458            .map(Some)
459            .ok_or_else(|| code_not_a_level(idx))
460    })
461}
462
463/// Convert an R factor SEXP to a `Vec<Option<T>>` using [`UnitEnumFactor`] (NA → `None`).
464///
465/// Used by the enum DataFrame reader to reconstruct `as_factor` columns.
466/// Unlike `factor_option_vec_from_sexp` (which requires `RFactor + MatchArg`),
467/// this accepts any `UnitEnumFactor` — including `#[derive(DataFrameRow)]`
468/// unit-only enums that do not implement `RFactor`.
469#[inline]
470pub fn unit_factor_option_vec_from_sexp<T: UnitEnumFactor>(
471    sexp: SEXP,
472) -> Result<Vec<Option<T>>, SexpError> {
473    validate_factor_levels(sexp, T::FACTOR_LEVELS)?;
474    read_factor_codes(sexp, |idx| {
475        if idx.is_na() {
476            return Ok(None);
477        }
478        T::from_factor_index(idx)
479            .map(Some)
480            .ok_or_else(|| code_not_a_level(idx))
481    })
482}
483// endregion
484
485// region: Newtype wrappers (for orphan rule workaround)
486
487/// Wrapper for `Vec<T: UnitEnumFactor>` enabling `IntoR`/`TryFromSexp`.
488///
489/// Accepts both `RFactor` types and unit-only `DataFrameRow` enums. Values are
490/// moved during conversion; neither `Copy` nor `Clone` is required.
491#[derive(#[automatically_derived]
impl<T: ::core::fmt::Debug> ::core::fmt::Debug for FactorVec<T> {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_tuple_field1_finish(f, "FactorVec",
            &&self.0)
    }
}Debug, #[automatically_derived]
impl<T: ::core::clone::Clone> ::core::clone::Clone for FactorVec<T> {
    #[inline]
    fn clone(&self) -> Self {
        FactorVec(::core::clone::Clone::clone(&self.0))
    }
}Clone)]
492pub struct FactorVec<T>(pub Vec<T>);
493
494impl<T> FactorVec<T> {
495    /// Wrap a `Vec<T>` so it can be converted to and from R factors.
496    pub fn new(vec: Vec<T>) -> Self {
497        Self(vec)
498    }
499
500    /// Extract the inner vector.
501    pub fn into_inner(self) -> Vec<T> {
502        self.0
503    }
504}
505
506impl<T> From<Vec<T>> for FactorVec<T> {
507    fn from(vec: Vec<T>) -> Self {
508        Self(vec)
509    }
510}
511
512impl<T> Deref for FactorVec<T> {
513    type Target = Vec<T>;
514    fn deref(&self) -> &Self::Target {
515        &self.0
516    }
517}
518
519impl<T> std::ops::DerefMut for FactorVec<T> {
520    fn deref_mut(&mut self) -> &mut Self::Target {
521        &mut self.0
522    }
523}
524
525impl<T: UnitEnumFactor> IntoR for FactorVec<T> {
526    type Error = std::convert::Infallible;
527    fn try_into_sexp(self) -> Result<crate::SEXP, Self::Error> {
528        Ok(self.into_sexp())
529    }
530    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::SEXP, Self::Error> {
531        self.try_into_sexp()
532    }
533    fn into_sexp(self) -> SEXP {
534        let indices: Vec<i32> = self.0.into_iter().map(T::to_factor_index).collect();
535        // build_factor_with_levels protects the levels STRSXP across the
536        // build_factor allocation — see "PROTECT discipline against R-devel GC".
537        build_factor_with_levels(&indices, T::FACTOR_LEVELS)
538    }
539}
540
541impl<T: UnitEnumFactor> TryFromSexp for FactorVec<T> {
542    type Error = SexpError;
543    const CHARACTER_ONLY: bool = true;
544    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
545        factor_vec_from_sexp(sexp).map(FactorVec)
546    }
547}
548
549/// Wrapper for `Vec<Option<T: RFactor>>` with NA support.
550#[derive(#[automatically_derived]
impl<T: ::core::fmt::Debug> ::core::fmt::Debug for FactorOptionVec<T> {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_tuple_field1_finish(f,
            "FactorOptionVec", &&self.0)
    }
}Debug, #[automatically_derived]
impl<T: ::core::clone::Clone> ::core::clone::Clone for FactorOptionVec<T> {
    #[inline]
    fn clone(&self) -> Self {
        FactorOptionVec(::core::clone::Clone::clone(&self.0))
    }
}Clone)]
551pub struct FactorOptionVec<T>(pub Vec<Option<T>>);
552
553impl<T> FactorOptionVec<T> {
554    /// Wrap a `Vec<Option<T>>` so it can be converted to and from R factors with NA support.
555    pub fn new(vec: Vec<Option<T>>) -> Self {
556        Self(vec)
557    }
558
559    /// Extract the inner vector.
560    pub fn into_inner(self) -> Vec<Option<T>> {
561        self.0
562    }
563}
564
565impl<T> From<Vec<Option<T>>> for FactorOptionVec<T> {
566    fn from(vec: Vec<Option<T>>) -> Self {
567        Self(vec)
568    }
569}
570
571impl<T> Deref for FactorOptionVec<T> {
572    type Target = Vec<Option<T>>;
573    fn deref(&self) -> &Self::Target {
574        &self.0
575    }
576}
577
578impl<T> std::ops::DerefMut for FactorOptionVec<T> {
579    fn deref_mut(&mut self) -> &mut Self::Target {
580        &mut self.0
581    }
582}
583
584// Blanket: every RFactor type is also a UnitEnumFactor (provides IntoR for FactorOptionVec<T>).
585impl<T: RFactor + crate::match_arg::MatchArg> UnitEnumFactor for T {
586    const FACTOR_LEVELS: &'static [&'static str] = T::CHOICES;
587    fn to_factor_index(self) -> i32 {
588        self.to_level_index()
589    }
590    fn from_factor_index(idx: i32) -> Option<Self> {
591        T::from_level_index(idx)
592    }
593}
594
595impl<T: RFactor> TryFromSexp for FactorOptionVec<T> {
596    type Error = SexpError;
597    const CHARACTER_ONLY: bool = true;
598    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
599        factor_option_vec_from_sexp(sexp).map(FactorOptionVec)
600    }
601}
602
603// region: UnitEnumFactor — factor trait for DataFrameRow unit-only enums
604
605/// Trait implemented by unit-only enums derived via `#[derive(DataFrameRow)]`.
606///
607/// Provides the level names and 1-based index needed to convert enum values
608/// into R factor SEXPs. Unlike `RFactor`, this trait does **not** require
609/// `Copy` or `MatchArg`, making it usable with `DataFrameRow`-derived types
610/// that only need to participate as factor columns in data frames.
611///
612/// Implemented automatically by `#[derive(DataFrameRow)]` on unit-only enums.
613/// `FactorVec<T>` and `FactorOptionVec<T>` provide the SEXP conversion used by the
614/// companion struct's `into_data_frame` method.
615///
616/// # Safety contract
617///
618/// `to_factor_index` must return a value in `1..=FACTOR_LEVELS.len() as i32`
619/// (or `NA_INTEGER` for missing) to produce a valid R factor SEXP.
620pub trait UnitEnumFactor: Sized {
621    /// Ordered level names (in the same order as the enum variants).
622    const FACTOR_LEVELS: &'static [&'static str];
623
624    /// Convert `self` to a 1-based R factor level index.
625    fn to_factor_index(self) -> i32;
626
627    /// Inverse: 1-based level index → variant, or `None` if out of range.
628    ///
629    /// Used by the enum DataFrame reader to reconstruct unit-only enum values
630    /// from an R factor column. The default blanket impl delegates to the
631    /// underlying `RFactor::from_level_index`.
632    fn from_factor_index(idx: i32) -> Option<Self>;
633}
634
635impl<T: UnitEnumFactor> IntoR for FactorOptionVec<T> {
636    type Error = std::convert::Infallible;
637    fn try_into_sexp(self) -> Result<crate::SEXP, Self::Error> {
638        Ok(self.into_sexp())
639    }
640    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::SEXP, Self::Error> {
641        self.try_into_sexp()
642    }
643    fn into_sexp(self) -> SEXP {
644        // Generic statics aren't allowed in Rust, so levels are built per call.
645        // build_factor_with_levels protects the levels STRSXP across the
646        // build_factor allocation — see "PROTECT discipline against R-devel GC".
647        let indices: Vec<i32> = self
648            .0
649            .into_iter()
650            .map(|opt| match opt {
651                None => NA_INTEGER,
652                Some(v) => v.to_factor_index(),
653            })
654            .collect();
655        build_factor_with_levels(&indices, T::FACTOR_LEVELS)
656    }
657}
658// endregion: UnitEnumFactor
659
660// region: Tests
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::match_arg::MatchArg;
666
667    #[derive(Copy, Clone, Debug, PartialEq)]
668    enum TestColor {
669        Red,
670        Green,
671        Blue,
672    }
673
674    impl MatchArg for TestColor {
675        const CHOICES: &'static [&'static str] = &["Red", "Green", "Blue"];
676
677        fn from_choice(choice: &str) -> Option<Self> {
678            match choice {
679                "Red" => Some(TestColor::Red),
680                "Green" => Some(TestColor::Green),
681                "Blue" => Some(TestColor::Blue),
682                _ => None,
683            }
684        }
685
686        fn to_choice(self) -> &'static str {
687            match self {
688                TestColor::Red => "Red",
689                TestColor::Green => "Green",
690                TestColor::Blue => "Blue",
691            }
692        }
693    }
694
695    impl RFactor for TestColor {
696        fn to_level_index(self) -> i32 {
697            match self {
698                TestColor::Red => 1,
699                TestColor::Green => 2,
700                TestColor::Blue => 3,
701            }
702        }
703
704        fn from_level_index(idx: i32) -> Option<Self> {
705            match idx {
706                1 => Some(TestColor::Red),
707                2 => Some(TestColor::Green),
708                3 => Some(TestColor::Blue),
709                _ => None,
710            }
711        }
712    }
713
714    #[test]
715    fn test_level_index_roundtrip() {
716        assert_eq!(
717            TestColor::from_level_index(TestColor::Red.to_level_index()),
718            Some(TestColor::Red)
719        );
720        assert_eq!(
721            TestColor::from_level_index(TestColor::Green.to_level_index()),
722            Some(TestColor::Green)
723        );
724        assert_eq!(
725            TestColor::from_level_index(TestColor::Blue.to_level_index()),
726            Some(TestColor::Blue)
727        );
728    }
729
730    #[test]
731    fn test_invalid_index() {
732        assert_eq!(TestColor::from_level_index(0), None);
733        assert_eq!(TestColor::from_level_index(4), None);
734        assert_eq!(TestColor::from_level_index(-1), None);
735    }
736
737    #[test]
738    fn test_levels_array() {
739        assert_eq!(TestColor::CHOICES, &["Red", "Green", "Blue"]);
740    }
741
742    // Test interaction factor (manual impl to verify logic)
743    #[derive(Copy, Clone, Debug, PartialEq)]
744    enum Size {
745        Small,
746        Large,
747    }
748
749    impl MatchArg for Size {
750        const CHOICES: &'static [&'static str] = &["Small", "Large"];
751
752        fn from_choice(choice: &str) -> Option<Self> {
753            match choice {
754                "Small" => Some(Size::Small),
755                "Large" => Some(Size::Large),
756                _ => None,
757            }
758        }
759
760        fn to_choice(self) -> &'static str {
761            match self {
762                Size::Small => "Small",
763                Size::Large => "Large",
764            }
765        }
766    }
767
768    impl RFactor for Size {
769        fn to_level_index(self) -> i32 {
770            match self {
771                Size::Small => 1,
772                Size::Large => 2,
773            }
774        }
775
776        fn from_level_index(idx: i32) -> Option<Self> {
777            match idx {
778                1 => Some(Size::Small),
779                2 => Some(Size::Large),
780                _ => None,
781            }
782        }
783    }
784
785    // Manual interaction factor impl (what derive should generate)
786    #[derive(Copy, Clone, Debug, PartialEq)]
787    enum ColorSize {
788        Red(Size),
789        Green(Size),
790        Blue(Size),
791    }
792
793    impl MatchArg for ColorSize {
794        const CHOICES: &'static [&'static str] = &[
795            "Red.Small",
796            "Red.Large",
797            "Green.Small",
798            "Green.Large",
799            "Blue.Small",
800            "Blue.Large",
801        ];
802
803        fn from_choice(choice: &str) -> Option<Self> {
804            let idx_1 = Self::CHOICES
805                .iter()
806                .position(|&l| l == choice)
807                .map(|i| i as i32 + 1)?;
808            Self::from_level_index(idx_1)
809        }
810
811        fn to_choice(self) -> &'static str {
812            Self::CHOICES[(self.to_level_index() - 1) as usize]
813        }
814    }
815
816    impl RFactor for ColorSize {
817        fn to_level_index(self) -> i32 {
818            match self {
819                Self::Red(inner) => {
820                    let inner_idx_0 = inner.to_level_index() - 1;
821                    inner_idx_0 + 1
822                }
823                Self::Green(inner) => {
824                    let inner_idx_0 = inner.to_level_index() - 1;
825                    2 + inner_idx_0 + 1
826                }
827                Self::Blue(inner) => {
828                    let inner_idx_0 = inner.to_level_index() - 1;
829                    2 * 2 + inner_idx_0 + 1
830                }
831            }
832        }
833
834        fn from_level_index(idx: i32) -> Option<Self> {
835            match idx {
836                1..=2 => {
837                    let inner_idx_1 = (idx - 1) % 2 + 1;
838                    Size::from_level_index(inner_idx_1).map(Self::Red)
839                }
840                3..=4 => {
841                    let inner_idx_1 = (idx - 1) % 2 + 1;
842                    Size::from_level_index(inner_idx_1).map(Self::Green)
843                }
844                5..=6 => {
845                    let inner_idx_1 = (idx - 1) % 2 + 1;
846                    Size::from_level_index(inner_idx_1).map(Self::Blue)
847                }
848                _ => None,
849            }
850        }
851    }
852
853    #[test]
854    fn test_interaction_levels() {
855        assert_eq!(
856            ColorSize::CHOICES,
857            &[
858                "Red.Small",
859                "Red.Large",
860                "Green.Small",
861                "Green.Large",
862                "Blue.Small",
863                "Blue.Large"
864            ]
865        );
866    }
867
868    #[test]
869    fn test_interaction_to_index() {
870        assert_eq!(ColorSize::Red(Size::Small).to_level_index(), 1);
871        assert_eq!(ColorSize::Red(Size::Large).to_level_index(), 2);
872        assert_eq!(ColorSize::Green(Size::Small).to_level_index(), 3);
873        assert_eq!(ColorSize::Green(Size::Large).to_level_index(), 4);
874        assert_eq!(ColorSize::Blue(Size::Small).to_level_index(), 5);
875        assert_eq!(ColorSize::Blue(Size::Large).to_level_index(), 6);
876    }
877
878    #[test]
879    fn test_interaction_from_index() {
880        assert_eq!(
881            ColorSize::from_level_index(1),
882            Some(ColorSize::Red(Size::Small))
883        );
884        assert_eq!(
885            ColorSize::from_level_index(2),
886            Some(ColorSize::Red(Size::Large))
887        );
888        assert_eq!(
889            ColorSize::from_level_index(3),
890            Some(ColorSize::Green(Size::Small))
891        );
892        assert_eq!(
893            ColorSize::from_level_index(4),
894            Some(ColorSize::Green(Size::Large))
895        );
896        assert_eq!(
897            ColorSize::from_level_index(5),
898            Some(ColorSize::Blue(Size::Small))
899        );
900        assert_eq!(
901            ColorSize::from_level_index(6),
902            Some(ColorSize::Blue(Size::Large))
903        );
904        assert_eq!(ColorSize::from_level_index(0), None);
905        assert_eq!(ColorSize::from_level_index(7), None);
906    }
907
908    #[test]
909    fn test_interaction_roundtrip() {
910        for i in 1..=6 {
911            let color_size = ColorSize::from_level_index(i).unwrap();
912            assert_eq!(color_size.to_level_index(), i);
913        }
914    }
915}
916// endregion