Wednesday, June 19, 2013

Comparing Excel files, take two

I already wrote this project once, on GitHub, but I can't say that I like the result. I wrote that code without TDD, mostly to see if I can still work in the "normal" fashion. I can, but I had a bug that took me a while to even discover and then was difficult to write tests for. I also hate the temporal dependency in that design (if you don't call the SortBy method first, the ExcludeRecords method will return incorrect results).

Anyway; here's attempt number two, writing that code using TDD.

Structure

I start by creating my usual structure for projects of this type - a library for the logic, a console application for the main program and a test project for tests. I also use the Enable NuGet Package Restore option (right-click on the solution node) to add the three NuGet files that will allow someone to automatically download all the packages I'm referencing without wasting space in the source code repository. (Right now I've only added the Moq package to the test project - I'm pretty much guaranteed to use that one.)

Here's what the solution tree looks like:

Solution Tree

Acceptance tests

I am fine with how the previous program worked, from the point of view of the end-user; what I dislike is the internal structure. That means that the acceptance test will look the same: launch the program with the correct arguments, capture its output, verify that the output matches the desired result:

  [TestClass]
  public class AcceptanceTests
  {
    [TestMethod]
    public void EndToEnd()
    {
      const string PATH = @"..\..\..\CompareExcelFiles\bin\Debug\CompareExcelFiles.exe";
      const string ARGS = @"..\..\..\file1.xlsx ..\..\..\file2.xlsx C A";
      const string EXPECTED_OUTPUT = @"** 1: ..\..\..\file1.xlsx ** --- 3 distinct rows out of 9
      C A
00001 2 1
00002 2 2
00003 2 3

** 2: ..\..\..\file2.xlsx ** --- 2 distinct rows out of 8
      C A
00001 1 4
00002 3 4

";

      var startInfo = new ProcessStartInfo
      {
        CreateNoWindow = true,
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        UseShellExecute = false,
        Arguments = ARGS,
        FileName = PATH,
      };
      var process = new Process { StartInfo = startInfo };
      process.Start();
      process.WaitForExit();

      var result = process.StandardOutput.ReadToEnd();

      Assert.AreEqual(EXPECTED_OUTPUT, result);
    }
  }

This fails, as expected. Time to think about the design of the program. What do I want the main program to do?

Well, it needs to read the name of two Excel files from the program arguments and then at least one column name. It needs to load the two files and try to match them by comparing the given column(s). Finally, it needs to display the non-matching rows for each files (the rows that exist in one file but have no correspondent in the other), again based only on the values in the given column(s).

One thing that comes to mind is: what happens if the program receives too few arguments? I've decided that, instead of asking for them, the program will just display a help message describing its purpose and syntax. In fact, this suggests a second acceptance test:

    [TestMethod]
    public void ReturnsHelp()
    {
      const string PATH = @"..\..\..\CompareExcelFiles\bin\Debug\CompareExcelFiles.exe";
      const string EXPECTED_OUTPUT = @"Syntax: CompareExcelFiles file1 file2 column [column...]
        file1     first file to compare
        file2     second file to compare
        column    name of column(s) to sort / compare by
";

      var startInfo = new ProcessStartInfo
      {
        CreateNoWindow = true,
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        UseShellExecute = false,
        Arguments = "",
        FileName = PATH,
      };
      var process = new Process { StartInfo = startInfo };
      process.Start();
      process.WaitForExit();

      var result = process.StandardOutput.ReadToEnd();

      Assert.AreEqual(EXPECTED_OUTPUT, result);
    }

