diff --git a/src/Moniker/Chars.cs b/src/Moniker/Chars.cs
new file mode 100644
index 0000000..36644aa
--- /dev/null
+++ b/src/Moniker/Chars.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Text;
+
+namespace Moniker;
+
+///
+/// Represents a reference to a string of characters.
+///
+public readonly ref struct Chars
+{
+ private readonly ReadOnlySpan _u8str;
+
+ internal Chars(ReadOnlySpan u8str, int charCount)
+ {
+ _u8str = u8str;
+ Length = charCount;
+ }
+
+ ///
+ /// The length in characters.
+ ///
+ public int Length { get; }
+
+ ///
+ /// The length when encoded in UTF-8 bytes.
+ ///
+ public int Utf8Length => _u8str.Length;
+
+ ///
+ /// Writes characters to the specified buffer.
+ ///
+ /// The buffer to write to.
+ /// The number of characters written.
+ public int Write(Span buffer)
+ {
+ var count = Length;
+ if (count > buffer.Length)
+ return 0;
+
+ Encoding.UTF8.GetChars(_u8str, buffer);
+ return count;
+ }
+
+ ///
+ /// Writes character in UTF-8 encoding to the specified buffer.
+ ///
+ /// The buffer to write to.
+ /// The number of characters written.
+ public int WriteUtf8(Span buffer) =>
+ _u8str.TryCopyTo(buffer) ? Utf8Length : 0;
+
+ ///
+ /// Returns a string with the same characters.
+ ///
+ public override string ToString()
+ {
+ var chars = Length <= 64 ? stackalloc char[Length] : new char[Length];
+ _ = Write(chars);
+ return new(chars);
+ }
+
+#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
+
+ ///
+ /// This method is unsupported and throws
+ /// because instances cannot be boxed. Use
+ /// instead.
+ ///
+ /// Always thrown.
+ [Obsolete("This method is unsupported and will always throw an exception. Use the equality operator instead.")]
+ public override bool Equals(object? obj) => throw new NotSupportedException();
+
+ ///
+ /// This method is unsupported and throws .
+ ///
+ /// Always thrown.
+ [Obsolete("This method is unsupported and will always throw an exception.")]
+ public override int GetHashCode() => throw new NotSupportedException();
+
+#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
+
+ ///
+ /// Compares to another for equality.
+ ///
+ /// The other to compare to.
+ /// if this instance compares equals to .
+ public bool Equals(Chars other) => _u8str.SequenceEqual(other._u8str);
+
+ internal bool Equals(ReadOnlySpan other) => _u8str.SequenceEqual(other);
+
+ ///
+ /// Compares two for equality.
+ ///
+ /// First operand to compare.
+ /// Second operand to compare.
+ /// if equals .
+ public static bool operator ==(Chars left, Chars right) => left.Equals(right);
+
+ ///
+ /// Compares two for inequality.
+ ///
+ /// First operand to compare.
+ /// Second operand to compare.
+ /// if does not equal .
+ public static bool operator !=(Chars left, Chars right) => !left.Equals(right);
+}
diff --git a/src/Moniker/NameGenerator.cs b/src/Moniker/NameGenerator.cs
index 0f4e986..42824c8 100644
--- a/src/Moniker/NameGenerator.cs
+++ b/src/Moniker/NameGenerator.cs
@@ -1,4 +1,6 @@
using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
namespace Moniker
{
@@ -21,12 +23,27 @@ public static class NameGenerator
///
public static string Generate(MonikerStyle monikerStyle, string delimiter = DefaultDelimiter)
{
- return monikerStyle switch
+ ValidateDelimiterArgument(delimiter);
+ Generate(monikerStyle, out var adjective, out var noun);
+ return Join(adjective, delimiter, noun);
+ }
+
+ ///
+ /// Generate a random name in the specified style.
+ ///
+ /// The style of random name.
+ /// The adjective part of the random name.
+ /// The adjective part of the random name.
+ /// The generated random name.
+ ///
+ public static void Generate(MonikerStyle monikerStyle, out Chars adjective, out Chars noun)
+ {
+ switch (monikerStyle)
{
- MonikerStyle.Moby => GenerateMoby(delimiter),
- MonikerStyle.Moniker => GenerateMoniker(delimiter),
- _ => throw new ArgumentOutOfRangeException(nameof(monikerStyle))
- };
+ case MonikerStyle.Moby: GenerateMoby(out adjective, out noun); break;
+ case MonikerStyle.Moniker: GenerateMoniker(out adjective, out noun); break;
+ default: throw new ArgumentOutOfRangeException(nameof(monikerStyle));
+ }
}
///
@@ -35,7 +52,18 @@ public static string Generate(MonikerStyle monikerStyle, string delimiter = Defa
/// An optional delimiter to use between adjective and noun.
/// The generated random name.
public static string GenerateMoniker(string delimiter = DefaultDelimiter)
- => BuildNamePair(MonikerDescriptors.Strings, MonikerAnimals.Strings, delimiter);
+ {
+ ValidateDelimiterArgument(delimiter);
+ GenerateMoniker(out var adjective, out var noun);
+ return Join(adjective, delimiter, noun);
+ }
+
+ ///
+ /// Generate a random name in the 'moniker' project style.
+ ///
+ /// The generated random name.
+ public static void GenerateMoniker(out Chars adjective, out Chars noun)
+ => BuildNamePair(MonikerDescriptors.Strings, out adjective, MonikerAnimals.Strings, out noun);
///
/// Generate a random name in the 'moby' project style.
@@ -43,42 +71,56 @@ public static string GenerateMoniker(string delimiter = DefaultDelimiter)
/// An optional delimiter to use between adjective and noun.
/// The generated random name.
public static string GenerateMoby(string delimiter = DefaultDelimiter)
- => BuildNamePair(MobyAdjectives.Strings, MobySurnames.Strings, delimiter);
-
- private static string BuildNamePair(
- Utf8Strings adjectives,
- Utf8Strings nouns,
- string delimiter)
{
- if (string.IsNullOrEmpty(delimiter))
- throw new ArgumentException("The delimiter must not be null or empty.", nameof(delimiter));
+ ValidateDelimiterArgument(delimiter);
+ GenerateMoby(out var adjective, out var noun);
+ return Join(adjective, delimiter, noun);
+ }
- BuildNamePair(adjectives, out var adjective, nouns, out var noun);
+ private static string Join(Chars adjective, ReadOnlySpan delimiter, Chars noun)
+ {
+ var length = adjective.Length + delimiter.Length + noun.Length;
+ var chars = length <= 64 ? stackalloc char[length] : new char[length];
- var length = adjective.CharCount + delimiter.Length + noun.CharCount;
+ var writeCount = adjective.Write(chars);
+ Debug.Assert(writeCount == adjective.Length);
- var chars = length <= 64 ? stackalloc char[length] : new char[length];
+ delimiter.CopyTo(chars[adjective.Length..]);
- _ = adjective.GetChars(chars);
- delimiter.CopyTo(chars[adjective.CharCount..]);
- noun.GetChars(chars[(adjective.CharCount + delimiter.Length)..]);
+ writeCount = noun.Write(chars[(adjective.Length + delimiter.Length)..]);
+ Debug.Assert(writeCount == noun.Length);
return new(chars);
}
+ ///
+ /// Generate a random name in the 'moby' project style.
+ ///
+ /// The generated random name.
+ public static void GenerateMoby(out Chars adjective, out Chars noun)
+ => BuildNamePair(MobyAdjectives.Strings, out adjective, MobySurnames.Strings, out noun);
+
+ private static void ValidateDelimiterArgument(
+ string delimiter,
+ [CallerArgumentExpression(nameof(delimiter))] string? paramName = null)
+ {
+ if (string.IsNullOrEmpty(delimiter))
+ throw new ArgumentException("The delimiter must not be null or empty.", paramName);
+ }
+
private static void BuildNamePair(
- Utf8Strings adjectives, out Utf8String adjective,
- Utf8Strings nouns, out Utf8String noun)
+ Utf8Strings adjectives, out Chars adjective,
+ Utf8Strings nouns, out Chars noun)
{
do
{
adjective = GetRandomEntry(adjectives);
noun = GetRandomEntry(nouns);
}
- while (adjective == "boring"u8 && noun == "wozniak"u8); // Steve Wozniak is not boring
+ while (adjective.Equals("boring"u8) && noun.Equals("wozniak"u8)); // Steve Wozniak is not boring
}
- private static Utf8String GetRandomEntry(Utf8Strings entries)
+ private static Chars GetRandomEntry(Utf8Strings entries)
{
var index = Random.Shared.Next(entries.Count);
return entries[index];
diff --git a/src/Moniker/Utf8Strings.cs b/src/Moniker/Utf8Strings.cs
index 4ef2256..e93c563 100644
--- a/src/Moniker/Utf8Strings.cs
+++ b/src/Moniker/Utf8Strings.cs
@@ -6,32 +6,6 @@
namespace Moniker;
-internal readonly ref struct Utf8String(ReadOnlySpan bytes, int charCount)
-{
- public readonly ReadOnlySpan Bytes = bytes;
- public readonly int CharCount = charCount;
-
- public int GetChars(Span chars) => Encoding.UTF8.GetChars(Bytes, chars);
-
- public override string ToString() => Encoding.UTF8.GetString(Bytes);
-
- // Equality members
-
- public static bool operator ==(Utf8String left, ReadOnlySpan right) => left.Equals(right);
- public static bool operator !=(Utf8String left, ReadOnlySpan right) => !left.Equals(right);
-
- private bool Equals(ReadOnlySpan other) => Bytes.SequenceEqual(other);
-
- // Unsupported equality members because this type cannot be boxed
-
- public override int GetHashCode() => throw new NotSupportedException();
- public override bool Equals(object? obj) => throw new NotSupportedException();
-
- // Implicit conversions
-
- public static implicit operator ReadOnlySpan(Utf8String data) => data.Bytes;
-}
-
internal readonly ref struct Utf8Strings
{
private readonly ReadOnlySpan _data;
@@ -52,7 +26,7 @@ public Utf8Strings(int count, ReadOnlySpan data, ReadOnlySpan offsets
_charCounts = charCounts;
}
- public Utf8String this[int index] => new(_data[_offsets[index].._offsets[index + 1]], _charCounts[index]);
+ public Chars this[int index] => new(_data[_offsets[index].._offsets[index + 1]], _charCounts[index]);
public override string ToString() => $"{{ Count = {Count}, Size = {_data.Length} }}";
@@ -67,7 +41,7 @@ public ref struct Enumerator(Utf8Strings strings)
/// Behaviour is undefined if has never been called or returned
/// .
///
- public Utf8String Current => _strings[_index];
+ public Chars Current => _strings[_index];
public bool MoveNext()
{
diff --git a/test/Moniker.ApprovalTests/Moniker.approved.txt b/test/Moniker.ApprovalTests/Moniker.approved.txt
index c1af879..e86feb8 100644
--- a/test/Moniker.ApprovalTests/Moniker.approved.txt
+++ b/test/Moniker.ApprovalTests/Moniker.approved.txt
@@ -1,5 +1,25 @@
namespace Moniker
{
+ [System.Obsolete("Types with embedded references are not supported in this version of your compiler" +
+ ".", true)]
+ [System.Runtime.CompilerServices.CompilerFeatureRequired("RefStructs")]
+ [System.Runtime.CompilerServices.IsByRefLike]
+ public readonly struct Chars
+ {
+ public int Length { get; }
+ public int Utf8Length { get; }
+ public bool Equals(Moniker.Chars other) { }
+ [System.Obsolete("This method is unsupported and will always throw an exception. Use the equality o" +
+ "perator instead.")]
+ public override bool Equals(object? obj) { }
+ [System.Obsolete("This method is unsupported and will always throw an exception.")]
+ public override int GetHashCode() { }
+ public override string ToString() { }
+ public int Write(System.Span buffer) { }
+ public int WriteUtf8(System.Span buffer) { }
+ public static bool operator !=(Moniker.Chars left, Moniker.Chars right) { }
+ public static bool operator ==(Moniker.Chars left, Moniker.Chars right) { }
+ }
public enum MonikerStyle
{
Moniker = 0,
@@ -9,7 +29,10 @@ namespace Moniker
{
public const string DefaultDelimiter = "-";
public static string Generate(Moniker.MonikerStyle monikerStyle, string delimiter = "-") { }
+ public static void Generate(Moniker.MonikerStyle monikerStyle, out Moniker.Chars adjective, out Moniker.Chars noun) { }
public static string GenerateMoby(string delimiter = "-") { }
+ public static void GenerateMoby(out Moniker.Chars adjective, out Moniker.Chars noun) { }
public static string GenerateMoniker(string delimiter = "-") { }
+ public static void GenerateMoniker(out Moniker.Chars adjective, out Moniker.Chars noun) { }
}
}
\ No newline at end of file
diff --git a/test/Moniker.PerformanceTests/NameGeneratorBenchmark.cs b/test/Moniker.PerformanceTests/NameGeneratorBenchmark.cs
index d4d790e..f3e662b 100644
--- a/test/Moniker.PerformanceTests/NameGeneratorBenchmark.cs
+++ b/test/Moniker.PerformanceTests/NameGeneratorBenchmark.cs
@@ -9,4 +9,10 @@ public class NameGeneratorBenchmark
[Benchmark]
public string GenerateMoniker() => NameGenerator.Generate(MonikerStyle.Moniker);
+
+ [Benchmark]
+ public void GenerateMobyPair() => NameGenerator.Generate(MonikerStyle.Moby, out _, out _);
+
+ [Benchmark]
+ public void GenerateMonikerPair() => NameGenerator.Generate(MonikerStyle.Moniker, out _, out _);
}
\ No newline at end of file
diff --git a/test/Moniker.Tests/CharsTests.cs b/test/Moniker.Tests/CharsTests.cs
new file mode 100644
index 0000000..b9edc6c
--- /dev/null
+++ b/test/Moniker.Tests/CharsTests.cs
@@ -0,0 +1,155 @@
+using System.Text;
+using System;
+using System.Linq;
+using FluentAssertions;
+using Xunit;
+
+namespace Moniker.Tests;
+
+public class CharsTests
+{
+ private static Chars Chars(string str) =>
+ new(Encoding.UTF8.GetBytes(str), str.Length);
+
+ [Theory]
+ [InlineData("foobar", 6, 6)]
+ [InlineData("☀️", 2, 6)]
+ [InlineData("🐟", 2, 4)]
+ [InlineData("⭐", 1, 3)]
+ [InlineData("🐕", 2, 4)]
+ public void Lengths(string str, int length, int utf8Length)
+ {
+ var chars = Chars(str);
+ chars.Length.Should().Be(length);
+ chars.Utf8Length.Should().Be(utf8Length);
+ }
+
+ [Theory]
+ [InlineData(6, "foobar")]
+ [InlineData(7, "foobar-")]
+ [InlineData(8, "foobar--")]
+ public void Write(int length, string expected)
+ {
+ var chars = Chars("foobar");
+ var buffer = new char[length];
+ buffer.AsSpan().Fill('-');
+
+ var result = chars.Write(buffer);
+
+ result.Should().Be(6);
+ new string(buffer).Should().Be(expected);
+ }
+
+ [Theory]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(5)]
+ public void WriteWhenBufferIsTooSmall(int length)
+ {
+ var chars = Chars("foobar");
+ var buffer = new char[length];
+ buffer.AsSpan().Fill('-');
+
+ var result = chars.Write(buffer);
+
+ result.Should().Be(0);
+ new string('-', length).Should().Be(new string(buffer));
+ }
+
+ [Theory]
+ [InlineData(6, "foobar")]
+ [InlineData(7, "foobar-")]
+ [InlineData(8, "foobar--")]
+ public void WriteUtf8(int length, string expected)
+ {
+ var chars = Chars("foobar");
+ var buffer = new byte[length];
+ buffer.AsSpan().Fill((byte)'-');
+
+ var result = chars.WriteUtf8(buffer);
+
+ result.Should().Be(6);
+ Encoding.UTF8.GetString(buffer).Should().Be(expected);
+ }
+
+ [Theory]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(5)]
+ public void WriteUtf8WhenBufferIsTooSmall(int length)
+ {
+ var chars = Chars("foobar");
+ var buffer = new byte[length];
+ buffer.AsSpan().Fill((byte)'-');
+
+ var result = chars.WriteUtf8(buffer);
+
+ result.Should().Be(0);
+ Encoding.UTF8.GetString(buffer).Should().Be(new string('-', length));
+ }
+
+ [Fact]
+ public void ToStringReturnsStringWithAllCharacters()
+ {
+ var samples = Enumerable.Range(0, 100)
+ .Select(n => string.Join(string.Empty, Enumerable.Repeat("foobar", n)))
+ .TakeWhile(s => s.Length <= 256);
+
+ foreach (var str in samples)
+ Chars(str).ToString().Should().Be(str);
+ }
+
+ [Fact]
+ public void EqualsComparesValues()
+ {
+ var chars1 = Chars("foobar");
+ var chars2 = Chars("foobar");
+ var chars3 = Chars("FOOBAR");
+ chars1.Equals(chars2).Should().BeTrue();
+ chars1.Equals(chars3).Should().BeFalse();
+ }
+
+ [Fact]
+ public void EqualityOperatorComparesValues()
+ {
+ var chars1 = Chars("foobar");
+ var chars2 = Chars("foobar");
+ var chars3 = Chars("FOOBAR");
+ (chars1 == chars2).Should().BeTrue();
+ (chars1 == chars3).Should().BeFalse();
+ }
+
+ [Fact]
+ public void InequalityOperatorComparesValues()
+ {
+ var chars1 = Chars("foobar");
+ var chars2 = Chars("foobar");
+ var chars3 = Chars("FOOBAR");
+ (chars1 != chars2).Should().BeFalse();
+ (chars1 != chars3).Should().BeTrue();
+ }
+
+ [Fact]
+ public void GetHashCodeIsUnsupported()
+ {
+ var act = () =>
+ {
+ var chars = Chars("foobar");
+ _ = chars.GetHashCode();
+ };
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void EqualsIsUnsupported()
+ {
+ var act = () =>
+ {
+ var chars = Chars("foobar");
+ _ = chars.Equals(42);
+ };
+
+ act.Should().Throw();
+ }
+}
diff --git a/test/Moniker.Tests/NameGeneratorTests.cs b/test/Moniker.Tests/NameGeneratorTests.cs
index 2f3bae4..6517352 100644
--- a/test/Moniker.Tests/NameGeneratorTests.cs
+++ b/test/Moniker.Tests/NameGeneratorTests.cs
@@ -52,5 +52,34 @@ public void GenerateWithMonikerStyleParameter(
moniker.Should().MatchRegex(expected);
}
+
+ [Theory]
+ [InlineData(MonikerStyle.Moby)]
+ [InlineData(MonikerStyle.Moniker)]
+ public void GeneratePairWithSpecificMonikerStyleMethods(MonikerStyle monikerStyle)
+ {
+ Chars adjective, noun;
+
+ switch (monikerStyle)
+ {
+ case MonikerStyle.Moniker: NameGenerator.GenerateMoniker(out adjective, out noun); break;
+ case MonikerStyle.Moby: NameGenerator.GenerateMoby(out adjective, out noun); break;
+ default: throw new ArgumentOutOfRangeException(nameof(monikerStyle), monikerStyle, null);
+ };
+ const string expected = /* lang=regex */ "^[a-zA-Z]+$";
+ adjective.ToString().Should().MatchRegex(expected);
+ noun.ToString().Should().MatchRegex(expected);
+ }
+
+ [Theory]
+ [InlineData(MonikerStyle.Moby)]
+ [InlineData(MonikerStyle.Moniker)]
+ public void GeneratePairWithMonikerStyleParameter(MonikerStyle monikerStyle)
+ {
+ NameGenerator.Generate(monikerStyle, out var adjective, out var noun);
+ const string expected = /* lang=regex */ "^[a-zA-Z]+$";
+ adjective.ToString().Should().MatchRegex(expected);
+ noun.ToString().Should().MatchRegex(expected);
+ }
}
}
diff --git a/test/Moniker.Tests/StringDataTests.cs b/test/Moniker.Tests/StringDataTests.cs
index 66ca3be..25887d2 100644
--- a/test/Moniker.Tests/StringDataTests.cs
+++ b/test/Moniker.Tests/StringDataTests.cs
@@ -66,15 +66,12 @@ into e
var index = 0;
using var line = lines.GetEnumerator();
- foreach (var u8Str in strings)
+ foreach (var chars in strings)
{
line.MoveNext().Should().BeTrue();
- var str = Encoding.UTF8.GetString(u8Str);
- str.Should().Be(line.Current);
- u8Str.ToString().Should().Be(line.Current);
-
- strings[index].Bytes.SequenceEqual(u8Str).Should().BeTrue();
- (u8Str == strings[index]).Should().BeTrue();
+ var str = chars.ToString();
+ chars.ToString().Should().Be(line.Current);
+ (chars == strings[index]).Should().BeTrue();
// Except for some coincidental cases, ensure string isn't interned.
// This list is not exhaustive and even may be brittle (subject to