/*
* Copyright (c) 2011, 2012 Roberto Tyley
*
* This file is part of 'Agit' - an Android Git client.
*
* Agit is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Agit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/ .
*/
package com.madgag.agit.filepath;
import static java.lang.Math.min;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.quote;
import com.google.common.base.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FilePathMatcher implements Predicate<FilePath> {
private final Pattern pattern;
private final String constraint;
private final int[] matchingLetters;
private final char[] constraintUC, constraintLC;
private final int constraintLen;
private final boolean userSpecifiedPathSegments;
public FilePathMatcher(String constraint) {
this.constraint = constraint;
userSpecifiedPathSegments = constraint.contains("/");
constraintLen = constraint.length();
// OPTIM : Don't case-fold every filepath - VERY slow! Instead try both lower & upper versions of constraint
constraintUC = constraint.toUpperCase().toCharArray();
constraintLC = constraint.toLowerCase().toCharArray();
pattern = patternFor(constraint);
matchingLetters = new int[constraintLen];
}
@Override
public boolean apply(FilePath filePath) {
String p = filePath.getPath();
int pathLen = p.length();
int index = 0;
for (int i = 0; i < constraintLen; ++i) {
char uc = constraintUC[i], lc = constraintLC[i];
int uci = p.indexOf(uc, index), lci = p.indexOf(lc, index); // OPTIM : indexOf is native and fast
index = 1 + min(uci >= 0 ? uci : pathLen, lci >= 0 ? lci : pathLen);
if (index > pathLen) {
return false;
}
}
return true;
}
/**
* @return int array which will be overwritten on the next call - consume or copy before next invocation!
*/
public int[] match(CharSequence filePath) {
Matcher matcher = pattern.matcher(filePath);
if (matcher.find()) {
for (int i = 0; i < matchingLetters.length; ++i) {
matchingLetters[i] = matcher.start(i + 1);
}
return matchingLetters;
} else {
return null;
}
}
private static Pattern patternFor(CharSequence constraint) {
StringBuilder builder = new StringBuilder(".*");
for (int i = 0; i < constraint.length(); ++i) {
builder.append("(").append(quote("" + constraint.charAt(i))).append(").*?");
}
builder.append("$");
return Pattern.compile(builder.toString(), CASE_INSENSITIVE);
}
public double score(FilePath fp) {
double pathScore = scoreSegment(fp, 0);
return userSpecifiedPathSegments?pathScore:(pathScore + scoreSegment(fp, fp.getPath().lastIndexOf('/') + 1));
}
/**
* This method is a tweaked/optimised filepath-specific adaptation of the MIT-licensed string_score algorithm by
* Joshaven Potter (https://github.com/joshaven/string_score), adapted from the Java version by
* Shingo Omura (https://github.com/everpeace/string-score).
*/
private double scoreSegment(FilePath filePath, int fromIndex) {
String path = filePath.getPath();
// If the path is equal to the abbreviation, perfect match.
if (path.substring(fromIndex).equals(constraint)) {
return 1.0d;
}
//if it's not a perfect match and is empty return 0
if (constraint.length()==0) {
return 0d;
}
int fpLen = path.length();
double totalCharacterScore = 0d;
double abbreviationScore = 0d;
// Walk through abbreviation and add up scores.
int fpPos = fromIndex;
for (int i = 0; i < constraint.length(); i++) {
double characterScore;
// OPTIM : indexOf is native and fast - *much* faster than lowercasing the filepath...
int uci = path.indexOf(constraintUC[i], fpPos), lci = path.indexOf(constraintLC[i], fpPos);
int indexInString = min(uci >= 0 ? uci : fpLen, lci >= 0 ? lci : fpLen);
//If no value is found
if (indexInString == fpLen) {
return 0d;
} else {
characterScore = 0.1d;
}
// Consecutive letter & start-of-path Bonus
if (indexInString == fpPos) {
// Increase the score when matching first character of the
// remainder of the path
characterScore += 0.6d;
} else {
// Acronym Bonus
// Weighing Logic: Typing the first character of an acronym is as if you
// preceded it with two perfect character matches.
if (path.charAt(indexInString - 1) == '/') { // used to be ' '
characterScore += 0.8d;
}
}
fpPos = indexInString + 1;
totalCharacterScore += characterScore;
}
double segmentLen = fpLen - fromIndex;
double abbreviationLength = constraint.length();
abbreviationScore = totalCharacterScore / abbreviationLength;
// Reduce penalty for longer strings.
return ((abbreviationScore * (abbreviationLength / segmentLen)) + abbreviationScore) / 2;
}
}