diff --git a/app/src/main/java/com/owncloud/android/utils/sort/AlphanumericComparator.kt b/app/src/main/java/com/owncloud/android/utils/sort/AlphanumericComparator.kt new file mode 100644 index 000000000000..5c581c1a1cc3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/sort/AlphanumericComparator.kt @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2017 Tobias Kaminsky + * SPDX-FileCopyrightText: 2012 Daniel Migowski + * SPDX-FileCopyrightText: 2012 Andre Bogus + * SPDX-FileCopyrightText: 2012 David Koelle + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * The Alphanum Algorithm is an improved sorting algorithm for strings + * containing numbers. Instead of sorting numbers in ASCII order like + * a standard sort, this algorithm sorts numbers in numeric order. + * + * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com + */ +package com.owncloud.android.utils.sort + +import com.owncloud.android.lib.resources.files.model.ServerFileInterface +import java.text.Collator + +/** + * Sorts names the way people read them, so `abc2` comes before `abc10` instead of after it. + */ +class AlphanumericComparator : Comparator { + + override fun compare(first: T, second: T): Int = compare(first.toString(), second.toString()) + + companion object { + + @JvmStatic + fun compare(first: ServerFileInterface, second: ServerFileInterface): Int = + compare(first.fileName, second.fileName) + + @JvmStatic + fun compare(first: String, second: String): Int = if (first == second) 0 else compareByChunks(first, second) + + private fun compareByChunks(first: String, second: String): Int { + var ourStart = 0 + var theirStart = 0 + + while (ourStart < first.length && theirStart < second.length) { + val ours = chunkAt(first, ourStart) + val theirs = chunkAt(second, theirStart) + + val byChunk = compareChunks(ours, theirs) + if (byChunk != 0) { + return byChunk + } + + ourStart += ours.length + theirStart += theirs.length + } + + return when (val byLength = first.length.compareTo(second.length)) { + 0 -> first.compareTo(second) + else -> byLength + } + } + + private val collators: ThreadLocal = ThreadLocal.withInitial { Collator.getInstance() } + + private val collator: Collator + get() = collators.get() ?: Collator.getInstance() + + private fun chunkAt(name: String, start: Int): String { + val kind = kindOf(name[start]) + var end = start + 1 + + if (kind != ChunkKind.SEPARATOR) { + while (end < name.length && kindOf(name[end]) == kind) { + end++ + } + } + + return name.substring(start, end) + } + + private fun compareChunks(ours: String, theirs: String): Int { + val kind = kindOf(ours[0]) + val byKind = kind.compareTo(kindOf(theirs[0])) + if (byKind != 0) { + return byKind + } + + return when (kind) { + ChunkKind.SEPARATOR -> compareSeparators(ours[0], theirs[0]) + ChunkKind.NUMBER -> compareNumbers(ours, theirs) + ChunkKind.TEXT -> compareText(ours, theirs) + } + } + + private fun kindOf(char: Char): ChunkKind = when { + char <= LAST_ASCII_PUNCTUATION && !char.isLetterOrDigit() -> ChunkKind.SEPARATOR + char in ZERO..NINE -> ChunkKind.NUMBER + else -> ChunkKind.TEXT + } + + private fun compareSeparators(ours: Char, theirs: Char): Int = when { + ours == theirs -> 0 + ours == DOT -> -1 + theirs == DOT -> 1 + else -> ours.compareTo(theirs) + } + + private fun compareNumbers(ours: String, theirs: String): Int { + val ourValue = ours.trimStart(ZERO) + val theirValue = theirs.trimStart(ZERO) + + val byDigitCount = ourValue.length.compareTo(theirValue.length) + if (byDigitCount != 0) { + return byDigitCount + } + + return when (val byValue = ourValue.compareTo(theirValue)) { + 0 -> ours.length.compareTo(theirs.length) + else -> byValue + } + } + + private fun compareText(ours: String, theirs: String): Int { + val byCollation = collator.compare(ours, theirs) + if (byCollation != 0) { + return byCollation + } + + return when (val byLength = ours.length.compareTo(theirs.length)) { + 0 -> ours.compareTo(theirs) + else -> byLength + } + } + + private const val DOT = '.' + private const val ZERO = '0' + private const val NINE = '9' + private const val LAST_ASCII_PUNCTUATION = '~' + } +} diff --git a/app/src/main/java/com/owncloud/android/utils/sort/ChunkKind.kt b/app/src/main/java/com/owncloud/android/utils/sort/ChunkKind.kt new file mode 100644 index 000000000000..9a521e1b13d3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/sort/ChunkKind.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.utils.sort + +internal enum class ChunkKind { + SEPARATOR, + NUMBER, + TEXT +}