5

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...

SuperStormer
  • 4,997
  • 5
  • 25
  • 35
Brian Kessler
  • 2,187
  • 6
  • 28
  • 58
  • 1
    *`write!` returns an instance of `()`* — it does not. It returns a `fmt::Result`, as shown by the _return type of `fmt`_. – Shepmaster Sep 01 '21 at 14:06
  • 1
    ...and `fmt::Result` is a `type` for `Result<(), fmt::Error>`. – smitop Sep 01 '21 at 14:07
  • You have already answered the question you have **asked** ("How can I Display an enum in lowercase?"). Please clarify exactly what it is you are asking beyond that. – Shepmaster Sep 01 '21 at 14:07
  • 1
    The question [you've linked to](https://stackoverflow.com/questions/32710187/how-do-i-get-an-enum-as-a-string) has an answer using strum, and [strum allows you to customize what the `Display` format will be](https://docs.rs/strum_macros/0.21.1/strum_macros/derive.Display.html). It seems like you've linked to a suitable solution. – Shepmaster Sep 01 '21 at 14:09
  • @Shepmaster, thanks for the response. @Smitop correctly understood my intention (below). Yes, I meant to indicate the success path for `Result` is `()` instead of `String` or something I might know how to work with. I saw `strum`, but couldn't get that to work either, but that's a question for a different day. – Brian Kessler Sep 01 '21 at 14:24
  • why the first solution is a problem for you that how you should do it. – Stargateur Sep 01 '21 at 17:01
  • @Stargateur, The first solution is a problem because the only thing I want to change is the case. This solution is duplicating information, is prone to human error, and won't scale nicely (imagine if there were hundreds of values in this list instead of just two). – Brian Kessler Sep 02 '21 at 08:48
  • rust will warn you, that why rust is nice, your code will literally not compile – Stargateur Sep 02 '21 at 11:13
  • @Stargateur, Rust can't warn you about everything. For example, it would not care if I provided the value of `Red` as "ref" or "green" or "fdsf342sef". – Brian Kessler Sep 02 '21 at 12:46
  • @BrianKessler have test :p but typo error are hard to see, you could do a typo IN the enum variant don't see it until someone tell you, same problem same result – Stargateur Sep 02 '21 at 13:14
  • @Stargateur, I think you are missing the point. I just wanted a concise way to automate converting enums to strings without needing to explicitly code each result and then maintain both the enums and the conversion process. – Brian Kessler Sep 02 '21 at 13:38

3 Answers3

10

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.

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
  • 1
    Kevin Reid, Cheers for response. It didn't work "as-is", but I've fixed it. :-) Some might quibble about adding the dependency, but admittedly this is a bit more elegant than @Smitop's solution. I'm not sure whether Stackoverflow allows me to change the accepted answer, but I'll try. :-) – Brian Kessler Sep 02 '21 at 09:10
  • 2
    @BrianKessler you can change anytime you like accepted answer is just "that what I choice to fix my problem" hint for future reader, don't worry about that. – Stargateur Sep 02 '21 at 13:13
6

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?

smitop
  • 4,770
  • 2
  • 20
  • 53
  • you format the argument you send to a formater ? that feel hacky and bad – Stargateur Sep 01 '21 at 16:56
  • @Stargateur the code does seem hacky, but the nested format is the only way to make the Debug output lowercase: there isn't any format specifier for general lowercasing (`LowerHex`/`LowerExp` exists but are only applicable to formatting numbers). – smitop Sep 01 '21 at 17:03
4

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.

Filipe Rodrigues
  • 1,843
  • 2
  • 12
  • 21