12: Santa and Maps

Welcome

Announcements

Do you have clicker A49D477E? You need to register it in Moodle.

Continuing on

Picking up from last class!

Sometimes it’s nice to have automated tests. But before we add a couple tests, let’s just check something:

In class exercise

Does x == y? Does `x.equals(y)?

A Unit Test

OK, now we’ll write a test.

import static org.junit.Assert.*;

import org.junit.Test;

public class LocationTest {
	@Test
	public void testLocationEquals() {
		Location x = new Location(0,0);
		Location y = x;
		Location z = new Location(0,0);
		assertTrue(x == y);
		assertFalse(x == z);

		assertEquals(x, y);
		assertEquals(x, z);
	}

	@Test
	public void testLocationNotEquals() {
		assertFalse(new Location(0,0).equals(new Location(0,1)));
	}
}

A reminder about assertEquals: it uses the objects’ built-in equals() method. If you have tests failing (say, in a project?), and the expected and actual look the same, maybe you forgot to define .equals on your objects?

Anyway, let’s go back to locationsVisited. According to the problem statement, the house at (0, 0) always gets a present:

public static Set<Location> locationsVisited(String directions) {
	Set<Location> visited = new HashSet<Location>();

	visited.add(new Location(0,0));

	return visited;
}

Does this work? Let’s add a test.

@Test
public void testEmptyDirections() {
	assertEquals(new HashSet<Location>(Arrays.asList(new Location(0,0))), DeliverySimulator.locationsVisited(""));
}

Then we need to think about where the sleigh goes, and deliver a present at each stop:

	public static Set<Location> locationsVisited(String directions) {
		Set<Location> visited = new HashSet<Location>();

		visited.add(new Location(0,0));
		int x = 0;
		int y = 0;

		for (int i = 0; i < directions.length(); i++) {
			if (directions.charAt(i) == '>') {
				x += 1;
			} else if (directions.charAt(i) == '<') {
				x -= 1;
			} else if (directions.charAt(i) == '^') {
				y += 1;
			} else if (directions.charAt(i) == 'v') {
				y -= 1;
			}
			else {
				throw new IllegalArgumentException();
			}
			visited.add(new Location(x, y));
		}

		return visited;
	}

And now we add some tests:

@Test
public void testSimple() {
	assertEquals(new HashSet<Location>(Arrays.asList(new Location(0, 0), new Location(1, 0))),
			DeliverySimulator.locationsVisited(">"));
}

@Test
public void testFour() {
	assertEquals(
			new HashSet<Location>(
					Arrays.asList(new Location(0, 0), new Location(0, 1), new Location(1, 1), new Location(1, 0))),
			DeliverySimulator.locationsVisited("^>v<"));
}

You could also add checks for housesVisited, either to their own test cases (which would be more true to the spirit of unit tests) or within the above.

A new problem, and a new data structure

What if, each time Santa visited a house, he delivered a present, and we wanted to know the total number of presents delivered? How might you go about tracking this?

You could, for example, keep a List of visited locations (instead of a Set). Then you could count the number of times each location appeared. But how would you tabulate it? You want to build some kind of table, a way to connect a set of things, and for each thing, some information about it.

(0,0) | (0,1) | (1,1) | (1,0)
------|-------|-------|------
  2      1        1       1

You might create two Lists, one of Location and one of Integer, where the ith element in each corresponded. But that’s clunky, hard to program, and has the poor performance of a list for lookups. You might edit the Location to contain a counter variable. That could work; but it might cause problems in other contexts, when the thing you’re storing is doesn’t have a clear “has-a” relationship with whatever you’re adding it to. Objects, like functions, should generally do a small set of things well.

What if I told you there was a data structure that solved this problem? What if I told you you’ve already (sorta) seen it? What if I told you it could be yours for no money down, and no monthly installments?

Introducing… the Map.

Map ADT

A Map “maps” keys to values. In other words, it associates one kind of thing with another kind of thing.

The first “thing” is the key. Keys are lookup keys; you can think of them as kinda analogous to an index card, or a page number, or a URL; they are the thing we can do lookups on (though they can also be useful data in and of themselves).

The second “thing” is a value – each value is associated with a key.

So Maps model, in essence, a table (on board). For our Santa problem, we might use Locations as keys and Integers as values.

The keys in a map form a set – there can only be one of each. And each value is associated with exactly one key. Keys are unique (as they are a set), but the same value can be associated with more than one key (as shown in our table above).

And, the key values should be immutable, or at least, should not change with respect to equals, for exactly the same reason as in Sets – in fact, if you recall, Sets are implemented using Maps.

Like Sets, Maps come in two flavors you’ll likely use: HashMaps and TreeMaps, with the same constraints (about efficient hashCode and compareTo methods).

The type signature is slightly different though: Map<K, V>; the key and value types are both parameterized. This should make sense: there’s no particular reason a map would store keys and values of the same type, only that all keys are of the same type, and all values are of the same type.

There are many useful Map methods, but we’ll start with the basics that are unique to Map:

Let’s use these to come up with an answer to our previous question (“how many presents per house”).

How many?

Let’s say we only want to build the table once for a given set of directions. It makes sense, maybe, to think about encapsulating this in a class. The class is instantiated with a set of directions, then can answer questions about what results from that set of directions. Here we go:

import java.util.Map;

public class SleighTrack {
	private Map<Location, Integer> locationCount;
	public SleighTrack(String directions) {
		locationCount = new HashMap<Location, Integer>();
		locationCount.put(new Location(0, 0), 1);
	}
}

And now let’s move our code over and adapt it to populate this map.

public SleighTrack(String directions) {
		locationCount = new HashMap<Location, Integer>();
		locationCount.put(new Location(0, 0), 1);

		int x = 0;
		int y = 0;
		for (int i = 0; i < directions.length(); i++) {
			if (directions.charAt(i) == '>') {
				x += 1;
			} else if (directions.charAt(i) == '<') {
				x -= 1;
			} else if (directions.charAt(i) == '^') {
				y += 1;
			} else if (directions.charAt(i) == 'v') {
				y -= 1;
			}
			else {
				throw new IllegalArgumentException();
			}
			Location loc = new Location(x, y);
			int t = locationCount.getOrDefault(loc, 0);
			locationCount.put(loc, t+1);
		}
	}

Let’s add a toString method and see if it works:

public String toString() {
	return locationCount.toString();
}

public static void main(String[] args) {
	SleighTrack st = new SleighTrack("^>v<");
	System.out.println(st);
}

What do we expect? Same as the table above. What do we get?

{(1, 0)=1, (0, 0)=2, (1, 1)=1, (0, 1)=1}

Bingo!

In class exercises

In our SleighTrack, how might we compute the number of houses visited, given the code written so far?

public int housesVisited() {
	// what goes here?
}

In our SleighTrack, how might we find the set of houses visited, given the code written so far?

public Set<Location> locationsVisited() {
	// what goes here?
}

In our SleighTrack, how might we find the number of presents delivered to a house?

public int numberPresents(Location loc) {
	return locationCount.getOrDefault(loc, 0);
}

On Map generality

Maps need not just be used to count things; they can be used to associate any (relatively immutable) set of keys with any values you like. For example, you could store: