Skip to content

Commit

Permalink
Merge pull request #4020 from seelchen/feature/sort-relevance-score
Browse files Browse the repository at this point in the history
Rework sort by relevance use score
  • Loading branch information
VishalNehra authored Jan 11, 2024
2 parents 9a11e3e + c39aab3 commit c4afb99
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import com.amaze.filemanager.filesystem.files.sort.DirSortBy
import com.amaze.filemanager.filesystem.files.sort.SortBy
import com.amaze.filemanager.filesystem.files.sort.SortType
import java.lang.Long
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit

/**
* [Comparator] implementation to sort [LayoutElementParcelable]s.
Expand All @@ -48,25 +50,44 @@ class FileListSorter(
}
} else {
Comparator { o1, o2 ->
// Sorts in a way that least relevant is first
val comparator = compareBy<ComparableParcelable> {
// first we compare by the match percentage of the name
searchTerm.length.toDouble() / it.getParcelableName().length.toDouble()
}.thenBy {
// if match percentage is the same, we compare if the name starts with the match
it.getParcelableName().startsWith(searchTerm, ignoreCase = true)
}.thenBy { file ->
// if the match in the name could a word because it is surrounded by separators, it could be more relevant
// e.g. "my-cat" more relevant than "mysterious"
file.getParcelableName().split('-', '_', '.', ' ').any {
val currentTime = Date().time
val comparator = compareBy<ComparableParcelable> { item ->
// the match percentage of the search term in the name
val matchPercentageScore =
searchTerm.length.toDouble() / item.getParcelableName().length.toDouble()

// if the name starts with the search term
val startScore =
item.getParcelableName().startsWith(searchTerm, ignoreCase = true).toInt()

// if the search term is surrounded by separators
// e.g. "my-cat" more relevant than "mysterious" for search term "my"
val wordScore = item.getParcelableName().split('-', '_', '.', ' ').any {
it.contentEquals(
searchTerm,
ignoreCase = true
)
}.toInt()

val modificationDate = item.getDate()
// the time difference as minutes
val timeDiff =
TimeUnit.MILLISECONDS.toMinutes(currentTime - modificationDate)
// 30 days as minutes
val relevantModificationPeriod = TimeUnit.DAYS.toMinutes(30)
val timeScore = if (timeDiff < relevantModificationPeriod) {
// if the file was modified within the last 30 days, the recency is normalized
(relevantModificationPeriod - timeDiff) /
relevantModificationPeriod.toDouble()
} else {
// for all older modification time, the recency doesn't change the relevancy
0.0
}
}.thenBy { file ->
// sort by modification date as last resort
file.getDate()

return@compareBy 1.2 * matchPercentageScore +
0.7 * startScore +
0.7 * wordScore +
0.6 * timeScore
}
// Reverts the sorting to make most relevant first
comparator.compare(o1, o2) * -1
Expand All @@ -77,6 +98,8 @@ class FileListSorter(
/** Constructor for convenience if there is no searchTerm */
constructor(dirArg: DirSortBy, sortType: SortType) : this(dirArg, sortType, null)

private fun Boolean.toInt() = if (this) 1 else 0

private fun isDirectory(path: ComparableParcelable): Boolean {
return path.isDirectory()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import java.util.Date
import java.util.concurrent.TimeUnit

/**
* because of test based on mock-up, extension testing isn't tested so, assume all extension is
Expand Down Expand Up @@ -1397,13 +1399,13 @@ class FileListSorterTest {
/**
* Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2,
* both start with search term, both contain the search term as a word and file1 date is more recent,
* result is positive
* result is negative
*
* Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc"
* compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc",
* both contain "abc" as word and file1 date is more recent
*
* Expected: return positive integer
* Expected: return negative integer
*/
@Test
fun testSortByRelevanceWithFile1MoreRecent() {
Expand All @@ -1412,6 +1414,7 @@ class FileListSorterTest {
SortType(SortBy.RELEVANCE, SortOrder.ASC),
"abc"
)
val currentTime = Date().time
val file1 = LayoutElementParcelable(
ApplicationProvider.getApplicationContext(),
"abc.efg",
Expand All @@ -1421,7 +1424,7 @@ class FileListSorterTest {
"100",
123L,
true,
"1234",
(currentTime - TimeUnit.MINUTES.toMillis(5)).toString(),
false,
false,
OpenMode.UNKNOWN
Expand All @@ -1435,24 +1438,24 @@ class FileListSorterTest {
"101",
124L,
true,
"1235",
(currentTime - TimeUnit.MINUTES.toMillis(10)).toString(),
false,
false,
OpenMode.UNKNOWN
)
Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong())
Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong())
}

/**
* Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2,
* both start with search term, both contain the search term as a word and file2 date is more recent,
* result is negative
* result is positive
*
* Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc"
* compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc",
* both contain "abc" as word and file2 date is more recent
*
* Expected: return negative integer
* Expected: return positive integer
*/
@Test
fun testSortByRelevanceWithFile2MoreRecent() {
Expand All @@ -1461,6 +1464,7 @@ class FileListSorterTest {
SortType(SortBy.RELEVANCE, SortOrder.ASC),
"abc"
)
val currentTime = Date().time
val file1 = LayoutElementParcelable(
ApplicationProvider.getApplicationContext(),
"abc.efg",
Expand All @@ -1470,7 +1474,7 @@ class FileListSorterTest {
"100",
123L,
true,
"1235",
(currentTime - TimeUnit.MINUTES.toMillis(10)).toString(),
false,
false,
OpenMode.UNKNOWN
Expand All @@ -1484,17 +1488,17 @@ class FileListSorterTest {
"101",
124L,
true,
"1234",
(currentTime - TimeUnit.MINUTES.toMillis(5)).toString(),
false,
false,
OpenMode.UNKNOWN
)
Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong())
Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong())
}

/**
* Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2,
* both start with search term, both contain the search term as a word and file2 date is more recent,
* both start with search term, both contain the search term as a word and date is same,
* result is zero
*
* Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc"
Expand Down Expand Up @@ -1540,4 +1544,63 @@ class FileListSorterTest {
)
Assert.assertEquals(0, fileListSorter.compare(file1, file2).toLong())
}

/**
* Purpose: when sort is [SortBy.RELEVANCE], if file2 matches the search term more than file1
* and file2 date is more recent, but file1 starts with search term and contains the
* search term as a word, the result is negative.
*
* Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc"
* compare(file1,file2) file2 title matches "abc" more than file1 title and is more recent both start with "abc",
* both contain "abc" as word and the date of both is the same
*
* Expected: return negative integer
*/
@Test
fun testSortByRelevanceWhole() {
val fileListSorter = FileListSorter(
DirSortBy.NONE_ON_TOP,
SortType(SortBy.RELEVANCE, SortOrder.ASC),
"abc"
)
val currentTime = Date().time

// matches 3/10
// starts with search term
// contains search as whole word
// modification time is less recent
val file1 = LayoutElementParcelable(
ApplicationProvider.getApplicationContext(),
"abc.efghij",
"C:\\AmazeFileManager\\abc.efghij",
"user",
"symlink",
"100",
123L,
true,
(currentTime - TimeUnit.MINUTES.toMillis(10)).toString(),
false,
false,
OpenMode.UNKNOWN
)
// matches 3/6
// doesn't start with search term
// doesn't contain as whole word
// modification time is more recent
val file2 = LayoutElementParcelable(
ApplicationProvider.getApplicationContext(),
"EFGABC",
"C:\\AmazeFileManager\\EFGABC",
"user",
"symlink",
"101",
124L,
true,
(currentTime - TimeUnit.MINUTES.toMillis(5)).toString(),
false,
false,
OpenMode.UNKNOWN
)
Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong())
}
}

0 comments on commit c4afb99

Please sign in to comment.