Groundwork Groundwork

Practice: a playlist manager

A playlist is two lists that travel together: the titles, and how long each one runs in seconds. Same length, same order, so titles.get(2) and seconds.get(2) are the same song. You are going to write the handful of methods a playlist app actually needs.

Everything here uses Chapters 1 to 7: ArrayList, get, size(), add, contains, subList, Collections.sort and loops.

The test playlist:

TitleSeconds
Ghost Town210
Ribbons185
Anvil240

No method here should change the list it is given. A method that quietly rearranges its caller's playlist is a bug that shows up three screens later, so where something needs to grow or be reordered, build a new list.

Each step names the method exactly: its return type, its name and its parameters. Check calls it with the cases under "What it should do," and those cases write each list in place, as new ArrayList<>(List.of("Anvil", "Ribbons")), or new ArrayList<>() for an empty one. Step 4's Check is the exception: it runs the program, because a change to the caller's list shows only in what main prints. One of step 7's cases passes " Ribbons".strip(), which is the plain word "Ribbons" made while the program runs: a separate String object from the "Ribbons" in the list, with the same letters. So an answer that compares titles with == fails that case, for the reason the end of Chapter 7's Common mistakes gives: == asks whether two variables hold the same object. Compare text with .equals.

Steps

Step 1. Write int totalSeconds(ArrayList<Integer> seconds), returning the total running time. An empty playlist totals 0.

What it should do:

  • totalSeconds(new ArrayList<>(List.of(210, 185, 240))) -> 635
  • totalSeconds(new ArrayList<>()) -> 0
  • totalSeconds(new ArrayList<>(List.of(90))) -> 90
Show a solution
int totalSeconds(ArrayList<Integer> seconds) {
    int total = 0;
    for (int length : seconds) {
        total += length;
    }
    return total;
}

void main() {
    ArrayList<Integer> seconds = new ArrayList<>(List.of(210, 185, 240));
    IO.println(totalSeconds(seconds));
}

An accumulator over the list. The empty case falls out for free, because the total starts at 0 and the loop never runs.

Step 2. Write double averageSeconds(ArrayList<Integer> seconds), returning the mean length, rounded to one decimal place.

What it should do:

  • averageSeconds(new ArrayList<>(List.of(210, 185, 240))) -> 211.7
  • averageSeconds(new ArrayList<>(List.of(100, 200))) -> 150.0
  • averageSeconds(new ArrayList<>(List.of(90))) -> 90.0
Show a solution
double averageSeconds(ArrayList<Integer> seconds) {
    int total = 0;
    for (int length : seconds) {
        total += length;
    }
    return Math.round((double) total / seconds.size() * 10) / 10.0;
}

void main() {
    ArrayList<Integer> seconds = new ArrayList<>(List.of(210, 185, 240));
    IO.println(averageSeconds(seconds));
}

Divide by size(), not by the number of songs you happen to be testing with, and cast first, so the division keeps its fraction. Then Chapter 3's rounding: times 10, Math.round, divided by 10.0.

Step 3. Write String longestTitle(ArrayList<String> titles), returning the title with the most characters—the longest name, not the longest song. The playlist always has at least one song.

What it should do:

  • longestTitle(new ArrayList<>(List.of("Anvil", "Ghost Town", "Ribbons"))) -> "Ghost Town"
  • longestTitle(new ArrayList<>(List.of("Ribbons"))) -> "Ribbons"
  • longestTitle(new ArrayList<>(List.of("Anvil", "Ribbons"))) -> "Ribbons"
Show a solution
String longestTitle(ArrayList<String> titles) {
    String best = titles.get(0);
    for (String title : titles) {
        if (title.length() > best.length()) {
            best = title;
        }
    }
    return best;
}

void main() {
    ArrayList<String> titles = new ArrayList<>(List.of("Ghost Town", "Ribbons", "Anvil"));
    IO.println(longestTitle(titles));
}

The largest-so-far loop, comparing lengths. Starting from the first title, not from an empty string, means the answer is always a real title.

Step 4. Write ArrayList<String> playlistWith(ArrayList<String> titles, String title), returning a new list with the title added at the end, and leaving the original list untouched: copy it with new ArrayList<>(titles), then add. Check runs the program, and its second line, the original list, must still show three songs.

Show a solution
ArrayList<String> playlistWith(ArrayList<String> titles, String title) {
    ArrayList<String> longer = new ArrayList<>(titles);
    longer.add(title);
    return longer;
}

void main() {
    ArrayList<String> titles = new ArrayList<>(List.of("Ghost Town", "Ribbons", "Anvil"));
    IO.println(playlistWith(titles, "Lanterns"));
    IO.println(titles);
}

Copy first, so the caller's playlist is left alone: main prints the original afterward, and it still has three songs. titles.add(title) would have changed the caller's list, since titles holds the same object the caller passed in.

Step 5. Write ArrayList<String> firstThree(ArrayList<String> titles), returning a new list of the first three titles. A playlist with fewer than three songs should come back whole, with no exception.

What it should do:

  • firstThree(new ArrayList<>(List.of("a", "b", "c", "d"))) -> new ArrayList<>(List.of("a", "b", "c"))
  • firstThree(new ArrayList<>(List.of("a"))) -> new ArrayList<>(List.of("a"))
  • firstThree(new ArrayList<>()) -> new ArrayList<>()
