More concise way to convert usize to enum

/// Level of detail for meshes.
/// (Not textures. Those have a different but similar system.)
#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, Hash, Eq, Ord)]
#[repr(i8)]
pub enum VolumeLod {
    Blank = -1,
    Lowest = 0,
    Low = 1,
    Medium = 2,
    High = 3,
}

impl VolumeLod {
    /// Maximum value
    pub const MAX: usize = Self::High as usize;

    ///  As nonnegative numeric value. "Blank" does not convert.
    pub fn as_usize(&self) -> Result<usize, Error> {
        Ok((*self as i8).try_into()?)
    }
    
    /// From nonnegative numeric value. "Blank" is not an option.
    pub fn from_usize(v: usize) -> Result<Self, Error> {
        //  There's probably a more conscise way to do this.
        const LOWEST: usize = VolumeLod::Lowest as usize;
        const LOW: usize = VolumeLod::Low as usize;
        const MEDIUM: usize = VolumeLod::Medium as usize;
        const HIGH: usize = VolumeLod::High as usize;
        match v {
            LOWEST => Ok(Self::Lowest),
            LOW => Ok(Self::Low),
            MEDIUM => Ok(Self::Medium),
            HIGH => Ok(Self::High),
            _ => Err(anyhow!("Invalid volume LOD value: {}", v))
        }  
    }
}

There's probably a more elegant way to do this. But what? I seem to have to write out those const values because match patterns must be a literal, not a constant expression.

(Use case: numeric value comes in over a network, and needs to become an enum, with checking.)

Could strum::FromRepr be helpful here?

1 Like

I was dealing with a similar issue. Common solutions to this involve strum, num-traits, tricks with macro_rules: Increasing list of numbers in macro_rules!

I ended up writing a procedural macro from scratch. No warranty. Not liable for damages:


pub struct Collect_Enum_Outer {}

impl Collect_Enum_Outer {
    pub fn new() -> Collect_Enum_Outer {
        Collect_Enum_Outer {}
    }

    pub fn output(self, outer_ident: &proc_macro2::Ident, vs: &Vec<Ident>) -> proc_macro2::TokenStream {
        let num_id = (0..vs.len()).map(|x| quote! { #x }).collect::<Vec<_>>();
        let first = vs.first().unwrap().clone();
        let len = vs.len();
        quote! {
            impl Enum_Usize_T for #outer_ident {
                const enum_len: usize = #len;
                fn to_usize(&self) -> usize {
                    match self {
                        #( #outer_ident::#vs  =>  #num_id, )*
                    }
                }

                fn from_usize(t: usize) -> Self {
                    match t {
                        #( #num_id => #outer_ident::#vs, )*
                        _ => #outer_ident::#first ,
                    }
                }
            }
        }
    }
}

#[proc_macro_derive(Enum_Usize)]
pub fn proc_my_jscode_inner(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let DeriveInput { ident: outer_ident, data, .. } = parse_macro_input!(input);

    let output = match data {
        syn::Data::Enum(DataEnum { variants, .. }) => {
            let builder = Collect_Enum_Outer::new();
            let mut vs = vec![];
            for x in variants.iter() {
                match x.fields {
                    Fields::Unit => {}
                    Fields::Named(_) => {
                        todo!("named field")
                    }
                    Fields::Unnamed(_) => {
                        todo!("unnamed field")
                    }
                }
                vs.push(x.ident.clone());
            }
            let t = builder.output(&outer_ident, &vs);
            t
        }
        _ => {
            todo!("error in x_enum_id")
        }
    };
    output.into()
}

This only works on enums. Only enums w/o fields. One fn collects it, the quote! section outputs it.

1 Like

This can be done pretty easily with num_derive and num_traits:

use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::{FromPrimitive, ToPrimitive};

#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, Hash, Eq, Ord, FromPrimitive, ToPrimitive)]
pub enum VolumeLod {
    Blank = -1,
    Lowest = 0,
    Low = 1,
    Medium = 2,
    High = 3,
}

fn main() {
    let x = ToPrimitive::to_usize(&VolumeLod::Medium).unwrap();

    println!("x: {x}");
    println!(
        "VolumeLod from x: {:?}",
        <VolumeLod as FromPrimitive>::from_usize(x).unwrap()
    );
}
2 Likes

Ended up with

pub fn from_usize(v: usize) -> Result<Self, Error> {
    Ok(VolumeLod::from_repr(v.try_into()?).ok_or_else(|| anyhow!("Invalid volume LOD value: {}", v))?)
}