Code Kata 4 Part 2

Now that I have finished Code Kata 4 Part 1, I can now focus on the next part. This is all about soccer scoring. Instead of a bunch of stuff, we just want a single number, the smallest value. So Code Kata 4 Part 2 comes down to a single value.

With that out of the way, I can start the program. There are three parts to this challenge. I have tried my best to not know what they are but the heading part count gives it away. Anyway, I know that it is a re-usability challenge and let’s face it. Yesterday’s post, is not reusable. With this second part and knowing there will be a third part. I need to render this one reusable.

So what does that mean for Code Kata 4 Part 2?

I will need to do some reordering of the universe. Now, it took me less than 15 minutes to put together part 1 because it was focused. This version actually took me about 2.5 hours. Why did it take so long?

Well, the soccer data does not have a 1 to 1 relationship with the header information. So everything is one or two values off. Also, I wanted to make the third part is easier so I gave it flexibility to pull the headers I said and give me the index associated with it.

Because this moved from simple scripting mindset to actual programming mindset, I needed unit tests. The unit tests also made it easy to determine if the bloody thing was acting correctly. Score one on the love column for unit tests.

I took the common items and dropped them into a new class StringPrep. This does well, string prep.

Finally I wrote a command and control where the string prep classes can be corralled and output arranged for each challenge.

So, let’s start with the unit tests.

Form1_tests.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CodeKata4;
using NUnit.Framework;
 
namespace CodeKata4_Tests
{
    [TestFixture]
    public class Form1_tests
    {
        Dictionary<stringint> header;
 
        [SetUp]
        public void Setup()
        {
            header = new Dictionary<stringint>();
            header.Add("Dy", 0);
            header.Add("MxT", 1);
            header.Add("MnT", 2);
            header.Add("AvT", 3);
            header.Add("HDDay", 4);
            header.Add("AvDP", 5);
            header.Add("1HrP", 6);
            header.Add("TPcpn", 7);
            header.Add("WxType", 8);
            header.Add("PDir", 9);
            header.Add("AvSp", 10);
            header.Add("Dir", 11);
            header.Add("MxS", 12);
            header.Add("SkyC", 13);
            header.Add("MxR", 14);
            header.Add("MnR", 15);
            header.Add("AvSLP", 16);
 
        }
 
        [TestCase(0, "Dy"new[] { "Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(0, "Dy"new[] { "  ""Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(0, "Dy"new[] { "     ""  ""Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(0, "Dy"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(1, "MxT"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(2, "MnT"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        public void cleanString_test(int expectedIndex, string requestedHeader, string[] dirtyString)
        {
            //arrange
            StringPrep prep = new StringPrep();
            List<string> cleanString = new List<string>();
            //act
            cleanString.AddRange(prep.cleanString(dirtyString.ToList()));
            //assert
            for (int i = 0; i < cleanString.Count; i++)
            {
                if (cleanString[i] == requestedHeader)
                {
                    Assert.AreEqual(expectedIndex, i);
                }
            }
        }
 
        [TestCase(0, "Dy"new[] { "Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(0, "Dy"new[] { "  ""Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(0, "Dy"new[] { "     ""  ""Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(0, "Dy"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(1, "MxT"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(2, "MnT"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        public void getHeaderIndexList_test(int expectedIndex, string requestedheader, string[] dirtyString)
        {
            //arrange
            StringPrep prep = new StringPrep();
            Dictionary<stringint> cleanString = new Dictionary<stringint>();
            //act
            cleanString = prep.getHeaderIndexList(dirtyString.ToList());
            //assert
            foreach (var i in cleanString)
            {
                if (i.Key == requestedheader)
                {
                    Assert.AreEqual(expectedIndex, i.Value);
                }
            }
        }
 
        [TestCase(1, "Dy"new[] { "Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(1, "Dy"new[] { "  ""Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(1, "Dy"new[] { "     ""  ""Dy""MxT""MnT""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(1, "Dy"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(2, "MxT"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        [TestCase(3, "MnT"new[] { "     ""  ""Dy""   ""MxT"" ""MnT""    ""AvT""HDDay""AvDP""1HrP""TPcpn""WxType""PDir""AvSp""Dir""MxS""SkyC""MxR""MnR""AvSLP" })]
        public void getHeaderIndexList_test_fails(int expectedIndex, string requestedheader, string[] dirtyString)
        {
            //arrange
            StringPrep prep = new StringPrep();
            Dictionary<stringint> cleanString = new Dictionary<stringint>();
            //act
            cleanString = prep.getHeaderIndexList(dirtyString.ToList());
            //assert
            foreach (var i in cleanString)
            {
                if (i.Key == requestedheader)
                {
                    Assert.AreNotEqual(expectedIndex, i.Value);
                }
            }
        }
 
        [TestCase(0, "Dy"new[] { "Dy""MxT""MnT" })]
        [TestCase(1, "MxT"new[] { "Dy""MxT""MnT" })]
        [TestCase(2, "MnT"new[] { "Dy""MxT""MnT" })]
        public void getFinalHeader_test(int expectedIndex, string requestedheader
            , string[] desiredHeaders)
        {
            //arrange
            StringPrep prep = new StringPrep();
            Dictionary<stringint> headers = new Dictionary<stringint>();
 
            //act
            headers = prep.getFinalHeader(header, desiredHeaders.ToList());
            //assert
 
            foreach(var i in headers)
            {
                if(i.Key == requestedheader)
                {
                    Assert.AreEqual(expectedIndex, i.Value);
                }
            }
        }
 
        [TestCase(1, "Dy"new[] { "Dy""MxT""MnT" })]
        [TestCase(2, "MxT"new[] { "Dy""MxT""MnT" })]
        [TestCase(0, "MnT"new[] { "Dy""MxT""MnT" })]
        public void getFinalHeader_test_fails(int expectedIndex, string requestedheader
          , string[] desiredHeaders)
        {
            //arrange
            StringPrep prep = new StringPrep();
            Dictionary<stringint> headers = new Dictionary<stringint>();
 
            //act
            headers = prep.getFinalHeader(header, desiredHeaders.ToList());
            //assert
 
            foreach (var i in headers)
            {
                if (i.Key == requestedheader)
                {
                    Assert.AreNotEqual(expectedIndex, i.Value);
                }
            }
        }
 
        [TearDown]
        public void TearDown()
        {
 
        }
    }
}

So while I don’t exactly have a great place to start this explanation, the unit tests seem to be as good as anywhere. I used the header row from the weather file because it was a pretty good representation of multiple column input and a human understandable way of testing if it is reading the row correctly. Since the header and data rows are nearly the same, it worked in my opinion to test both.

In my Setup function, I created a single header row in the model of weather.dat. I chose a dictionary because I want the key to be the value of the header text and the integer to be the index for that value. It seemed tailor made for a dictionary since the values did not duplicate. I could have created a data storage object that kept that data and held it in a list but I did n’t want to add any more complexity.

My first function to test is the cleanString function. I created the new StringPrep function and sent in a dirty string with all the empty space and such and cleaned it away. Cycling through a for loop, I checked the cleaned string, in the cleanString variable, for the desired heading. When found I compared its index with the expected index. Bam, tested function.

We will get back to the unit tests but lets take a look at the StringPrep class and its cleanString function.

cleanString

public List<string> cleanString(List<string> dirtyString)
      {
          List<string> cleanString = new List<string>();
 
          foreach (string segment in dirtyString)
          {
              if (segment.Trim() != string.Empty)
              {
                  cleanString.Add(segment.Trim());
              }
          }
 
          return cleanString;
      }

So this function takes a list of strings then it frames as the dirtyString.

We prepare the return object for the cleaned up data with the cleanString declaration and instantiation. Then we hit the for loop. In this loop, which should look familiar from yesterday’s post, we clear out all the empty space strings out of the list and send the cleaned string back.

Slipping back the unit tests, we can look at the getHeaderList_test function. We do almost the same thing as the previous test, but instead of cycling with a for loop, we use a foreach. Otherwise, it is almost doing the exact same thing, only with the header, rather than the data.

Just to wrap up the unit test, the getFinalHeader_test, does the same stuff as above only testing the cleaned up string in theory rather than the whole data set.

Otherwise the rest of the test are expected failed versions of these tests.

getHeaderIndexList

public Dictionary<stringint> getHeaderIndexList(List<string>line)
       {
           Dictionary<stringint> header = new Dictionary<stringint>();
 
           foreach(string v in line)
           {
               if(v.Trim() != string.Empty)
               {
                   header.Add(v, header.Count); 
               }
           }
 
           return header;
       }

In the getHeaderIndexList function we take in the line and build the header list from that line. It is a cleaner, only for the header and it brings back a dictionary object with the column name as the key and the index as the value.

getFinalHeader

public Dictionary<stringint> getFinalHeader(Dictionary<string,int> header, List<string> desiredHeaders)
        {
            Dictionary<stringint> finalHeader = new Dictionary<stringint>();
            
            foreach(var i in desiredHeaders)
            {
                foreach(var x in header)
                {
                    if(i == x.Key)
                    {
                        finalHeader.Add(x.Key, x.Value);
                    }
                }
            }
 
            return finalHeader;
        }

So with the headers known and cleaned, its time to get the desired subset. That is pretty much what this does. I create my return value finalHeader. Then I cycle through the header collection the user wants and if the column name stored in the header is equal to the header being looked at, it goes into the final header.

So that is the StringPrep function. I really wish that was all that was left to talk about. This post is getting crazy long but we must forge on into the coding wilderness.

btnWeatherProcess_Click

private void btnWeatherProcess_Click(object sender, EventArgs e)
       {
           List<string> header = new List<string>();
           header.Add("Dy");
           header.Add("MxT");
           header.Add("MnT");
 
           processWeatherFile(txtWeatherPath.Text, header);
       }

We had to change the name of the button since we will have two separate buttons. So first we changed the name. Next we have the new code. This is simple. All the complexity hidden away. A marvel of engineering at least for this part. We create a list of string that contain the headers we want from the dat file. Then we call the processWeatherFile giving it the dat path and that header list.

processWeatherFile

While the previous function was pretty this one handles all the GUI interacts and calls all the formatting done in the StringPrep class. Hold on this is going to be a ride.

private void processWeatherFile(string path, List<string> desiredHeaders)
       {
           string output = "";
           StringPrep prep = new StringPrep();
           Dictionary<stringint> headersIndexes;
           List<string> line = new List<string>();
           List<string> cleanLine = new List<string>();
           
 
           lbResults.Items.Clear();
 
           using (StreamReader reader = new StreamReader(path))
           {
               headersIndexes = new Dictionary<stringint>();
 
               if (reader.Peek() != -1)
               {
                   headersIndexes = prep.getFinalHeader(prep.getHeaderIndexList(reader.ReadLine().Trim().Split(' ').ToList()), desiredHeaders);
 
                   foreach (var i in headersIndexes)
                   {
                       output += i.Key + "\t";
                   }
                   lbResults.Items.Add(output);
                   output = "";
               }
 
               while (reader.Peek() != -1)
               {
                   cleanLine = new List<string>();
                   cleanLine = prep.cleanString(reader.ReadLine().Trim().Split(' ').ToList());
 
                   if (cleanLine.Count > 1)
                   {
                       foreach (var i in headersIndexes)
                       {
                           output += cleanLine[i.Value] + "\t";
                       }
 
                       lbResults.Items.Add(output);
                       output = "";
                   }
               }
           }
       }

So I might have been being dramatic on this function. First we get all the things we will need declared and instantiated. The output string and StringPrep object are pretty easy to understand. The headerIndexes will have the header name as the key and the index of the column the data is found in the value. line and cleanLine are both pretty close the previous example. They hold the dirty and clean lines.

We clear out any existing results from the listsbox, open the file using a stream reader object and start working through the document.

We check the reader to make sure there is a row and read the first line into the headersIndexes. This is both the get the dirty list and refine it to clean list. From there we create an output string that has all keys values or the column names in it that we desire. Record the output to the listbox and clear up the old output so it is ready for another row.

Speaking of other rows, we then check the file for a new line and if it s present start looping through the file, line by line.

We start by cleaning the cleanLine list with a new list, and setting it to the clean string values. If there is more than a single value in the row, which all valid data has, we process through the desired headers found in the headersIndexes and create the output using the index we so carefully grabbed. Put the values into the results and clear the output for the next line.

It work pretty well.

So that brings us to the soccer data. As you can see above, I got the path and button all set for it already. Let’s look at the button click.

btnProcessSoccer

private void btnProcessSoccer_Click(object sender, EventArgs e)
        {
            List<string> header = new List<string>();
            header.Add("F");
            header.Add("A");
            header.Add("Team");
 
            processSoccerFile(txtSoccerPath.Text, header);
        }

Just like the btnWeatherProcess’s click event, this one sets the header desired and calls the process SoccerFile function with the file path of the dat and the desired headers going into it.

processSoccerFile

private void processSoccerFile(string path, List<string> desiredHeaders)
        {
            StringPrep prep = new StringPrep();
            Dictionary<stringint> headersIndexes;
            List<string> line = new List<string>();
            List<string> cleanLine = new List<string>();
            Dictionary<stringint> finalHeaders = new Dictionary<stringint>();
            string allowed = "10000";
            string scored = "1000";
            int smallest = 100;
 
            lbResults.Items.Clear();
 
            using (StreamReader reader = new StreamReader(path))
            {
                headersIndexes = new Dictionary<stringint>();
 
                if (reader.Peek() != -1)
                {
                    headersIndexes = prep.getFinalHeader(prep.getHeaderIndexList(reader.ReadLine().Trim().Split(' ').ToList()), desiredHeaders);
                }
 
                while (reader.Peek() != -1)
                {
                    cleanLine = new List<string>();
                    cleanLine = prep.cleanString(reader.ReadLine().Trim().Split(' ').ToList());
 
                    if (cleanLine.Count > 1)
                    {
                        foreach (var i in headersIndexes)
                        {
                            if (i.Key == "F")
                            {
                                scored = cleanLine[i.Value + 1].ToString();
                            }
                            else if (i.Key == "A")
                            {
                                allowed = cleanLine[i.Value + 2].ToString();
                            }
                        }
 
                        lbResults.Items.Add(scored + "\t" + allowed);
 
                        if (int.Parse(scored) - int.Parse(allowed) < smallest )
                        {
                            smallest = int.Parse(scored) - int.Parse(allowed);
                        }
                    }
                }
            }
            lbResults.Items.Add(smallest);
        }

So this and the process weather function, have quite a bit of stuff in common. I suspect when I get my third data set, I can unify all those segments together and make this simpler. But not right now, here we declare and instantiate the variables, clear the results listbox. We open the file, build the headers as before and then go into a while loop to cycle through the values line by line.

Once in the line, we continue doing the same thing and create the clean line from the dirty line list and check to make sure it has multiple items. The soccer file’s valid row all have more than 1 item. Once we get into the foreach loop things change. We look for the key F so we can assign a scored value, and A so that we can assign the allowed variable. I have a technical throw away line that list each value pair and that should be removed from the function since it is not required. Now with the way the soccer file is oriented we have missing values in the header so everything is suffering a one off error. I corrected that by give each index a +1 or +2 to make that adjustment for missing headers.

After all that work, we check if the new pair is smaller than the current smallest value. If it is then we set the new smallest, otherwise we ignore it.

Wow, that was a bunch of stuff, you never said. Does it work? Did you complete Code Kata 4 Part 2?

Finally after all that, we display the low number in the results listbox. We have the lowest number. It is a really bad soccer team. Hint, it is the difference between the last two values.

Oi, that was a bunch of stuff. How did something so easy become so hard? I blame it on dataset 3. Since I don’t know what is coming down the pipeline, I decided to build a reasonably robust file reader that and deal with the columns and ignore the multiple lengths of white space and such. I figure the third set will be simplified with this work.

After all that work, I still don’t like Code Kata 4 Part 2’s code. Yeah, it works but at what cost. Trying to code for this unknown reality ended up make two 15 minute tasks into a 2.5 hour task. I would have never done this much work for a client for such a task. Quite frankly, it would be a bad value for them. I did it here because there is a third part and the challenge has to do with reusability. Honestly, this needs a pretty through code review to simplify it down.

Anyway, here is hoping that Code Kata 4 Part 3 will be easier as a result of today’s work.

Leave a Reply

Your email address will not be published.