This fails. I know it cries for a refactoring (it's almost entirely a copy of the first acceptance test) but I must resist. Don't refactor a failing test. First, make it pass, which is rather easy in this case:

  internal class Program
  {
    private static void Main(string[] args)
    {
      Console.WriteLine("Syntax: CompareExcelFiles file1 file2 column [column...]");
      Console.WriteLine("        file1     first file to compare");
      Console.WriteLine("        file2     second file to compare");
      Console.WriteLine("        column    name of column(s) to sort / compare by");
    }
  }

That acceptance test is green. Now I can refactor the tests:

  [TestClass]
  public class AcceptanceTests
  {
    [TestMethod]
    public void EndToEnd()
    {
      const string ARGS = @"..\..\..\file1.xlsx ..\..\..\file2.xlsx C A";
      const string EXPECTED_OUTPUT = @"** 1: ..\..\..\file1.xlsx ** --- 3 distinct rows out of 9
      C A
00001 2 1
00002 2 2
00003 2 3

** 2: ..\..\..\file2.xlsx ** --- 2 distinct rows out of 8
      C A
00001 1 4
00002 3 4

";

      var result = RunAndCaptureOutput(ARGS);

      Assert.AreEqual(EXPECTED_OUTPUT, result);
    }

    [TestMethod]
    public void ReturnsHelp()
    {
      const string EXPECTED_OUTPUT = @"Syntax: CompareExcelFiles file1 file2 column [column...]
        file1     first file to compare
        file2     second file to compare
        column    name of column(s) to sort / compare by
";
      var result = RunAndCaptureOutput("");

      Assert.AreEqual(EXPECTED_OUTPUT, result);
    }

    //

    private static string RunAndCaptureOutput(string args)
    {
      const string PATH = @"..\..\..\CompareExcelFiles\bin\Debug\CompareExcelFiles.exe";

      var startInfo = new ProcessStartInfo
      {
        CreateNoWindow = true,
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        UseShellExecute = false,
        Arguments = args,
        FileName = PATH,
      };
      var process = new Process { StartInfo = startInfo };
      process.Start();
      process.WaitForExit();

      return process.StandardOutput.ReadToEnd();
    }
  }

The first acceptance test fails, the second passes. Good; time for the unit tests.

Unit tests

As I said, I want to load the two Excel files whose name I got as arguments. What is the result of this action? Because I know that I intend to compare the contents of these files, they will have to be basically tables; because I'm passing column names, the first row will be the header row. That means a bi-dimensional structure, with the column names as a separate property for easy reference. Oh, another thing: I have no need to modify the values so the property exposing them should be read-only.

I'll start by creating the interface expressing these concepts (of course, I can always change it later if I discover something I haven't planned for); add this to the library project:

  public interface Table
  {
    int RowCount { get; }
    int ColCount { get; }

    string[] Columns { get; }
    string[][] Data { get; }
  }

So far so good. Now I need a class that can load an Excel file (actually, a sheet from an Excel file) into such a structure. Unfortunately, testing such a class involves actually opening an Excel file, which means it doesn't qualify as a unit test (see this set of rules). It's ok, I'll add this to the AcceptanceTests class:

    [TestMethod]
    public void LoadsExcelFile()
    {
      const string FILE_NAME = @"..\..\..\file1.xlsx";

      var sut = new ExcelLoader();

      var result = sut.Load(FILE_NAME);

      Assert.AreEqual(9, result.RowCount); // number of *data* rows
      Assert.AreEqual(4, result.ColCount);
      CollectionAssert.AreEqual(new[] { "A", "B", "C", "D" }, result.Columns);
      CollectionAssert.AreEqual(new[] { "1", "10", "1", "100" }, result.Data[0]);
      CollectionAssert.AreEqual(new[] { "2", "20", "1", "200" }, result.Data[1]);
      CollectionAssert.AreEqual(new[] { "3", "30", "1", "300" }, result.Data[2]);
      CollectionAssert.AreEqual(new[] { "1", "40", "2", "400" }, result.Data[3]);
      CollectionAssert.AreEqual(new[] { "2", "50", "2", "500" }, result.Data[4]);
      CollectionAssert.AreEqual(new[] { "3", "60", "2", "600" }, result.Data[5]);
      CollectionAssert.AreEqual(new[] { "1", "70", "3", "700" }, result.Data[6]);
      CollectionAssert.AreEqual(new[] { "2", "80", "3", "800" }, result.Data[7]);
      CollectionAssert.AreEqual(new[] { "3", "90", "3", "900" }, result.Data[8]);
    }

Of course, this doesn't even compile; I need to add the ExcelLoader class to the library project:

  public class ExcelLoader
  {
    public Table Load(string fileName)
    {
      return null;
    }
  }

Now the code compiles and the test fails.

MemoryTable

Unfortunately, the test fails for the wrong reason: I'm returning null. I need to return an implementation of the Table interface but with the wrong data; I'll call it MemoryTable. Of course, I need a test first; I've decided that the MemoryTable class will be initialized with a list of string arrays, so that's what I'll use in my test:

  [TestClass]
  public class MemoryTableTests
  {
    [TestClass]
    public class RowCount : MemoryTableTests
    {
      [TestMethod]
      public void SingleRow()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B" },
          new[] { "1", "2" },
        });

        Assert.AreEqual(1, sut.RowCount);
      }
    }
  }

