Composite Pattern: Folder Sizes
Problem
A file system is described by lines: depth dir name or depth file name size. The depth tells where an item is: depth 0 is the root, and an item with depth d + 1 that follows an item with depth d belongs to the nearest earlier directory with depth d. The size of a file is given; the size of a directory is the sum of all files inside it, also in sub-directories. Print name: size for every directory, in the order they appear in the input. Use the Composite pattern: files and directories share one interface with getSize().
Constraints
- 1 <= lines <= 10^4
- The first line has depth 0 and is a dir
- |size| <= 10^6
Examples
Input
0 dir root 1 file a 10 1 dir sub 2 file b 5
Output
root: 15 sub: 5
Solutions
The Composite pattern lets a group of objects be used like a single object. The directory does not ask if a child is a file or a directory: it just calls getSize(). The recursion happens by itself. The same idea is used for GUI components, menus and organisation charts.
/*
* Problem : Process lines.
* Input : Several lines.
* Output : One size per directory.
* Approach: Tree of files and directories.
* Time : O(n * depth)
* Space : O(n)
* Java : 8 or newer.
*/
import java.util.Scanner;
public class Main {
// Step 1: Files and directories share one interface. The caller does not need to know which one it has.
interface Entry {
long getSize();
}
static class FileEntry implements Entry {
private final long size;
FileEntry(long size) {
this.size = size;
}
public long getSize() {
return size;
}
}
// Step 2: A directory is also an Entry. Its size is the sum of the sizes of its children (files or directories).
static class DirEntry implements Entry {
final String name;
final java.util.List<Entry> children = new java.util.ArrayList<>();
DirEntry(String name) {
this.name = name;
}
public long getSize() {
long total = 0;
for (Entry child : children) {
total += child.getSize();
}
return total;
}
}
// This method solves the task. It receives the input and returns the answer.
static String[] solve(String[] lines) {
// Step 3: A stack of directories holds the path to the current place. stack[d] is the directory at depth d.
java.util.List<DirEntry> stack = new java.util.ArrayList<>();
java.util.List<DirEntry> all = new java.util.ArrayList<>();
for (String line : lines) {
String[] part = line.split(" ");
int depth = Integer.parseInt(part[0]);
while (stack.size() > depth) {
stack.remove(stack.size() - 1);
}
if (part[1].equals("dir")) {
DirEntry directory = new DirEntry(part[2]);
if (depth > 0) {
stack.get(depth - 1).children.add(directory);
}
stack.add(directory);
all.add(directory);
} else {
stack.get(depth - 1).children.add(new FileEntry(Long.parseLong(part[3])));
}
}
String[] output = new String[all.size()];
for (int i = 0; i < output.length; i++) {
output[i] = all.get(i).name + ": " + all.get(i).getSize();
}
return output;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
java.util.List<String> all = new java.util.ArrayList<>();
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
if (!line.isEmpty()) {
all.add(line);
}
}
String[] lines = all.toArray(new String[0]);
System.out.println(String.join("\n", solve(lines)));
}
}
Hints (1)
- What makes the recursion possible without an if for the child type?