Show a solution
ArrayList<String> firstThree(ArrayList<String> titles) {
    if (titles.size() <= 3) {
        return new ArrayList<>(titles);
    }
    return new ArrayList<>(titles.subList(0, 3));
}

void main() {
    ArrayList<String> titles = new ArrayList<>(List.of("Ghost Town", "Ribbons", "Anvil"));
    titles.add("Lanterns");
    IO.println(firstThree(titles));
}

subList(0, 3) stops the program with an exception when the list is shorter than three, so a short playlist is copied whole before it gets there. Each return hands back a new list, never the caller's.

Step 6. Write boolean hasSong(ArrayList<String> titles, String title), returning true or false.

What it should do:

  • hasSong(new ArrayList<>(List.of("Anvil", "Ribbons")), "Ribbons") -> true
  • hasSong(new ArrayList<>(List.of("Anvil", "Ribbons")), "Nope") -> false
  • hasSong(new ArrayList<>(), "Anvil") -> false
  • hasSong(new ArrayList<>(List.of("Anvil", "Ribbons")), "Anvil") -> true
Show a solution
boolean hasSong(ArrayList<String> titles, String title) {
    return titles.contains(title);
}

void main() {
    ArrayList<String> titles = new ArrayList<>(List.of("Ghost Town", "Ribbons", "Anvil"));
    IO.println(hasSong(titles, "Anvil"));
    IO.println(hasSong(titles, "Lanterns"));
}

contains asks whether the list holds the title, and its answer is already the true or false the method returns.

Step 7. Write ArrayList<Integer> positions(ArrayList<String> titles, String title), returning a list of every index where that title appears, since a playlist may well repeat a song. An absent title gives an empty list.

What it should do:

  • positions(new ArrayList<>(List.of("Ribbons", "Anvil", "Ribbons")), "Ribbons") -> new ArrayList<>(List.of(0, 2))
  • positions(new ArrayList<>(List.of("Anvil")), "Anvil") -> new ArrayList<>(List.of(0))
  • positions(new ArrayList<>(List.of("Anvil")), "Ribbons") -> new ArrayList<>()
  • positions(new ArrayList<>(List.of("Ribbons", "Anvil", "Ribbons")), " Ribbons".strip()) -> new ArrayList<>(List.of(0, 2))
Show a solution
ArrayList<Integer> positions(ArrayList<String> titles, String title) {
    ArrayList<Integer> found = new ArrayList<>();
    for (int i = 0; i < titles.size(); i++) {
        if (titles.get(i).equals(title)) {
            found.add(i);
        }
    }
    return found;
}

void main() {
    ArrayList<String> titles = new ArrayList<>(List.of("Ribbons", "Anvil", "Ribbons"));
    IO.println(positions(titles, "Ribbons"));
}

A counting loop gives the index and the title together, and the filtering pattern keeps the indexes that match. Titles are text, so they are compared with .equals. The last case makes its title at run time, with strip(), so it is a separate String object with the same letters: .equals matches it, and == would not.

Step 8. Write ArrayList<String> byLength(ArrayList<String> titles, ArrayList<Integer> seconds), returning the titles ordered shortest song first; two songs of the same length stay in their playlist order. There is no way yet to sort one list by another (Chapter 9 has one), so build the answer by hand: copy both lists, then, until the copies are empty, find the shortest length left, add its title to the answer, and remove that length and its title from the copies.

What it should do:

  • byLength(new ArrayList<>(List.of("Anvil", "Ribbons", "Ghost Town")), new ArrayList<>(List.of(240, 185, 210))) -> new ArrayList<>(List.of("Ribbons", "Ghost Town", "Anvil"))
  • byLength(new ArrayList<>(List.of("a", "b")), new ArrayList<>(List.of(2, 1))) -> new ArrayList<>(List.of("b", "a"))
  • byLength(new ArrayList<>(), new ArrayList<>()) -> new ArrayList<>()
  • byLength(new ArrayList<>(List.of("a", "b", "c")), new ArrayList<>(List.of(200, 100, 200))) -> new ArrayList<>(List.of("b", "a", "c"))
Show a solution
ArrayList<String> byLength(ArrayList<String> titles, ArrayList<Integer> seconds) {
    ArrayList<String> titlesLeft = new ArrayList<>(titles);
    ArrayList<Integer> secondsLeft = new ArrayList<>(seconds);
    ArrayList<String> ordered = new ArrayList<>();

    while (!secondsLeft.isEmpty()) {
        int shortest = 0;
        for (int i = 1; i < secondsLeft.size(); i++) {
            if (secondsLeft.get(i) < secondsLeft.get(shortest)) {
                shortest = i;
            }
        }
        ordered.add(titlesLeft.get(shortest));
        titlesLeft.remove(shortest);
        secondsLeft.remove(shortest);
    }
    return ordered;
}

void main() {
    ArrayList<String> titles = new ArrayList<>(List.of("Ghost Town", "Ribbons", "Anvil"));
    ArrayList<Integer> seconds = new ArrayList<>(List.of(210, 185, 240));
    IO.println(byLength(titles, seconds));
}

Each pass of the while finds the index of the shortest length left, moves that song's title to the answer, and removes the pair from both copies at the same index, so the copies stay parallel. shortest is an int, so remove takes it as an index, which is what is wanted here. <, not <=, keeps the first of two equal lengths, so ties stay in playlist order.