Making it compile is easy

  public class MemoryTable : Table
  {
    public int RowCount { get; private set; }
    public int ColCount { get; private set; }
    public string[] Columns { get; private set; }
    public string[][] Data { get; private set; }

    public MemoryTable(IEnumerable<string[]> cells)
    {
      //
    }
  }

Making it pass is not that complicated either (yes, I know it looks ridiculous; doing it this way forces me to really test the class):

    public MemoryTable(IEnumerable<string[]> cells)
    {
      RowCount = 1;
    }

Ok, the second test needs to force me to change that code:

      [TestMethod]
      public void MultipleRows()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B" },
          new[] { "1", "2" },
          new[] { "3", "4" },
          new[] { "5", "6" },
        });

        Assert.AreEqual(3, sut.RowCount);
      }

The fix:

    public MemoryTable(IEnumerable<string[]> cells)
    {
      RowCount = cells.Count() - 1;
    }

That code raises two questions. First, what happens if the cells enumeration is empty? I should throw an exception in that case, since I need at least one row (the header):

      [TestMethod]
      [ExpectedException(typeof (Exception))]
      public void ThrowsWhenNoRows()
      {
        var sut = new MemoryTable(new List<string[]>());
      }
%

The test fails because I don't throw an exception; easy to fix:

[%
    public MemoryTable(IEnumerable<string[]> cells)
    {
      cells = cells.ToList();
      if (!cells.Any())
        throw new Exception("At least one row is required.");

      RowCount = cells.Count() - 1;
    }

(I call the ToList method because I don't want to enumerate cells twice - once for Any and once for Count.)

The second question is: what happens if I send a null argument? I say that it should be treated the same as an empty sequence:

      [TestMethod]
      [ExpectedException(typeof(Exception))]
      public void ThrowsWhenNull()
      {
        var sut = new MemoryTable(null);
      }

The method fails because the exception being thrown is an ArgumentNullException instead of Exception. I think a regular Exception (in fact, the same one as for an non-null but empty sequence) is still the correct thing to do:

    public MemoryTable(IEnumerable<string[]> cells)
    {
      cells = (cells ?? new List<string[]>()).ToList();
      if (!cells.Any())
        throw new Exception("At least one row is required.");

      RowCount = cells.Count() - 1;
    }

Ok, that's it for the RowCount property. Here are the similar tests for ColCount:

    [TestClass]
    public class ColCount : MemoryTableTests
    {
      [TestMethod]
      public void SingleColumn()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A" },
          new[] { "1" },
          new[] { "2" },
        });

        Assert.AreEqual(1, sut.ColCount);
      }

      [TestMethod]
      public void MultipleColumns()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B", "C" },
          new[] { "1", "2", "3" },
          new[] { "4", "5", "6" },
        });

        Assert.AreEqual(3, sut.ColCount);
      }

      [TestMethod]
      [ExpectedException(typeof (Exception))]
      public void ThrowsWhenNoColumns()
      {
        var sut = new MemoryTable(new[]
        {
          new string[0],
          new string[0],
        });
      }
    }

(Note that this is an inner class for MemoryTableTests.)

The code for the MemoryTable constructor is now

    public MemoryTable(IEnumerable<string[]> cells)
    {
      cells = (cells ?? new List<string[]>()).ToList();

      RowCount = cells.Count() - 1;
      if (RowCount < 0)
        throw new Exception("At least one row is required.");

      ColCount = cells.First().Length;
      if (ColCount < 1)
        throw new Exception("At least one column is required.");
    }

Finally, these are the tests for the Columns and Data properties:

    [TestClass]
    public class Columns : MemoryTableTests
    {
      [TestMethod]
      public void SingleColumn()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A" },
          new[] { "1" },
          new[] { "2" },
        });

        CollectionAssert.AreEqual(new[] { "A" }, sut.Columns);
      }

      [TestMethod]
      public void MultipleColumns()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B", "C" },
          new[] { "1", "2", "3" },
          new[] { "4", "5", "6" },
        });

        CollectionAssert.AreEqual(new[] { "A", "B", "C" }, sut.Columns);
      }
    }

    [TestClass]
    public class Data : MemoryTableTests
    {
      [TestMethod]
      public void MultipleRowsAndColumns()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B", "C" },
          new[] { "1", "2", "3" },
          new[] { "4", "5", "6" },
        });

        Assert.AreEqual("1", sut.Data[0][0]);
        Assert.AreEqual("2", sut.Data[0][1]);
        Assert.AreEqual("3", sut.Data[0][2]);
        Assert.AreEqual("4", sut.Data[1][0]);
        Assert.AreEqual("5", sut.Data[1][1]);
        Assert.AreEqual("6", sut.Data[1][2]);
      }
    }

