App 2: Handling Delete

The “Delete” use case is very similar to the “Add” case, but instead of having a string after the a command, there will be an integer after the d command. To be clear, delete commands look like this:

d 1
d 3

Those commands mean, “Delete the first task” and “Delete the third task,” respectively.

Handling the input

Just like the last lesson, the first thing we need to do is to separate the delete command — the letter d — from the rest of the string. Again we do this with the drop method:

"d 1".drop(2)    // "1"
"d 3".drop(2)    // "3"

and then we use this approach to pass that string to a handleDelete function:

case del if del.startsWith("d ") => 
    handleDelete(del.drop(2))

handleDelete

Now I need to write a handleDelete function, and I already know three things about:

  • It’s going to take a String as input
  • It needs to attempt to delete that task from the database, using the task-id that the user sees
  • Because it’s going to access the database, it probably needs to return a Try

Therefore, I can start to sketch its signature like this:

def handleAdd(taskIdString: String): Try

Because this function takes a string that represents the task-id that the user sees — such as "1" or "3" — the function’s algorithm should be:

  • Attempt to convert the given string to an integer.
  • If that fails, the function should short-circuit at that time.
  • If the conversion works, subtract 1 from that value to get the real index. This is because Scala lists are 0-based, just like arrays and lists in other programming languages. (What I mean by this is that the user sees the first task as number 1, so they type that. And that 1 corresponds to index 0 of a sequence.)
  • This function should return the number of rows that are deleted. Because I’m only letting users delete one task, this should always be 1.
  • As usual, because we’re accessing the outside world, the function should return a Try that wraps that integer.

Here’s the implementation of that algorithm, with a few comments added:

def handleDelete(taskIdString: String): Try[Int] = Try {
    // convert the input String to an Int. this attempt will throw
    // an exception if this fails, so the code will short-circuit
    // here, and a Failure will be returned, wrapped around the
    // exception:
    val taskId = taskIdString.toInt
    // the task-id we show starts with 1, so subtract 1 here,
    // because sequences are 0-based:
    val maybeCount = db.delete(taskId - 1)
    maybeCount.get
}

And to make it easier to read, here’s that function again without all those comments:

def handleDelete(taskIdString: String): Try[Int] = Try {
    val taskId: Int = taskIdString.toInt
    val maybeCount = db.delete(taskId - 1)
    maybeCount.get
}

I almost never call the get method on a Try value, but I do it for two reasons here:

  • The handleDelete function is wrapped in a Try, so it will return a Failure if get throws an exception. And if it throws an exception, that means something went wrong inside delete.
  • I believe the code is relatively easy to read for someone who’s new to Scala

As usual, you can shorten that function, if desired:

def handleDelete(taskIdString: String): Try[Int] = Try {
    val taskId: Int = taskIdString.toInt
    db.delete(taskId - 1).get
}

Back to the handleUserInput case

And with handleDelete now working, its case could be finished here:

case del if del.startsWith("d ") => 
    handleDelete(del.drop(2))

But again, I know that I want to see my list of to-do items after a delete command, so I add the handleView call here:

case del if del.startsWith("d ") => 
    handleDelete(del.drop(2))
    handleView()

And because handleView returns Try[Unit], the return type of this case match the return type of handleUserInput. Now handleUserInput’s match expression has these cases:

def handleUserInput(input: String): Try[Unit] = input match
    case "q" => 
        Try(System.exit(0))
    case "h" => 
        IOHelper.showHelp()
    case "v" | "l" => 
        handleView()
    case add if add.startsWith("a ") => 
        handleAdd(add.drop(2))
        handleView()
    case del if del.startsWith("d ") =>      // <== THE NEW CODE
        handleDelete(del.drop(2))
        handleView()
    // more cases here ...

Now we just have one more case to handle.