Skip to content

Commit 1cf4cd2

Browse files
Added EnumValue and EnumValueCaseIgnored.
1 parent d452d48 commit 1cf4cd2

5 files changed

Lines changed: 307 additions & 4 deletions

File tree

README.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ A set of useful extensions for working with strings, spans, and value formatting
99
* v3.x is a major overhaul with much improved methods and expanded tests and coverage.
1010
* Avoids allocation wherever possible.
1111

12+
---
13+
1214
### String vs Span Equality
1315

1416
Optimized `.Equals(...)` extension methods for comparing spans and strings.
1517

18+
---
19+
1620
### String & Span Splitting
1721

1822
#### `SplitToEnumerable`
@@ -23,24 +27,85 @@ Returns each string segment of the split through an enumerable instead of all at
2327

2428
Produces an enumerable where each segment is yielded as a `ReadOnlyMemory<char>`.
2529

30+
---
31+
2632
### Trimming
2733

2834
#### `TrimStartPattern` & `TrimEndPattern`
2935

3036
Similar to their character trimming counterparts, these methods can trim sequences of characters or regular expression patterns.
3137

38+
---
39+
3240
### String Segments
3341

3442
Similar to `ArraySegment`, `StringSegment` offers methods for operating on strings without requiring allocation.
3543

3644
Instead of extensions like `string.BeforeFirst(search)`, now you can call `string.First(search).Preceding()`.
3745

38-
### StringBuilder Extensions
46+
---
47+
48+
### `StringBuilder` Extensions
3949

4050
* Extensions for adding segments with separators.
4151
* Extensions for adding spans without creating a string first.
4252
* Extensions for converting enumerables to a `StringBuilder`.
4353

54+
---
55+
56+
### `StringComparable` Extensions
57+
58+
```cs
59+
if(myString.AsCaseInsensitive()=="HELLO!") { }
60+
```
61+
62+
instead of
63+
64+
```cs
65+
if(myString.Equals("HELLO!", StringComparison.OrdinalIgnoreCase)) { }
66+
```
67+
68+
### `EnumValue<TEnum>` & `EnumValueIgnoreCase<TEnum>`
69+
70+
Implicit conversion makes it easy. Optimized methods make it fast.
71+
72+
Consider the following:
73+
74+
```cs
75+
enum Greek { Alpha, Beta, Gamma }
76+
77+
void DoSomethingWithGreek(Greek value) { }
78+
79+
DoSomethingWithGreek(Greek.Alpha);
80+
```
81+
82+
It's nice that `Greek` is an enum because it won't be null, and it has to be one of the values.
83+
But what if you want to write a single function that will take an `Greek` or a string?
84+
This gets problematic as the string value has to be parsed and you'll likely need an overload.
85+
86+
`EnumValue<TEnum>` solves this problem:
87+
88+
```cs
89+
enum Greek { Alpha, Beta, Gamma }
90+
91+
void DoSomethingWithGreek(EnumValue<Greek> value) { }
92+
93+
// Both work fine.
94+
DoSomethingWithGreek("Alpha");
95+
DoSomethingWithGreek(Greek.Alpha);
96+
97+
// Throws an ArgumentException:
98+
DoSomethingWithGreek("Theta");
99+
```
100+
101+
The implicit conversion between a `string` and `EnumValue<TEnum>` make this possible.
102+
103+
If you need to allow for case-insensitive comparison then simply use `EnumValueCaseIgnored<TEnum>` instead.
104+
105+
The performance is outstanding as it uses an expression tree for case-sensitive matching and a dictionary for case-insensitive.
106+
107+
---
108+
44109
### ... And more
45110

46111
Various formatting and `Regex` extensions including `Capture.AsSpan()` for getting a `ReadOnlySpan<char>` instead of allocating a string.

