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::ffi::CString;
27use std::marker::PhantomData;
28use std::ops::Deref;
29use std::sync::OnceLock;
30
31use crate::altrep_traits::NA_INTEGER;
32use crate::ffi::{INTEGER, Rf_allocVector, Rf_install, Rf_xlength, SEXP, SEXPTYPE, SexpExt};
33use crate::from_r::{SexpError, TryFromSexp, charsxp_to_str};
34use crate::into_r::IntoR;
35
36// region: Cached "factor" class STRSXP
37
38static FACTOR_CLASS: OnceLock<SEXP> = OnceLock::new();
39
40pub(crate) fn factor_class_sexp() -> SEXP {
41    *FACTOR_CLASS.get_or_init(|| unsafe {
42        let class_sexp = Rf_allocVector(SEXPTYPE::STRSXP, 1);
43        crate::ffi::R_PreserveObject(class_sexp);
44        // Use symbol PRINTNAME for permanent CHARSXP
45        let sym = Rf_install(c"factor".as_ptr());
46        class_sexp.set_string_elt(0, sym.printname());
47        class_sexp
48    })
49}
50// endregion
51
52// region: RFactor trait
53
54/// Trait for mapping Rust enums to R factors.
55///
56/// Typically implemented via `#[derive(RFactor)]` for C-style enums.
57/// The derive macro also generates `IntoR` and `TryFromSexp` implementations.
58pub trait RFactor: crate::match_arg::MatchArg + Copy + 'static {
59    /// Convert variant to 1-based level index.
60    fn to_level_index(self) -> i32;
61
62    /// Convert 1-based level index to variant, or `None` if out of range.
63    fn from_level_index(idx: i32) -> Option<Self>;
64}
65// endregion
66
67// region: Core building functions
68
69/// Build a levels STRSXP using symbol PRINTNAMEs for permanent CHARSXP protection.
70///
71/// The returned STRSXP is NOT protected - caller must protect or preserve it.
72pub fn build_levels_sexp(levels: &[&str]) -> SEXP {
73    unsafe {
74        let sexp = Rf_allocVector(SEXPTYPE::STRSXP, levels.len() as isize);
75        for (i, level) in levels.iter().enumerate() {
76            // Install as symbol - symbols and their PRINTNAMEs are never GC'd
77            let c_str = CString::new(*level).expect("level name contains null byte");
78            let sym = Rf_install(c_str.as_ptr());
79            sexp.set_string_elt(i as isize, sym.printname());
80        }
81        sexp
82    }
83}
84
85/// Build a levels STRSXP and preserve it permanently (for caching).
86pub fn build_levels_sexp_cached(levels: &[&str]) -> SEXP {
87    unsafe {
88        let sexp = build_levels_sexp(levels);
89        crate::ffi::R_PreserveObject(sexp);
90        sexp
91    }
92}
93
94/// Build a factor SEXP from indices and a levels STRSXP.
95pub fn build_factor(indices: &[i32], levels: SEXP) -> SEXP {
96    unsafe {
97        let (sexp, dst) = crate::into_r::alloc_r_vector::<i32>(indices.len());
98        dst.copy_from_slice(indices);
99        sexp.set_levels(levels);
100        sexp.set_class(factor_class_sexp());
101        sexp
102    }
103}
104// endregion
105
106// region: Factor - view into an R factor's data
107
108/// A borrowed view into an R factor's integer indices.
109///
110/// Provides `Deref` to `&[i32]` for direct slice access to the factor's
111/// underlying integer data. The indices are 1-based (matching R's convention)
112/// with `NA_INTEGER` for missing values.
113///
114/// # Example
115///
116/// ```ignore
117/// let factor = Factor::try_new(sexp)?;
118/// for &idx in factor.iter() {
119///     if idx == NA_INTEGER {
120///         println!("NA");
121///     } else {
122///         println!("level index: {}", idx);
123///     }
124/// }
125/// ```
126pub struct Factor<'a> {
127    indices: &'a [i32],
128    levels_sexp: SEXP,
129    _marker: PhantomData<&'a ()>,
130}
131
132impl<'a> Factor<'a> {
133    /// Create a Factor from a factor SEXP.
134    ///
135    /// Returns an error if the SEXP is not a factor.
136    pub fn try_new(sexp: SEXP) -> Result<Self, SexpError> {
137        if !sexp.is_factor() {
138            return Err(SexpError::InvalidValue("expected a factor".into()));
139        }
140
141        let len = unsafe { Rf_xlength(sexp) } as usize;
142        let ptr = unsafe { INTEGER(sexp) };
143        let indices = unsafe { crate::from_r::r_slice(ptr, len) };
144        let levels_sexp = sexp.get_levels();
145
146        Ok(Self {
147            indices,
148            levels_sexp,
149            _marker: PhantomData,
150        })
151    }
152
153    /// Number of elements in the factor.
154    #[inline]
155    pub fn len(&self) -> usize {
156        self.indices.len()
157    }
158
159    /// Whether the factor is empty.
160    #[inline]
161    pub fn is_empty(&self) -> bool {
162        self.indices.is_empty()
163    }
164
165    /// The levels STRSXP.
166    #[inline]
167    pub fn levels_sexp(&self) -> SEXP {
168        self.levels_sexp
169    }
170
171    /// Number of levels.
172    #[inline]
173    pub fn n_levels(&self) -> usize {
174        unsafe { Rf_xlength(self.levels_sexp) as usize }
175    }
176
177    /// Get level string at 0-based index.
178    #[inline]
179    pub fn level(&self, idx: usize) -> &'a str {
180        assert!(
181            idx < self.n_levels(),
182            "level index {idx} out of bounds (n_levels = {})",
183            self.n_levels()
184        );
185        let charsxp = self.levels_sexp.string_elt(idx as isize);
186        unsafe { charsxp_to_str(charsxp) }
187    }
188}
189
190impl Deref for Factor<'_> {
191    type Target = [i32];
192
193    #[inline]
194    fn deref(&self) -> &Self::Target {
195        self.indices
196    }
197}
198
199impl<'a> TryFromSexp for Factor<'a> {
200    type Error = SexpError;
201
202    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
203        Self::try_new(sexp)
204    }
205}
206// endregion
207
208// region: FactorMut - mutable view into an R factor's data
209
210/// A mutable borrowed view into an R factor's integer indices.
211///
212/// Provides `DerefMut` to `&mut [i32]` for direct mutable slice access.
213/// The indices are 1-based (matching R's convention) with `NA_INTEGER` for NA.
214///
215/// # Example
216///
217/// ```ignore
218/// let mut factor_mut = FactorMut::try_new(sexp)?;
219/// // Set all values to level 1
220/// for idx in factor_mut.iter_mut() {
221///     *idx = 1;
222/// }
223/// ```
224pub struct FactorMut<'a> {
225    indices: &'a mut [i32],
226    levels_sexp: SEXP,
227    _marker: PhantomData<&'a mut ()>,
228}
229
230impl<'a> FactorMut<'a> {
231    /// Create a FactorMut from a factor SEXP.
232    ///
233    /// Returns an error if the SEXP is not a factor.
234    pub fn try_new(sexp: SEXP) -> Result<Self, SexpError> {
235        if !sexp.is_factor() {
236            return Err(SexpError::InvalidValue("expected a factor".into()));
237        }
238
239        let len = unsafe { Rf_xlength(sexp) } as usize;
240        let ptr = unsafe { INTEGER(sexp) };
241        let indices = unsafe { crate::from_r::r_slice_mut(ptr, len) };
242        let levels_sexp = sexp.get_levels();
243
244        Ok(Self {
245            indices,
246            levels_sexp,
247            _marker: PhantomData,
248        })
249    }
250
251    /// Number of elements in the factor.
252    #[inline]
253    pub fn len(&self) -> usize {
254        self.indices.len()
255    }
256
257    /// Whether the factor is empty.
258    #[inline]
259    pub fn is_empty(&self) -> bool {
260        self.indices.is_empty()
261    }
262
263    /// The levels STRSXP.
264    #[inline]
265    pub fn levels_sexp(&self) -> SEXP {
266        self.levels_sexp
267    }
268
269    /// Number of levels.
270    #[inline]
271    pub fn n_levels(&self) -> usize {
272        unsafe { Rf_xlength(self.levels_sexp) as usize }
273    }
274
275    /// Get level string at 0-based index.
276    #[inline]
277    pub fn level(&self, idx: usize) -> &'a str {
278        assert!(
279            idx < self.n_levels(),
280            "level index {idx} out of bounds (n_levels = {})",
281            self.n_levels()
282        );
283        let charsxp = self.levels_sexp.string_elt(idx as isize);
284        unsafe { charsxp_to_str(charsxp) }
285    }
286}
287
288impl Deref for FactorMut<'_> {
289    type Target = [i32];
290
291    #[inline]
292    fn deref(&self) -> &Self::Target {
293        self.indices
294    }
295}
296
297impl std::ops::DerefMut for FactorMut<'_> {
298    #[inline]
299    fn deref_mut(&mut self) -> &mut Self::Target {
300        self.indices
301    }
302}
303// endregion
304
305// region: Validation helper
306
307/// Validate that a factor has the expected levels.
308pub(crate) fn validate_factor_levels(sexp: SEXP, expected: &[&str]) -> Result<(), SexpError> {
309    if !sexp.is_factor() {
310        return Err(SexpError::InvalidValue("expected a factor".into()));
311    }
312
313    let levels = sexp.get_levels();
314    if levels.type_of() != SEXPTYPE::STRSXP {
315        return Err(SexpError::InvalidValue("levels is not STRSXP".into()));
316    }
317
318    let n = unsafe { Rf_xlength(levels) } as usize;
319    if n != expected.len() {
320        return Err(SexpError::InvalidValue(format!(
321            "expected {} levels, got {}",
322            expected.len(),
323            n
324        )));
325    }
326
327    for (i, exp) in expected.iter().enumerate() {
328        let charsxp = levels.string_elt(i as isize);
329        let actual = unsafe { charsxp_to_str(charsxp) };
330        if actual != *exp {
331            return Err(SexpError::InvalidValue(format!(
332                "level {}: expected '{}', got '{}'",
333                i + 1,
334                exp,
335                actual
336            )));
337        }
338    }
339
340    Ok(())
341}
342// endregion
343
344// region: Conversion helpers (used by derive macro)
345
346/// Convert an R factor SEXP to a single enum value.
347#[inline]
348pub fn factor_from_sexp<T: RFactor>(sexp: SEXP) -> Result<T, SexpError> {
349    validate_factor_levels(sexp, T::CHOICES)?;
350
351    let len = unsafe { Rf_xlength(sexp) };
352    if len != 1 {
353        return Err(SexpError::InvalidValue(format!(
354            "expected length 1, got {}",
355            len
356        )));
357    }
358
359    let idx = sexp.integer_elt(0);
360    if idx == NA_INTEGER {
361        return Err(SexpError::InvalidValue("unexpected NA".into()));
362    }
363
364    T::from_level_index(idx).ok_or_else(|| SexpError::InvalidValue("index out of range".into()))
365}
366
367/// Convert an R factor SEXP to a Vec of enum values.
368#[inline]
369pub(crate) fn factor_vec_from_sexp<T: RFactor>(sexp: SEXP) -> Result<Vec<T>, SexpError> {
370    validate_factor_levels(sexp, T::CHOICES)?;
371
372    let len = unsafe { Rf_xlength(sexp) } as usize;
373    let mut result = Vec::with_capacity(len);
374
375    for i in 0..len {
376        let idx = sexp.integer_elt(i as isize);
377        if idx == NA_INTEGER {
378            return Err(SexpError::InvalidValue(format!("NA at index {}", i)));
379        }
380        result.push(
381            T::from_level_index(idx)
382                .ok_or_else(|| SexpError::InvalidValue("index out of range".into()))?,
383        );
384    }
385
386    Ok(result)
387}
388
389/// Convert an R factor SEXP to a Vec of Option enum values (NA → None).
390#[inline]
391pub(crate) fn factor_option_vec_from_sexp<T: RFactor>(
392    sexp: SEXP,
393) -> Result<Vec<Option<T>>, SexpError> {
394    validate_factor_levels(sexp, T::CHOICES)?;
395
396    let len = unsafe { Rf_xlength(sexp) } as usize;
397    let mut result = Vec::with_capacity(len);
398
399    for i in 0..len {
400        let idx = sexp.integer_elt(i as isize);
401        if idx == NA_INTEGER {
402            result.push(None);
403        } else {
404            result.push(Some(T::from_level_index(idx).ok_or_else(|| {
405                SexpError::InvalidValue("index out of range".into())
406            })?));
407        }
408    }
409
410    Ok(result)
411}
412// endregion
413
414// region: Newtype wrappers (for orphan rule workaround)
415
416/// Wrapper for `Vec<T: RFactor>` enabling `IntoR`/`TryFromSexp`.
417#[derive(Debug, Clone)]
418pub struct FactorVec<T>(pub Vec<T>);
419
420impl<T> FactorVec<T> {
421    /// Wrap a `Vec<T>` so it can be converted to and from R factors.
422    pub fn new(vec: Vec<T>) -> Self {
423        Self(vec)
424    }
425
426    /// Extract the inner vector.
427    pub fn into_inner(self) -> Vec<T> {
428        self.0
429    }
430}
431
432impl<T> From<Vec<T>> for FactorVec<T> {
433    fn from(vec: Vec<T>) -> Self {
434        Self(vec)
435    }
436}
437
438impl<T> Deref for FactorVec<T> {
439    type Target = Vec<T>;
440    fn deref(&self) -> &Self::Target {
441        &self.0
442    }
443}
444
445impl<T> std::ops::DerefMut for FactorVec<T> {
446    fn deref_mut(&mut self) -> &mut Self::Target {
447        &mut self.0
448    }
449}
450
451impl<T: RFactor> IntoR for FactorVec<T> {
452    type Error = std::convert::Infallible;
453    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
454        Ok(self.into_sexp())
455    }
456    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
457        self.try_into_sexp()
458    }
459    fn into_sexp(self) -> SEXP {
460        let indices: Vec<i32> = self.0.iter().map(|v| v.to_level_index()).collect();
461        build_factor(&indices, build_levels_sexp(T::CHOICES))
462    }
463}
464
465impl<T: RFactor> TryFromSexp for FactorVec<T> {
466    type Error = SexpError;
467    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
468        factor_vec_from_sexp(sexp).map(FactorVec)
469    }
470}
471
472/// Wrapper for `Vec<Option<T: RFactor>>` with NA support.
473#[derive(Debug, Clone)]
474pub struct FactorOptionVec<T>(pub Vec<Option<T>>);
475
476impl<T> FactorOptionVec<T> {
477    /// Wrap a `Vec<Option<T>>` so it can be converted to and from R factors with NA support.
478    pub fn new(vec: Vec<Option<T>>) -> Self {
479        Self(vec)
480    }
481
482    /// Extract the inner vector.
483    pub fn into_inner(self) -> Vec<Option<T>> {
484        self.0
485    }
486}
487
488impl<T> From<Vec<Option<T>>> for FactorOptionVec<T> {
489    fn from(vec: Vec<Option<T>>) -> Self {
490        Self(vec)
491    }
492}
493
494impl<T> Deref for FactorOptionVec<T> {
495    type Target = Vec<Option<T>>;
496    fn deref(&self) -> &Self::Target {
497        &self.0
498    }
499}
500
501impl<T> std::ops::DerefMut for FactorOptionVec<T> {
502    fn deref_mut(&mut self) -> &mut Self::Target {
503        &mut self.0
504    }
505}
506
507impl<T: RFactor> IntoR for FactorOptionVec<T> {
508    type Error = std::convert::Infallible;
509    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
510        Ok(self.into_sexp())
511    }
512    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
513        self.try_into_sexp()
514    }
515    fn into_sexp(self) -> SEXP {
516        let indices: Vec<i32> = self
517            .0
518            .iter()
519            .map(|v| v.map_or(NA_INTEGER, |x| x.to_level_index()))
520            .collect();
521        build_factor(&indices, build_levels_sexp(T::CHOICES))
522    }
523}
524
525impl<T: RFactor> TryFromSexp for FactorOptionVec<T> {
526    type Error = SexpError;
527    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
528        factor_option_vec_from_sexp(sexp).map(FactorOptionVec)
529    }
530}
531// endregion
532
533// region: Tests
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use crate::match_arg::MatchArg;
539
540    #[derive(Copy, Clone, Debug, PartialEq)]
541    enum TestColor {
542        Red,
543        Green,
544        Blue,
545    }
546
547    impl MatchArg for TestColor {
548        const CHOICES: &'static [&'static str] = &["Red", "Green", "Blue"];
549
550        fn from_choice(choice: &str) -> Option<Self> {
551            match choice {
552                "Red" => Some(TestColor::Red),
553                "Green" => Some(TestColor::Green),
554                "Blue" => Some(TestColor::Blue),
555                _ => None,
556            }
557        }
558
559        fn to_choice(self) -> &'static str {
560            match self {
561                TestColor::Red => "Red",
562                TestColor::Green => "Green",
563                TestColor::Blue => "Blue",
564            }
565        }
566    }
567
568    impl RFactor for TestColor {
569        fn to_level_index(self) -> i32 {
570            match self {
571                TestColor::Red => 1,
572                TestColor::Green => 2,
573                TestColor::Blue => 3,
574            }
575        }
576
577        fn from_level_index(idx: i32) -> Option<Self> {
578            match idx {
579                1 => Some(TestColor::Red),
580                2 => Some(TestColor::Green),
581                3 => Some(TestColor::Blue),
582                _ => None,
583            }
584        }
585    }
586
587    #[test]
588    fn test_level_index_roundtrip() {
589        assert_eq!(
590            TestColor::from_level_index(TestColor::Red.to_level_index()),
591            Some(TestColor::Red)
592        );
593        assert_eq!(
594            TestColor::from_level_index(TestColor::Green.to_level_index()),
595            Some(TestColor::Green)
596        );
597        assert_eq!(
598            TestColor::from_level_index(TestColor::Blue.to_level_index()),
599            Some(TestColor::Blue)
600        );
601    }
602
603    #[test]
604    fn test_invalid_index() {
605        assert_eq!(TestColor::from_level_index(0), None);
606        assert_eq!(TestColor::from_level_index(4), None);
607        assert_eq!(TestColor::from_level_index(-1), None);
608    }
609
610    #[test]
611    fn test_levels_array() {
612        assert_eq!(TestColor::CHOICES, &["Red", "Green", "Blue"]);
613    }
614
615    // Test interaction factor (manual impl to verify logic)
616    #[derive(Copy, Clone, Debug, PartialEq)]
617    enum Size {
618        Small,
619        Large,
620    }
621
622    impl MatchArg for Size {
623        const CHOICES: &'static [&'static str] = &["Small", "Large"];
624
625        fn from_choice(choice: &str) -> Option<Self> {
626            match choice {
627                "Small" => Some(Size::Small),
628                "Large" => Some(Size::Large),
629                _ => None,
630            }
631        }
632
633        fn to_choice(self) -> &'static str {
634            match self {
635                Size::Small => "Small",
636                Size::Large => "Large",
637            }
638        }
639    }
640
641    impl RFactor for Size {
642        fn to_level_index(self) -> i32 {
643            match self {
644                Size::Small => 1,
645                Size::Large => 2,
646            }
647        }
648
649        fn from_level_index(idx: i32) -> Option<Self> {
650            match idx {
651                1 => Some(Size::Small),
652                2 => Some(Size::Large),
653                _ => None,
654            }
655        }
656    }
657
658    // Manual interaction factor impl (what derive should generate)
659    #[derive(Copy, Clone, Debug, PartialEq)]
660    enum ColorSize {
661        Red(Size),
662        Green(Size),
663        Blue(Size),
664    }
665
666    impl MatchArg for ColorSize {
667        const CHOICES: &'static [&'static str] = &[
668            "Red.Small",
669            "Red.Large",
670            "Green.Small",
671            "Green.Large",
672            "Blue.Small",
673            "Blue.Large",
674        ];
675
676        fn from_choice(choice: &str) -> Option<Self> {
677            let idx_1 = Self::CHOICES
678                .iter()
679                .position(|&l| l == choice)
680                .map(|i| i as i32 + 1)?;
681            Self::from_level_index(idx_1)
682        }
683
684        fn to_choice(self) -> &'static str {
685            Self::CHOICES[(self.to_level_index() - 1) as usize]
686        }
687    }
688
689    impl RFactor for ColorSize {
690        fn to_level_index(self) -> i32 {
691            match self {
692                Self::Red(inner) => {
693                    let inner_idx_0 = inner.to_level_index() - 1;
694                    inner_idx_0 + 1
695                }
696                Self::Green(inner) => {
697                    let inner_idx_0 = inner.to_level_index() - 1;
698                    2 + inner_idx_0 + 1
699                }
700                Self::Blue(inner) => {
701                    let inner_idx_0 = inner.to_level_index() - 1;
702                    2 * 2 + inner_idx_0 + 1
703                }
704            }
705        }
706
707        fn from_level_index(idx: i32) -> Option<Self> {
708            match idx {
709                1..=2 => {
710                    let inner_idx_1 = (idx - 1) % 2 + 1;
711                    Size::from_level_index(inner_idx_1).map(Self::Red)
712                }
713                3..=4 => {
714                    let inner_idx_1 = (idx - 1) % 2 + 1;
715                    Size::from_level_index(inner_idx_1).map(Self::Green)
716                }
717                5..=6 => {
718                    let inner_idx_1 = (idx - 1) % 2 + 1;
719                    Size::from_level_index(inner_idx_1).map(Self::Blue)
720                }
721                _ => None,
722            }
723        }
724    }
725
726    #[test]
727    fn test_interaction_levels() {
728        assert_eq!(
729            ColorSize::CHOICES,
730            &[
731                "Red.Small",
732                "Red.Large",
733                "Green.Small",
734                "Green.Large",
735                "Blue.Small",
736                "Blue.Large"
737            ]
738        );
739    }
740
741    #[test]
742    fn test_interaction_to_index() {
743        assert_eq!(ColorSize::Red(Size::Small).to_level_index(), 1);
744        assert_eq!(ColorSize::Red(Size::Large).to_level_index(), 2);
745        assert_eq!(ColorSize::Green(Size::Small).to_level_index(), 3);
746        assert_eq!(ColorSize::Green(Size::Large).to_level_index(), 4);
747        assert_eq!(ColorSize::Blue(Size::Small).to_level_index(), 5);
748        assert_eq!(ColorSize::Blue(Size::Large).to_level_index(), 6);
749    }
750
751    #[test]
752    fn test_interaction_from_index() {
753        assert_eq!(
754            ColorSize::from_level_index(1),
755            Some(ColorSize::Red(Size::Small))
756        );
757        assert_eq!(
758            ColorSize::from_level_index(2),
759            Some(ColorSize::Red(Size::Large))
760        );
761        assert_eq!(
762            ColorSize::from_level_index(3),
763            Some(ColorSize::Green(Size::Small))
764        );
765        assert_eq!(
766            ColorSize::from_level_index(4),
767            Some(ColorSize::Green(Size::Large))
768        );
769        assert_eq!(
770            ColorSize::from_level_index(5),
771            Some(ColorSize::Blue(Size::Small))
772        );
773        assert_eq!(
774            ColorSize::from_level_index(6),
775            Some(ColorSize::Blue(Size::Large))
776        );
777        assert_eq!(ColorSize::from_level_index(0), None);
778        assert_eq!(ColorSize::from_level_index(7), None);
779    }
780
781    #[test]
782    fn test_interaction_roundtrip() {
783        for i in 1..=6 {
784            let color_size = ColorSize::from_level_index(i).unwrap();
785            assert_eq!(color_size.to_level_index(), i);
786        }
787    }
788}
789// endregion