mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Rune extensions micro-optimizations (#3910)
* Add benchmarks for potentially optimizable RuneExtensions * Add new RuneExtensions.DecodeSurrogatePair benchmark implementation Avoids intermediate heap array allocations which is especially nice when the rune is not surrogate pair because then array heap allocations are completely avoided. * Enable nullable reference types in RuneExtensions * Make RuneExtensions.MaxUnicodeCodePoint readonly Makes sure no one can accidentally change the value. Ideally would be const value. * Optimize RuneExtensions.DecodeSurrogatePair * Remove duplicate Rune.GetUnicodeCategory call * Add new RuneExtensions.IsSurrogatePair benchmark implementation Avoids intermediate heap allocations by using stack allocated buffer. * Optimize RuneExtensions.IsSurrogatePair * Add RuneExtensions.GetEncodingLength tests * Optimize RuneExtensions.GetEncodingLength * Optimize RuneExtensions.Encode * Print encoding name in benchmark results * Rename variable to better match return description * Add RuneExtensions.EncodeSurrogatePair benchmark --------- Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
20
Benchmarks/Benchmarks.csproj
Normal file
20
Benchmarks/Benchmarks.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Terminal.Gui.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
20
Benchmarks/Program.cs
Normal file
20
Benchmarks/Program.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace Terminal.Gui.Benchmarks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main (string [] args)
|
||||
{
|
||||
var config = DefaultConfig.Instance;
|
||||
|
||||
// Uncomment for faster but less accurate intermediate iteration.
|
||||
// Final benchmarks should be run with at least the default run length.
|
||||
//config = config.AddJob (BenchmarkDotNet.Jobs.Job.ShortRun);
|
||||
|
||||
BenchmarkSwitcher
|
||||
.FromAssembly (typeof (Program).Assembly)
|
||||
.Run(args, config);
|
||||
}
|
||||
}
|
||||
66
Benchmarks/Text/RuneExtensions/DecodeSurrogatePair.cs
Normal file
66
Benchmarks/Text/RuneExtensions/DecodeSurrogatePair.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Tui = Terminal.Gui;
|
||||
|
||||
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for <see cref="Tui.RuneExtensions.DecodeSurrogatePair"/> performance fine-tuning.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
|
||||
public class DecodeSurrogatePair
|
||||
{
|
||||
/// <summary>
|
||||
/// Benchmark for previous implementation.
|
||||
/// </summary>
|
||||
/// <param name="rune"></param>
|
||||
/// <returns></returns>
|
||||
[Benchmark]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public char []? Previous (Rune rune)
|
||||
{
|
||||
_ = RuneToStringToCharArray (rune, out char []? chars);
|
||||
return chars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark for current implementation.
|
||||
///
|
||||
/// Utilizes Rune methods that take Span argument avoiding intermediate heap array allocation when combined with stack allocated intermediate buffer.
|
||||
/// When rune is not surrogate pair there will be no heap allocation.
|
||||
///
|
||||
/// Final surrogate pair array allocation cannot be avoided due to the current method signature design.
|
||||
/// Changing the method signature, or providing an alternative method, to take a destination Span would allow further optimizations by allowing caller to reuse buffer for consecutive calls.
|
||||
/// </summary>
|
||||
[Benchmark (Baseline = true)]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public char []? Current (Rune rune)
|
||||
{
|
||||
_ = Tui.RuneExtensions.DecodeSurrogatePair (rune, out char []? chars);
|
||||
return chars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Previous implementation with intermediate string allocation.
|
||||
///
|
||||
/// The IsSurrogatePair implementation at the time had hidden extra string allocation so there were intermediate heap allocations even if rune is not surrogate pair.
|
||||
/// </summary>
|
||||
private static bool RuneToStringToCharArray (Rune rune, out char []? chars)
|
||||
{
|
||||
if (rune.IsSurrogatePair ())
|
||||
{
|
||||
chars = rune.ToString ().ToCharArray ();
|
||||
return true;
|
||||
}
|
||||
|
||||
chars = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static IEnumerable<object> DataSource ()
|
||||
{
|
||||
yield return new Rune ('a');
|
||||
yield return "𝔹".EnumerateRunes ().Single ();
|
||||
}
|
||||
}
|
||||
72
Benchmarks/Text/RuneExtensions/Encode.cs
Normal file
72
Benchmarks/Text/RuneExtensions/Encode.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Tui = Terminal.Gui;
|
||||
|
||||
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for <see cref="Tui.RuneExtensions.Encode"/> performance fine-tuning.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
|
||||
public class Encode
|
||||
{
|
||||
/// <summary>
|
||||
/// Benchmark for previous implementation.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public byte [] Previous (Rune rune, byte [] destination, int start, int count)
|
||||
{
|
||||
_ = StringEncodingGetBytes (rune, destination, start, count);
|
||||
return destination;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark for current implementation.
|
||||
///
|
||||
/// Avoids intermediate heap allocations with stack allocated intermediate buffer.
|
||||
/// </summary>
|
||||
[Benchmark (Baseline = true)]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public byte [] Current (Rune rune, byte [] destination, int start, int count)
|
||||
{
|
||||
_ = Tui.RuneExtensions.Encode (rune, destination, start, count);
|
||||
return destination;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Previous implementation with intermediate byte array and string allocation.
|
||||
/// </summary>
|
||||
private static int StringEncodingGetBytes (Rune rune, byte [] dest, int start = 0, int count = -1)
|
||||
{
|
||||
byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
|
||||
var length = 0;
|
||||
|
||||
for (var i = 0; i < (count == -1 ? bytes.Length : count); i++)
|
||||
{
|
||||
if (bytes [i] == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
dest [start + i] = bytes [i];
|
||||
length++;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public static IEnumerable<object []> DataSource ()
|
||||
{
|
||||
Rune[] runes = [ new Rune ('a'),"𝔞".EnumerateRunes().Single() ];
|
||||
|
||||
foreach (var rune in runes)
|
||||
{
|
||||
yield return new object [] { rune, new byte [16], 0, -1 };
|
||||
yield return new object [] { rune, new byte [16], 8, -1 };
|
||||
// Does not work in original implementation
|
||||
//yield return new object [] { rune, new byte [16], 8, 8 };
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Benchmarks/Text/RuneExtensions/EncodeSurrogatePair.cs
Normal file
36
Benchmarks/Text/RuneExtensions/EncodeSurrogatePair.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Tui = Terminal.Gui;
|
||||
|
||||
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for <see cref="Tui.RuneExtensions.EncodeSurrogatePair"/> performance fine-tuning.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
|
||||
public class EncodeSurrogatePair
|
||||
{
|
||||
/// <summary>
|
||||
/// Benchmark for current implementation.
|
||||
/// </summary>
|
||||
[Benchmark (Baseline = true)]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public Rune Current (char highSurrogate, char lowSurrogate)
|
||||
{
|
||||
_ = Tui.RuneExtensions.EncodeSurrogatePair (highSurrogate, lowSurrogate, out Rune rune);
|
||||
return rune;
|
||||
}
|
||||
|
||||
public static IEnumerable<object []> DataSource ()
|
||||
{
|
||||
string[] runeStrings = ["🍕", "🧠", "🌹"];
|
||||
foreach (string symbol in runeStrings)
|
||||
{
|
||||
if (symbol is [char high, char low])
|
||||
{
|
||||
yield return [high, low];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Benchmarks/Text/RuneExtensions/GetEncodingLength.cs
Normal file
74
Benchmarks/Text/RuneExtensions/GetEncodingLength.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Tui = Terminal.Gui;
|
||||
|
||||
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for <see cref="Tui.RuneExtensions.GetEncodingLength"/> performance fine-tuning.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
|
||||
public class GetEncodingLength
|
||||
{
|
||||
/// <summary>
|
||||
/// Benchmark for previous implementation.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public int Previous (Rune rune, PrettyPrintedEncoding encoding)
|
||||
{
|
||||
return WithEncodingGetBytesArray (rune, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark for current implementation.
|
||||
/// </summary>
|
||||
[Benchmark (Baseline = true)]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public int Current (Rune rune, PrettyPrintedEncoding encoding)
|
||||
{
|
||||
return Tui.RuneExtensions.GetEncodingLength (rune, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Previous implementation with intermediate byte array, string, and char array allocation.
|
||||
/// </summary>
|
||||
private static int WithEncodingGetBytesArray (Rune rune, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
byte [] bytes = encoding.GetBytes (rune.ToString ().ToCharArray ());
|
||||
var offset = 0;
|
||||
|
||||
if (bytes [^1] == 0)
|
||||
{
|
||||
offset++;
|
||||
}
|
||||
|
||||
return bytes.Length - offset;
|
||||
}
|
||||
|
||||
public static IEnumerable<object []> DataSource ()
|
||||
{
|
||||
PrettyPrintedEncoding[] encodings = [ new(Encoding.UTF8), new(Encoding.Unicode), new(Encoding.UTF32) ];
|
||||
Rune[] runes = [ new Rune ('a'), "𝔹".EnumerateRunes ().Single () ];
|
||||
|
||||
foreach (var encoding in encodings)
|
||||
{
|
||||
foreach (Rune rune in runes)
|
||||
{
|
||||
yield return [rune, encoding];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="System.Text.Encoding"/> wrapper to display proper encoding name in benchmark results.
|
||||
/// </summary>
|
||||
public record PrettyPrintedEncoding (Encoding Encoding)
|
||||
{
|
||||
public static implicit operator Encoding (PrettyPrintedEncoding ppe) => ppe.Encoding;
|
||||
|
||||
public override string ToString () => Encoding.HeaderName;
|
||||
}
|
||||
}
|
||||
50
Benchmarks/Text/RuneExtensions/IsSurrogatePair.cs
Normal file
50
Benchmarks/Text/RuneExtensions/IsSurrogatePair.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Tui = Terminal.Gui;
|
||||
|
||||
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks for <see cref="Tui.RuneExtensions.IsSurrogatePair"/> performance fine-tuning.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
|
||||
public class IsSurrogatePair
|
||||
{
|
||||
/// <summary>
|
||||
/// Benchmark for previous implementation.
|
||||
/// </summary>
|
||||
/// <param name="rune"></param>
|
||||
[Benchmark]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public bool Previous (Rune rune)
|
||||
{
|
||||
return WithToString (rune);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark for current implementation.
|
||||
///
|
||||
/// Avoids intermediate heap allocations by using stack allocated buffer.
|
||||
/// </summary>
|
||||
[Benchmark (Baseline = true)]
|
||||
[ArgumentsSource (nameof (DataSource))]
|
||||
public bool Current (Rune rune)
|
||||
{
|
||||
return Tui.RuneExtensions.IsSurrogatePair (rune);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Previous implementation with intermediate string allocation.
|
||||
/// </summary>
|
||||
private static bool WithToString (Rune rune)
|
||||
{
|
||||
return char.IsSurrogatePair (rune.ToString (), 0);
|
||||
}
|
||||
|
||||
public static IEnumerable<object> DataSource ()
|
||||
{
|
||||
yield return new Rune ('a');
|
||||
yield return "𝔹".EnumerateRunes ().Single ();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
#nullable enable
|
||||
|
||||
using System.Globalization;
|
||||
using Wcwidth;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
@@ -7,7 +9,7 @@ namespace Terminal.Gui;
|
||||
public static class RuneExtensions
|
||||
{
|
||||
/// <summary>Maximum Unicode code point.</summary>
|
||||
public static int MaxUnicodeCodePoint = 0x10FFFF;
|
||||
public static readonly int MaxUnicodeCodePoint = 0x10FFFF;
|
||||
|
||||
/// <summary>Reports if the provided array of bytes can be encoded as UTF-8.</summary>
|
||||
/// <param name="buffer">The byte array to probe.</param>
|
||||
@@ -32,17 +34,25 @@ public static class RuneExtensions
|
||||
/// <param name="rune">The rune to decode.</param>
|
||||
/// <param name="chars">The chars if the rune is a surrogate pair. Null otherwise.</param>
|
||||
/// <returns><see langword="true"/> if the rune is a valid surrogate pair; <see langword="false"/> otherwise.</returns>
|
||||
public static bool DecodeSurrogatePair (this Rune rune, out char [] chars)
|
||||
public static bool DecodeSurrogatePair (this Rune rune, out char []? chars)
|
||||
{
|
||||
if (rune.IsSurrogatePair ())
|
||||
bool isSingleUtf16CodeUnit = rune.IsBmp;
|
||||
if (isSingleUtf16CodeUnit)
|
||||
{
|
||||
chars = rune.ToString ().ToCharArray ();
|
||||
chars = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const int maxCharsPerRune = 2;
|
||||
Span<char> charBuffer = stackalloc char[maxCharsPerRune];
|
||||
int charsWritten = rune.EncodeToUtf16 (charBuffer);
|
||||
if (charsWritten >= 2 && char.IsSurrogatePair (charBuffer [0], charBuffer [1]))
|
||||
{
|
||||
chars = charBuffer [..charsWritten].ToArray ();
|
||||
return true;
|
||||
}
|
||||
|
||||
chars = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,21 +65,24 @@ public static class RuneExtensions
|
||||
/// <returns>he number of bytes written into the destination buffer.</returns>
|
||||
public static int Encode (this Rune rune, byte [] dest, int start = 0, int count = -1)
|
||||
{
|
||||
byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
|
||||
var length = 0;
|
||||
const int maxUtf8BytesPerRune = 4;
|
||||
Span<byte> bytes = stackalloc byte[maxUtf8BytesPerRune];
|
||||
int writtenBytes = rune.EncodeToUtf8 (bytes);
|
||||
|
||||
for (var i = 0; i < (count == -1 ? bytes.Length : count); i++)
|
||||
int bytesToCopy = count == -1
|
||||
? writtenBytes
|
||||
: Math.Min (count, writtenBytes);
|
||||
int bytesWritten = 0;
|
||||
for (int i = 0; i < bytesToCopy; i++)
|
||||
{
|
||||
if (bytes [i] == 0)
|
||||
if (bytes [i] == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
dest [start + i] = bytes [i];
|
||||
length++;
|
||||
bytesWritten++;
|
||||
}
|
||||
|
||||
return length;
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to encode (as UTF-16) a surrogate pair.</summary>
|
||||
@@ -105,18 +118,26 @@ public static class RuneExtensions
|
||||
/// <param name="rune">The rune to probe.</param>
|
||||
/// <param name="encoding">The encoding used; the default is UTF8.</param>
|
||||
/// <returns>The number of bytes required.</returns>
|
||||
public static int GetEncodingLength (this Rune rune, Encoding encoding = null)
|
||||
public static int GetEncodingLength (this Rune rune, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
byte [] bytes = encoding.GetBytes (rune.ToString ().ToCharArray ());
|
||||
var offset = 0;
|
||||
|
||||
if (bytes [^1] == 0)
|
||||
const int maxCharsPerRune = 2;
|
||||
// Get characters with UTF16 to keep that part independent of selected encoding.
|
||||
Span<char> charBuffer = stackalloc char[maxCharsPerRune];
|
||||
int charsWritten = rune.EncodeToUtf16(charBuffer);
|
||||
Span<char> chars = charBuffer[..charsWritten];
|
||||
|
||||
int maxEncodedLength = encoding.GetMaxByteCount (charsWritten);
|
||||
Span<byte> byteBuffer = stackalloc byte[maxEncodedLength];
|
||||
int bytesEncoded = encoding.GetBytes (chars, byteBuffer);
|
||||
ReadOnlySpan<byte> encodedBytes = byteBuffer[..bytesEncoded];
|
||||
|
||||
if (encodedBytes [^1] == '\0')
|
||||
{
|
||||
offset++;
|
||||
return encodedBytes.Length - 1;
|
||||
}
|
||||
|
||||
return bytes.Length - offset;
|
||||
return encodedBytes.Length;
|
||||
}
|
||||
|
||||
/// <summary>Returns <see langword="true"/> if the rune is a combining character.</summary>
|
||||
@@ -127,7 +148,7 @@ public static class RuneExtensions
|
||||
{
|
||||
UnicodeCategory category = Rune.GetUnicodeCategory (rune);
|
||||
|
||||
return Rune.GetUnicodeCategory (rune) == UnicodeCategory.NonSpacingMark
|
||||
return category == UnicodeCategory.NonSpacingMark
|
||||
|| category == UnicodeCategory.SpacingCombiningMark
|
||||
|| category == UnicodeCategory.EnclosingMark;
|
||||
}
|
||||
@@ -136,7 +157,19 @@ public static class RuneExtensions
|
||||
/// <remarks>This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.</remarks>
|
||||
/// <param name="rune">The rune to probe.</param>
|
||||
/// <returns><see langword="true"/> if the rune is a surrogate code point; <see langword="false"/> otherwise.</returns>
|
||||
public static bool IsSurrogatePair (this Rune rune) { return char.IsSurrogatePair (rune.ToString (), 0); }
|
||||
public static bool IsSurrogatePair (this Rune rune)
|
||||
{
|
||||
bool isSingleUtf16CodeUnit = rune.IsBmp;
|
||||
if (isSingleUtf16CodeUnit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int maxCharsPerRune = 2;
|
||||
Span<char> charBuffer = stackalloc char[maxCharsPerRune];
|
||||
int charsWritten = rune.EncodeToUtf16 (charBuffer);
|
||||
return charsWritten >= 2 && char.IsSurrogatePair (charBuffer [0], charBuffer [1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the rune is not a control character and can be displayed by translating characters below 0x20 to
|
||||
|
||||
@@ -48,6 +48,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfContained", "SelfContai
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "NativeAot\NativeAot.csproj", "{E6D716C6-AC94-4150-B10A-44AE13F79344}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -86,6 +88,10 @@ Global
|
||||
{E6D716C6-AC94-4150-B10A-44AE13F79344}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -902,6 +902,23 @@ public class RuneTests
|
||||
Assert.Equal (3, splitOnComma.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData ("a", "utf-8", 1)]
|
||||
[InlineData ("a", "utf-16", 1)]
|
||||
[InlineData ("a", "utf-32", 3)]
|
||||
[InlineData ("𝔹", "utf-8", 4)]
|
||||
[InlineData ("𝔹", "utf-16", 4)]
|
||||
[InlineData ("𝔹", "utf-32", 3)]
|
||||
public void GetEncodingLength_ReturnsLengthBasedOnSelectedEncoding (string runeStr, string encodingName, int expectedLength)
|
||||
{
|
||||
Rune rune = runeStr.EnumerateRunes ().Single ();
|
||||
var encoding = Encoding.GetEncoding (encodingName);
|
||||
|
||||
int actualLength = rune.GetEncodingLength (encoding);
|
||||
|
||||
Assert.Equal (expectedLength, actualLength);
|
||||
}
|
||||
|
||||
private int CountLettersInString (string s)
|
||||
{
|
||||
var letterCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user