Mukil Deepthi
Mukil Deepthi

Reputation: 6492

Validate pipe separated line against specified format

I am reading lines from a text file. each line is pipe separated. I want to validate the line i.e each item is according to a specified format after splitting by delimiter. eg:- Here is my line

D|111111|87654321|Bar|BCreace|GBP|24/08/2010

Check if the above line follows the following format for eg:

Field Ref Field Length  
S0          1
S1          6
S2          34
...
S6          10

Currently i am using if condition as below:

    var sortCode = 0;
    if (!int.TryParse(items[1], out sortCode) || items[1].Length > 6)
        errorMessage.Add("Sort Code", "Invalid sort code");

But can anyone help me if this can be done in a proper way?

Thanks

Upvotes: 1

Views: 1245

Answers (4)

Mathieu Guindon
Mathieu Guindon

Reputation: 71227

Initializing sortCode to 0 is redundant, because an out parameter is guaranteed by the compiler to be initialized by the TryParse function... and the default value for an int value type is 0 anyway.

So instead of this:

var sortCode = 0;

You could have this:

int sortCode;

if (!int.TryParse(items[1], out sortCode) || items[1].Length > 6)

6 is a magic number here, it would be better to make it a constant with a meaningful name. Also, there might be a bit too much logic crammed into that one line. How about this?

var parsed = int.TryParse(items[1], out sortCode);
if (!parsed || items[1].Length > SORTCODE_LENGTH)
{
    errorMessage.Add("Sort Code", "Invalid sort code");
}

Depending on context, throwing an exception might be a better idea here - if your method is doing more than what you've shown, it's probably doing too many things and could benefit from another abstraction level.

Notice the explicit scope under the if - for an example of what can happen with implicit scopes, try searching for "Apple SSL goto fail" ;-)


If this is the kind of answer you're looking for, you need to try Code Review.

Upvotes: 1

t3chb0t
t3chb0t

Reputation: 18744

There is no one proper way of doing this but I have a suggestion for you that uses a custom attribute.

I named it ColumnInfoAttribute:

class ColumnInfoAttribute : Attribute
{
    public int Index { get; set; }
    public int MaxLength { get; set; }
}

It allows you to specify the index and max length of a field so you can apply it all properties that should receive a value:

class LineItem
{
    [ColumnInfo(Index = 0, MaxLength = 1)]
    public string S0 { get; set; }

    [ColumnInfo(Index = 1, MaxLength = 6)]
    public string S1 { get; set; }

    [ColumnInfo(Index = 2, MaxLength = 34)]
    public string S2 { get; set; }

    [ColumnInfo(Index = 3, MaxLength = 34)]
    public string S3 { get; set; }

    [ColumnInfo(Index = 4, MaxLength = 34)]
    public string S4 { get; set; }

    [ColumnInfo(Index = 5, MaxLength = 34)]
    public string S5 { get; set; }

    [ColumnInfo(Index = 6, MaxLength = 34)]
    public DateTime S6 { get; set; }

    public static LineItem Parse(string line)
    {
        var propertyDictionary =
            typeof(LineItem)
            .GetProperties(BindingFlags.Instance | BindingFlags.Public)
            // create an anonymous object to hold the property and the ColumnInfo
            .Select(p => new
            {
                Property = p,
                ColumnInfo = p.GetCustomAttribute<ColumnInfoAttribute>()
            })
            // get only those where the ColumnInfo is not null (in case there are other properties)
            .Where(ci => ci.ColumnInfo != null)
            // create a dictionary with the Index as a key
            .ToDictionary(ci => ci.ColumnInfo.Index);

        var result = new LineItem();

        var values = line.Split('|');
        for (var i = 0; i < values.Length; i++)
        {
            // validate the length of the value
            var isValidLength = values[i].Length > propertyDictionary[i].ColumnInfo.MaxLength;
            if (!isValidLength)
            {
                // todo: throw some appropriate exception or do other error handling
            }

            // set the corresponding property
            var converterdValue = Convert.ChangeType(
                values[i], 
                propertyDictionary[i].Property.PropertyType);
            propertyDictionary[i].Property.SetValue(result, converterdValue);
        }
        return result;
    }
}

The same class has also a Parse method that via reflection gets all properties with the ColumnInfo attribute and creates a dictionary. The key of the dictionary is the Index.

Now you can for-loop over all values and use the i to get a ColumnInfo. You then check if the length of the field is valid and if so you use the property.SetValue to assign a value to the property.

Usage:

var line = "D|111111|87654321|Bar|BCreace|GBP|24/08/2010";
var lineItem = LineItem.Parse(line);

It's easily extensible and very generic. If you have more such cases you can put this code in a base class and add the attributes to the derived classes.

Upvotes: 1

user2023861
user2023861

Reputation: 8208

How about a regex?

[^|]\|[^|]{6}\|[^|]{8}\|[^|]{3}\|[^|]{7}\|[^|]{3}\|[^|]{10}

Try it out here https://regex101.com/r/rI6zN6/1

Upvotes: -1

Devon Burriss
Devon Burriss

Reputation: 2532

This might be a little less performant but I find it easier to read and maintain. You can parse the line to a strongly typed object then validate the object. In the example below I am using FluentValidation.

public class LineItem
{
    public static LineItem Parse(string line)
    {
        var split = line.Split('|');
        return new LineItem(split[0], split[1], split[2], split[3], split[4], split[5], split[6]);
    }

    public LineItem(string s0, string s1, string s2, string s3, string s4, string s5, string s6)
    {
        //any param value checks

        S0 = s0;
        S1 = s1;
        S2 = s2;
        S3 = s3;
        S4 = s4;
        S5 = s5;
        S6 = s6;
    }

    public string S0 { get; set; }
    public string S1 { get; set; }
    public string S2 { get; set; }
    public string S3 { get; set; }
    public string S4 { get; set; }
    public string S5 { get; set; }
    public string S6 { get; set; }
}

public class LineItemValidator : AbstractValidator<LineItem>
{
    public LineItemValidator()
    {
        RuleFor(line => line.S0).Length(1);
        RuleFor(line => line.S2).Length(6);
        //etc
    }
}

Then usage would be something like this:

public class FileValidatorTests
{
    [Fact]
    public void Spike()
    {
        var line = "D|111111|87654321|Bar|BCreace|GBP|24/08/2010";
        var lineItem = LineItem.Parse(line);
        var result = new LineItemValidator().Validate(lineItem);
        Assert.True(result.IsValid);
    }
}

Upvotes: 0

Related Questions