Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions RecordParser.Test/FixedLengthReaderBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using RecordParser.Builders.Writer;
using RecordParser.Parsers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -134,6 +135,51 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st
Baz: "baz"));
}

private class MyCustomException : Exception
{
public MyCustomException(string msg) : base(msg) { }
}

[Fact]
public void Given_partial_invalid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
{
var reader = new FixedLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
.Map(x => x.Name, 0, 5)
.Map(x => x.Birthday, 5, 10)
.Map(x => x.Money, 15, 7)
.Map(x => x.Color1, 22, 15)
.Map(x => x.Color2, 37, 15)
.Map(x => x.IsValid, 52, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
.Build();

var erros = new List<(Exception ex, int index)>();
var result = reader.Parse(" foo _datehere_ 123.45_invalid_color_ LightBlue _FOO_", (ex, index) => erros.Add((ex, index)));

result.Should().BeEquivalentTo((Name: "foo",
Birthday: default(DateTime),
Money: 123.45M,
Color1: default(Color),
Color2: Color.LightBlue,
IsValid: default(bool?)));

erros.Should().SatisfyRespectively(
e1 =>
{
e1.ex.Should().Match<FormatException>(ex => ex.Message.StartsWith("String '_datehere_' was not recognized as a valid DateTime."));
e1.index.Should().Be(5);
},
e2 =>
{
e2.ex.Should().Match<ArgumentException>(ex => ex.Message.StartsWith("Requested value '_invalid_color_' was not found."));
e2.index.Should().Be(22);
},
e3 =>
{
e3.ex.Should().Match<MyCustomException>(ex => ex.Message.StartsWith($"Invalid boolean value: _FOO_"));
e3.index.Should().Be(52);
});
}

[Fact]
public void Given_invalid_record_called_with_try_parse_should_not_throw()
{
Expand All @@ -148,6 +194,31 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw()
result.Should().Be(default);
}

[Fact]
public void Given_valid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
{
var reader = new FixedLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
.Map(x => x.Name, 0, 5)
.Map(x => x.Birthday, 5, 10)
.Map(x => x.Money, 15, 7)
.Map(x => x.Color1, 22, 15)
.Map(x => x.Color2, 37, 15)
.Map(x => x.IsValid, 52, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
.Build();

var erros = new List<(Exception ex, int index)>();
var result = reader.Parse(" foo 2026.03.19 123.45 White LightBlue TRUE ", (ex, index) => erros.Add((ex, index)));

result.Should().BeEquivalentTo((Name: "foo",
Birthday: new DateTime(2026, 03, 19),
Money: 123.45M,
Color1: Color.White,
Color2: Color.LightBlue,
IsValid: true));

erros.Should().BeEmpty();
}

[Fact]
public void Given_valid_record_called_with_try_parse_should_set_out_parameter_with_result()
{
Expand Down
71 changes: 71 additions & 0 deletions RecordParser.Test/VariableLengthReaderBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using RecordParser.Builders.Writer;
using RecordParser.Parsers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -161,6 +162,51 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st
Baz: "baz"));
}

private class MyCustomException : Exception
{
public MyCustomException(string msg) : base(msg) { }
}