Source/EnumValue.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Diagnostics;
5+
using System.Linq;
6+
using System.Linq.Expressions;
7+
using System.Runtime.CompilerServices;
8+
9+
namespace Open.Text
10+
{
11+
[DebuggerDisplay("{GetDebuggerDisplay()}")]
12+
public struct EnumValue<TEnum>
13+
where TEnum : Enum
14+
{
15+
public EnumValue(TEnum value)
16+
{
17+
Value = value;
18+
}
19+
20+
public EnumValue(string value)
21+
{
22+
if (value is null)
23+
throw new ArgumentNullException(nameof(value));
24+
25+
Value = Parse(value);
26+
}
27+
28+
public TEnum Value { get; }
29+
30+
public override string ToString() => Value.ToString();
31+
32+
static readonly Func<string, TEnum> Parser = CreateParseEnumDelegate();
33+
public static TEnum Parse(string value)
34+
{
35+
try
36+
{
37+
return Parser(value);
38+
}
39+
catch (Exception ex)
40+
{
41+
throw new ArgumentException($"Requested value '{value}' was not found.", nameof(value), ex);
42+
}
43+
}
44+
45+
// https://stackoverflow.com/questions/26678181/enum-parse-vs-switch-performance
46+
static Func<string, TEnum> CreateParseEnumDelegate()
47+
{
48+
var eValue = Expression.Parameter(typeof(string), "value"); // (string value)
49+
var tEnum = typeof(TEnum);
50+
51+
return
52+
Expression.Lambda<Func<string, TEnum>>(
53+
Expression.Block(tEnum,
54+
Expression.Switch(tEnum, eValue,
55+
Expression.Block(tEnum,
56+
Expression.Throw(Expression.New(typeof(Exception).GetConstructor(Type.EmptyTypes))),
57+
Expression.Default(tEnum)
58+
),
59+
null,
60+
Enum.GetValues(tEnum).Cast<object>().Select(v => Expression.SwitchCase(
61+
Expression.Constant(v),
62+
Expression.Constant(v.ToString())
63+
)).ToArray()
64+
)
65+
), eValue
66+
).Compile();
67+
}
68+
69+
public bool Equals(EnumValue<TEnum> other) => Value.Equals(other.Value);
70+
public static bool operator ==(EnumValue<TEnum> left, EnumValue<TEnum> right) => left.Value.Equals(right.Value);
71+
public static bool operator !=(EnumValue<TEnum> left, EnumValue<TEnum> right) => !left.Value.Equals(right.Value);
72+
73+
public bool Equals(EnumValueCaseIgnored<TEnum> other) => Value.Equals(other.Value);
74+
public static bool operator ==(EnumValue<TEnum> left, EnumValueCaseIgnored<TEnum> right) => left.Value.Equals(right.Value);
75+
public static bool operator !=(EnumValue<TEnum> left, EnumValueCaseIgnored<TEnum> right) => !left.Value.Equals(right.Value);
76+
77+
public bool Equals(TEnum other) => Value.Equals(other);
78+
public static bool operator ==(EnumValue<TEnum> left, TEnum right) => left.Value.Equals(right);
79+
public static bool operator !=(EnumValue<TEnum> left, TEnum right) => !left.Value.Equals(right);
80+
81+
public override bool Equals(object? obj)
82+
{
83+
return obj is TEnum e && Value.Equals(e)
84+
|| obj is EnumValue<TEnum> v1 && Value.Equals(v1.Value)
85+
|| obj is EnumValueCaseIgnored<TEnum> v2 && Value.Equals(v2.Value);
86+
}
87+
88+
public override int GetHashCode() => Value.GetHashCode();
89+
90+
public static implicit operator EnumValue<TEnum>(EnumValueCaseIgnored<TEnum> value) => new(value.Value);
91+
92+
public static implicit operator TEnum(EnumValue<TEnum> value) => value.Value;
93+
94+
public static implicit operator EnumValue<TEnum>(string value) => new(value);
95+
96+
private string GetDebuggerDisplay()
97+
{
98+
var eType = typeof(TEnum);
99+
return $"{eType.Name}.{Value} [EnumValue<{eType.FullName}>]";
100+
}
101+
}
102+
103+
[DebuggerDisplay("{GetDebuggerDisplay()}")]
104+
public struct EnumValueCaseIgnored<TEnum>
105+
where TEnum : Enum
106+
{
107+
public EnumValueCaseIgnored(TEnum value)
108+
{
109+
Value = value;
110+
}
111+
112+
public EnumValueCaseIgnored(string value)
113+
{
114+
if (value is null)
115+
throw new ArgumentNullException(nameof(value));
116+
117+
Value = Parse(value);
118+
}
119+
120+
public TEnum Value { get; }
121+
122+
public override string ToString() => Value.ToString();
123+
124+
internal static readonly ImmutableDictionary<string, TEnum> CaseInsensitiveLookup
125+
= CreateCaseInsensitiveDictionary();
126+
127+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
128+
public static TEnum Parse(string value)
129+
{
130+
try
131+
{
132+
return CaseInsensitiveLookup[value];
133+
}
134+
catch (KeyNotFoundException ex)
135+
{
136+
throw new ArgumentException($"Requested value '{value}' was not found.", nameof(value), ex);
137+
}
138+
}
139+
140+
static ImmutableDictionary<string, TEnum> CreateCaseInsensitiveDictionary()
141+
=> Enum
142+
.GetValues(typeof(TEnum))
143+
.Cast<TEnum>()
144+
.ToImmutableDictionary(v => v.ToString(), v => v, StringComparer.OrdinalIgnoreCase);
145+
146+
public bool Equals(EnumValue<TEnum> other) => Value.Equals(other.Value);
147+
public static bool operator ==(EnumValueCaseIgnored<TEnum> left, EnumValue<TEnum> right) => left.Value.Equals(right.Value);
148+
public static bool operator !=(EnumValueCaseIgnored<TEnum> left, EnumValue<TEnum> right) => !left.Value.Equals(right.Value);
149+
150+
public bool Equals(EnumValueCaseIgnored<TEnum> other) => Value.Equals(other.Value);
151+
public static bool operator ==(EnumValueCaseIgnored<TEnum> left, EnumValueCaseIgnored<TEnum> right) => left.Value.Equals(right.Value);
152+
public static bool operator !=(EnumValueCaseIgnored<TEnum> left, EnumValueCaseIgnored<TEnum> right) => !left.Value.Equals(right.Value);
153+
154+
public bool Equals(TEnum other) => Value.Equals(other);
155+
public static bool operator ==(EnumValueCaseIgnored<TEnum> left, TEnum right) => left.Value.Equals(right);
156+
public static bool operator !=(EnumValueCaseIgnored<TEnum> left, TEnum right) => !left.Value.Equals(right);
157+
158+
public override bool Equals(object? obj)
159+
{
160+
return obj is TEnum e && Value.Equals(e)
161+
|| obj is EnumValueCaseIgnored<TEnum> v1 && Value.Equals(v1.Value)
162+
|| obj is EnumValue<TEnum> v2 && Value.Equals(v2.Value);
163+
}
164+
165+
public override int GetHashCode() => Value.GetHashCode();
166+
167+
public static implicit operator EnumValueCaseIgnored<TEnum>(EnumValue<TEnum> value) => new(value.Value);
168+
169+
public static implicit operator TEnum(EnumValueCaseIgnored<TEnum> value) => value.Value;
170+
171+
public static implicit operator EnumValueCaseIgnored<TEnum>(string value) => new(value);
172+
173+
private string GetDebuggerDisplay()
174+
{
175+
var eType = typeof(TEnum);
176+
return $"{eType.Name}.{Value} [EnumValueCaseIgnored<{eType.FullName}>]";
177+
}
178+
}
179+
}