and the MemoryTable constructor is now

  public class MemoryTable : Table
  {
    public int RowCount { get; private set; }
    public int ColCount { get; private set; }
    public string[] Columns { get; private set; }
    public string[][] Data { get; private set; }

    public MemoryTable(IEnumerable<string[]> cells)
    {
      cells = (cells ?? new List<string[]>()).ToList();

      RowCount = cells.Count() - 1;
      if (RowCount < 0)
        throw new Exception("At least one row is required.");

      Columns = cells.First();
      ColCount = Columns.Length;
      if (ColCount < 1)
        throw new Exception("At least one column is required.");

      Data = cells.Skip(1).ToArray();
    }
  }

Whew.

ExcelLoader

Ok, back to the ExcelLoader class. Let's see if I can make the test fail correctly now:

  public class ExcelLoader
  {
    public Table Load(string fileName)
    {
      return new MemoryTable(new[]
      {
        new[] { "A" },
      });
    }
  }

I run the tests and, indeed, the acceptance test is now failing with the message "Assert.AreEqual failed. Expected:<9>. Actual:<0>.". It's better when a test fails because of an assertion than because of some unexpected error.

To make the test pass, I first need to add the EPPlus package to the library project. At the time when I write this, the last version of that package is 3.1.3.3 so be careful if you're using another one (especially if the major version is different).

