Practice: adding up a folder tree
"How big is this folder?" is a question with no bottom to it. A folder holds files, and it holds folders, and those hold folders. You cannot write a loop that goes deep enough, because you do not know how deep it goes—which is exactly the shape recursion is for.
A folder is a record, record Folder(String name, List<Integer> files, List<Folder> folders) {}: a name, the sizes of the files directly inside it, and the folders directly inside it, each one the same shape again. Printed, one looks like this:
Folder[name=docs, files=[100, 250], folders=[]]
A folder with nothing in it has two empty lists, and that is the base case for nearly everything here.
The tree to test with:
root files 10, 20
docs files 100
images files 300, 40
raw files 1000
In a box, it is one new Folder(...) with the others written inside it, a folder's folders in its last list. List.of(), with nothing between the parentheses, is an empty list:
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
Everything here uses Chapters 1 to 12. Each method needs to answer for the folder it is given by asking the same question of the folders inside it.
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 build each folder with new Folder(...) and each list with List.of(...), so no method here can add to a folder's own lists: where a step builds a list, make a new one. A case written " raw".strip() is the plain name "raw" made while the program runs, so an answer that compares folder names with == fails it, as in Chapter 7's practice: compare text with .equals. Every box declares the Folder record on its first line, since each box is a whole program: keep that line.
Steps
Step 1. Write int totalSize(Folder folder), returning every byte at or below it—1470 for root, and 0 for an empty folder. Add up its own files, then add what each folder inside reports.
What it should do:
totalSize(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> 1470totalSize(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> 1340totalSize(new Folder("docs", List.of(100), List.of())) -> 100totalSize(new Folder("empty", List.of(), List.of())) -> 0
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
int totalSize(Folder folder) {
int size = 0;
for (int file : folder.files()) {
size += file;
}
for (Folder inner : folder.folders()) {
size += totalSize(inner);
}
return size;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
IO.println(totalSize(root));
}
Its own files, plus whatever each folder inside reports. No base case is written out: a folder with no folders inside simply never enters the second loop.
Step 2. Write int fileCount(Folder folder), returning how many files there are altogether, all the way down. Six for root.
What it should do:
fileCount(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> 6fileCount(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> 3fileCount(new Folder("empty", List.of(), List.of())) -> 0fileCount(new Folder("a", List.of(), List.of(new Folder("b", List.of(), List.of())))) -> 0
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
int fileCount(Folder folder) {
int count = folder.files().size();
for (Folder inner : folder.folders()) {
count += fileCount(inner);
}
return count;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
IO.println(fileCount(root));
}
The same shape as step 1, with size() in place of adding the sizes up. Most recursive methods over a tree look like this.
Step 3. Write int depth(Folder folder), returning how many levels deep the tree goes. A folder with no folders inside is depth 1, so root is 3.
What it should do:
depth(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> 3depth(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> 2depth(new Folder("docs", List.of(100), List.of())) -> 1depth(new Folder("empty", List.of(), List.of())) -> 1depth(new Folder("wide", List.of(), List.of(new Folder("x", List.of(), List.of()), new Folder("y", List.of(), List.of()), new Folder("z", List.of(), List.of())))) -> 2depth(new Folder("d1", List.of(), List.of(new Folder("d2", List.of(), List.of(new Folder("d3", List.of(), List.of(new Folder("d4", List.of(), List.of())))))))) -> 4
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
int depth(Folder folder) {
int deepest = 0;
for (Folder inner : folder.folders()) {
int innerDepth = depth(inner);
if (innerDepth > deepest) {
deepest = innerDepth;
}
}
return 1 + deepest;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
IO.println(depth(root));
}
One for this folder, plus the deepest of what is inside—not how many are inside. On this tree those two happen to agree everywhere, so Check's wide and deep cases are there to pull them apart.
Step 4. Write ArrayList<String> folderNames(Folder folder), returning every folder name, the folder itself first, then each subtree in order: root, docs, images, raw.
What it should do:
folderNames(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> new ArrayList<>(List.of("root", "docs", "images", "raw"))folderNames(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> new ArrayList<>(List.of("images", "raw"))folderNames(new Folder("empty", List.of(), List.of())) -> new ArrayList<>(List.of("empty"))
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
ArrayList<String> folderNames(Folder folder) {
ArrayList<String> names = new ArrayList<>();
names.add(folder.name());
for (Folder inner : folder.folders()) {
names.addAll(folderNames(inner));
}
return names;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
IO.println(folderNames(root));
}
This folder first, then each subtree in full: addAll puts every name a folder inside gives back on the end. The version that lists only the folders one level down misses raw entirely.
Step 5. Write int largestFile(Folder folder), returning the size of the biggest file anywhere below. The biggest one is three levels down, so this cannot only look at its own files. A tree with no files has no biggest file, and 0 cannot say so, since a file can be empty. A size is never negative, so -1 could stand in, as it did for "no score" in Chapter 6's project. But your fileCount from step 2 already tells you whether there are any files, so the caller checks before it calls: if (fileCount(root) > 0). Check never hands it a tree with no files, though a folder in it may hold none of its own.
What it should do:
largestFile(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> 1000largestFile(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> 1000largestFile(new Folder("docs", List.of(100), List.of())) -> 100largestFile(new Folder("a", List.of(), List.of(new Folder("b", List.of(7), List.of())))) -> 7largestFile(new Folder("c", List.of(5), List.of(new Folder("d", List.of(), List.of())))) -> 5largestFile(new Folder("zero", List.of(0), List.of())) -> 0
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
int fileCount(Folder folder) {
int count = folder.files().size();
for (Folder inner : folder.folders()) {
count += fileCount(inner);
}
return count;
}
int largestFile(Folder folder) {
int biggest = 0;
for (int size : folder.files()) {
if (size > biggest) {
biggest = size;
}
}
for (Folder inner : folder.folders()) {
int innerBiggest = largestFile(inner);
if (innerBiggest > biggest) {
biggest = innerBiggest;
}
}
return biggest;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
if (fileCount(root) > 0) {
IO.println(largestFile(root));
} else {
IO.println("No files");
}
}
Start at 0, and let every file of its own and every folder's answer beat it. A folder inside with no files answers 0, which beats nothing. Starting at 0 is safe because the caller has checked that a file exists somewhere: a tree with no files would answer 0 as well, the same as a tree whose only file is empty, and only the caller's fileCount(root) > 0 tells those apart.
Step 6. Write Folder findFolder(Folder folder, String name), returning the folder record with that name, searching the whole tree, or null if there is no such folder.
What it should do:
findFolder(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of()))))), "raw") -> new Folder("raw", List.of(1000), List.of())findFolder(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of()))))), "root") -> new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))findFolder(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of()))))), "docs") -> new Folder("docs", List.of(100), List.of())findFolder(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of()))))), "images") -> new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))findFolder(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of()))))), "nowhere") -> nullfindFolder(new Folder("empty", List.of(), List.of()), "raw") -> nullfindFolder(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of()))))), " raw".strip()) -> new Folder("raw", List.of(1000), List.of())
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
Folder findFolder(Folder folder, String name) {
if (folder.name().equals(name)) {
return folder;
}
for (Folder inner : folder.folders()) {
Folder found = findFolder(inner, name);
if (found != null) {
return found;
}
}
return null;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
IO.println(findFolder(root, "raw"));
IO.println(findFolder(root, "nowhere"));
}
Stop as soon as something is found, and keep looking otherwise: null from a folder inside means "not in there," so the loop goes on to the next one. Names compare with .equals. Checking only the folders one level down finds docs and images but never raw, which is two levels down.
Step 7. Write ArrayList<Integer> allSizes(Folder folder), returning a flat list of every file size, in the same order as step 4 visits the folders.
What it should do:
allSizes(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> new ArrayList<>(List.of(10, 20, 100, 300, 40, 1000))allSizes(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> new ArrayList<>(List.of(300, 40, 1000))allSizes(new Folder("empty", List.of(), List.of())) -> new ArrayList<>()
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
ArrayList<Integer> allSizes(Folder folder) {
ArrayList<Integer> sizes = new ArrayList<>(folder.files());
for (Folder inner : folder.folders()) {
sizes.addAll(allSizes(inner));
}
return sizes;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
IO.println(allSizes(root));
}
new ArrayList<>(folder.files()) copies the folder's own sizes into a new list that can grow; the folder's own list, made by List.of, cannot, and should not. Then each folder inside adds its sizes on the end, in step 4's order.
Step 8. Write ArrayList<String> sizeReport(Folder folder), returning one line per folder, reading docs: 100, with each folder's own total from step 1, in the same order again. Bring in your totalSize from step 1.
What it should do:
sizeReport(new Folder("root", List.of(10, 20), List.of(new Folder("docs", List.of(100), List.of()), new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))))) -> new ArrayList<>(List.of("root: 1470", "docs: 100", "images: 1340", "raw: 1000"))sizeReport(new Folder("images", List.of(300, 40), List.of(new Folder("raw", List.of(1000), List.of())))) -> new ArrayList<>(List.of("images: 1340", "raw: 1000"))sizeReport(new Folder("empty", List.of(), List.of())) -> new ArrayList<>(List.of("empty: 0"))
Show a solution
record Folder(String name, List<Integer> files, List<Folder> folders) {}
int totalSize(Folder folder) {
int size = 0;
for (int file : folder.files()) {
size += file;
}
for (Folder inner : folder.folders()) {
size += totalSize(inner);
}
return size;
}
ArrayList<String> sizeReport(Folder folder) {
ArrayList<String> lines = new ArrayList<>();
lines.add(folder.name() + ": " + totalSize(folder));
for (Folder inner : folder.folders()) {
lines.addAll(sizeReport(inner));
}
return lines;
}
void main() {
Folder root = new Folder("root", List.of(10, 20), List.of(
new Folder("docs", List.of(100), List.of()),
new Folder("images", List.of(300, 40), List.of(
new Folder("raw", List.of(1000), List.of())))));
for (String line : sizeReport(root)) {
IO.println(line);
}
}
Two recursions working together: sizeReport walks the tree, and totalSize walks each subtree again to add it up. Slow on a big tree, and perfectly clear on this one.