Source/Open.Text.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<RepositoryUrl>https://github.com/Open-NET-Libraries/Open.Text</RepositoryUrl>
1818
<RepositoryType>git</RepositoryType>
1919
<PackageTags>dotnet, dotnetcore, string, span, readonlyspan, text, format, split, trim, equals, trimmed equals, first, last, preceding, following, stringbuilder, extensions</PackageTags>
20-
<Version>3.2.2</Version>
20+
<Version>3.2.3</Version>
2121
<PackageReleaseNotes></PackageReleaseNotes>
2222
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2323
<PublishRepositoryUrl>true</PublishRepositoryUrl>

Source/StringComparable.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,21 @@ public override bool Equals(object obj)
3535
};
3636
}
3737

38+
#if NETSTANDARD2_1_OR_GREATER
39+
public override int GetHashCode()
40+
=> HashCode.Combine(Source, Type);
41+
#else
3842
public override int GetHashCode()
3943
{
4044
int hashCode = 141257509;
4145
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Source);
4246
hashCode = hashCode * -1521134295 + Type.GetHashCode();
4347
return hashCode;
4448
}
49+
#endif
4550

46-
public static bool operator ==(StringComparable a, StringComparable? b) => a.Equals(b);
47-
public static bool operator !=(StringComparable a, StringComparable? b) => !a.Equals(b);
51+
public static bool operator ==(StringComparable a, StringComparable b) => a.Equals(b);
52+
public static bool operator !=(StringComparable a, StringComparable b) => !a.Equals(b);
4853

