A Design Problem

Given a row number and a seat letter from a boarding pass, draw the airplane cabin with that one seat marked. The starter handout gives an image of the cabin and the constants that describe the cabin's layout: how far apart the seats are, how wide the aisle is, the margins, the number of rows, and the seat letters.

The program we want has this signature:

mark-seat(row :: Number, letter :: String) -> Image

The -> Image after the parameters is the return type: it says what kind of value the function produces, the same way :: Number says what kind of value a parameter expects.

Three Planning Components

Before writing any code, we plan. A plan has three components:

Stop and think

Try this yourself before reading on. For each component, write down what you would put there, and mark the parts you aren't sure how to begin.

The Code Plan

Sometimes, a useful way to develop a code plan is to work backward from the final computation. The last step is to place a marker on the picture of the cabin. To do that, we need an x-coordinate and a y-coordinate for the seat. Each of those is a computation of its own, so each is a candidate for a helper function. In class, we wrote thist list from the bottom upwards.

Each of these helpers is small enough to write and test on its own. That is the point of creating them: mark-seat becomes a short function that combines answers we already trust.

The Constraints

The constraints capture what must be true of our input data in order for the program to meaningfully do its task. mark-seattakes a seat row and name, which are a number and a string, respectively. What needs to be true of these values?

The Testing Plan

The testing plan lists out high-level scenarios that we should test about the program we're developing. These scenarios should cover normal behaviors, edge cases (scenarios that are at the boundary between valid inputs and invalid ones), and scenarios that might be subtle due to details of the problem. For mark-seat, the following scenarios are relevant:

This is a long list for a small function, but it comes about because this particular problem is about ranges of numbers and letters, so there are a lot of boundary cases. Not all problems will have these features, as we'll soon see.

Developing the Code

With the planning done, we can proceed to write the code. Let's first write the code for the functions in the code plan, but ignoring the constraints for now. We should get something like the following:

include shared-gdrive("cabin-layout.arr", "1O12EHjbGeKBU63oLwA9oxLKityOD_g8A")

MARKER = circle(8, "solid", "red")
LEFT-AISLE = "C"

fun seat-letter-to-index(seat :: String) -> Number:
  doc: ```0-based position of a seat letter across the cabin.
       Returns -1 when the letter is not a seat letter.```
  string-index-of(LETTERS, string-to-upper(seat))
where:
  seat-letter-to-index("A") is 0
  seat-letter-to-index("D") is 3
end

fun seat-x(seat :: String) -> Number:
  doc: "convert seat name to an x-coord based on seat spacing"
  col = seat-letter-to-index(seat)
  MARGIN-X + (col * SEAT-PITCH) + (SEAT-PITCH / 2)
    + (if seat >= LEFT-AISLE: AISLE else: 0 end)
end

fun row-y(row :: Number) -> Number:
  doc: "convert row number to an y-coord based on seat spacing"
  MARGIN-Y + ((row - 1) * SEAT-PITCH) + (SEAT-PITCH / 2)
end

fun mark-seat(row :: Number, seat :: String) -> Image:
  doc: "puts a marker on the given plane seat"
  x = seat-x(seat)
  y = row-y(row)
  place-image(MARKER, x, y, CABIN)
end

Things to note:

Handling the Constraints

We turn each constraint into a corresponding function that checks the constraint and raises an error if the constraint is violated. These functions should have a strong set of where examples that illustrate the difference between valid and invalid values.

fun check-row-valid(row :: Number) -> Boolean:
  if (row >= 1) and (row <= ROWS): true
  else: raise("invalid row")
  end
where:
  check-row-valid(12) is true  # a basic example
  check-row-valid(1) is true    # boundary
  check-row-valid(0) is false   # boundary
  check-row-valid(30) is true  # boundary
  check-row-valid(31) is false # boundary
  check-row-valid(2.5) is false # additional case
end

fun check-seat-valid(seat :: String) -> Boolean:
  if (seat >= "A") and (seat <= "F"): true
  else: raise("invalid seat")
  end
where:
  check-seat-valid("C") is true    # basic example
  check-seat-valid("A") is true    # boundary
  check-seat-valid("F") is true    # boundary
  check-seat-valid("G") is false   # boundary
  check-seat-valid("") is false      # additional case
  check-seat-valid("AB") is false  # additional case
  check-seat-valid("a") is true      # additional case
end

Each of these functions has a where example that we hadn't previously articulated: non-integer numbers, empty strings, multi-letter strings, and lower-case strings. These are examples of inputs that could happen due to typos or other human error. We might not have thought of them when we outlined the testing plan, but we should add them to the function examples when we do think of them.

Did you try running the code?If you haven't already, try running the code and make sure that all of our functions are working properly. You might find it helpful to know the Pyret functions num-is-integer, string-length, and string-to-lower.

Where should we call the constraints functions?

Our code has two functions that work with the row numbers: mark-seat and row-y. Should both of them check that the row numbers are valid? Since row-y is only every called from mark-seat, it's sufficient to have mark-seat check the inputs before running the rest of the plan. The following code adds the two checks at the top of the mark-seat function:

fun mark-seat(row :: Number, seat :: String) -> Image:
  doc: "puts a marker on the given plane seat"
  check-row-valid(row)
  check-seat-valid(seat)
  x = seat-x(seat)
  y = row-y(row)
  place-image(MARKER, x, y, CABIN)
end

Stop and think

Each of the constraint checking functions return booleans, but we don't seem to do anything with the results. Don't we need to put these questions in the question position of an if expression?

Accounting for the Testing Plan

Now review the testing plan: have we accounted for everything?

It may feel like we did when we put all the where examples on the constraint checkers, but those functions only tested whether the input data was within the ranges we expected. They don't test whether the code actually puts the seat marker at the right spot on the image. How do we test that?

As we have already seen, it is easiest to test images either with visual inspection or by giving the image and a description of what you expect to an AI-based tool. We therefore can't write much by way of meaningful Pyret tests for mark-seat. We can, however, add some tests to the functions that produce the x and y coordinates.

Those tests would be a bit tedious, because to figure out the expected answers, you'd basically be evaluating the function manually to figure out the same answers that Pyret is computing. In this specific case, it seems to make more sense to just inspect these answers visually. This will not be true as we go on in the course. In general, you should make sure that everything in your testing plan is actually covered by some test.

An Additional Note: Modifying the Design

A good plan tells you where to make a change when the requirements move. Try two:

Stop and think

Modification 1: Most airlines don't have a row 13. Where do we need to edit the code to accommodate that?

Modification 2: Someone points out that this display assumes the user is standing at the back of the plane, not the front. Where would we need to edit the code to put the front of the plane at the bottom?

In both cases, the answer should be one or two helper functions, and the testing plan tells you which tests those edits will change. If the answer were "everywhere", that would be a sign the code plan had not separated the steps cleanly.