From c39aab31a777344f7746620d9e5e10cd7af585fc Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 20 Dec 2023 22:13:50 +0100 Subject: [PATCH] rework sort by relevance to assign a score to each item --- .../filesystem/files/FileListSorter.kt | 51 ++++++++--- .../filesystem/files/FileListSorterTest.kt | 85 ++++++++++++++++--- 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt index ba9c45ee92..9bd93791a2 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt @@ -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. @@ -48,25 +50,44 @@ class FileListSorter( } } else { Comparator { o1, o2 -> - // Sorts in a way that least relevant is first - val comparator = compareBy { - // 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 { 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 @@ -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() } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt index 034a47aded..841231b640 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt @@ -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 @@ -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() { @@ -1412,6 +1414,7 @@ class FileListSorterTest { SortType(SortBy.RELEVANCE, SortOrder.ASC), "abc" ) + val currentTime = Date().time val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), "abc.efg", @@ -1421,7 +1424,7 @@ class FileListSorterTest { "100", 123L, true, - "1234", + (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), false, false, OpenMode.UNKNOWN @@ -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() { @@ -1461,6 +1464,7 @@ class FileListSorterTest { SortType(SortBy.RELEVANCE, SortOrder.ASC), "abc" ) + val currentTime = Date().time val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), "abc.efg", @@ -1470,7 +1474,7 @@ class FileListSorterTest { "100", 123L, true, - "1235", + (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), false, false, OpenMode.UNKNOWN @@ -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" @@ -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()) + } }