Skip to main content

hex_rgb/
lib.rs

1use std::{fmt, num::ParseIntError};
2
3#[derive(Debug)]
4pub struct Color {
5    pub red: u8,
6    pub green: u8,
7    pub blue: u8,
8}
9
10pub enum HexError {
11    OddLength,
12    Empty,
13    Invalid,
14}
15
16impl fmt::Display for HexError {
17    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
18        match self {
19            HexError::OddLength => {
20                "Invalid hexadecimal color code representation. hexcode has odd number of bytes"
21                    .fmt(f)
22            }
23            HexError::Empty => "hexcode is empty.".fmt(f),
24            HexError::Invalid => "Invalid hexadecimal color code representation.".fmt(f),
25        }
26    }
27}
28
29impl Color {
30    pub fn new(hex_code: &str) -> Result<Color, String> {
31        if hex_code.is_empty() {
32            return Err(HexError::Empty.to_string());
33        }
34
35        // remove # from hex_code
36        let hex_code = if hex_code.starts_with('#') {
37            crop_letters(hex_code, 1)
38        } else {
39            hex_code
40        };
41
42        // convert shorthand RGB hexcode to RRGGBB
43        let hex_code = if hex_code.len() == 3 {
44            repeat_letters(hex_code, 1)
45        } else {
46            hex_code.to_owned()
47        };
48
49        if hex_code.len() % 2 != 0 {
50            return Err(HexError::Invalid.to_string());
51        }
52
53        let decoded_values = decode_hex(&hex_code).unwrap_or_default();
54        if decoded_values.is_empty() || decoded_values.len() > 4 {
55            return Err(HexError::Invalid.to_string());
56        }
57
58        let color = Color {
59            red: decoded_values[0],
60            green: decoded_values[1],
61            blue: decoded_values[2],
62        };
63
64        Ok(color)
65    }
66}
67
68impl PartialEq for Color {
69    fn eq(&self, other: &Color) -> bool {
70        self.red == other.red && self.green == other.green && self.blue == other.blue
71    }
72}
73
74/// Crops letters from 0 to pos-1 and returns the rest of the string slice.
75fn crop_letters(s: &str, pos: usize) -> &str {
76    match s.char_indices().nth(pos) {
77        Some((pos, _)) => &s[pos..],
78        None => "",
79    }
80}
81
82/// Repeats every character in a string.
83fn repeat_letters(s: &str, repetitions: i32) -> String {
84    let mut output = String::from("");
85    for char in s.chars() {
86        for _ in 0..=repetitions {
87            output.push(char);
88        }
89    }
90
91    output
92}
93
94/// Converts hexcode into a vector of rgb values
95fn decode_hex(s: &str) -> Result<Vec<u8>, ParseIntError> {
96    (0..s.len())
97        .step_by(2)
98        .map(|i| u8::from_str_radix(&s[i..i + 2], 16))
99        .collect()
100}
101
102/// Converts a hexcode to an rgb string
103pub fn convert_hexcode_to_rgb(hex_code: String) -> Result<Color, String> {
104    match Color::new(&hex_code) {
105        Ok(color) => Ok(color),
106        Err(e) => Err(e),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn vec_compare<T: std::cmp::PartialEq>(va: &[T], vb: &[T]) -> bool {
115        (va.len() == vb.len()) &&  // zip stops at the shortest
116     va.iter()
117       .zip(vb)
118       .all(|(a,b)| *a == *b)
119    }
120
121    #[test]
122    fn should_convert_hexcode_to_rgb() {
123        let hex = String::from("#ffffff");
124        let expected_color: Color = Color {
125            red: 255,
126            green: 255,
127            blue: 255,
128        };
129
130        assert_eq!(expected_color, convert_hexcode_to_rgb(hex).unwrap())
131    }
132
133    #[test]
134    fn should_convert_shorthand_hexcode_to_rgb() {
135        let hex = String::from("#fff");
136        let expected_color: Color = Color {
137            red: 255,
138            green: 255,
139            blue: 255,
140        };
141
142        assert_eq!(expected_color, convert_hexcode_to_rgb(hex).unwrap())
143    }
144
145    #[test]
146
147    fn should_throw_error_when_hexcode_is_invalid() {
148        let hex = String::from("#fasfsfff");
149        let err = convert_hexcode_to_rgb(hex).unwrap_err();
150        assert_eq!(err, HexError::Invalid.to_string());
151    }
152
153    #[test]
154
155    fn should_throw_error_when_shorthand_hexcode_is_invalid() {
156        let hex = String::from("#a0g");
157        let err = convert_hexcode_to_rgb(hex).unwrap_err();
158        assert_eq!(err, HexError::Invalid.to_string());
159    }
160
161    #[test]
162
163    fn should_decode_hex() {
164        let decoded = decode_hex("ffffff").unwrap();
165        let expected: Vec<u8> = vec![255, 255, 255];
166        assert!(vec_compare(&expected, &decoded), true);
167    }
168
169    #[test]
170    fn should_repeat_letters_once() {
171        assert_eq!("ff33ff".to_owned(), repeat_letters("f3f", 1));
172    }
173
174    #[test]
175    fn should_crop_letters() {
176        assert_eq!("ello", crop_letters("hello", 1));
177        assert_eq!("fff", crop_letters("#fff", 1));
178        assert_eq!("word", crop_letters("longword", 4));
179    }
180}