4954
public static bool operator ==(StringComparable a, string? b) => a.Equals(b);
5055
public static bool operator !=(StringComparable a, string? b) => !a.Equals(b);

Tests/EnumValueTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using Xunit;
3+
4+
namespace Open.Text.Tests
5+
{
6+
public static class EnumValueTests
7+
{
8+
enum Greek
9+
{
10+
Alpha, Beta, Gamma
11+
}
12+
13+
[Theory]
14+
[InlineData("Alpha")]
15+
[InlineData("Beta")]
16+
[InlineData("Gamma")]
17+
public static void EvalValueParse(string value)
18+
{
19+
CheckImplicit(value, (Greek)Enum.Parse(typeof(Greek), value));
20+
var lower = value.ToLower();
21+
CheckImplicitCaseIgnored(lower, (Greek)Enum.Parse(typeof(Greek), lower, true));
22+
}
23+
24+
[Theory]
25+
[InlineData("Cappa")]
26+
[InlineData("Theta")]
27+
public static void EvalValueParseFail(string value)
28+
{
29+
Assert.Throws<ArgumentException>(()=> Enum.Parse(typeof(Greek), value));
30+
Assert.Throws<ArgumentException>(() => _ = new EnumValue<Greek>(value));
31+
Assert.Throws<ArgumentException>(() => _ = new EnumValueCaseIgnored<Greek>(value));
32+
}
33+
34+
static void CheckImplicit(EnumValue<Greek> value, Greek expected)
35+
{
36+
Assert.Equal(expected, value);
37+
Assert.True(value==expected);
38+
Assert.True(value.Equals(expected));
39+
Assert.True(value == new EnumValueCaseIgnored<Greek>(expected));
40+
Assert.False(value != expected);
41+
Assert.False(value != new EnumValueCaseIgnored<Greek>(expected));
42+
}
43+
44+
static void CheckImplicitCaseIgnored(EnumValueCaseIgnored<Greek> value, Greek expected)
45+
{
46+
Assert.Equal(expected, value);
47+
Assert.True(value == expected);
48+
Assert.True(value.Equals(expected));
49+
Assert.True(value == new EnumValueCaseIgnored<Greek>(expected));
50+
Assert.False(value != expected);
51+
Assert.False(value != new EnumValueCaseIgnored<Greek>(expected));
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)