diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index ee50710e8e..fa122f267e 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -200,6 +200,94 @@ public static implicit operator Color(uint rawValue) public static implicit operator uint(Color color) => color.RawValue; + /// + /// Converts the string representation of a color to a Color object. + /// + /// String to be parsed to a color + /// Color format of the string + /// A Color object with a color value parsed from a string + /// The argument is not a number or of incorrect length + public static Color Parse(string rawValue, ColorType colorType = ColorType.CssHexColor) + { + if (TryParse(rawValue, out var color, colorType)) + return color; + + throw new ArgumentOutOfRangeException(nameof(rawValue), "Value must be a number of valid length - matching the specified ColorType"); + } + + /// + /// Converts the string representation of a color to a Color object. The return value indicates whether the conversion succeeded. + /// + /// String to be parsed to a color + /// When this method returns true, contains a Color that represents the parsed string. + /// Color format of the string + /// true if the conversion succeeded; false otherwise. + public static bool TryParse(string rawValue, out Color color, ColorType colorType = ColorType.CssHexColor) + { + color = new Color(); + + if (string.IsNullOrWhiteSpace(rawValue)) + return false; + + rawValue = rawValue.TrimStart('#'); + + if (rawValue.StartsWith("0x")) + rawValue = rawValue.Substring(2); + + if (!uint.TryParse(rawValue, System.Globalization.NumberStyles.HexNumber, null, out var parsedValue)) + return false; + + uint r; + uint g; + uint b; + uint fullHex; + + switch (rawValue.Length, colorType) + { + case (3, ColorType.CssHexColor): + r = parsedValue >> 8; + g = (parsedValue & 0xF0) >> 4; + b = parsedValue & 0xF; + + r = (r << 4) | r; + g = (g << 4) | g; + b = (b << 4) | b; + + fullHex = (r << 16) | (g << 8) | b; + + break; + + case (4, ColorType.CssHexColor): + r = (parsedValue & 0xF00) >> 8; + g = (parsedValue & 0xF0) >> 4; + b = parsedValue & 0xF; + + r = (r << 4) | r; + g = (g << 4) | g; + b = (b << 4) | b; + + fullHex = (r << 16) | (g << 8) | b; + + break; + + case (6, ColorType.CssHexColor): + color = new Color(parsedValue); + return true; + + case (8, ColorType.CssHexColor): + parsedValue &= 0xFFFFFF; + color = new Color(parsedValue); + return true; + + default: + return false; + } + + color = new Color(fullHex); + + return true; + } + public override bool Equals(object obj) => obj is Color c && RawValue == c.RawValue; diff --git a/src/Discord.Net.Core/Entities/Roles/ColorType.cs b/src/Discord.Net.Core/Entities/Roles/ColorType.cs new file mode 100644 index 0000000000..a0b594ec82 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/ColorType.cs @@ -0,0 +1,21 @@ +using System; + +namespace Discord; + +public enum ColorType +{ + /// + /// Color in one of the following formats. + /// + /// #RGB The three-value syntax + /// + /// + /// #RGBA The four-value syntax + /// + /// + /// #RRGGBB The six-value syntax + /// + /// #RRGGBBAA The eight-value syntax + /// + CssHexColor = 0 +} diff --git a/test/Discord.Net.Tests.Unit/ColorTests.cs b/test/Discord.Net.Tests.Unit/ColorTests.cs index 48a6041e5b..6259238e97 100644 --- a/test/Discord.Net.Tests.Unit/ColorTests.cs +++ b/test/Discord.Net.Tests.Unit/ColorTests.cs @@ -74,6 +74,26 @@ public void Color_FromRgb_Float_OutOfRange() Assert.Throws(() => new Color(2.0f, 2.0f, 2.0f)); } [Fact] + public void Color_FromRgb_String_CssHexColor() + { + Assert.Equal(0xFF0000u, Color.Parse("#F00", ColorType.CssHexColor).RawValue); + Assert.Equal(0x22BB44u, Color.Parse("#2B4", ColorType.CssHexColor).RawValue); + Assert.Equal(0xAABBAAu, Color.Parse("FABA", ColorType.CssHexColor).RawValue); + Assert.Equal(0x00F672u, Color.Parse("00F672", ColorType.CssHexColor).RawValue); + Assert.Equal(0x257777u, Color.Parse("0xFF257777", ColorType.CssHexColor).RawValue); + } + [Fact] + public void Color_FromRgb_String_Invalid() + { + Assert.Throws(() => Color.Parse(" ", ColorType.CssHexColor)); + Assert.Throws(() => Color.Parse(null, ColorType.CssHexColor)); + Assert.Throws(() => Color.Parse("#F")); + Assert.Throws(() => Color.Parse("F0", ColorType.CssHexColor)); + Assert.Throws(() => Color.Parse("FF000")); + Assert.Throws(() => Color.Parse("FF00000")); + Assert.Throws(() => Color.Parse("FF0000000")); + } + [Fact] public void Color_Red() { Assert.Equal(0xAF, new Color(0xAF1390).R);