diff --git a/README.md b/README.md index 699ac122..198baaf4 100644 --- a/README.md +++ b/README.md @@ -2633,6 +2633,10 @@ namespace nietras.SeparatedValues where T : System.ISpanFormattable { } public void Format(T value, System.ReadOnlySpan format) where T : System.ISpanFormattable { } + public void FormatEnum(T value) + where T : struct, System.Enum { } + public void FormatEnum(T value, System.ReadOnlySpan format) + where T : struct, System.Enum { } public void Set(System.ReadOnlySpan utf8Span) { } public void Set(System.ReadOnlySpan span) { } public void Set([System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref nietras.SeparatedValues.SepWriter.Col.FormatInterpolatedStringHandler handler) { } diff --git a/src/Sep.Test/SepWriterColTest.cs b/src/Sep.Test/SepWriterColTest.cs index afd9dfc2..6892f87a 100644 --- a/src/Sep.Test/SepWriterColTest.cs +++ b/src/Sep.Test/SepWriterColTest.cs @@ -22,13 +22,15 @@ public class SepWriterColTest [TestMethod] public async ValueTask SepWriterColTest_ColIndex() { - await Run(col => Assert.AreEqual(0, col.ColIndex), null); + await Run(static col => Assert.AreEqual(0, col.ColIndex), + expectedColValue: null); } [TestMethod] public async ValueTask SepWriterColTest_ColName() { - await Run(col => Assert.AreEqual(ColName, col.ColName), null); + await Run(static col => Assert.AreEqual(ColName, col.ColName), + expectedColValue: null); } [TestMethod] @@ -46,13 +48,15 @@ public async ValueTask SepWriterColTest_Set_String_Long() [TestMethod] public async ValueTask SepWriterColTest_Set_Utf8Span() { - await Run(col => col.Set(Encoding.UTF8.GetBytes(ColText))); + var bytes = Encoding.UTF8.GetBytes(ColText); + await Run(col => col.Set(bytes)); } [TestMethod] public async ValueTask SepWriterColTest_Set_Utf8Span_Long() { - await Run(col => col.Set(Encoding.UTF8.GetBytes(ColTextLong)), ColTextLong); + var bytes = Encoding.UTF8.GetBytes(ColTextLong); + await Run(col => col.Set(bytes), ColTextLong); } [TestMethod] @@ -92,13 +96,15 @@ public async ValueTask SepWriterColTest_Set_InterpolatedString_F2() [TestMethod] public async ValueTask SepWriterColTest_Set_InterpolatedString_F2_CultureInfoAsConfig() { - await Run(col => col.Set($"{ColValue:F2}"), ColText + ",00", CultureInfo.GetCultureInfo("da-DK")); + var cultureInfo = CultureInfo.GetCultureInfo("da-DK"); + await Run(col => col.Set($"{ColValue:F2}"), ColText + ",00", cultureInfo); } [TestMethod] public async ValueTask SepWriterColTest_Set_InterpolatedString_F2_CultureInfoAsParam() { - await Run(col => col.Set(CultureInfo.GetCultureInfo("da-DK"), $"{ColValue:F2}"), ColText + ",00"); + var cultureInfo = CultureInfo.GetCultureInfo("da-DK"); + await Run(col => col.Set(cultureInfo, $"{ColValue:F2}"), ColText + ",00"); } [TestMethod] @@ -176,6 +182,52 @@ public async ValueTask SepWriterColTest_Format_Long() await Run(col => col.Format(f), f.Text); } + enum TestEnum { Aaaa = 42 } + [TestMethod] + public async ValueTask SepWriterColTest_Set_Enum() + { + await Run(col => col.Set($"{TestEnum.Aaaa}"), TestEnum.Aaaa.ToString(), expectedAllocatedBytes: 0); + } + [TestMethod] + public async ValueTask SepWriterColTest_Format_Enum() + { + // Unfortunately, formatting enum via T : ISpanFormatable allocates on + // each format, due to boxing enum. Prefer FormatEnum/Set(..) instead. + var expectedAllocatedBytes = System.Runtime.CompilerServices.Unsafe.SizeOf() * 3; + await Run(col => col.Format(TestEnum.Aaaa), TestEnum.Aaaa.ToString(), + expectedAllocatedBytes: expectedAllocatedBytes); + await Run(col => col.Format(TestEnum.Aaaa, "D"), ((int)TestEnum.Aaaa).ToString(), + expectedAllocatedBytes: expectedAllocatedBytes); + } + [TestMethod] + public async ValueTask SepWriterColTest_FormatEnum_Enum() + { + await Run(col => col.FormatEnum(TestEnum.Aaaa), TestEnum.Aaaa.ToString(), expectedAllocatedBytes: 0); + await Run(col => col.FormatEnum(TestEnum.Aaaa, "D"), ((int)TestEnum.Aaaa).ToString(), expectedAllocatedBytes: 0); + } + + [Flags] + enum LongFlags + { + Flag000 = 1 << 0, Flag001 = 1 << 1, Flag002 = 1 << 2, Flag003 = 1 << 3, + Flag004 = 1 << 4, Flag005 = 1 << 5, Flag006 = 1 << 6, Flag007 = 1 << 7, + Flag008 = 1 << 8, Flag009 = 1 << 9, Flag010 = 1 << 10, Flag011 = 1 << 11, + Flag012 = 1 << 12, Flag013 = 1 << 13, Flag014 = 1 << 14, Flag015 = 1 << 15, + Flag016 = 1 << 16, Flag017 = 1 << 17, Flag018 = 1 << 18, Flag019 = 1 << 19, + Flag020 = 1 << 20, Flag021 = 1 << 21, Flag022 = 1 << 22, Flag023 = 1 << 23, + Flag024 = 1 << 24, Flag025 = 1 << 25, Flag026 = 1 << 26, Flag027 = 1 << 27, + Flag028 = 1 << 28, Flag029 = 1 << 29, Flag030 = 1 << 30, + } + [TestMethod] + public async ValueTask SepWriterColTest_FormatEnum_Enum_Long() + { + var value = (LongFlags)0x7FFFFFFF; + var expected = value.ToString(); + Assert.IsGreaterThan(256, expected.Length); + + await Run(col => col.FormatEnum(value), expected); + } + public class LongSpanFormattable : ISpanFormattable { public string Text { get; } = ColTextLong; @@ -230,7 +282,8 @@ static void Assert(string expectedCol, SepWriter writer) } } - static async ValueTask Run(SepWriter.ColAction action, string? expectedColValue = ColText, CultureInfo? cultureInfo = null) + static async ValueTask Run(SepWriter.ColAction action, string? expectedColValue = ColText, + CultureInfo? cultureInfo = null, long? expectedAllocatedBytes = null) { Func[] createWriters = [ @@ -245,7 +298,20 @@ static async ValueTask Run(SepWriter.ColAction action, string? expectedColValue using var writer = createWriter(); { using var row = writer.NewRow(); + // First row, first access will allocate ColImpl and supports, so warming up action(row[ColName]); + + var a0 = GC.GetAllocatedBytesForCurrentThread(); + var col = row[ColName]; + var a1 = GC.GetAllocatedBytesForCurrentThread(); + action(col); + var a2 = GC.GetAllocatedBytesForCurrentThread(); + // Getting col should always allocate zero bytes + Assert.AreEqual(0, a1 - a0); + if (expectedAllocatedBytes.HasValue) + { + Assert.AreEqual(expectedAllocatedBytes.Value, a2 - a1); + } } AssertCol(expectedColValue, writer); } diff --git a/src/Sep/SepWriter.Col.cs b/src/Sep/SepWriter.Col.cs index d73f4f6b..5fb1f9e6 100644 --- a/src/Sep/SepWriter.Col.cs +++ b/src/Sep/SepWriter.Col.cs @@ -145,6 +145,27 @@ public void Format(T value, ReadOnlySpan format) where T : ISpanFormatt MarkSet(); } + public void FormatEnum(T value) where T : struct, Enum + { + FormatEnum(value, null); + } + public void FormatEnum(T value, ReadOnlySpan format) where T : struct, Enum + { + var impl = _impl; + impl.Clear(); + if (Enum.TryFormat(value, impl._buffer, out var charsWritten, format)) + { + impl._position = charsWritten; + } + else + { + var handler = new FormatInterpolatedStringHandler(0, 1, this); + handler.AppendFormatted(value); + handler.Finish(); + } + MarkSet(); + } + /// /// Provides a handler used by the language compiler to append /// interpolated strings into instances.