3
\$\begingroup\$

My application (legacy code) reads a CSV input file with an expected format with regards to Column names as follows:

  • 1st column - Should say "Marker"
  • 2nd column - Should say "Category"
  • 3rd column ... to ... N columns - These are the actual data columns. Should have number starting with 1 and increasing by 1 going right.

In other words, 3rd column should have "1", 4th column header should have "2".

Example CSV file with 10 data columns (total of 12 columns in file):

Marker,Category,1,2,3,4,5,6,7,8,9,10
TODD,Sea,0.001062344,0.001931534,0.002334951,0.002738369,0.003141786,0.003545203,0.004788256,0.005917489,0.007025671,0.008078547
JAMIE,Fork,0.00133847,0.002433581,0.002941854,0.003450128,0.003958402,0.004466676,0.006032825,0.007455569,0.00885179,0.010178331
LENNY,PITCH,0.001686365,0.003066119,0.003706504,0.004346889,0.004987274,0.005627659,0.007600883,0.009393428,0.011152557,0.012823893

My code that does this validation (and it works) is as follows:

fileStream = new FileStream(delimitedFileName, FileMode.Open,
   FileAccess.Read, FileShare.Read);
csvStreamReader = new StreamReader(fileStream);
string headerLine = csvStreamReader.ReadLine();
var headers = headerLine.Split(delimiter, '\\');

if (!string.Equals(headers[0], "Marker", StringComparison.InvariantCultureIgnoreCase) 
|| (!string.Equals(headers[1], "Category", StringComparison.InvariantCultureIgnoreCase)))
{
    throw new Exception("Invalid file format. Please use template (MarkerTemplate.csv).");
}

for (int i = 2; i < headers.Length; i++)
{
    if (!string.Equals(headers[i], (i-1).ToString(),
        StringComparison.InvariantCultureIgnoreCase))
    {
    throw new Exception("Invalid file format. Please use template (MarkerTemplate.csv).");
    }
}

I want to avoid doing it like this, in 2 steps.

Is there a way to do some sort of delegate or Action to do the 2nd section, i.e. validate that the data column names are correct, for a variable number of data columns?

I want to avoid doing it in 2 separate sections as shown above, and have duplicate throw new Exception code for essentially the same exception.

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Move the data column validation into a function

bool invalidDataColumns(string[] headers) {
    int[] numbers = headers.Skip(2).Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
    return !(numbers[0] == 1 && numbers.Zip(numbers.Skip(1), (previous, current) => current - previous).All(diff => diff == 1));
}

And call it within the if condition.

//...

if (!string.Equals(headers[0], "Marker", StringComparison.InvariantCultureIgnoreCase)
    || (!string.Equals(headers[1], "Category", StringComparison.InvariantCultureIgnoreCase))
    || invalidDataColumns(headers)
    ) {
    throw new Exception("Invalid file format. Please use template (MarkerTemplate.csv).");
}

//...

The condition could be reduced further by moving the entire thing into its own function as well

bool invalidHeaders(string[] headers) {
    return !string.Equals(headers[0], "Marker", StringComparison.InvariantCultureIgnoreCase)
        || (!string.Equals(headers[1], "Category", StringComparison.InvariantCultureIgnoreCase))
        || invalidDataColumns(headers);
}

which now simplifies the code to

//...

if (invalidHeaders(headers)) {
    throw new Exception("Invalid file format. Please use template (MarkerTemplate.csv).");
}

//...

Unit test used to verify expected behavior

[TestClass]
public class InvalidDataColumnsTests {
    [TestMethod]
    [DataRow("Marker,Category,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15", false)]
    [DataRow("Marker,Category,1,2,3,4,5,6,7,8,9,10", false)]
    [DataRow("Marker,Category,1,2,3,4,5", false)]
    [DataRow("Marker,Category,x,2,3,4,5,6,7,8,9,10", true)]
    [DataRow("Category,1,2,3,4,5", true)]
    public void Should_Validate_Columns(string headerLine, bool expected) {
        //string headerLine = "Marker,Category,1,2,3,4,5,6,7,8,9,10";
        string[] headers = headerLine.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
        invalidHeaders(headers).Should().Be(expected);

        ////Example Use case
        //if (invalidHeaders(headers)) {
        //    throw new Exception("Invalid file format. Please use template (MarkerTemplate.csv).");
        //}
    }

    bool invalidHeaders(string[] headers) {
        return !string.Equals(headers[0], "Marker", StringComparison.InvariantCultureIgnoreCase)
            || (!string.Equals(headers[1], "Category", StringComparison.InvariantCultureIgnoreCase))
            || invalidDataColumns(headers);
    }

    bool invalidDataColumns(string[] headers) {
        int[] numbers = headers.Skip(2).Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
        return !(numbers[0] == 1 && numbers.Zip(numbers.Skip(1), (previous, current) => current - previous).All(diff => diff == 1));
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks you, pretty much what I was hoping for as an answer to my question. The unit test are a bonus! \$\endgroup\$
    – Shiva
    Commented Dec 8, 2019 at 21:21
2
\$\begingroup\$

You could do something like this:

fileStream = new FileStream(delimitedFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
csvStreamReader = new StreamReader(fileStream);
string headerLine = csvStreamReader.ReadLine();
var headers = headerLine.Split(delimiter, '\\');

for (int i = 0; i < headers.Length; i++)
{
    if (i >= 2 && !string.Equals(headers[i], (i-1).ToString(), StringComparison.InvariantCultureIgnoreCase)
    || (i == 1 && !string.Equals(headers[1], "Category", StringComparison.InvariantCultureIgnoreCase))
    || (i == 0 && !string.Equals(headers[0], "Marker", StringComparison.InvariantCultureIgnoreCase)))
    {
        throw new Exception("Invalid file format. Please use template (MarkerTemplate.csv).");
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ This solution works too, and it's really simple. I could only choose 1 answer, so I chose the other and upvoted yours. \$\endgroup\$
    – Shiva
    Commented Dec 8, 2019 at 21:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.