How can I display an enum in lowercase?

Viewed 1427

I have an enum:

pub enum BoxColour {
    Red,
    Blue,
}

I not only want to get this value as a string, but I want the value to be converted to lower case.

This works:

impl Display for BoxColour {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.write_str(match self {
            BoxColour::Red => "red",
            BoxColour::Blue => "blue",
        })?;
        Ok(())
    }
}

When the list of colours grows, this list would need to be updated.

If I use the write! macro, it does not seem possible to manipulate the result because write! returns an instance of () instead of a String:

impl Display for BoxColour {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "{:?}", self)
    }
}

This suggests that this is working through side effects and maybe we could hack the same place in memory the value is going, but even if that is possible, it probably isn't a good idea...

3 Answers

The strum crate provides a derive macro for implementing Display for an enum, and optionally lowercasing the variant names:

use strum_macros::Display;

#[derive(Display)]
// If we don't care about inner capitals, we don't need to set `serialize_all` 
// and can leave parenthesis empty.
#[strum(serialize_all = "snake_case")]
pub enum BoxColour {
    Red,
    Blue,
    LightGreen,  // example of how inner capitals are treated
}

fn main() {
    for c in [BoxColour::Red, BoxColor::Blue, BoxColor::LightGreen] {
        println!("{}", c);
    }
}

You will also need the corresponding dependency on strum in your Cargo.toml:

[dependencies]
strum = { version = "0.21", features = ["derive"] }

This should print:

red
blue
light_green

strum will generate code similar to the match with BoxColour::Red => "red", cases that you mentioned, but without the need to update it manually.

Here is a way to do that without needing to manually update Display::fmt every time you add a new colour by using the derived Debug implementation and lowercasing that:

#[derive(Debug)]
pub enum BoxColour {
    Red,
    Blue,
}

impl fmt::Display for BoxColour {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "{}", format!("{:?}", self).to_lowercase())
    }
}

(playground)

Note that write! returns a Result<(), fmt::Error> (which is a fmt::Result), not a raw ().

But consider if it would just be better to manually update the list, or use a macro to specify that. Also consider how colours with multiple words (such as LightBlue) should be lowercased: is lightblue what you want there?

As an extension to @Smitop's answer, it is possible to generally display any value in lowercase by using a custom formatter, as such:

use std::fmt::{self, Write};

struct LowercaseFormatter<'a, 'b>(pub &'a mut fmt::Formatter<'b>);

impl<'a, 'b> fmt::Write for LowercaseFormatter<'a, 'b> {
    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
        for ch in s.chars() {
            self.0.write_fmt(format_args!("{}", ch.to_lowercase()))?;
        }
        
        Ok(())
    }
}

Then use it as such:

#[derive(Debug)]
pub enum BoxColour {
    Red,
    Blue,
}

impl fmt::Display for BoxColour {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(LowercaseFormatter(formatter), "{:?}", self)
    }
}

fn main() {
    println!("{}", BoxColour::Red); // "red"
}

Playground

This version doesn't allocate a string on every formatting, but it is also not very optimized, calling write_fmt for every character is relatively costly.

One alternative for just ascii characters, is to call write_char(ch.to_ascii_lowercase()), but writing a string a character at a time is also relatively costly.

A proper solution would partition the string in some way to be able to write all already lowercase characters all at once, and only special case the uppercase ones.

Related