After that, change the code of the ExcelLoader class to

  public class ExcelLoader
  {
    public Table Load(string fileName)
    {
      return new MemoryTable(ReadExcel(fileName));
    }

    //

    private static IEnumerable<string[]> ReadExcel(string fileName)
    {
      using (var file = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
      using (var excel = new ExcelPackage(file))
      {
        var sheet = excel.Workbook.Worksheets[1];
        var lastRow = sheet.Dimension.End.Row;
        var lastCol = sheet.Dimension.End.Column;

        for (var row = 1; row <= lastRow; row++)
        {
          var record = new List<string>();

          for (var col = 1; col <= lastCol; col++)
            record.Add(sheet.Cells[row, col].GetValue<string>() ?? "");

          yield return record.ToArray();
        }
      }
    }
  }

The second acceptance test is now passing; I can read Excel files.

Comparing tables

I have now reached the "compare the two tables" part of the exercise. I'll create a new TableComparer class, starting with a test:

  [TestClass]
  public class TableComparerTests
  {
    [TestClass]
    public class Compare : TableComparerTests
    {
      [TestMethod]
      public void ReturnsAllRowsWhenTheOtherTableIsEmpty()
      {
        var table1 = new MemoryTable(new[] { new[] { "A", "B" }, new[] { "1", "2" }, new[] { "3", "4" } });
        var table2 = new MemoryTable(new[] { new[] { "A", "B" } });
        var sut = new TableComparer(new[] { "A" });

        var result = sut.Compare(table1, table2);

        Assert.AreEqual(2, result.RowCount);
      }
    }
  }

To make it compile, add a new class to the library project:

  public class TableComparer
  {
    public TableComparer(string[] columns)
    {
      //
    }

    public Table Compare(Table table1, Table table2)
    {
      return null;
    }
  }

This fails; making it pass is easy - just return the first table:

    public Table Compare(Table table1, Table table2)
    {
      return table1;
    }

That means I need a new test:

      [TestMethod]
      public void ReturnsRowsThatDifferInOneColumn()
      {
        var table1 = new MemoryTable(new[]
        {
          new[] { "A", "B" },
          new[] { "1", "2" },
          new[] { "3", "4" },
          new[] { "5", "6" },
          new[] { "7", "8" },
        });
        var table2 = new MemoryTable(new[]
        {
          new[] { "A", "B" },
          new[] { "1", "2" },
          new[] { "5", "6" },
        });
        var sut = new TableComparer(new[] { "A" });

        var result = sut.Compare(table1, table2);

        Assert.AreEqual(2, result.RowCount);
        CollectionAssert.AreEqual(new[] { "3", "4" }, result.Data[0]);
        CollectionAssert.AreEqual(new[] { "7", "8" }, result.Data[1]);
      }

Making this pass takes a bit more work than I usually need on a test:

  public class TableComparer
  {
    public TableComparer(string[] columns)
    {
      this.columns = columns;
    }

    public Table Compare(Table table1, Table table2)
    {
      var indices = GetIndices(table1.Columns).ToList();

      var rows = table1
        .Data
        .Where(row => !RowFoundIn(row, table2.Data, indices));

      return new MemoryTable(Enumerable.Repeat(table1.Columns, 1).Concat(rows));
    }

    //

    private readonly string[] columns;

    private IEnumerable<int> GetIndices(string[] tableColumns)
    {
      return columns.Select(col => Array.IndexOf(tableColumns, col));
    }

    private static bool RowFoundIn(IList<string> row, IEnumerable<string[]> data, IEnumerable<int> indices)
    {
      // can't use .FirstOrDefault() here, the default is 0
      var comparisons = data
        .Select(otherRow => CompareRows(row, otherRow, indices))
        .Where(comparison => comparison == 0)
        .Take(1)
        .ToList();

      return comparisons.Any();
    }

    private static int CompareRows(IList<string> row1, IList<string> row2, IEnumerable<int> indices)
    {
      return indices
        .Select(index => string.Compare(row1[index], row2[index], StringComparison.InvariantCultureIgnoreCase))
        .Where(comparison => comparison != 0)
        .FirstOrDefault();
    }
  }

This is quite inefficient – O(n2), where n is the total number of elements in a table, that is rows * columns – but I don't need to optimize until I discover that that's indeed a problem. I'm more interested in making it work first. (The Make it work, make it right, make it fast mantra comes to mind here.)

What else should I test? I believe I already made the algorithm work for multiple columns, but making sure can't hurt:

      [TestMethod]
      public void ReturnsRowsThatDifferInMultipleColumns()
      {
        var table1 = new MemoryTable(new[]
        {
          new[] { "A", "B" },
          new[] { "1", "0" },
          new[] { "1", "2" },
          new[] { "1", "4" },
          new[] { "1", "6" },
          new[] { "1", "8" },
        });
        var table2 = new MemoryTable(new[]
        {
          new[] { "A", "B" },
          new[] { "1", "2" },
          new[] { "2", "0" },
          new[] { "1", "0" },
        });
        var sut = new TableComparer(new[] { "A", "B" });

        var result = sut.Compare(table1, table2);

        Assert.AreEqual(3, result.RowCount);
        CollectionAssert.AreEqual(new[] { "1", "4" }, result.Data[0]);
        CollectionAssert.AreEqual(new[] { "1", "6" }, result.Data[1]);
        CollectionAssert.AreEqual(new[] { "1", "8" }, result.Data[2]);
      }

Yep; this one passes too.

Writing out the results

All that remains to be done is the part where I display the results of the comparisons. The main part of that will be done by the MemoryTable class, which means a new inner class for MemoryTableTests. I'll start by verifying the header:

    [TestClass]
    public class Dump
    {
      [TestMethod]
      public void PrintsOutTheHeader()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B", "C" },
          new[] { "1", "2", "3" },
          new[] { "4", "5", "6" },
        });
        var output = new List<string>();

        sut.Dump(new[] { "A", "C" }, output.Add);

        Assert.AreEqual("      A C", output[0]);
      }
    }

A new method has to be added to the MemoryTable class to make this compile:

    public void Dump(string[] columns, Action<string> writeLine)
    {
      //
    }

Making it pass is not difficult:

    public void Dump(string[] columns, Action<string> writeLine)
    {
      writeLine(string.Format("      {0}", string.Join(" ", columns)));
    }

Good. Now the data:

      [TestMethod]
      public void PrintsOutTheData()
      {
        var sut = new MemoryTable(new[]
        {
          new[] { "A", "B", "C" },
          new[] { "1", "2", "3" },
          new[] { "4", "5", "6" },
        });
        var output = new List<string>();

        sut.Dump(new[] { "A", "C" }, output.Add);

        Assert.AreEqual(3, output.Count);
        Assert.AreEqual("00001 1 3", output[1]);
        Assert.AreEqual("00002 4 6", output[2]);
      }

Making it pass:

    public void Dump(string[] columns, Action<string> writeLine)
    {
      writeLine(string.Format("      {0}", string.Join(" ", columns)));

      var indices = GetIndices(columns).ToList();
      var lineNo = 1;

      var lines = Data.Select(row => string.Join(" ", indices.Select(index => row[index])));
      foreach (var line in lines)
        writeLine(string.Format("{0:d5} {1}", lineNo++, line));
    }

    //

    private IEnumerable<int> GetIndices(IEnumerable<string> requiredColumns)
    {
      return requiredColumns.Select(col => Array.IndexOf(Columns, col));
    }

Removing duplication

I will introduce a new class in order to remove the duplication; add this to the library project:

  public static class Helper
  {
    public static IEnumerable<int> GetIndices(this string[] allColumns, IEnumerable<string> requiredColumns)
    {
      return requiredColumns.Select(col => Array.IndexOf(allColumns, col));
    }
  }

The MemoryTable.Dump method becomes

    public void Dump(string[] columns, Action<string> writeLine)
    {
      writeLine(string.Format("      {0}", string.Join(" ", columns)));

      var indices = Columns.GetIndices(columns).ToList();
      var lineNo = 1;

      var lines = Data.Select(row => string.Join(" ", indices.Select(index => row[index])));
      foreach (var line in lines)
        writeLine(string.Format("{0:d5} {1}", lineNo++, line));
    }

and the TableComparer.Compare method changes to

    public Table Compare(Table table1, Table table2)
    {
      var indices = table1.Columns.GetIndices(columns).ToList();

      var rows = table1
        .Data
        .Where(row => !RowFoundIn(row, table2.Data, indices));

      return new MemoryTable(Enumerable.Repeat(table1.Columns, 1).Concat(rows));
    }

The two private GetIndices methods disappear and the unit tests are still green.

Main program

I've finally come to the changes I need to make to the main program to make the last failing test pass:

  internal class Program
  {
    private static void Main(string[] args)
    {
      if (args.Length < 3)
      {
        Console.WriteLine("Syntax: CompareExcelFiles file1 file2 column [column...]");
        Console.WriteLine("        file1     first file to compare");
        Console.WriteLine("        file2     second file to compare");
        Console.WriteLine("        column    name of column(s) to sort / compare by");

        return;
      }

      var excel = new ExcelLoader();
      var table1 = excel.Load(args[0]);
      var table2 = excel.Load(args[1]);

      var columns = args.Skip(2).ToArray();
      var comparer = new TableComparer(columns);

      var diff1 = comparer.Compare(table1, table2);
      var diff2 = comparer.Compare(table2, table1);

      DumpResult(1, args[0], diff1, table1.RowCount, columns);
      DumpResult(2, args[1], diff2, table2.RowCount, columns);
    }

    //

    private static void DumpResult(int fileIndex, string fileName, Table diff, int totalRows, string[] columns)
    {
      Console.WriteLine("** {0}: {1} ** --- {2} distinct rows out of {3}", fileIndex, fileName, diff.RowCount, totalRows);
      diff.Dump(columns, Console.WriteLine);
      Console.WriteLine();
    }
  }

I had to add the Dump method the Table interface to make this work:

  public interface Table
  {
    int RowCount { get; }
    int ColCount { get; }

    string[] Columns { get; }
    string[][] Data { get; }

    void Dump(string[] columns, Action<string> writeLine);
  }

The test… fails? Ah… when I wrote the acceptance test I had expected to sort the tables, which I never got to do. This time the test is wrong so I change it:

    [TestMethod]
    public void EndToEnd()
    {
      const string ARGS = @"..\..\..\file1.xlsx ..\..\..\file2.xlsx C A";
      const string EXPECTED_OUTPUT = @"** 1: ..\..\..\file1.xlsx ** --- 3 distinct rows out of 9
      C A
00001 2 1
00002 2 2
00003 2 3

** 2: ..\..\..\file2.xlsx ** --- 2 distinct rows out of 8
      C A
00001 3 4
00002 1 4

";

      var result = RunAndCaptureOutput(ARGS);

      Assert.AreEqual(EXPECTED_OUTPUT, result);
    }

I run the tests and now everything passes. Yay!

(This took way longer than I expected, although to be honest most of the time was spent on writing the blog article, not the code.)

The final code is available on GitHub. The classes look a bit better than in the previous attempt but not significantly so; the rewrite is probably not justified - but then again, it took less than a day even while writing this article at the same time, so it wasn't that much of a waste.

Well… that's it for now. Hope this helps.

No comments: