Contents

Kata 9

Setup instructions

  • Download the starting folder for this Kata.
  • Open a Command Prompt window and navigate to the starting folder using cd.
  • Open the starting folder for this Kata in VSCode.

Overview

In this Kata we’ll stretch our knowledge of Object Oriented programming, and learn about reading input from external sources.

Kata instructions

Currently the quiz questions are defined in code - in of DoQuizQuestions in QuizGame. This isn’t great, because we need to make changes to the code in order to add new questions, or change existing ones. We don’t want to have to change our code to add a new question, since there’s so much potential for making mistakes that might ruin the program!

It makes more sense to read the questions in from a file. In this Kata we’ll alter the quiz game so that it can read questions in from a text file.

First create a new file, called questions.txt inside the App folder. questions.txt will contain the quiz questions.

The format of questions.txt will be as follows:

  • Zero or more lines containing questions
  • Each question consists of:
    • Question text that ends with a question mark - ?
    • A space
    • The answer

A sample question file looks like this:

In what year did the Titanic sink? 1912
In what year did the Titanic II sink? 1913

Copy the sample into your questions.txt. Make sure there are no blank lines at the end of the file, or empty spaces at the end of each line.

Create a file called QuestionParser.cs inside the App folder. This class is going to be responsible for parsing the questions from the Standard Input stream. Parsing means reading some data, and converting it into a format that is more usable. In our case, we’re going to read some text from questions.txt and convert it into a C# Question object.

Create the file for QuestionParser.cs, and make it look like this:

using System;

class QuestionParser {

    public Question[] ReadQuestionsFromFile(string filename) {
        // No code yet...
    }
}

It has one public facing method, ReadQuestionsFromFile. Callers will use this method to read questions from the file provided in filename.

Reading from a file

The ReadQuestionsFromFile method needs to be able to parse a file into an array of Question objects. Let’s write the code to do that.

First, we need to read the file that filename points at. We can read the file into a string array using File.ReadAllLines. This is an operation available in System.IO, so we need to import System.IO with a using statement at the top of the file.

At the top of the file, write:

using System.IO;

Now we have access to System.IO, we need to write the code to read the file. Inside ReadQuestionsFromFile write:

string[] questionFileLines = File.ReadAllLines(filename);

File.ReadAllLines reads all the lines from a given file, and returns them as an array. Each index in the array contains a single line from the file. We store this in questionFileLines.

Next we need to iterate over the the questionFileLines array in order to parse each individual line from the file into a Question object. We’ll want to use a for loop to do this.

Looking at a single line

Before we write the for loop, let’s store the number of questions in a variable. Write:

int numberOfQuestions = questionFileLines.Length;

I’ve used the Length field on questionFileLines in order to get how many lines we have read from the file - and therefore how many questions there are in the file.

Now we can use a for loop to look at each index of questionFileLines exactly as many times as we need to read all the questions. Write:

for (int i = 0; i < numberOfQuestions; i++) {
    string input = questionFileLines[i];
}

How long is a piece of string

input now represents a single line in the file, like:

In what year did the Titanic sink? 1912

The question and answer are all in a single string. We’d like separate variables for both the question and the answer. We therefore need a way to split up this string.

First, let’s look at how C# sees a string. We can visualize the text:

in which year did the titanic sink? 1912

As a collection of individual characters:

image

image

Like arrays, individual characters in strings can be accessed by an index. Also like arrays, indexes for strings start at 0.

We know the ‘question’ part of this text ends at the ? character. In this case, the space after the ? is at index 34. However, we also know that the questions can vary in length. In order to know where to split the question from the answer, we need a way to find the index of the question mark character for any given question.

Luckily, C# provides a handy method, indexOf, to do this. Write:

int indexOfQuestionMark = input.IndexOf("?");

In this case, indexOfQuestionMark would be equal to 34.

More string jiggery-pokery

Now we can use the Substring method to grab a new string consisting of every character up until the ? character - which we know is the text of question.

The Substring method creates a new string, taking only part of the original string. It’s first argument is the index to start from, and it’s second argument is the length of the new string to create.

We want to create a new string, starting from the beginning, and ending with the ?. Therefore, write:

string questionText = input.Substring(0, indexOfQuestionMark + 1);

Notice that we need to use indexOfQuestionMark + 1. To understand why, look again at the above diagram. We can see the ? character is at the 33rd index, but is actually the 34th character in the string.

Substring asks for the length of the new string to create. Therefore, if we just used indexOfCharacter as the length - in this case 33, we would get the following:

image

image

It’s missing the ? - because index 33 is actually the 34th character.

We have to do indexOfQuestionMark + 1 because Substring is looking for the length of the string to create. indexOfQuestionMark is actually one less than the length of the new string we want to create.

Parsing the answer

Next we need to parse the answer. This requires the use of Substring again:

string answerText = input.Substring(indexOfQuestionMark + 2);

This time we’ve used Substring slightly differently. Substring can be called with either one, or two arguments - technically speaking this means Substring is overloaded.

This time we want to use Substring with just one argument, which creates a sub-string from the supplied index all the way to the end of the string.

Let’s visualize this again. Given that indexOfQuestionMark is 34. If we call Substring with 34 we get:

image

Which is wrong, because it contains the ? and the space character! We actually want to start the substring at index 36 - the beginning of the answer - therefore we add 2.

Finally, create the Question object with new:

Question question = new Question(questionText, answerText);

Returning the questions

We need to return an array of Questions.

Before the for loop, create an array with a size based on numberOfQuestions. Assign each Question object in the for loop, then return it at the end.

The entire method should look like this:

public Question[] ReadQuestionsFromFile(string filename) {
    string[] questionFileLines = File.ReadAllLines(filename);
    int numberOfQuestions = questionFileLines.Length;
    Question[] questions = new Question[numberOfQuestions];

    for (int i = 0; i < numberOfQuestions; i++) {
        string input = questionFileLines[i];
        int indexOfQuestionMark = input.IndexOf("?");
        string questionText = input.Substring(0, indexOfQuestionMark + 1);
        string answerText = input.Substring(indexOfQuestionMark + 2);
        Question question = new Question(questionText, answerText);

        questions[i] = question;
    }

    return questions;
}

QuestionParser is now complete!

Using the QuestionParser

In QuizGame.cs, alter the DoQuizQuestions method to ask for a Question array as an argument:

private void DoQuizQuestions(Question[] questions)

Now we have questions, you can remove the code that sets up the array of questions at the top of DoQuizQuestions.

In StartGame, new up and use the QuestionParser. Do this before the call to GreetUser:

QuestionParser questionParser = new QuestionParser();
Question[] questions = questionParser.ReadQuestionsFromFile("questions.txt");

GreetUser();
// Existing code ...

if(isAgeValid == true) {
    // Make sure to pass `questions` here
    DoQuizQuestions(questions);
} else {
    Console.WriteLine("Age is invalid...Sorry!");
}

Use dotnet run to see if it works.

Passing filename as a command line argument

Notice I’ve passed "questions.txt" as an argument to ReadQuestionsFromFile. This is only temporary - now let’s pass the filename as a second command line argument. Update StartGame to take an argument, questionsFilename. Write:

public void StartGame(string questionsFilename) {
    QuestionParser questionParser = new QuestionParser();
    Question[] questions = questionParser.ReadQuestionsFromFile(questionsFilename);

Next, amend Main in Program.cs to look for the filename as the first argument for the program. In Main in Program.cs, write:

static void Main(string[] args)
{
    string questionsFilename = args[0];
    QuizGame quizGame = new QuizGame();
    quizGame.StartGame(questionsFilename);
}

You can now call the program from the Command Prompt, providing the filename as an argument, like so:

dotnet run "questions.txt"

Copyright Mikiel Agutu 2019