moved FuzzyMap to commons-util module
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
|
||||
import org.springframework.ide.vscode.commons.util.StringUtil;
|
||||
|
||||
/**
|
||||
* A collection of data that can be searched with a simple 'fuzzy' string
|
||||
* matching algorithm. Clients must override 'getKey' method to define how
|
||||
* a search 'key' is associated with each data item.
|
||||
* <p>
|
||||
* The collection can then be searched for items who's key matches
|
||||
* simple 'fuzzy' patterns.
|
||||
*/
|
||||
public abstract class FuzzyMap<E> implements Iterable<E> {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(FuzzyMap.class.getName());
|
||||
|
||||
public static class Match<E> {
|
||||
public double score;
|
||||
public final E data;
|
||||
private String pattern;
|
||||
|
||||
public Match(String pattern, double score, E e) {
|
||||
this.pattern = pattern;
|
||||
this.score = score;
|
||||
this.data = e;
|
||||
}
|
||||
public static <E> Match<E> getBest(Collection<Match<E>> matches) {
|
||||
double bestScore = Double.NEGATIVE_INFINITY;
|
||||
Match<E> best = null;
|
||||
for (Match<E> match : matches) {
|
||||
if (match.score>bestScore) {
|
||||
best = match;
|
||||
bestScore = match.score;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Match(score="+score+", data="+data+")";
|
||||
}
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<E> iterator() {
|
||||
return entries.values().iterator();
|
||||
}
|
||||
|
||||
private TreeMap<String,E> entries = new TreeMap<String, E>();
|
||||
|
||||
protected abstract String getKey(E entry);
|
||||
|
||||
public void add(E value) {
|
||||
//This assumes no two entries have the same id.
|
||||
String key = getKey(value);
|
||||
E existing = entries.get(key);
|
||||
if (existing==null) {
|
||||
entries.put(getKey(value), value);
|
||||
} else {
|
||||
LOG.warning(FuzzyMap.class.getName()+": Multiple entries for key "+key+" some entries discarded");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for pattern. A pattern is just a sequence of characters which have to found in
|
||||
* an entrie's key in the same order as they are in the pattern.
|
||||
* <p>
|
||||
* Note that returned list doesn't yet have elements sorted according to score (instead they
|
||||
* are sorted lexicographically thanks to the fact we use a Tree representation).
|
||||
*/
|
||||
public List<Match<E>> find(String pattern) {
|
||||
if ("".equals(pattern)) {
|
||||
//Special case because
|
||||
// 1) no need to search. Matches everything
|
||||
// 2) want to use different way of sorting / scoring. See https://issuetracker.springsource.com/browse/STS-4008
|
||||
ArrayList<Match<E>> matches = new ArrayList<Match<E>>(entries.size());
|
||||
for (E v : entries.values()) {
|
||||
matches.add(new Match<E>(pattern, 1.0, v));
|
||||
}
|
||||
return matches;
|
||||
} else {
|
||||
//TODO: optimize somehow with a smarter index? (right now searches all map entries sequentially)
|
||||
ArrayList<Match<E>> matches = new ArrayList<Match<E>>();
|
||||
for (Entry<String, E> e : entries.entrySet()) {
|
||||
String key = e.getKey();
|
||||
double score = FuzzyMatcher.matchScore(pattern, key);
|
||||
if (score!=0.0) {
|
||||
matches.add(new Match<E>(pattern, score, e.getValue()));
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the index for the longest string which is both
|
||||
* - a prefix of propertyName
|
||||
* - a prefix of some key in the map.
|
||||
* Note: If the map is empty, then this returns null, since
|
||||
* no string, not even the empty string is a prefix of a
|
||||
* key in the map.
|
||||
*/
|
||||
public String findValidPrefix(String propertyName) {
|
||||
E best = findLongestCommonPrefixEntry(propertyName);
|
||||
return best==null?null:StringUtil.commonPrefix(propertyName, getKey(best));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find property with longest common prefix for given key.
|
||||
*/
|
||||
public E findLongestCommonPrefixEntry(String propertyName) {
|
||||
//We can implementation this O(log(n)) because the properties are kept in a TreeMap which is sorted.
|
||||
//This means that entries with common prefix will occur 'next to eachother'
|
||||
//The 'best' entry must therefore be either the entry just before or just after
|
||||
//the property we are searching for.
|
||||
|
||||
Entry<String, E> ceiln = entries.ceilingEntry(propertyName);
|
||||
Entry<String, E> floor = entries.floorEntry(propertyName);
|
||||
Entry<String, E> best;
|
||||
if (floor==null || floor==ceiln) {
|
||||
best = ceiln;
|
||||
} else if (ceiln==null) {
|
||||
best = floor;
|
||||
} else {
|
||||
int floorScore = floor==null?0:StringUtil.commonPrefixLength(floor.getKey(), propertyName);
|
||||
int ceilnScore = ceiln==null?0:StringUtil.commonPrefixLength(ceiln.getKey(), propertyName);
|
||||
best = floorScore>ceilnScore ? floor : ceiln;
|
||||
}
|
||||
return best==null?null:best.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an exact match if it exists.
|
||||
*/
|
||||
public E get(String id) {
|
||||
return entries.get(id);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return entries==null || entries.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
|
||||
|
||||
public class FuzzyMapTest {
|
||||
|
||||
@Test
|
||||
public void testMatches() {
|
||||
assertMatch(true, "", "");
|
||||
assertMatch(true, "", "abc");
|
||||
assertMatch(true, "server.port", "server.port");
|
||||
assertMatch(true, "port", "server.port");
|
||||
assertMatch(true, "sport", "server.port");
|
||||
assertMatch(false, "spox", "server.port");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrder() {
|
||||
assertMatchOrder("port",
|
||||
"port",
|
||||
"server.port",
|
||||
"server.port-mapping",
|
||||
"piano.sorting"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrefixAlfaOrder() {
|
||||
//all matches are prefix matches so should come in alpha order
|
||||
assertMatchOrder("spring",
|
||||
"spring.abracdabra",
|
||||
"spring.boot",
|
||||
"spring.candel",
|
||||
"spring.shoe",
|
||||
"spring.springer"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrefixMixedOrder() {
|
||||
assertMatchOrder("spring",
|
||||
//prefix matches first
|
||||
"spring.abracdabra",
|
||||
"spring.boot",
|
||||
"spring.candel",
|
||||
"spring.shoe",
|
||||
"spring.springer",
|
||||
//non prefix matches after prefix matches in 'similarity order'
|
||||
"zspring",
|
||||
"asprouting"
|
||||
);
|
||||
}
|
||||
|
||||
public class TestMap extends FuzzyMap<String> {
|
||||
public TestMap(String... entries) {
|
||||
for (String e : entries) {
|
||||
add(e);
|
||||
}
|
||||
}
|
||||
protected String getKey(String entry) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommonPrefix() {
|
||||
String[] entries = {
|
||||
"a",
|
||||
"archipel",
|
||||
"aardappel",
|
||||
"aardbei",
|
||||
"aardvark",
|
||||
"zoroaster"
|
||||
};
|
||||
String[] expectPrefix = {
|
||||
"a",
|
||||
"a",
|
||||
"aard",
|
||||
"aard",
|
||||
"aard",
|
||||
""
|
||||
};
|
||||
for (int focusOn = 0; focusOn < entries.length; focusOn++) {
|
||||
TestMap map = new TestMap();
|
||||
for (int other = 0; other < entries.length; other++) {
|
||||
if (focusOn!=other) {
|
||||
map.add(entries[other]);
|
||||
}
|
||||
}
|
||||
String prefix = map.findValidPrefix(entries[focusOn]);
|
||||
String prefixEntry = map.findLongestCommonPrefixEntry(entries[focusOn]);
|
||||
assertEquals(expectPrefix[focusOn], prefix);
|
||||
assertTrue(prefixEntry.startsWith(prefixEntry));
|
||||
assertTrue(prefixEntry.length()>prefix.length());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommonPrefixWithExactMatch() {
|
||||
String[] entries = {
|
||||
"a",
|
||||
"archipel",
|
||||
"aardappel",
|
||||
"aardbei",
|
||||
"aardvark",
|
||||
"zoroaster"
|
||||
};
|
||||
TestMap map = new TestMap(entries);
|
||||
for (String find : entries) {
|
||||
String found = map.findLongestCommonPrefixEntry(find);
|
||||
assertEquals(find, found);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommonPrefixEmptyMap() {
|
||||
TestMap empty = new TestMap();
|
||||
assertEquals(null, empty.findValidPrefix("foo"));
|
||||
assertEquals(null, empty.findValidPrefix(""));
|
||||
assertEquals(null, empty.findLongestCommonPrefixEntry("aaa"));
|
||||
assertEquals(null, empty.findLongestCommonPrefixEntry(""));
|
||||
}
|
||||
|
||||
|
||||
private void assertMatchOrder(String pattern, String... datas) {
|
||||
TestMap map = new TestMap(datas);
|
||||
List<Match<String>> found = map.find(pattern);
|
||||
|
||||
//Note that found elements are scored but not sorted.
|
||||
Collections.sort(found, new Comparator<Match<String>>() {
|
||||
public int compare(Match<String> o1, Match<String> o2) {
|
||||
return Double.valueOf(o2.score).compareTo(o1.score);
|
||||
}
|
||||
});
|
||||
|
||||
//all the datas should be found and be in the order given.
|
||||
assertEquals(found.size(), datas.length);
|
||||
for (int i = 0; i < datas.length; i++) {
|
||||
assertEquals(datas[i], found.get(i).data);
|
||||
}
|
||||
|
||||
// also check that scores are decreasing.
|
||||
double previousScore = found.get(0).score;
|
||||
assertTrue(previousScore!=0.0);
|
||||
for (int i = 1; i < datas.length; i++) {
|
||||
String data = datas[i];
|
||||
double score = found.get(i).score;
|
||||
assertTrue("Wrong score order: '"+datas[i-1]+"'["+previousScore+"] '"+data+"' ["+score+"]", previousScore>=score);
|
||||
previousScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
private void assertMatch(boolean expect, String pattern, String data) {
|
||||
boolean actual = FuzzyMatcher.matchScore(pattern, data)!=0.0;
|
||||
assertEquals(expect, actual);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user