[Fact]
public void Given_partial_invalid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
{
var reader = new VariableLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
.Map(x => x.Name, 0)
.Map(x => x.Birthday, 1)
.Map(x => x.Money, 2)
.Map(x => x.Color1, 3)
.Map(x => x.Color2, 4)
.Map(x => x.IsValid, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
.Build(";");

var erros = new List<(Exception ex, int index)>();
var result = reader.Parse("foo;_datehere_;123.45;_invalid_color_;LightBlue;_FOO_", (ex, index) => erros.Add((ex, index)));

result.Should().BeEquivalentTo((Name: "foo",
Birthday: default(DateTime),
Money: 123.45M,
Color1: default(Color),
Color2: Color.LightBlue,
IsValid: default(bool?)));

erros.Should().SatisfyRespectively(
e1 =>
{
e1.ex.Should().Match<FormatException>(ex => ex.Message.StartsWith("String '_datehere_' was not recognized as a valid DateTime."));
e1.index.Should().Be(1);
},
e2 =>
{
e2.ex.Should().Match<ArgumentException>(ex => ex.Message.StartsWith("Requested value '_invalid_color_' was not found."));
e2.index.Should().Be(3);
},
e3 =>
{
e3.ex.Should().Match<MyCustomException>(ex => ex.Message.StartsWith($"Invalid boolean value: _FOO_"));
e3.index.Should().Be(5);
});
}

[Fact]
public void Given_invalid_record_called_with_try_parse_should_not_throw()
{
Expand All @@ -175,6 +221,31 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw()
result.Should().Be(default);
}

[Fact]
public void Given_valid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
{
var reader = new VariableLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
.Map(x => x.Name, 0)
.Map(x => x.Birthday, 1)
.Map(x => x.Money, 2)
.Map(x => x.Color1, 3)
.Map(x => x.Color2, 4)
.Map(x => x.IsValid, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
.Build(";");

var erros = new List<(Exception ex, int index)>();
var result = reader.Parse("foo bar baz;2026.03.19;123.45;White;LightBlue;TRUE", (ex, index) => erros.Add((ex, index)));

result.Should().BeEquivalentTo((Name: "foo bar baz",
Birthday: new DateTime(2026, 03, 19),
Money: 123.45M,
Color1: Color.White,
Color2: Color.LightBlue,
IsValid: true));

erros.Should().BeEmpty();
}

[Fact]
public void Given_valid_record_called_with_try_parse_should_set_out_parameter_with_result()
{
Expand Down
6 changes: 4 additions & 2 deletions RecordParser/Builders/Reader/FixedLengthReaderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ public IFixedLengthReader<T> Build(CultureInfo cultureInfo = null, Func<T> facto
{
var map = MappingReadConfiguration.Merge(list, dic);
var func = ReaderEngine.RecordParserSpanFlat(map, factory);

func = CultureInfoVisitor.ReplaceCulture(func, cultureInfo);

return new FixedLengthReader<T>(func.Compile());
var func2 = ReaderEngine.RecordParserSpanFlatSafe(map, factory);
func2 = CultureInfoVisitor.ReplaceCulture(func2, cultureInfo);

return new FixedLengthReader<T>(func.Compile(), func2.Compile());
}
}
}
5 changes: 4 additions & 1 deletion RecordParser/Builders/Reader/MappingReadConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ public static IEnumerable<MappingReadConfiguration> Merge(
});

result = result
.OrderBy(x => x.start)
.Select((x, i) => (item: x, index: i))
.OrderBy(x => x.item.start)
.ThenBy(x => x.index)
.Select(x => x.item)
.ToList();

return result;
Expand Down
7 changes: 5 additions & 2 deletions RecordParser/Builders/Reader/VariableLengthReaderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,14 @@ public IVariableLengthReader<T> Build(string separator, CultureInfo cultureInfo
var quote = QuoteHelper.Quote.Char;

var map = MappingReadConfiguration.Merge(list.Select(x => x.Value), dic);
var func = ReaderEngine.RecordParserSpanCSV(map, factory);

var func = ReaderEngine.RecordParserSpanCSV(map, factory);
func = CultureInfoVisitor.ReplaceCulture(func, cultureInfo);

return new VariableLengthReader<T>(func.Compile(), separator, quote);
var func2 = ReaderEngine.RecordParserSpanCSVSafe(map, factory);
func2 = CultureInfoVisitor.ReplaceCulture(func2, cultureInfo);

return new VariableLengthReader<T>(func.Compile(), func2.Compile(), separator, quote);
}
}
}
70 changes: 61 additions & 9 deletions RecordParser/Engines/Reader/ReaderEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
using System.Linq.Expressions;
using static RecordParser.Engines.ExpressionHelper;

internal delegate T FuncSpanIntT<T>(ReadOnlySpan<T> span, int index);
public delegate T FuncSpanT<T>(ReadOnlySpan<char> text);
internal delegate T FuncSpanTSafe<T>(ReadOnlySpan<char> text, Action<Exception, int> exceptionHandler);

internal delegate T FuncSpanArrayT<T>(in TextFindHelper finder);
internal delegate T FuncSpanArrayTSafe<T>(in TextFindHelper finder, Action<Exception, int> exceptionHandler);

namespace RecordParser.Engines.Reader
{
Expand All @@ -23,7 +25,7 @@ public static Expression<FuncSpanArrayT<T>> RecordParserSpanCSV<T>(IEnumerable<M
// variables
var instanceVariable = Expression.Variable(typeof(T), "inst");

var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, (i, mapConfig) =>
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
{
return Expression.Call(configParameter, nameof(TextFindHelper.GetValue), Type.EmptyTypes, Expression.Constant(mapConfig.start));
});
Expand All @@ -34,6 +36,32 @@ public static Expression<FuncSpanArrayT<T>> RecordParserSpanCSV<T>(IEnumerable<M
return result;
}

public static Expression<FuncSpanArrayTSafe<T>> RecordParserSpanCSVSafe<T>(IEnumerable<MappingReadConfiguration> mappedColumns, Func<T> factory)
{
// parameters
var configParameter = Expression.Parameter(typeof(TextFindHelper).MakeByRefType(), "config");
var exceptionHandler = Expression.Parameter(typeof(Action<Exception, int>), "exceptionHandler");

// variables
var instanceVariable = Expression.Variable(typeof(T), "inst");

var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
{
return Expression.Call(configParameter, nameof(TextFindHelper.GetValue), Type.EmptyTypes, Expression.Constant(mapConfig.start));
},
(mapConfig, assign) =>
{
var visitor = new TryCatchVisitor(exceptionHandler, mapConfig.start);
var result = visitor.Visit(assign);
return result;
});

var body = MountBody(instanceVariable, blockThatSetProperties, mappedColumns, factory);
var result = Expression.Lambda<FuncSpanArrayTSafe<T>>(body, configParameter, exceptionHandler);

return result;
}

public static Expression<FuncSpanT<T>> RecordParserSpanFlat<T>(IEnumerable<MappingReadConfiguration> mappedColumns, Func<T> factory)
{
// parameters
Expand All @@ -42,7 +70,7 @@ public static Expression<FuncSpanT<T>> RecordParserSpanFlat<T>(IEnumerable<Mappi
// variables
var instanceVariable = Expression.Variable(typeof(T), "inst");

var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, (i, mapConfig) =>
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
{
return Slice(line, Expression.Constant(mapConfig.start), Expression.Constant(mapConfig.length.Value));
});
Expand All @@ -53,6 +81,32 @@ public static Expression<FuncSpanT<T>> RecordParserSpanFlat<T>(IEnumerable<Mappi
return result;
}

public static Expression<FuncSpanTSafe<T>> RecordParserSpanFlatSafe<T>(IEnumerable<MappingReadConfiguration> mappedColumns, Func<T> factory)
{
// parameters
var line = Expression.Parameter(typeof(ReadOnlySpan<char>), "span");
var exceptionHandler = Expression.Parameter(typeof(Action<Exception, int>), "exceptionHandler");

// variables
var instanceVariable = Expression.Variable(typeof(T), "inst");

var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
{
return Slice(line, Expression.Constant(mapConfig.start), Expression.Constant(mapConfig.length.Value));
},
(mapConfig, assign) =>
{
var visitor = new TryCatchVisitor(exceptionHandler, mapConfig.start);
var result = visitor.Visit(assign);
return result;
});

var body = MountBody(instanceVariable, blockThatSetProperties, mappedColumns, factory);
var result = Expression.Lambda<FuncSpanTSafe<T>>(body, line, exceptionHandler);

return result;
}

private static BlockExpression MountBody<T>(
ParameterExpression instanceVariable,
BlockExpression blockThatSetProperties,
Expand All @@ -75,17 +129,15 @@ private static BlockExpression MountBody<T>(
private static BlockExpression MountSetProperties(
ParameterExpression objectParameter,
IEnumerable<MappingReadConfiguration> mappedColumns,
Func<int, MappingReadConfiguration, Expression> getTextValue)
Func<MappingReadConfiguration, Expression> getTextValue,
Func<MappingReadConfiguration, BinaryExpression, Expression> assignHandler = null)
{
var replacer = new ParameterReplacerVisitor(objectParameter);
var assignsExpressions = new List<Expression>();
var i = -1;

foreach (var x in mappedColumns)
{
i++;

Expression textValue = getTextValue(i, x);
Expression textValue = getTextValue(x);

if (x.ShouldTrim)
textValue = Trim(textValue);
Expand Down Expand Up @@ -115,7 +167,7 @@ private static BlockExpression MountSetProperties(

var assign = Expression.Assign(replacer.Visit(x.prop), valueToBeSetExpression);

assignsExpressions.Add(assign);
assignsExpressions.Add(assignHandler?.Invoke(x, assign) ?? assign);
}

assignsExpressions.Add(objectParameter);
Expand Down
Loading
Loading