diff --git a/RecordParser.Benchmark/Common.cs b/RecordParser.Benchmark/Common.cs index 08c84cb..83ef94a 100644 --- a/RecordParser.Benchmark/Common.cs +++ b/RecordParser.Benchmark/Common.cs @@ -58,7 +58,7 @@ private static T ProcessSequence(ReadOnlySequence sequence, FuncSpanT chunk) if (_dataBufUsed != 0) { VisitPartialFieldContents(chunk); - chunk = _dataBuf.AsSpan(.._dataBufUsed); + chunk = _dataBuf.AsSpan(0, _dataBufUsed); _dataBufUsed = 0; } @@ -55,8 +55,8 @@ public override void VisitEndOfField(ReadOnlySpan chunk) case 3: // M/d/yyyy format is not supported by this for some reason. ////_ = Utf8Parser.TryParse(chunk, out _person.birthday, out _); - Span birthdayChars = _decodeBuf.AsSpan(..Encoding.UTF8.GetChars(chunk, _decodeBuf)); - _person.birthday = DateTime.Parse(birthdayChars, DateTimeFormatInfo.InvariantInfo); + Span birthdayChars = _decodeBuf.AsSpan(0, Encoding.UTF8.GetChars(chunk, _decodeBuf)); + _person.birthday = Parse.DateTime(birthdayChars, DateTimeFormatInfo.InvariantInfo); break; case 4: @@ -67,7 +67,7 @@ public override void VisitEndOfField(ReadOnlySpan chunk) // N.B.: there are ways to improve the efficiency of this for earlier // targets, but I think it's fine for performance-sensitive applications to // have to upgrade to .NET 6.0 or higher... - _person.gender = Enum.Parse(Encoding.UTF8.GetString(chunk)); + _person.gender = Parse.Enum(Encoding.UTF8.GetString(chunk).AsSpan()); #endif break; @@ -91,7 +91,7 @@ public override void VisitEndOfRecord() public override void VisitPartialFieldContents(ReadOnlySpan chunk) { EnsureCapacity(_dataBufUsed + chunk.Length); - chunk.CopyTo(_dataBuf.AsSpan(_dataBufUsed..)); + chunk.CopyTo(_dataBuf.AsSpan(_dataBufUsed)); _dataBufUsed += chunk.Length; } diff --git a/RecordParser.Benchmark/FixedLengthReaderBenchmark.cs b/RecordParser.Benchmark/FixedLengthReaderBenchmark.cs index 0e1d2fa..cbf0ff2 100644 --- a/RecordParser.Benchmark/FixedLengthReaderBenchmark.cs +++ b/RecordParser.Benchmark/FixedLengthReaderBenchmark.cs @@ -44,7 +44,7 @@ public async Task Read_FixedLength_ManualString() name = line.Substring(2, 30).Trim(), age = int.Parse(line.Substring(32, 2)), birthday = DateTime.Parse(line.Substring(39, 10), CultureInfo.InvariantCulture), - gender = Enum.Parse(line.Substring(85, 6)), + gender = Parse.Enum(line.Substring(85, 6).AsSpan()), email = line.Substring(92, 22).Trim(), children = bool.Parse(line.Substring(121, 5)) }; @@ -151,12 +151,12 @@ await ProcessFlatFile((ReadOnlySpan line) => return new Person { alfa = line[0], - name = new string(line.Slice(2, 30).Trim()), - age = int.Parse(line.Slice(32, 2)), - birthday = DateTime.Parse(line.Slice(39, 10), CultureInfo.InvariantCulture), - gender = Enum.Parse(line.Slice(85, 6)), - email = new string(line.Slice(92, 22).Trim()), - children = bool.Parse(line.Slice(121, 5)) + name = line.Slice(2, 30).Trim().ToString(), + age = Parse.Int32(line.Slice(32, 2)), + birthday = Parse.DateTime(line.Slice(39, 10), CultureInfo.InvariantCulture), + gender = Parse.Enum(line.Slice(85, 6)), + email = line.Slice(92, 22).Trim().ToString(), + children = Parse.Boolean(line.Slice(121, 5)) }; }); diff --git a/RecordParser.Benchmark/Parse.cs b/RecordParser.Benchmark/Parse.cs new file mode 100644 index 0000000..797874f --- /dev/null +++ b/RecordParser.Benchmark/Parse.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace RecordParser.Benchmark +{ + internal static class Parse + { +#if NETSTANDARD2_0 || NETFRAMEWORK + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ProcessSpan(ReadOnlySpan span) => span.ToString(); +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ReadOnlySpan ProcessSpan(ReadOnlySpan span) => span; +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Byte(ReadOnlySpan utf8Text, IFormatProvider provider = null) => byte.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte SByte(ReadOnlySpan utf8Text, IFormatProvider provider = null) => sbyte.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Double(ReadOnlySpan utf8Text, IFormatProvider provider = null) => double.Parse(ProcessSpan(utf8Text), NumberStyles.AllowThousands | NumberStyles.Float, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Single(ReadOnlySpan utf8Text, IFormatProvider provider = null) => float.Parse(ProcessSpan(utf8Text), NumberStyles.AllowThousands | NumberStyles.Float, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Int32(ReadOnlySpan utf8Text, IFormatProvider provider = null) => int.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Guid(ReadOnlySpan utf8Text) => System.Guid.Parse(ProcessSpan(utf8Text)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTime DateTime(ReadOnlySpan utf8Text, IFormatProvider provider = null) => System.DateTime.Parse(ProcessSpan(utf8Text), provider, DateTimeStyles.AllowWhiteSpaces); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Boolean(ReadOnlySpan utf8Text) => bool.Parse(ProcessSpan(utf8Text)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TEnum Enum(ReadOnlySpan utf8Text) where TEnum : struct, Enum + { +#if NETSTANDARD2_0 || NETFRAMEWORK + return (TEnum)System.Enum.Parse(typeof(TEnum), utf8Text.ToString()); +#elif NETSTANDARD2_1 + return System.Enum.Parse(utf8Text.ToString()); +#else + return System.Enum.Parse(utf8Text); +#endif + } + } +} diff --git a/RecordParser.Benchmark/RecordParser.Benchmark.csproj b/RecordParser.Benchmark/RecordParser.Benchmark.csproj index b85a32f..c0362c3 100644 --- a/RecordParser.Benchmark/RecordParser.Benchmark.csproj +++ b/RecordParser.Benchmark/RecordParser.Benchmark.csproj @@ -2,11 +2,15 @@ Exe - net8.0 + net472;net6.0;net7.0;net8.0 latest + + true + + @@ -19,6 +23,13 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/RecordParser.Benchmark/Shims.cs b/RecordParser.Benchmark/Shims.cs new file mode 100644 index 0000000..925905d --- /dev/null +++ b/RecordParser.Benchmark/Shims.cs @@ -0,0 +1,72 @@ +#if NETSTANDARD2_0 || NETFRAMEWORK + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace RecordParser.Benchmark +{ + internal static class Shims + { + public static int GetChars(this Encoding encoding, ReadOnlySpan bytes, Span chars) + { + unsafe + { + fixed (byte* b = &MemoryMarshal.GetReference(bytes)) + { + int charCount = encoding.GetCharCount(b, bytes.Length); + if (charCount > chars.Length) return 0; + + fixed (char* c = &MemoryMarshal.GetReference(chars)) + { + return encoding.GetChars(b, bytes.Length, c, chars.Length); + } + } + } + } + + public static string GetString(this Encoding encoding, scoped ReadOnlySpan bytes) + { + if (bytes.IsEmpty) return string.Empty; + + unsafe + { + fixed (byte* pB = &MemoryMarshal.GetReference(bytes)) + { + return encoding.GetString(pB, bytes.Length); + } + } + } + + public static Task WriteLineAsync(this TextWriter writer, ReadOnlyMemory value, CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(value, out var arraySegment)) + { + return arraySegment.Array is null ? Task.CompletedTask : writer.WriteLineAsync(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + } + + return Impl(writer, value); + + static async Task Impl(TextWriter writer, ReadOnlyMemory value) + { + var pool = ArrayPool.Shared; + var array = pool.Rent(value.Length); + try + { + value.CopyTo(array.AsMemory()); + await writer.WriteLineAsync(array, 0, value.Length); + } + finally + { + pool.Return(array); + } + } + } + } +} + +#endif diff --git a/RecordParser.Benchmark/VariableLengthReaderBenchmark.cs b/RecordParser.Benchmark/VariableLengthReaderBenchmark.cs index 77ec9f3..5fd837c 100644 --- a/RecordParser.Benchmark/VariableLengthReaderBenchmark.cs +++ b/RecordParser.Benchmark/VariableLengthReaderBenchmark.cs @@ -46,14 +46,14 @@ public async Task Read_VariableLength_ManualString() { if (i++ == LimitRecord) return; - var coluns = line.Split(","); + var coluns = line.Split(','); var person = new Person() { id = Guid.Parse(coluns[0]), name = coluns[1].Trim(), age = int.Parse(coluns[2]), birthday = DateTime.Parse(coluns[3], CultureInfo.InvariantCulture), - gender = Enum.Parse(coluns[4]), + gender = Parse.Enum(coluns[4].AsSpan()), email = coluns[5].Trim(), children = bool.Parse(coluns[7]) }; @@ -203,7 +203,7 @@ Person PersonFactory(Func getColumnValue) name = getColumnValue(1).Trim(), age = int.Parse(getColumnValue(2)), birthday = DateTime.Parse(getColumnValue(3), CultureInfo.InvariantCulture), - gender = Enum.Parse(getColumnValue(4)), + gender = Parse.Enum(getColumnValue(4).AsSpan()), email = getColumnValue(5).Trim(), children = bool.Parse(getColumnValue(7)) }; @@ -262,13 +262,13 @@ await ProcessCSVFile((ReadOnlySpan line) => return new Person { - id = Guid.Parse(id), + id = Parse.Guid(id), name = name.ToString(), - age = int.Parse(age), - birthday = DateTime.Parse(birthday, DateTimeFormatInfo.InvariantInfo), - gender = Enum.Parse(gender), + age = Parse.Int32(age), + birthday = Parse.DateTime(birthday, DateTimeFormatInfo.InvariantInfo), + gender = Parse.Enum(gender), email = email.ToString(), - children = bool.Parse(children) + children = Parse.Boolean(children) }; }); } diff --git a/RecordParser.Benchmark/VariableLengthWriterBenchmark.cs b/RecordParser.Benchmark/VariableLengthWriterBenchmark.cs index c4a2a2e..6449301 100644 --- a/RecordParser.Benchmark/VariableLengthWriterBenchmark.cs +++ b/RecordParser.Benchmark/VariableLengthWriterBenchmark.cs @@ -79,7 +79,7 @@ public async Task Write_VariableLength_ManualString() sb.Append(";"); sb.Append(person.children); - await streamWriter.WriteLineAsync(sb); + await streamWriter.WriteLineAsync(sb.ToString()); sb.Clear(); } } diff --git a/RecordParser.Test/FileWriterTest.cs b/RecordParser.Test/FileWriterTest.cs index 408176c..5e6c16a 100644 --- a/RecordParser.Test/FileWriterTest.cs +++ b/RecordParser.Test/FileWriterTest.cs @@ -59,7 +59,7 @@ public void Write_csv_file(int repeat, bool parallel, bool ordered) var reader = new VariableLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color, int Index)>() .Map(x => x.Name, 0) - .Map(x => x.Birthday, 1, value => new DateTime(long.Parse(value))) + .Map(x => x.Birthday, 1, value => new DateTime(Parse.Int64(value))) .Map(x => x.Money, 2) .Map(x => x.Color, 3) .Map(x => x.Index, 4) @@ -97,4 +97,4 @@ public void Write_csv_file(int repeat, bool parallel, bool ordered) items.Should().BeEquivalentTo(expectedItems); } } -} \ No newline at end of file +} diff --git a/RecordParser.Test/FixedLengthReaderBuilderTest.cs b/RecordParser.Test/FixedLengthReaderBuilderTest.cs index 3354192..8787a57 100644 --- a/RecordParser.Test/FixedLengthReaderBuilderTest.cs +++ b/RecordParser.Test/FixedLengthReaderBuilderTest.cs @@ -24,7 +24,7 @@ public void Given_factory_method_should_invoke_it_on_parse() .Map(x => x.Money, 23, 7) .Build(factory: () => { called++; return (default, date, default); }); - var result = reader.Parse("foo bar baz yyyy.MM.dd 0123.45"); + var result = reader.Parse("foo bar baz yyyy.MM.dd 0123.45".AsSpan()); called.Should().Be(1); @@ -42,7 +42,7 @@ public void Given_value_using_standard_format_should_parse_without_extra_configu .Map(x => x.Money, 23, 7) .Build(); - var result = reader.Parse("foo bar baz 2020.05.23 0123.45"); + var result = reader.Parse("foo bar baz 2020.05.23 0123.45".AsSpan()); result.Should().BeEquivalentTo((Name: "foo bar baz", Birthday: new DateTime(2020, 05, 23), @@ -56,11 +56,11 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Balance, 0, 12) .Map(x => x.Date, 13, 8) .Map(x => x.Debit, 22, 6) - .DefaultTypeConvert(value => decimal.Parse(value) / 100) - .DefaultTypeConvert(value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .DefaultTypeConvert(value => Parse.Decimal(value) / 100) + .DefaultTypeConvert(value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Build(); - var result = reader.Parse("012345678901 23052020 012345"); + var result = reader.Parse("012345678901 23052020 012345".AsSpan()); result.Should().BeEquivalentTo((Balance: 0123456789.01M, Date: new DateTime(2020, 05, 23), @@ -72,12 +72,12 @@ public void Given_members_with_custom_format_should_use_custom_parser() { var reader = new FixedLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, 0, 12, value => value.ToUpper()) - .Map(x => x.Birthday, 12, 8, value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .Map(x => x.Birthday, 12, 8, value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Map(x => x.Money, 21, 7) .Map(x => x.Nickname, 28, 8, value => value.Slice(0, 4).ToString()) .Build(); - var result = reader.Parse("foo bar baz 23052020 012345 nickname"); + var result = reader.Parse("foo bar baz 23052020 012345 nickname".AsSpan()); result.Should().BeEquivalentTo((Name: "FOO BAR BAZ", Birthday: new DateTime(2020, 05, 23), @@ -89,13 +89,13 @@ public void Given_members_with_custom_format_should_use_custom_parser() public void Given_specified_custom_parser_for_member_should_have_priority_over_custom_parser_for_type() { var reader = new FixedLengthReaderBuilder<(int Age, int MotherAge, int FatherAge)>() - .Map(x => x.Age, 0, 4, value => int.Parse(value) * 2) + .Map(x => x.Age, 0, 4, value => Parse.Int32(value) * 2) .Map(x => x.MotherAge, 4, 4) .Map(x => x.FatherAge, 8, 4) - .DefaultTypeConvert(value => int.Parse(value) + 2) + .DefaultTypeConvert(value => Parse.Int32(value) + 2) .Build(); - var result = reader.Parse(" 15 40 50 "); + var result = reader.Parse(" 15 40 50 ".AsSpan()); result.Should().BeEquivalentTo((Age: 30, MotherAge: 42, @@ -111,7 +111,7 @@ public void Custom_format_configurations_can_be_simplified_with_user_defined_ext .Map(x => x.Name, 22, 7) .MyBuild(); - var result = reader.Parse("012345678901 23052020 FOOBAR "); + var result = reader.Parse("012345678901 23052020 FOOBAR ".AsSpan()); result.Should().BeEquivalentTo((Name: "foobar", Balance: 012345678.901M, @@ -127,7 +127,7 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st .Map(x => x.Baz, 8, 5) .Build(); - var result = reader.Parse(" foo bar baz "); + var result = reader.Parse(" foo bar baz ".AsSpan()); result.Should().BeEquivalentTo((Foo: "foo", Bar: "bar", @@ -142,7 +142,7 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw() .Map(x => x.Birthday, 5, 10) .Build(); - var parsed = reader.TryParse(" foo datehere", out var result); + var parsed = reader.TryParse(" foo datehere".AsSpan(), out var result); parsed.Should().BeFalse(); result.Should().Be(default); @@ -157,7 +157,7 @@ public void Given_valid_record_called_with_try_parse_should_set_out_parameter_wi .Map(x => x.Money, 23, 7) .Build(); - var parsed = reader.TryParse("foo bar baz 2020.05.23 0123.45", out var result); + var parsed = reader.TryParse("foo bar baz 2020.05.23 0123.45".AsSpan(), out var result); parsed.Should().BeTrue(); result.Should().BeEquivalentTo((Name: "foo bar baz", @@ -176,7 +176,7 @@ public void Given_nested_mapped_property_should_create_nested_instance_to_parse( .Map(x => x.Mother.Name, 30, 12) .Build(); - var result = reader.Parse("2020.05.23 son name 1980.01.15 mother name"); + var result = reader.Parse("2020.05.23 son name 1980.01.15 mother name".AsSpan()); result.Should().BeEquivalentTo(new Person { @@ -201,7 +201,7 @@ public void Given_non_member_expression_on_mapping_should_parse() .Map(_ => money, 23, 7) .Build(); - _ = reader.Parse("foo bar baz 2020.05.23 0123.45"); + _ = reader.Parse("foo bar baz 2020.05.23 0123.45".AsSpan()); var result = (name, birthday, money); @@ -244,7 +244,7 @@ public void Builder_should_use_passed_cultureinfo_to_parse_record(string culture var line = string.Join(string.Empty, values); - var result = reader.Parse(line); + var result = reader.Parse(line.AsSpan()); result.Should().BeEquivalentTo(expected); } @@ -286,11 +286,11 @@ public void Given_fixed_length_reader_used_in_multi_thread_context_parse_method_ var resultParallel = lines .AsParallel() - .Select(line => reader.Parse(line)) + .Select(line => reader.Parse(line.AsSpan())) .ToList(); var resultSequential = lines - .Select(line => reader.Parse(line)) + .Select(line => reader.Parse(line.AsSpan())) .ToList(); // Assert @@ -306,7 +306,7 @@ public static IFixedLengthReaderBuilder MyMap( Expression> ex, int startIndex, int length, string format) { - return source.Map(ex, startIndex, length, value => DateTime.ParseExact(value, format, null)); + return source.Map(ex, startIndex, length, value => Parse.DateTimeExact(value, format, null)); } public static IFixedLengthReaderBuilder MyMap( @@ -314,7 +314,7 @@ public static IFixedLengthReaderBuilder MyMap( Expression> ex, int startIndex, int length, int decimalPlaces) { - return source.Map(ex, startIndex, length, value => decimal.Parse(value) / (decimal)Math.Pow(10, decimalPlaces)); + return source.Map(ex, startIndex, length, value => Parse.Decimal(value) / (decimal)Math.Pow(10, decimalPlaces)); } public static IFixedLengthReader MyBuild(this IFixedLengthReaderBuilder source) diff --git a/RecordParser.Test/FixedLengthReaderSequentialBuilderTest.cs b/RecordParser.Test/FixedLengthReaderSequentialBuilderTest.cs index abfb402..9158bc1 100644 --- a/RecordParser.Test/FixedLengthReaderSequentialBuilderTest.cs +++ b/RecordParser.Test/FixedLengthReaderSequentialBuilderTest.cs @@ -21,7 +21,7 @@ public void Given_factory_method_should_invoke_it_on_parse() .Map(x => x.Money, 7) .Build(factory: () => { called++; return (default, date, default); }); - var result = reader.Parse("foo bar baz yyyy.MM.dd 0123.45"); + var result = reader.Parse("foo bar baz yyyy.MM.dd 0123.45".AsSpan()); called.Should().Be(1); @@ -41,7 +41,7 @@ public void Given_value_using_standard_format_should_parse_without_extra_configu .Map(x => x.Money, 7) .Build(); - var result = reader.Parse("foo bar baz 2020.05.23 0123.45"); + var result = reader.Parse("foo bar baz 2020.05.23 0123.45".AsSpan()); result.Should().BeEquivalentTo((Name: "foo bar baz", Birthday: new DateTime(2020, 05, 23), @@ -57,11 +57,11 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Date, 8) .Skip(1) .Map(x => x.Debit, 6) - .DefaultTypeConvert(value => decimal.Parse(value) / 100) - .DefaultTypeConvert(value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .DefaultTypeConvert(value => Parse.Decimal(value) / 100) + .DefaultTypeConvert(value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Build(); - var result = reader.Parse("012345678901 23052020 012345"); + var result = reader.Parse("012345678901 23052020 012345".AsSpan()); result.Should().BeEquivalentTo((Balance: 0123456789.01M, Date: new DateTime(2020, 05, 23), @@ -74,13 +74,13 @@ public void Given_members_with_custom_format_should_use_custom_parser() var reader = new FixedLengthReaderSequentialBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, 11, value => value.ToUpper()) .Skip(1) - .Map(x => x.Birthday, 8, value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .Map(x => x.Birthday, 8, value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Skip(1) .Map(x => x.Money, 7) .Map(x => x.Nickname, 8, value => value.Slice(0, 4).ToString()) .Build(); - var result = reader.Parse("foo bar baz 23052020 012345 nickname"); + var result = reader.Parse("foo bar baz 23052020 012345 nickname".AsSpan()); result.Should().BeEquivalentTo((Name: "FOO BAR BAZ", Birthday: new DateTime(2020, 05, 23), @@ -92,13 +92,13 @@ public void Given_members_with_custom_format_should_use_custom_parser() public void Given_specified_custom_parser_for_member_should_have_priority_over_custom_parser_for_type() { var reader = new FixedLengthReaderSequentialBuilder<(int Age, int MotherAge, int FatherAge)>() - .Map(x => x.Age, 4, value => int.Parse(value) * 2) + .Map(x => x.Age, 4, value => Parse.Int32(value) * 2) .Map(x => x.MotherAge, 4) .Map(x => x.FatherAge, 4) - .DefaultTypeConvert(value => int.Parse(value) + 2) + .DefaultTypeConvert(value => Parse.Int32(value) + 2) .Build(); - var result = reader.Parse(" 15 40 50 "); + var result = reader.Parse(" 15 40 50 ".AsSpan()); result.Should().BeEquivalentTo((Age: 30, MotherAge: 42, @@ -116,7 +116,7 @@ public void Custom_format_configurations_can_be_simplified_with_user_defined_ext .Map(x => x.Name, 7) .MyBuild(); - var result = reader.Parse("012345678901 23052020 FOOBAR "); + var result = reader.Parse("012345678901 23052020 FOOBAR ".AsSpan()); result.Should().BeEquivalentTo((Name: "foobar", Balance: 012345678.901M, @@ -132,7 +132,7 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st .Map(x => x.Baz, 5) .Build(); - var result = reader.Parse(" foo bar baz "); + var result = reader.Parse(" foo bar baz ".AsSpan()); result.Should().BeEquivalentTo((Foo: "foo", Bar: "bar", @@ -147,7 +147,7 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw() .Map(x => x.Birthday, 10) .Build(); - var parsed = reader.TryParse(" foo datehere", out var result); + var parsed = reader.TryParse(" foo datehere".AsSpan(), out var result); parsed.Should().BeFalse(); result.Should().Be(default); @@ -164,7 +164,7 @@ public void Given_valid_record_called_with_try_parse_should_set_out_parameter_wi .Map(x => x.Money, 7) .Build(); - var parsed = reader.TryParse("foo bar baz 2020.05.23 0123.45", out var result); + var parsed = reader.TryParse("foo bar baz 2020.05.23 0123.45".AsSpan(), out var result); parsed.Should().BeTrue(); result.Should().BeEquivalentTo((Name: "foo bar baz", @@ -183,7 +183,7 @@ public void Given_nested_mapped_property_should_create_nested_instance_to_parse( .Map(x => x.Mother.Name, 12) .Build(); - var result = reader.Parse("2020.05.23 son name 1980.01.15 mother name"); + var result = reader.Parse("2020.05.23 son name 1980.01.15 mother name".AsSpan()); result.Should().BeEquivalentTo(new Person { @@ -210,7 +210,7 @@ public void Given_non_member_expression_on_mapping_should_parse() .Map(_ => money, 7) .Build(); - _ = reader.Parse("foo bar baz 2020.05.23 0123.45"); + _ = reader.Parse("foo bar baz 2020.05.23 0123.45".AsSpan()); var result = (name, birthday, money); @@ -253,7 +253,7 @@ public void Builder_should_use_passed_cultureinfo_to_parse_record(string culture var line = string.Join(string.Empty, values); - var result = reader.Parse(line); + var result = reader.Parse(line.AsSpan()); result.Should().BeEquivalentTo(expected); } @@ -266,7 +266,7 @@ public static IFixedLengthReaderSequentialBuilder MyMap( Expression> ex, int length, string format) { - return source.Map(ex, length, value => DateTime.ParseExact(value, format, null)); + return source.Map(ex, length, value => Parse.DateTimeExact(value, format, null)); } public static IFixedLengthReaderSequentialBuilder MyMap( @@ -274,7 +274,7 @@ public static IFixedLengthReaderSequentialBuilder MyMap( Expression> ex, int length, int decimalPlaces) { - return source.Map(ex, length, value => decimal.Parse(value) / (decimal)Math.Pow(10, decimalPlaces)); + return source.Map(ex, length, value => Parse.Decimal(value) / (decimal)Math.Pow(10, decimalPlaces)); } public static IFixedLengthReader MyBuild(this IFixedLengthReaderSequentialBuilder source) diff --git a/RecordParser.Test/FixedLengthWriterBuilderTest.cs b/RecordParser.Test/FixedLengthWriterBuilderTest.cs index 8d0b9c2..0f51716 100644 --- a/RecordParser.Test/FixedLengthWriterBuilderTest.cs +++ b/RecordParser.Test/FixedLengthWriterBuilderTest.cs @@ -1,7 +1,6 @@ using AutoFixture; using FluentAssertions; using RecordParser.Builders.Writer; -using RecordParser.Test; using System; using System.Linq; using System.Linq.Expressions; @@ -41,7 +40,7 @@ public void Given_value_using_standard_format_should_parse_without_extra_configu success.Should().BeTrue(); charsWritten.Should().Be(50); - var expected = string.Join('\0', new[] + var expected = string.Join("\0", new[] { instance.Name.PadRight(15, ' '), instance.Birthday.ToString("yyyy.MM.dd"), @@ -156,7 +155,7 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Date, 13, 8) .Map(x => x.Debit, 22, 6, padding: Padding.Left, paddingChar: '0') .DefaultTypeConvert((span, value) => (((long)(value * 100)).TryFormat(span, out var written), written)) - .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy"), written)) + .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Build(); var instance = (Balance: 0123456789.01M, @@ -188,7 +187,7 @@ public void Given_members_with_custom_format_should_use_custom_parser() var writer = new FixedLengthWriterBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, 0, 12, (span, text) => (true, text.AsSpan().ToUpperInvariant(span))) - .Map(x => x.Birthday, 12, 8, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy"), written)) + .Map(x => x.Birthday, 12, 8, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Map(x => x.Money, 21, 7, padding: Padding.Left, paddingChar: '0') .Map(x => x.Nickname, 29, 8, (span, text) => (true, text.AsSpan().Slice(0, 4).ToUpperInvariant(span)), paddingChar: '-') .Build(); @@ -378,7 +377,7 @@ public static IFixedLengthWriterBuilder Map(this IFixedLengthWriterBuilder var multiply = (int)Math.Pow(10, precision); return builder.Map(ex, startIndex, length, - (span, value) => (((int)(value * multiply)).TryFormat(span, out var written, format), written), + (span, value) => (((int)(value * multiply)).TryFormat(span, out var written, format.AsSpan()), written), padding, paddingChar); } } diff --git a/RecordParser.Test/FixedLengthWriterSequentialBuilderTest.cs b/RecordParser.Test/FixedLengthWriterSequentialBuilderTest.cs index 2927cf0..d130c9d 100644 --- a/RecordParser.Test/FixedLengthWriterSequentialBuilderTest.cs +++ b/RecordParser.Test/FixedLengthWriterSequentialBuilderTest.cs @@ -1,6 +1,5 @@ using FluentAssertions; using RecordParser.Builders.Writer; -using RecordParser.Test; using System; using System.Linq.Expressions; using Xunit; @@ -42,7 +41,7 @@ public void Given_value_using_standard_format_should_parse_without_extra_configu success.Should().BeTrue(); charsWritten.Should().Be(50); - var expected = string.Join('\0', new[] + var expected = string.Join("\0", new[] { instance.Name.PadRight(15, ' '), instance.Birthday.ToString("yyyy.MM.dd"), @@ -164,7 +163,7 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Skip(1) .Map(x => x.Debit, 6, padding: Padding.Left, paddingChar: '0') .DefaultTypeConvert((span, value) => (((long)(value * 100)).TryFormat(span, out var written), written)) - .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy"), written)) + .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Build(); var instance = (Balance: 0123456789.01M, @@ -196,7 +195,7 @@ public void Given_members_with_custom_format_should_use_custom_parser() var writer = new FixedLengthWriterSequentialBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, 12, (span, text) => (true, text.AsSpan().ToUpperInvariant(span))) - .Map(x => x.Birthday, 8, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy"), written)) + .Map(x => x.Birthday, 8, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Skip(2) .Map(x => x.Money, 7, padding: Padding.Left, paddingChar: '0') .Skip(1) @@ -347,7 +346,7 @@ public static IFixedLengthWriterSequentialBuilder Map(this IFixedLengthWri var multiply = (int)Math.Pow(10, precision); return builder.Map(ex, length, - (span, value) => (((int)(value * multiply)).TryFormat(span, out var written, format), written), + (span, value) => (((int)(value * multiply)).TryFormat(span, out var written, format.AsSpan()), written), padding, paddingChar); } } diff --git a/RecordParser.Test/Format.cs b/RecordParser.Test/Format.cs new file mode 100644 index 0000000..d1e479a --- /dev/null +++ b/RecordParser.Test/Format.cs @@ -0,0 +1,53 @@ +#if NETSTANDARD2_0 || NETFRAMEWORK + +using System; + +namespace RecordParser.Test +{ + internal static class Format + { + public static bool TryFormat(this int @this, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider provider = null) + { + string value = @this.ToString(format.ToString(), provider); + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = value.Length; + value.AsSpan().CopyTo(destination); + return true; + } + + public static bool TryFormat(this long @this, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider provider = null) + { + string value = @this.ToString(format.ToString(), provider); + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = value.Length; + value.AsSpan().CopyTo(destination); + return true; + } + + public static bool TryFormat(this DateTime @this, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider provider = null) + { + string value = @this.ToString(format.ToString(), provider); + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = value.Length; + value.AsSpan().CopyTo(destination); + return true; + } + } +} + +#endif diff --git a/RecordParser.Test/Parse.cs b/RecordParser.Test/Parse.cs new file mode 100644 index 0000000..34c95cf --- /dev/null +++ b/RecordParser.Test/Parse.cs @@ -0,0 +1,41 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace RecordParser.Test +{ + internal static class Parse + { +#if NETSTANDARD2_0 || NETFRAMEWORK + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ProcessSpan(ReadOnlySpan span) => span.ToString(); +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ReadOnlySpan ProcessSpan(ReadOnlySpan span) => span; +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Int32(ReadOnlySpan utf8Text, IFormatProvider provider = null) => int.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Int64(ReadOnlySpan utf8Text, IFormatProvider provider = null) => long.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTime DateTimeExact(ReadOnlySpan utf8Text, string format, IFormatProvider provider = null) => DateTime.ParseExact(ProcessSpan(utf8Text), format, provider, DateTimeStyles.AllowWhiteSpaces); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static decimal Decimal(ReadOnlySpan utf8Text, IFormatProvider provider = null) => decimal.Parse(ProcessSpan(utf8Text), NumberStyles.Number, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TEnum Enum(ReadOnlySpan utf8Text) where TEnum : struct, Enum + { +#if NETSTANDARD2_0 || NETFRAMEWORK + return (TEnum)System.Enum.Parse(typeof(TEnum), utf8Text.ToString()); +#elif NETSTANDARD2_1 + return System.Enum.Parse(utf8Text.ToString()); +#else + return System.Enum.Parse(utf8Text); +#endif + } + } +} diff --git a/RecordParser.Test/RecordParser.Test.csproj b/RecordParser.Test/RecordParser.Test.csproj index 4cd1565..86c346f 100644 --- a/RecordParser.Test/RecordParser.Test.csproj +++ b/RecordParser.Test/RecordParser.Test.csproj @@ -1,7 +1,7 @@  - net8.0 + net472;net6.0;net7.0;net8.0 latest false @@ -34,6 +34,13 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/RecordParser.Test/TextFindHelperTest.cs b/RecordParser.Test/TextFindHelperTest.cs index 4900c22..c2dc59d 100644 --- a/RecordParser.Test/TextFindHelperTest.cs +++ b/RecordParser.Test/TextFindHelperTest.cs @@ -15,7 +15,7 @@ public void Given_column_mapped_more_than_once_should_works() var color = "LightBlue"; var record = $"{id};{date};{color}"; - var finder = new TextFindHelper(record, ";", ('"', "\"")); + var finder = new TextFindHelper(record.AsSpan(), ";", ('"', "\"")); // Act @@ -50,7 +50,7 @@ public void Given_access_to_past_column_should_throw() var action = () => { - var finder = new TextFindHelper(record, ";", ('"', "\"")); + var finder = new TextFindHelper(record.AsSpan(), ";", ('"', "\"")); a = finder.GetValue(0).ToString(); b = finder.GetValue(1).ToString(); diff --git a/RecordParser.Test/VariableLengthReaderBuilderQuotedFieldTest.cs b/RecordParser.Test/VariableLengthReaderBuilderQuotedFieldTest.cs index 8580aba..30022ca 100644 --- a/RecordParser.Test/VariableLengthReaderBuilderQuotedFieldTest.cs +++ b/RecordParser.Test/VariableLengthReaderBuilderQuotedFieldTest.cs @@ -18,7 +18,7 @@ public void Given_all_fields_with_quotes() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("\"1997\",\"Ford\",\"Super, luxurious truck\",\"30100.99\""); + var result = reader.Parse("\"1997\",\"Ford\",\"Super, luxurious truck\",\"30100.99\"".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford", @@ -38,7 +38,7 @@ public void Given_all_fields_with_quotes_and_spaces_between_field_and_quote(stri .Map(x => x.Price, 3) .Build(separator); - var result = reader.Parse(" \"1997\" , \"Ford\" , \"Super, \"\"luxurious\"\" truck\" , \"30100.99\" "); + var result = reader.Parse(" \"1997\" , \"Ford\" , \"Super, \"\"luxurious\"\" truck\" , \"30100.99\" ".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford", @@ -56,7 +56,7 @@ public void Given_all_fields_with_quotes_and_spaces_between_field_and_quote_igno .Map(x => x.Comment, 2) .Build(separator); - var result = reader.Parse(" \"1997\" , \"Ford\" , \"Super, \"\"luxurious\"\" truck\" , \"30100.99\" "); + var result = reader.Parse(" \"1997\" , \"Ford\" , \"Super, \"\"luxurious\"\" truck\" , \"30100.99\" ".AsSpan()); result.Should().BeEquivalentTo((Year: default(int), Model: default(string), @@ -73,7 +73,7 @@ public void Given_all_fields_with_quotes_and_spaces_between_field_and_quote_igno .Map(x => x.Price, 3) .Build(separator); - var result = reader.Parse(" \"1997\" , \"Ford\" , \"Super, \"\"luxurious\"\" truck\" , \"30100.99\" "); + var result = reader.Parse(" \"1997\" , \"Ford\" , \"Super, \"\"luxurious\"\" truck\" , \"30100.99\" ".AsSpan()); result.Should().BeEquivalentTo((Year: default(int), Model: default(string), @@ -91,7 +91,7 @@ public void Given_some_fields_with_quotes() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("1997,Ford,\"Super, luxurious truck\",30100.99"); + var result = reader.Parse("1997,Ford,\"Super, luxurious truck\",30100.99".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford", @@ -109,7 +109,7 @@ public void Given_quoted_field_with_trailing_quotes() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("1997,Ford,\"\"\"It is fast\"\"\",30100.99"); + var result = reader.Parse("1997,Ford,\"\"\"It is fast\"\"\",30100.99".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford", @@ -127,7 +127,7 @@ public void Given_quoted_field_with_property_convert() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("1997,Ford,\"\"\"It is fast\"\"\",30100.99"); + var result = reader.Parse("1997,Ford,\"\"\"It is fast\"\"\",30100.99".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "FORD", @@ -144,7 +144,7 @@ public void Given_skip_quoted_field() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("1997,Ford,\"Super, luxurious truck\",30100.99"); + var result = reader.Parse("1997,Ford,\"Super, luxurious truck\",30100.99".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford", @@ -164,7 +164,7 @@ public void Given_fields_missing_end_quote(string line) .Map(x => x.Price, 3) .Build(","); - Action result = () => reader.Parse(line); + Action result = () => reader.Parse(line.AsSpan()); result.Should().Throw().WithMessage("Quoted field is missing closing quote."); } @@ -180,7 +180,7 @@ public void Given_extra_data_after_a_quoted_field(string line) .Map(x => x.Price, 3) .Build(","); - Action result = () => reader.Parse(line); + Action result = () => reader.Parse(line.AsSpan()); result.Should().Throw().WithMessage("Double quote is not escaped or there is extra data after a quoted field."); } @@ -195,7 +195,7 @@ public void Given_unquoted_fields_which_contains_quotes_should_interpret_as_is() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("1997,TV 47\", Super \"luxurious\" truck,30100.99"); + var result = reader.Parse("1997,TV 47\", Super \"luxurious\" truck,30100.99".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "TV 47\"", @@ -213,7 +213,7 @@ public void Given_embedded_quotes_escaped_with_two_double_quotes() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("1997,Ford,\"Super, \"\"luxurious\"\" truck\",30100.99"); + var result = reader.Parse("1997,Ford,\"Super, \"\"luxurious\"\" truck\",30100.99".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford", @@ -241,7 +241,7 @@ public void Given_empty_fields_should_parse(string model, string comment, string .Map(x => x.Owner, 4) .Build(","); - var result = reader.Parse($"{model},1997,{comment},30100.99,{owner}"); + var result = reader.Parse($"{model},1997,{comment},30100.99,{owner}".AsSpan()); result.Should().BeEquivalentTo((Model: model, Year: 1997, @@ -260,7 +260,7 @@ public void Given_fields_with_new_line_character_interpret_as_is() .Map(x => x.Price, 3) .Build(","); - var result = reader.Parse("\"\n1997\",Ford \n Model, Super \"luxu\nrious\" truck,30100.99\n"); + var result = reader.Parse("\"\n1997\",Ford \n Model, Super \"luxu\nrious\" truck,30100.99\n".AsSpan()); result.Should().BeEquivalentTo((Year: 1997, Model: "Ford \n Model", diff --git a/RecordParser.Test/VariableLengthReaderBuilderTest.cs b/RecordParser.Test/VariableLengthReaderBuilderTest.cs index 91f2256..c5de388 100644 --- a/RecordParser.Test/VariableLengthReaderBuilderTest.cs +++ b/RecordParser.Test/VariableLengthReaderBuilderTest.cs @@ -26,7 +26,7 @@ public void Given_factory_method_should_invoke_it_on_parse() .Map(x => x.Money, 2) .Build(";", factory: () => { called++; return (default, date, default, color); }); - var result = reader.Parse("foo bar baz ; yyyy.MM.dd ; 0123.45; IGNORE "); + var result = reader.Parse("foo bar baz ; yyyy.MM.dd ; 0123.45; IGNORE ".AsSpan()); called.Should().Be(1); @@ -46,7 +46,7 @@ public void Given_value_using_standard_format_should_parse_without_extra_configu .Map(x => x.Color, 3) .Build(";"); - var result = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue"); + var result = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue".AsSpan()); result.Should().BeEquivalentTo((Name: "foo bar baz", Birthday: new DateTime(2020, 05, 23), @@ -66,7 +66,7 @@ public void Given_non_member_expression_on_mapping_should_parse() .Map(_ => color, 3) .Build(";"); - _ = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue"); + _ = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue".AsSpan()); var result = (name, birthday, money, color); @@ -83,11 +83,11 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Balance, 0) .Map(x => x.Date, 1) .Map(x => x.Debit, 2) - .DefaultTypeConvert(value => decimal.Parse(value) / 100) - .DefaultTypeConvert(value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .DefaultTypeConvert(value => Parse.Decimal(value) / 100) + .DefaultTypeConvert(value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Build(";"); - var result = reader.Parse("012345678901 ; 23052020 ; 012345"); + var result = reader.Parse("012345678901 ; 23052020 ; 012345".AsSpan()); result.Should().BeEquivalentTo((Balance: 0123456789.01M, Date: new DateTime(2020, 05, 23), @@ -99,12 +99,12 @@ public void Given_members_with_custom_format_should_use_custom_parser() { var reader = new VariableLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, 0, value => value.ToUpper()) - .Map(x => x.Birthday, 1, value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .Map(x => x.Birthday, 1, value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Map(x => x.Money, 2) .Map(x => x.Nickname, 3, value => value.Slice(0, 4).ToString()) .Build(";"); - var result = reader.Parse("foo bar baz ; 23052020 ; 012345 ; nickname"); + var result = reader.Parse("foo bar baz ; 23052020 ; 012345 ; nickname".AsSpan()); result.Should().BeEquivalentTo((Name: "FOO BAR BAZ", Birthday: new DateTime(2020, 05, 23), @@ -116,13 +116,13 @@ public void Given_members_with_custom_format_should_use_custom_parser() public void Given_specified_custom_parser_for_member_should_have_priority_over_custom_parser_for_type() { var reader = new VariableLengthReaderBuilder<(int Age, int MotherAge, int FatherAge)>() - .Map(x => x.Age, 0, value => int.Parse(value) * 2) + .Map(x => x.Age, 0, value => Parse.Int32(value) * 2) .Map(x => x.MotherAge, 1) .Map(x => x.FatherAge, 2) - .DefaultTypeConvert(value => int.Parse(value) + 2) + .DefaultTypeConvert(value => Parse.Int32(value) + 2) .Build(";"); - var result = reader.Parse(" 15 ; 40 ; 50 "); + var result = reader.Parse(" 15 ; 40 ; 50 ".AsSpan()); result.Should().BeEquivalentTo((Age: 30, MotherAge: 42, @@ -138,7 +138,7 @@ public void Custom_format_configurations_can_be_simplified_with_user_defined_ext .MyMap(x => x.Date, 1, format: "ddMMyyyy") .MyBuild(); - var result = reader.Parse("012345678.901 ; 23052020 ; FOOBAR "); + var result = reader.Parse("012345678.901 ; 23052020 ; FOOBAR ".AsSpan()); result.Should().BeEquivalentTo((Name: "foobar", Balance: 012345678.901M, @@ -154,7 +154,7 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st .Map(x => x.Baz, 2) .Build(";"); - var result = reader.Parse(" foo ; bar ; baz "); + var result = reader.Parse(" foo ; bar ; baz ".AsSpan()); result.Should().BeEquivalentTo((Foo: "foo", Bar: "bar", @@ -169,7 +169,7 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw() .Map(x => x.Birthday, 1) .Build(";"); - var parsed = reader.TryParse(" foo ; datehere", out var result); + var parsed = reader.TryParse(" foo ; datehere".AsSpan(), out var result); parsed.Should().BeFalse(); result.Should().Be(default); @@ -185,7 +185,7 @@ public void Given_valid_record_called_with_try_parse_should_set_out_parameter_wi .Map(x => x.Color, 3) .Build(";"); - var parsed = reader.TryParse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue", out var result); + var parsed = reader.TryParse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue".AsSpan(), out var result); parsed.Should().BeTrue(); result.Should().BeEquivalentTo((Name: "foo bar baz", @@ -210,7 +210,7 @@ public void Given_record_with_nullable_struct_field_should_parse_properly(string ? date : (DateTime?) null; - var parsed = reader.TryParse($"foo bar baz ; {birthday} ; 0123.45; LightBlue", out var result); + var parsed = reader.TryParse($"foo bar baz ; {birthday} ; 0123.45; LightBlue".AsSpan(), out var result); parsed.Should().BeTrue(); result.Should().BeEquivalentTo((Name: "foo bar baz", @@ -229,7 +229,7 @@ public void Given_nested_mapped_property_should_create_nested_instance_to_parse( .Map(x => x.Mother.Name, 3) .Build(";"); - var result = reader.Parse("2020.05.23 ; son name ; 1980.01.15 ; mother name"); + var result = reader.Parse("2020.05.23 ; son name ; 1980.01.15 ; mother name".AsSpan()); result.Should().BeEquivalentTo(new Person { @@ -273,9 +273,9 @@ public void Builder_should_use_passed_cultureinfo_to_parse_record(string culture expected.Color.ToString(), }; - var line = string.Join(';', values.Select(x => $" {x} ")); + var line = string.Join(";", values.Select(x => $" {x} ")); - var result = reader.Parse(line); + var result = reader.Parse(line.AsSpan()); result.Should().BeEquivalentTo(expected); } @@ -297,7 +297,7 @@ public void Parse_enum_same_way_framework(string text, Color expected) .Map(x => x, 0) .Build(";"); - reader.Parse(text).Should().Be(expected); + reader.Parse(text.AsSpan()).Should().Be(expected); } [Theory] @@ -314,7 +314,7 @@ public void Parse_flag_enum_same_way_framework(FlaggedEnum expected) var text = expected.ToString(); - reader.Parse(text).Should().Be(expected); + reader.Parse(text.AsSpan()).Should().Be(expected); } [Fact] @@ -324,8 +324,8 @@ public void Parse_enum_with_text_not_present_in_enum_should_be_same_way_framewor .Map(x => x, 0) .Build(";"); - var actualEx = AssertionExtensions.Should(() => reader.Parse("foo")).Throw().Which; - var expectedEx = AssertionExtensions.Should(() => Enum.Parse("foo")).Throw().Which; + var actualEx = AssertionExtensions.Should(() => reader.Parse("foo".AsSpan())).Throw().Which; + var expectedEx = AssertionExtensions.Should(() => Parse.Enum("foo".AsSpan())).Throw().Which; actualEx.Should().BeEquivalentTo(expectedEx, cfg => cfg.Excluding(x => x.StackTrace)); } @@ -337,7 +337,7 @@ public void Given_empty_enum_should_parse_same_way_framework() .Map(x => x, 0) .Build(";"); - reader.Parse("777").Should().Be((EmptyEnum)777); + reader.Parse("777".AsSpan()).Should().Be((EmptyEnum)777); } [Fact] @@ -375,11 +375,11 @@ public void Given_variable_length_reader_used_in_multi_thread_context_parse_meth var resultParallel = lines .AsParallel() - .Select(line => reader.Parse(line)) + .Select(line => reader.Parse(line.AsSpan())) .ToList(); var resultSequential = lines - .Select(line => reader.Parse(line)) + .Select(line => reader.Parse(line.AsSpan())) .ToList(); // Assert @@ -395,7 +395,7 @@ public static IVariableLengthReaderBuilder MyMap( Expression> ex, int startIndex, string format) { - return source.Map(ex, startIndex, value => DateTime.ParseExact(value, format, null)); + return source.Map(ex, startIndex, value => Parse.DateTimeExact(value, format, null)); } public static IVariableLengthReader MyBuild(this IVariableLengthReaderBuilder source) diff --git a/RecordParser.Test/VariableLengthReaderSequentialBuilderTest.cs b/RecordParser.Test/VariableLengthReaderSequentialBuilderTest.cs index 41e1a0d..5fabf82 100644 --- a/RecordParser.Test/VariableLengthReaderSequentialBuilderTest.cs +++ b/RecordParser.Test/VariableLengthReaderSequentialBuilderTest.cs @@ -24,7 +24,7 @@ public void Given_factory_method_should_invoke_it_on_parse() .Map(x => x.Money) .Build(";", factory: () => { called++; return (default, date, default, color); }); - var result = reader.Parse("foo bar baz ; yyyy.MM.dd ; 0123.45; IGNORE "); + var result = reader.Parse("foo bar baz ; yyyy.MM.dd ; 0123.45; IGNORE ".AsSpan()); called.Should().Be(1); @@ -43,7 +43,7 @@ public void Given_value_using_standard_format_should_parse_without_extra_configu .Map(x => x.Money) .Build(";"); - var result = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45"); + var result = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45".AsSpan()); result.Should().BeEquivalentTo((Name: "foo bar baz", Birthday: new DateTime(2020, 05, 23), @@ -61,7 +61,7 @@ public void Given_columns_to_ignore_and_value_using_standard_format_should_parse .Map(x => x.Money) .Build(";"); - var result = reader.Parse("foo bar baz ; IGNORE; 2020.05.23 ; IGNORE ; IGNORE ; 0123.45"); + var result = reader.Parse("foo bar baz ; IGNORE; 2020.05.23 ; IGNORE ; IGNORE ; 0123.45".AsSpan()); result.Should().BeEquivalentTo((Name: "foo bar baz", Birthday: new DateTime(2020, 05, 23), @@ -75,11 +75,11 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Balance) .Map(x => x.Date) .Map(x => x.Debit) - .DefaultTypeConvert(value => decimal.Parse(value) / 100) - .DefaultTypeConvert(value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .DefaultTypeConvert(value => Parse.Decimal(value) / 100) + .DefaultTypeConvert(value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Build(";"); - var result = reader.Parse("012345678901 ; 23052020 ; 012345"); + var result = reader.Parse("012345678901 ; 23052020 ; 012345".AsSpan()); result.Should().BeEquivalentTo((Balance: 0123456789.01M, Date: new DateTime(2020, 05, 23), @@ -91,12 +91,12 @@ public void Given_members_with_custom_format_should_use_custom_parser() { var reader = new VariableLengthReaderSequentialBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, value => value.ToUpper()) - .Map(x => x.Birthday, value => DateTime.ParseExact(value, "ddMMyyyy", null)) + .Map(x => x.Birthday, value => Parse.DateTimeExact(value, "ddMMyyyy", null)) .Map(x => x.Money) .Map(x => x.Nickname, value => value.Slice(0, 4).ToString()) .Build(";"); - var result = reader.Parse("foo bar baz ; 23052020 ; 012345 ; nickname"); + var result = reader.Parse("foo bar baz ; 23052020 ; 012345 ; nickname".AsSpan()); result.Should().BeEquivalentTo((Name: "FOO BAR BAZ", Birthday: new DateTime(2020, 05, 23), @@ -108,13 +108,13 @@ public void Given_members_with_custom_format_should_use_custom_parser() public void Given_specified_custom_parser_for_member_should_have_priority_over_custom_parser_for_type() { var reader = new VariableLengthReaderSequentialBuilder<(int Age, int MotherAge, int FatherAge)>() - .Map(x => x.Age, value => int.Parse(value) * 2) + .Map(x => x.Age, value => Parse.Int32(value) * 2) .Map(x => x.MotherAge) .Map(x => x.FatherAge) - .DefaultTypeConvert(value => int.Parse(value) + 2) + .DefaultTypeConvert(value => Parse.Int32(value) + 2) .Build(";"); - var result = reader.Parse(" 15 ; 40 ; 50 "); + var result = reader.Parse(" 15 ; 40 ; 50 ".AsSpan()); result.Should().BeEquivalentTo((Age: 30, MotherAge: 42, @@ -128,12 +128,12 @@ public void Given_columns_to_ignore_and_specified_custom_parser_for_member_shoul .Skip(2) .Map(x => x.MotherAge) .Skip(1) - .Map(x => x.Age, value => int.Parse(value) * 2) + .Map(x => x.Age, value => Parse.Int32(value) * 2) .Map(x => x.FatherAge) - .DefaultTypeConvert(value => int.Parse(value) + 2) + .DefaultTypeConvert(value => Parse.Int32(value) + 2) .Build(";"); - var result = reader.Parse(" XX ; XX ; 40 ; XX ; 15 ; 50 ; XX"); + var result = reader.Parse(" XX ; XX ; 40 ; XX ; 15 ; 50 ; XX".AsSpan()); result.Should().BeEquivalentTo((Age: 30, MotherAge: 42, @@ -149,7 +149,7 @@ public void Custom_format_configurations_can_be_simplified_with_user_defined_ext .Map(x => x.Name) .MyBuild(); - var result = reader.Parse("012345678.901 ; 23052020 ; FOOBAR "); + var result = reader.Parse("012345678.901 ; 23052020 ; FOOBAR ".AsSpan()); result.Should().BeEquivalentTo((Name: "foobar", Balance: 012345678.901M, @@ -168,7 +168,7 @@ public void Given_non_member_expression_on_mapping_should_parse() .Map(_ => color) .Build(";"); - _ = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue"); + _ = reader.Parse("foo bar baz ; 2020.05.23 ; 0123.45; LightBlue".AsSpan()); var result = (name, birthday, money, color); @@ -208,9 +208,9 @@ public void Builder_should_use_passed_cultureinfo_to_parse_record(string culture expected.Color.ToString(), }; - var line = string.Join(';', values.Select(x => $" {x} ")); + var line = string.Join(";", values.Select(x => $" {x} ")); - var result = reader.Parse(line); + var result = reader.Parse(line.AsSpan()); result.Should().BeEquivalentTo(expected); } @@ -308,9 +308,9 @@ public void Registered_primitives_types_should_have_default_converters_which_use }; CultureInfo.CurrentCulture = new CultureInfo(cultureName); - var line = string.Join(';', values.Select(x => $" {x} ")); + var line = string.Join(";", values.Select(x => $" {x} ")); - var result = reader.Parse(line); + var result = reader.Parse(line.AsSpan()); result.Should().BeEquivalentTo(expected); } @@ -323,7 +323,7 @@ public static IVariableLengthReaderSequentialBuilder MyMap( Expression> ex, string format) { - return source.Map(ex, value => DateTime.ParseExact(value, format, null)); + return source.Map(ex, value => Parse.DateTimeExact(value, format, null)); } public static IVariableLengthReader MyBuild(this IVariableLengthReaderSequentialBuilder source) diff --git a/RecordParser.Test/VariableLengthWriterBuilderTest.cs b/RecordParser.Test/VariableLengthWriterBuilderTest.cs index fe82648..fa2be39 100644 --- a/RecordParser.Test/VariableLengthWriterBuilderTest.cs +++ b/RecordParser.Test/VariableLengthWriterBuilderTest.cs @@ -1,7 +1,6 @@ using AutoFixture; using FluentAssertions; using RecordParser.Builders.Writer; -using RecordParser.Test; using System; using System.Linq; using Xunit; @@ -220,7 +219,7 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Date, 1) .Map(x => x.Debit, 2) .DefaultTypeConvert((span, value) => (((long)(value * 100)).TryFormat(span, out var written), written)) - .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy"), written)) + .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Build(" ; "); var instance = (Balance: 0123456789.01M, @@ -252,7 +251,7 @@ public void Given_members_with_custom_format_should_use_custom_parser() var writer = new VariableLengthWriterBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Map(x => x.Name, 1, SpanExtensions.ToUpperInvariant) - .Map(x => x.Birthday, 2, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy"), written)) + .Map(x => x.Birthday, 2, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Map(x => x.Money, 3) .Map(x => x.Nickname, 4, (span, text) => SpanExtensions.ToUpperInvariant(span, text.Slice(0, 4))) .Build(" ; "); diff --git a/RecordParser.Test/VariableLengthWriterSequentialBuilderTest.cs b/RecordParser.Test/VariableLengthWriterSequentialBuilderTest.cs index 3b37805..1459e1f 100644 --- a/RecordParser.Test/VariableLengthWriterSequentialBuilderTest.cs +++ b/RecordParser.Test/VariableLengthWriterSequentialBuilderTest.cs @@ -226,7 +226,7 @@ public void Given_types_with_custom_format_should_allow_define_default_parser_fo .Map(x => x.Date) .Map(x => x.Debit) .DefaultTypeConvert((span, value) => (((long)(value * 100)).TryFormat(span, out var written), written)) - .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy"), written)) + .DefaultTypeConvert((span, value) => (value.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Build(" ; "); var instance = (Balance: 0123456789.01M, @@ -259,7 +259,7 @@ public void Given_members_with_custom_format_should_use_custom_parser() var writer = new VariableLengthWriterSequentialBuilder<(string Name, DateTime Birthday, decimal Money, string Nickname)>() .Skip(1) .Map(x => x.Name, SpanExtensions.ToUpperInvariant) - .Map(x => x.Birthday, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy"), written)) + .Map(x => x.Birthday, (span, date) => (date.TryFormat(span, out var written, "ddMMyyyy".AsSpan()), written)) .Map(x => x.Money) .Map(x => x.Nickname, (span, text) => SpanExtensions.ToUpperInvariant(span, text.Slice(0, 4))) .Build(" ; "); diff --git a/RecordParser.sln b/RecordParser.sln index 845ee3e..5499796 100644 --- a/RecordParser.sln +++ b/RecordParser.sln @@ -1,13 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.181 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecordParser", "RecordParser\RecordParser.csproj", "{BA47D31A-475B-452F-BDE1-604465A57811}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecordParser.Test", "RecordParser.Test\RecordParser.Test.csproj", "{DD1F677C-4BFE-4772-A2D7-C507F2B9C0A9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RecordParser.Benchmark", "RecordParser.Benchmark\RecordParser.Benchmark.csproj", "{D0F836BF-821C-49CE-994E-E4C866B5B7DC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecordParser.Benchmark", "RecordParser.Benchmark\RecordParser.Benchmark.csproj", "{D0F836BF-821C-49CE-994E-E4C866B5B7DC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C0189076-8DB4-4B93-91EE-BFF5DE424ACE}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs b/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs index 1f1c0b1..6290465 100644 --- a/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs +++ b/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs @@ -17,30 +17,30 @@ static PrimitiveTypeReaderEngine() { var mapping = new Dictionary<(Type from, Type to), Func>(); - mapping.AddMapForReadOnlySpan(span => new string(span)); + mapping.AddMapForReadOnlySpan(span => span.ToString()); mapping.AddMapForReadOnlySpan(span => ToChar(span)); - mapping.AddMapForReadOnlySpan(span => byte.Parse(span, NumberStyles.Integer, null)); - mapping.AddMapForReadOnlySpan(span => sbyte.Parse(span, NumberStyles.Integer, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Byte(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.SByte(span, null)); - mapping.AddMapForReadOnlySpan(span => double.Parse(span, NumberStyles.AllowThousands | NumberStyles.Float, null)); - mapping.AddMapForReadOnlySpan(span => float.Parse(span, NumberStyles.AllowThousands | NumberStyles.Float, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Double(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Single(span, null)); - mapping.AddMapForReadOnlySpan(span => int.Parse(span, NumberStyles.Integer, null)); - mapping.AddMapForReadOnlySpan(span => uint.Parse(span, NumberStyles.Integer, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Int32(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.UInt32(span, null)); - mapping.AddMapForReadOnlySpan(span => long.Parse(span, NumberStyles.Integer, null)); - mapping.AddMapForReadOnlySpan(span => ulong.Parse(span, NumberStyles.Integer, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Int64(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.UInt64(span, null)); - mapping.AddMapForReadOnlySpan(span => short.Parse(span, NumberStyles.Integer, null)); - mapping.AddMapForReadOnlySpan(span => ushort.Parse(span, NumberStyles.Integer, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Int16(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.UInt16(span, null)); - mapping.AddMapForReadOnlySpan(span => Guid.Parse(span)); - mapping.AddMapForReadOnlySpan(span => DateTime.Parse(span, null, DateTimeStyles.AllowWhiteSpaces)); - mapping.AddMapForReadOnlySpan(span => TimeSpan.Parse(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Guid(span)); + mapping.AddMapForReadOnlySpan(span => Parse.DateTime(span, null)); + mapping.AddMapForReadOnlySpan(span => Parse.TimeSpan(span, null)); - mapping.AddMapForReadOnlySpan(span => bool.Parse(span)); - mapping.AddMapForReadOnlySpan(span => decimal.Parse(span, NumberStyles.Number, null)); + mapping.AddMapForReadOnlySpan(span => Parse.Boolean(span)); + mapping.AddMapForReadOnlySpan(span => Parse.Decimal(span, null)); mapping[(typeof(ReadOnlySpan), typeof(Enum))] = GetEnumFromSpanParseExpression; @@ -93,15 +93,22 @@ private static Expression GetEnumFromSpanParseExpression(Type type, Expression s }) .Reverse() .Aggregate((Expression)Expression.Condition( +#if NETSTANDARD2_0 + test: Expression.Call(under, "TryParse", Type.EmptyTypes, SpanAsString(trim), Expression.Constant(NumberStyles.Number), Expression.Constant(null, typeof(IFormatProvider)), number), +#else test: Expression.Call(under, "TryParse", Type.EmptyTypes, trim, number), +#endif + ifTrue: Expression.Convert(number, type), - ifFalse: Expression.Call(typeof(Enum), "Parse", new[] { type }, + #if NET6_0_OR_GREATER - span, + ifFalse: Expression.Call(typeof(Enum), "Parse", [type], span, Expression.Constant(true))), +#elif NETSTANDARD2_1_OR_GREATER + ifFalse: Expression.Call(typeof(Enum), "Parse", [type], SpanAsString(trim), Expression.Constant(true))), #else - SpanAsString(trim), + ifFalse: Expression.Convert(Expression.Call(typeof(Enum), "Parse", Type.EmptyTypes, Expression.Constant(type), SpanAsString(trim), Expression.Constant(true)), type)), #endif - Expression.Constant(true))), + (acc, item) => Expression.Condition( item.condition, diff --git a/RecordParser/Engines/Writer/QuoteCSVWrite.cs b/RecordParser/Engines/Writer/QuoteCSVWrite.cs index 75ac51d..dbb6a7a 100644 --- a/RecordParser/Engines/Writer/QuoteCSVWrite.cs +++ b/RecordParser/Engines/Writer/QuoteCSVWrite.cs @@ -50,7 +50,7 @@ private static FuncSpanTIntBool Quote(char quote, string separator) return (false, 0); } - var newLengh = MinLengthToQuote(text, separator, quote); + var newLengh = MinLengthToQuote(text, separator.AsSpan(), quote); return TryFormat(text, span, quote, newLengh); }; @@ -64,7 +64,7 @@ private static FuncSpanTIntBool Quote(this FuncSpanTIntBool f, char quote, strin #endif (Span span, ReadOnlySpan text) => { - var newLengh = MinLengthToQuote(text, separator, quote); + var newLengh = MinLengthToQuote(text, separator.AsSpan(), quote); if (newLengh == text.Length) return f(span, text); @@ -101,7 +101,7 @@ private static FuncSpanTIntBool Quote(this FuncSpanTIntBool f, c #endif (Span span, string text) => { - var newLengh = MinLengthToQuote(text, separator, quote); + var newLengh = MinLengthToQuote(text.AsSpan(), separator.AsSpan(), quote); if (newLengh == text.Length) return f(span, text); @@ -115,12 +115,12 @@ private static FuncSpanTIntBool Quote(this FuncSpanTIntBool f, c : stackalloc char[newLengh]) .Slice(0, newLengh); - var (success, written) = TryFormat(text, temp, quote, newLengh); + var (success, written) = TryFormat(text.AsSpan(), temp, quote, newLengh); Debug.Assert(success); Debug.Assert(written == newLengh); - return f(span, new string(temp)); + return f(span, temp.ToString()); } finally { diff --git a/RecordParser/Engines/Writer/WriteEngine.cs b/RecordParser/Engines/Writer/WriteEngine.cs index 475f225..ea7a770 100644 --- a/RecordParser/Engines/Writer/WriteEngine.cs +++ b/RecordParser/Engines/Writer/WriteEngine.cs @@ -79,11 +79,17 @@ public static Expression DAs(Expression prop, MappingWriteConfiguration map, Par _ when prop.Type == typeof(char) || prop.Type == typeof(bool) => Expression.Call(typeof(WriteEngine), "TryFormat", Type.EmptyTypes, prop, temp, charsWritten), +#if NETSTANDARD2_0 + _ => Expression.Call(typeof(Format), "TryFormat", [prop.Type], prop, temp, charsWritten, format, + Expression.Constant(cultureInfo, typeof(CultureInfo))), +#else + _ when prop.Type == typeof(Guid) => Expression.Call(prop, "TryFormat", Type.EmptyTypes, temp, charsWritten, format), _ => Expression.Call(prop, "TryFormat", Type.EmptyTypes, temp, charsWritten, format, Expression.Constant(cultureInfo, typeof(CultureInfo))) +#endif }; return Expression.IfThen(Expression.Not(tryFormat), gotoReturn); @@ -223,4 +229,4 @@ private static bool TryFormatEnumFallback(TEnum value, Span destina return true; } } -} \ No newline at end of file +} diff --git a/RecordParser/Format.cs b/RecordParser/Format.cs new file mode 100644 index 0000000..ca62356 --- /dev/null +++ b/RecordParser/Format.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_0 + +using System; + +namespace RecordParser +{ + internal static class Format + { + public static bool TryFormat(this T @this, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider provider = null) where T : IFormattable + { + string value = @this.ToString(format.ToString(), provider); + if (value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = value.Length; + value.AsSpan().CopyTo(destination); + return true; + } + } +} + +#endif diff --git a/RecordParser/Parse.cs b/RecordParser/Parse.cs new file mode 100644 index 0000000..fb6e489 --- /dev/null +++ b/RecordParser/Parse.cs @@ -0,0 +1,62 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace RecordParser +{ + internal static class Parse + { +#if NETSTANDARD2_0 || NETFRAMEWORK + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ProcessSpan(ReadOnlySpan span) => span.ToString(); +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ReadOnlySpan ProcessSpan(ReadOnlySpan span) => span; +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Byte(ReadOnlySpan utf8Text, IFormatProvider provider = null) => byte.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte SByte(ReadOnlySpan utf8Text, IFormatProvider provider = null) => sbyte.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Double(ReadOnlySpan utf8Text, IFormatProvider provider = null) => double.Parse(ProcessSpan(utf8Text), NumberStyles.AllowThousands | NumberStyles.Float, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Single(ReadOnlySpan utf8Text, IFormatProvider provider = null) => float.Parse(ProcessSpan(utf8Text), NumberStyles.AllowThousands | NumberStyles.Float, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Int32(ReadOnlySpan utf8Text, IFormatProvider provider = null) => int.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint UInt32(ReadOnlySpan utf8Text, IFormatProvider provider = null) => uint.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Int64(ReadOnlySpan utf8Text, IFormatProvider provider = null) => long.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong UInt64(ReadOnlySpan utf8Text, IFormatProvider provider = null) => ulong.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Int16(ReadOnlySpan utf8Text, IFormatProvider provider = null) => short.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort UInt16(ReadOnlySpan utf8Text, IFormatProvider provider = null) => ushort.Parse(ProcessSpan(utf8Text), NumberStyles.Integer, provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Guid(ReadOnlySpan utf8Text) => System.Guid.Parse(ProcessSpan(utf8Text)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTime DateTime(ReadOnlySpan utf8Text, IFormatProvider provider = null) => System.DateTime.Parse(ProcessSpan(utf8Text), provider, DateTimeStyles.AllowWhiteSpaces); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TimeSpan TimeSpan(ReadOnlySpan utf8Text, IFormatProvider provider = null) => System.TimeSpan.Parse(ProcessSpan(utf8Text), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Boolean(ReadOnlySpan utf8Text) => bool.Parse(ProcessSpan(utf8Text)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static decimal Decimal(ReadOnlySpan utf8Text, IFormatProvider provider = null) => decimal.Parse(ProcessSpan(utf8Text), NumberStyles.Number, provider); + } +} diff --git a/RecordParser/Parsers/VariableLengthWriter.cs b/RecordParser/Parsers/VariableLengthWriter.cs index 12c6708..99d9155 100644 --- a/RecordParser/Parsers/VariableLengthWriter.cs +++ b/RecordParser/Parsers/VariableLengthWriter.cs @@ -22,7 +22,7 @@ public VariableLengthWriter(FuncSpanSpanTInt parse, string separator) public bool TryFormat(T instance, Span destination, out int charsWritten) { - var result = parse(destination, separator, instance); + var result = parse(destination, separator.AsSpan(), instance); charsWritten = result.charsWritten; return result.success; diff --git a/RecordParser/RecordParser.csproj b/RecordParser/RecordParser.csproj index 9e00fba..3250e95 100644 --- a/RecordParser/RecordParser.csproj +++ b/RecordParser/RecordParser.csproj @@ -2,45 +2,53 @@ RecordParser - netstandard2.1;net6.0;net7.0;net8.0 + netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 latest Leandro Fernandes Vieira (leandromoh) - RecordParser is a expression tree based parser that helps you to write maintainable parsers with high-performance and zero allocations, thanks to Span type. - It makes easier for developers to do parsing by automating non-relevant code, which allow you to focus on the essentials of mapping. - Include readers and writers for variable-length and fixed-length records. + RecordParser is a expression tree based parser that helps you to write maintainable parsers with high-performance and zero allocations, thanks to Span type. + It makes easier for developers to do parsing by automating non-relevant code, which allow you to focus on the essentials of mapping. + Include readers and writers for variable-length and fixed-length records. Copyright 2023 (c) Leandro F. Vieira (leandromoh). All rights reserved. https://github.com/leandromoh/RecordParser https://github.com/leandromoh/RecordParser tsv parser performance csv mapper file flat reader dotnet-core span flatfile expression-tree delimited fixedlength - 2.3.0 + 2.3.1 - https://github.com/leandromoh/RecordParser/blob/master/release_notes.md + https://github.com/leandromoh/RecordParser/blob/master/release_notes.md true - LICENSE.md - 1591 + LICENSE.md + 1591 - + - - - <_Parameter1>RecordParser.Test - - + + + <_Parameter1>RecordParser.Test + + - true + true - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/RecordParser/Shims.cs b/RecordParser/Shims.cs new file mode 100644 index 0000000..1863625 --- /dev/null +++ b/RecordParser/Shims.cs @@ -0,0 +1,33 @@ +#if NETSTANDARD2_0 + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace RecordParser +{ + internal static class Shims + { + public static bool Contains(this string @this, char charToFind) => @this.IndexOf(charToFind) != -1; + + public static bool StartsWith(this ReadOnlySpan @this, string value) => @this.StartsWith(value.AsSpan()); + + public static bool EndsWith(this ReadOnlySpan @this, string value) => @this.EndsWith(value.AsSpan()); + + public static int IndexOf(this ReadOnlySpan @this, string value) => @this.IndexOf(value.AsSpan()); + + public static bool TryPop(this Stack stack, [MaybeNullWhen(false)] out T result) + { + if (stack.Count > 0) + { + result = stack.Pop(); + return true; + } + + result = default; + return false; + } + } +} + +#endif