Use a bimap for reverse lookups on injective maps (#5681)

* Use a bimap for reverse lookups on injective maps

- A BiMap provides constant-time lookups for mapping values to keys

* Address comments

* Fix Tests
This commit is contained in:
Dylan Wylie 2018-05-08 02:46:21 +01:00 committed by Charles Allen
parent 67d0b0ee42
commit e1277d306c
3 changed files with 40 additions and 11 deletions

View File

@ -23,9 +23,9 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@ -36,6 +36,7 @@ import javax.annotation.Nullable;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -43,6 +44,7 @@ import java.util.Map;
public class MapLookupExtractor extends LookupExtractor public class MapLookupExtractor extends LookupExtractor
{ {
private final Map<String, String> map; private final Map<String, String> map;
private final Map<String, String> reverseMap;
private final boolean isOneToOne; private final boolean isOneToOne;
@ -52,8 +54,18 @@ public class MapLookupExtractor extends LookupExtractor
@JsonProperty("isOneToOne") boolean isOneToOne @JsonProperty("isOneToOne") boolean isOneToOne
) )
{ {
this.map = Preconditions.checkNotNull(map, "map");
Preconditions.checkNotNull(map, "map");
this.isOneToOne = isOneToOne; this.isOneToOne = isOneToOne;
if (this.isOneToOne) {
this.map = HashBiMap.create(map);
this.reverseMap = ((HashBiMap<String, String>) this.map).inverse();
} else {
this.map = map;
this.reverseMap = null;
}
} }
@JsonProperty @JsonProperty
@ -72,14 +84,14 @@ public class MapLookupExtractor extends LookupExtractor
@Override @Override
public List<String> unapply(final String value) public List<String> unapply(final String value)
{ {
return Lists.newArrayList(Maps.filterKeys(map, new Predicate<String>() String valueToLookup = Strings.nullToEmpty(value);
{
@Override public boolean apply(@Nullable String key)
{
return map.get(key).equals(Strings.nullToEmpty(value));
}
}).keySet());
if (this.reverseMap != null) {
String val = this.reverseMap.get(valueToLookup);
return (val != null) ? Collections.singletonList(val) : Collections.emptyList();
} else {
return Lists.newArrayList(Maps.filterKeys(map, key -> map.get(key).equals(valueToLookup)).keySet());
}
} }
@Override @Override

View File

@ -74,7 +74,7 @@ public abstract class LookupExtractor
* Null and empty are considered to be the same value = nullToEmpty(value) * Null and empty are considered to be the same value = nullToEmpty(value)
* *
* @return the list of keys that maps to value or empty list. * @return the list of keys that maps to value or empty list.
* Note that for the case of a none existing value in the lookup we have to cases either return an empty list OR list with null element. * Note that for the case of a none existing value in the lookup we have two cases either return an empty list OR list with null element.
* returning an empty list implies that user want to ignore such a lookup value. * returning an empty list implies that user want to ignore such a lookup value.
* In the other hand returning a list with the null element implies user want to map the none existing value to the key null. * In the other hand returning a list with the null element implies user want to map the none existing value to the key null.
*/ */

View File

@ -35,7 +35,7 @@ public class MapLookupExtractorTest
private final MapLookupExtractor fn = new MapLookupExtractor(lookupMap, false); private final MapLookupExtractor fn = new MapLookupExtractor(lookupMap, false);
@Test @Test
public void testUnApply() public void testNonInjectiveUnApply()
{ {
Assert.assertEquals(Arrays.asList("foo"), fn.unapply("bar")); Assert.assertEquals(Arrays.asList("foo"), fn.unapply("bar"));
Assert.assertEquals(Sets.newHashSet("null", "empty String"), Sets.newHashSet(fn.unapply(""))); Assert.assertEquals(Sets.newHashSet("null", "empty String"), Sets.newHashSet(fn.unapply("")));
@ -46,6 +46,23 @@ public class MapLookupExtractorTest
Assert.assertEquals("not existing value returns empty list", Collections.EMPTY_LIST, fn.unapply("not There")); Assert.assertEquals("not existing value returns empty list", Collections.EMPTY_LIST, fn.unapply("not There"));
} }
@Test
public void testInjectiveUnApply()
{
MapLookupExtractor injectiveFn = new MapLookupExtractor(
ImmutableMap.of("foo", "bar", "null", "", "", "empty_string"), true
);
Assert.assertEquals(Arrays.asList("foo"), injectiveFn.unapply("bar"));
Assert.assertEquals(
"Null value should be equal to empty string",
Sets.newHashSet("null"),
Sets.newHashSet(injectiveFn.unapply((String) null))
);
Assert.assertEquals(Sets.newHashSet(""), Sets.newHashSet(injectiveFn.unapply("empty_string")));
Assert.assertEquals("not existing value returns empty list", Collections.EMPTY_LIST, injectiveFn.unapply("not There"));
}
@Test @Test
public void testGetMap() public void testGetMap()
{ {