Pages

Wednesday, March 16, 2011

Stumbling around Google collections

I came across a situation in some code I need to modify where maps were being 'filtered' to create tailored data for different situations. I couldn't come up with the solution I wanted in the moment so I cobbled up an example to see what might work.


As a simple example:
public class FilterExample {
private Map<String, String> data = new HashMap<String, String>();
public FilterExample() {
data.put("A", "one");
data.put("B", "two");
data.put("C", "three");
data.put("D", "four");
data.put("E", "five");
}
public Map<String, String> getNewMapWithVowelKeys() {
Map<String, String> result = new HashMap<String, String>();
for (String keyVal: data.keySet()) {
if (keyVal.matches("^[aeiouAEIOU].*")) {
result.put(keyVal, data.get(keyVal));
}
}
return result;
}
public Map<String, String> getNewMapWithVowelValues() {
Map<String, String> result = new HashMap<String, String>();
for (Map.Entry<String, String> entry: data.entrySet()) {
if (entry.getValue().matches("^[aeiouAEIOU].*")) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
public Map<String, String> getNewMapWithConsonantKeys() {
Map<String, String> result = new HashMap<String, String>();
for (String keyVal: data.keySet()) {
if (keyVal.matches("^[^aeiouAEIOU].*")) {
result.put(keyVal, data.get(keyVal));
}
}
return result;
}
public Map<String, String> getNewMapWithConsonantValues() {
Map<String, String> result = new HashMap<String, String>();
for (Map.Entry<String, String> entry: data.entrySet()) {
if (entry.getValue().matches("^[^aeiouAEIOU].*")) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
}


We're looking at two different criteria (string starts with a vowel or a consonant) and two different targets -- the map key or the value.

The looping is duplicated and the determination of whether the values should be in the new map screams for a predicate.

In Google collections this is usually just a filter taking in the collection and the predicate. But 'filter' wasn't available on a Map -- it works on a collection.

Well, first I needed a predicate:
public class EntryKeyIsVowel implements Predicate<Map.Entry<String,String>> {
public boolean apply(Map.Entry<String, String> input) {
return input.getKey().matches("^[aeiouAEIOU].*");
}
public boolean equals(Object obj) {
return this.getClass() == obj.getClass();
}
}


That was easy enough.

My first try was to filter the entry set of the map and then add the filtered elements to the result:
public Map<String, String> getMapFor(Predicate<Map.Entry<String,String>> mapPredicate) {
Map<String, String> result = newHashMap();
Collection<Map.Entry<String, String>> newEntries = filter(data.entrySet(), mapPredicate);
for (Map.Entry<String, String> entry: newEntries) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}


Not too happy that I'm looping over something twice in the worst case.
Well, I can just use the predicate without using anything else from Google collections:
public Map<String, String> anotherMapFor(Predicate<Map.Entry<String,String>> mapPredicate) {
Map<String, String> result = newHashMap();
for (Map.Entry<String, String> entry: data.entrySet()) {
if (mapPredicate.apply(entry)) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}


But, I just felt there should be a better way. Asked a couple of people while I was trying different searches. Finally I found that Maps has a tranform, but it also has different filter methods! Just what I needed.

The one-liner:
public Map<String, String> transformMapFor(Predicate<Map.Entry<String,String>> mapPredicate) {
return Maps.filterEntries(data, mapPredicate);
}


That will wrap it up for now.

No comments:

Post a Comment