PM-10071 ensure that lowercase letters take priority over the upperca… (#3707)

This commit is contained in:
Dave Severns 2024-08-09 14:55:24 -04:00 committed by GitHub
parent e717183239
commit 06f6f19255
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 122 additions and 49 deletions

View file

@ -0,0 +1,67 @@
package com.x8bit.bitwarden.data.platform.util
import java.util.Locale
/**
* String [Comparator] where the characters are compared giving precedence to
* special characters.
*/
object SpecialCharWithPrecedenceComparator : Comparator<String> {
override fun compare(str1: String, str2: String): Int {
val minLength = minOf(str1.length, str2.length)
for (i in 0 until minLength) {
val char1 = str1[i]
val char2 = str2[i]
val compareResult = compareCharsSpecialCharsWithPrecedence(char1, char2)
if (compareResult != 0) {
return compareResult
}
}
// If all compared chars are the same give precedence to the shorter String.
return str1.length - str2.length
}
}
/**
* Compare two characters, where a special character is considered with higher precedence over
* letters and numbers. If both characters are a letter and they are equal ignoring the case,
* give priority to the lowercase instance. If they are both a digit or a non-equal letter
* use the default [String.compareTo] converting the chars to the [Locale] specific uppercase
* String.
*/
private fun compareCharsSpecialCharsWithPrecedence(c1: Char, c2: Char): Int {
return when {
c1.isLetterOrDigit() && !c2.isLetterOrDigit() -> 1
!c1.isLetterOrDigit() && c2.isLetterOrDigit() -> -1
c1.isLetter() && c2.isLetter() && c1.equals(other = c2, ignoreCase = true) -> {
compareLettersLowerCaseFirst(c1 = c1, c2 = c2)
}
else -> {
val upperCaseStr1 = c1.toString().uppercase(Locale.getDefault())
val upperCaseStr2 = c2.toString().uppercase(Locale.getDefault())
upperCaseStr1.compareTo(upperCaseStr2)
}
}
}
/**
* Compare two equal letters ignoring case (i.e. 'A' == 'a'), give precedence to the
* the character which is lowercase. If both [c1] and [c2] are equal and the
* same case return 0 to indicate they are the same.
*/
private fun compareLettersLowerCaseFirst(c1: Char, c2: Char): Int {
require(
value = c1.isLetter() &&
c2.isLetter() &&
c1.equals(other = c2, ignoreCase = true),
) {
"Both character must be the same letter, case does not matter."
}
return when {
!c1.isLowerCase() && c2.isLowerCase() -> 1
c1.isLowerCase() && !c2.isLowerCase() -> -1
else -> 0
}
}

View file

@ -1,38 +0,0 @@
package com.x8bit.bitwarden.data.platform.util
import java.util.Locale
/**
* Compare two characters, where a special character is considered with higher precedence over
* letters and numbers. If both characters are a letter or a digit use the default
* [Char.compareTo].
*/
private fun compareCharsSpecialCharsWithPrecedence(c1: Char, c2: Char): Int {
return when {
c1.isLetterOrDigit() && !c2.isLetterOrDigit() -> 1
!c1.isLetterOrDigit() && c2.isLetterOrDigit() -> -1
else -> c1.compareTo(c2)
}
}
/**
* String [Comparator] where the characters are compared giving precedence to
* special characters.
*/
object CompareStringSpecialCharWithPrecedence : Comparator<String> {
override fun compare(str1: String, str2: String): Int {
val uppercaseStr1 = str1.uppercase(Locale.getDefault())
val uppercaseStr2 = str2.uppercase(Locale.getDefault())
val minLength = minOf(uppercaseStr1.length, uppercaseStr2.length)
for (i in 0 until minLength) {
val char1 = uppercaseStr1[i]
val char2 = uppercaseStr2[i]
val compareResult = compareCharsSpecialCharsWithPrecedence(char1, char2)
if (compareResult != 0) {
return compareResult
}
}
// If all compared chars are the same give precedence to the shorter String.
return uppercaseStr1.length - uppercaseStr2.length
}
}

View file

@ -18,7 +18,7 @@ import com.bitwarden.vault.PasswordHistory
import com.bitwarden.vault.SecureNote
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.platform.util.CompareStringSpecialCharWithPrecedence
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherRepromptTypeJson
@ -560,7 +560,7 @@ fun FieldTypeJson.toSdkFieldType(): FieldType =
fun List<CipherView>.sortAlphabetically(): List<CipherView> {
return this.sortedWith(
comparator = { cipher1, cipher2 ->
CompareStringSpecialCharWithPrecedence.compare(cipher1.name, cipher2.name)
SpecialCharWithPrecedenceComparator.compare(cipher1.name, cipher2.name)
},
)
}

View file

@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository.util
import com.bitwarden.vault.Collection
import com.bitwarden.vault.CollectionView
import com.x8bit.bitwarden.data.platform.util.CompareStringSpecialCharWithPrecedence
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
/**
@ -33,7 +33,7 @@ fun List<SyncResponseJson.Collection>.toEncryptedSdkCollectionList(): List<Colle
fun List<CollectionView>.sortAlphabetically(): List<CollectionView> {
return this.sortedWith(
comparator = { collection1, collection2 ->
CompareStringSpecialCharWithPrecedence.compare(collection1.name, collection2.name)
SpecialCharWithPrecedenceComparator.compare(collection1.name, collection2.name)
},
)
}

View file

@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository.util
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.platform.util.CompareStringSpecialCharWithPrecedence
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@ -38,7 +38,7 @@ fun Folder.toEncryptedNetworkFolder(): FolderJsonRequest =
fun List<FolderView>.sortAlphabetically(): List<FolderView> {
return this.sortedWith(
comparator = { folder1, folder2 ->
CompareStringSpecialCharWithPrecedence.compare(folder1.name, folder2.name)
SpecialCharWithPrecedenceComparator.compare(folder1.name, folder2.name)
},
)
}

View file

@ -5,7 +5,7 @@ import com.bitwarden.send.SendFile
import com.bitwarden.send.SendText
import com.bitwarden.send.SendType
import com.bitwarden.send.SendView
import com.x8bit.bitwarden.data.platform.util.CompareStringSpecialCharWithPrecedence
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@ -133,7 +133,7 @@ private fun SendTypeJson.toSdkSendType(): SendType =
fun List<SendView>.sortAlphabetically(): List<SendView> {
return this.sortedWith(
comparator = { send1, send2 ->
CompareStringSpecialCharWithPrecedence.compare(send1.name, send2.name)
SpecialCharWithPrecedenceComparator.compare(send1.name, send2.name)
},
)
}

View file

@ -11,7 +11,7 @@ import com.bitwarden.vault.CipherView
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.util.CompareStringSpecialCharWithPrecedence
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.platform.util.subtitle
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.removeDiacritics
@ -369,7 +369,7 @@ private fun SendView.toDisplayItem(
*/
private fun List<SearchState.DisplayItem>.sortAlphabetically(): List<SearchState.DisplayItem> {
return this.sortedWith { item1, item2 ->
CompareStringSpecialCharWithPrecedence.compare(item1.title, item2.title)
SpecialCharWithPrecedenceComparator.compare(item1.title, item2.title)
}
}

View file

@ -0,0 +1,44 @@
package com.x8bit.bitwarden.data.platform.util
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
class SpecialCharWithPrecedenceComparatorTest {
@Test
fun `Sorting with comparator should return expected result of sorted string`() {
val unsortedList = listOf(
"__Za",
"z",
"___",
"1a3",
"aBc",
"__a",
"__A",
"__a",
"__4",
"Z",
"__3",
"Abc",
)
val expectedSortedList = listOf(
"___",
"__3",
"__4",
"__a",
"__a",
"__A",
"__Za",
"1a3",
"aBc",
"Abc",
"z",
"Z",
)
assertEquals(
expectedSortedList,
unsortedList.sortedWith(SpecialCharWithPrecedenceComparator),
)
}
}

View file

@ -318,8 +318,8 @@ class VaultSdkCipherExtensionsTest {
createMockCipherView(1).copy(name = "_"),
createMockCipherView(1).copy(name = "7"),
createMockCipherView(1).copy(name = "8"),
createMockCipherView(1).copy(name = "A"),
createMockCipherView(1).copy(name = "aAb"),
createMockCipherView(1).copy(name = "A"),
createMockCipherView(1).copy(name = "AbA"),
createMockCipherView(1).copy(name = "B"),
createMockCipherView(1).copy(name = "c"),