music: decouple settings somewhat

Try to decouple the stateful music settings object from the stateless
internals of the music loader. This should make unit testing far
easier.
This commit is contained in:
Alexander Capehart 2023-11-12 11:54:37 -07:00
parent 0ad7a8955a
commit 9ae6b20fd1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 133 additions and 167 deletions

View file

@ -36,6 +36,8 @@ import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.fs.MediaStoreExtractor
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.metadata.TagExtractor
import org.oxycblt.auxio.music.user.MutableUserLibrary
import org.oxycblt.auxio.music.user.UserLibrary
@ -223,7 +225,8 @@ constructor(
private val mediaStoreExtractor: MediaStoreExtractor,
private val tagExtractor: TagExtractor,
private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory
private val userLibraryFactory: UserLibrary.Factory,
private val musicSettings: MusicSettings
) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@ -356,6 +359,8 @@ constructor(
}
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
// TODO: Find a way to break up this monster of a method, preferably as another class.
val start = System.currentTimeMillis()
// Make sure we have permissions before going forward. Theoretically this would be better
// done at the UI level, but that intertwines logic and display too much.
@ -365,6 +370,17 @@ constructor(
throw NoAudioPermissionException()
}
// Obtain configuration information
val constraints =
MediaStoreExtractor.Constraints(musicSettings.excludeNonMusic, musicSettings.musicDirs)
val separators = Separators.from(musicSettings.separators)
val nameFactory =
if (musicSettings.intelligentSorting) {
Name.Known.IntelligentFactory
} else {
Name.Known.SimpleFactory
}
// Begin with querying MediaStore and the music cache. The former is needed for Auxio
// to figure out what songs are (probably) on the device, and the latter will be needed
// for discovery (described later). These have no shared state, so they are done in
@ -376,7 +392,7 @@ constructor(
worker.scope.async {
val query =
try {
mediaStoreExtractor.query()
mediaStoreExtractor.query(constraints)
} catch (e: Exception) {
// Normally, errors in an async call immediately bubble up to the Looper
// and crash the app. Thus, we have to wrap any error into a Result
@ -445,7 +461,8 @@ constructor(
worker.scope.async(Dispatchers.Default) {
val deviceLibrary =
try {
deviceLibraryFactory.create(completeSongs, processedSongs)
deviceLibraryFactory.create(
completeSongs, processedSongs, separators, nameFactory)
} catch (e: Exception) {
processedSongs.close(e)
return@async Result.failure(e)
@ -518,7 +535,7 @@ constructor(
logD("Awaiting DeviceLibrary creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow()
logD("Starting UserLibrary creation")
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory)
// Loading process is functionally done, indicate such
logD(

View file

@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.fs.useQuery
@ -110,19 +109,19 @@ interface DeviceLibrary {
suspend fun create(
rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>,
separators: Separators,
nameFactory: Name.Known.Factory
): DeviceLibraryImpl
}
}
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) :
DeviceLibrary.Factory {
class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
override suspend fun create(
rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>
processedSongs: Channel<RawSong>,
separators: Separators,
nameFactory: Name.Known.Factory
): DeviceLibraryImpl {
val nameFactory = Name.Known.Factory.from(musicSettings)
val separators = Separators.from(musicSettings)
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>()
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>()

View file

@ -24,12 +24,11 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.music.MusicSettings
@Module
@InstallIn(SingletonComponent::class)
class FsModule {
@Provides
fun mediaStoreExtractor(@ApplicationContext context: Context, musicSettings: MusicSettings) =
MediaStoreExtractor.from(context, musicSettings)
fun mediaStoreExtractor(@ApplicationContext context: Context) =
MediaStoreExtractor.from(context)
}

View file

@ -29,7 +29,6 @@ import androidx.core.database.getStringOrNull
import java.io.File
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.cache.Cache
import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Date
@ -50,9 +49,11 @@ interface MediaStoreExtractor {
/**
* Query the media database.
*
* @param constraints Configuration parameter to restrict what music should be ignored when
* querying.
* @return A new [Query] returned from the media database.
*/
suspend fun query(): Query
suspend fun query(constraints: Constraints): Query
/**
* Consume the [Cursor] loaded after [query].
@ -84,46 +85,44 @@ interface MediaStoreExtractor {
fun populateTags(rawSong: RawSong)
}
data class Constraints(val excludeNonMusic: Boolean, val musicDirs: MusicDirectories)
companion object {
/**
* Create a framework-backed instance.
*
* @param context [Context] required.
* @param musicSettings [MusicSettings] required.
* @return A new [MediaStoreExtractor] that will work best on the device's API level.
*/
fun from(context: Context, musicSettings: MusicSettings): MediaStoreExtractor =
fun from(context: Context): MediaStoreExtractor =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
Api30MediaStoreExtractor(context, musicSettings)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
Api29MediaStoreExtractor(context, musicSettings)
else -> Api21MediaStoreExtractor(context, musicSettings)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreExtractor(context)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreExtractor(context)
else -> Api21MediaStoreExtractor(context)
}
}
}
private abstract class BaseMediaStoreExtractor(
protected val context: Context,
private val musicSettings: MusicSettings
) : MediaStoreExtractor {
final override suspend fun query(): MediaStoreExtractor.Query {
private abstract class BaseMediaStoreExtractor(protected val context: Context) :
MediaStoreExtractor {
final override suspend fun query(
constraints: MediaStoreExtractor.Constraints
): MediaStoreExtractor.Query {
val start = System.currentTimeMillis()
val args = mutableListOf<String>()
var selector = BASE_SELECTOR
// Filter out audio that is not music, if enabled.
if (musicSettings.excludeNonMusic) {
if (constraints.excludeNonMusic) {
logD("Excluding non-music")
selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1"
}
// Set up the projection to follow the music directory configuration.
val dirs = musicSettings.musicDirs
if (dirs.dirs.isNotEmpty()) {
if (constraints.musicDirs.dirs.isNotEmpty()) {
selector += " AND "
if (!dirs.shouldInclude) {
if (!constraints.musicDirs.shouldInclude) {
logD("Excluding directories in selector")
// Without a NOT, the query will be restricted to the specified paths, resulting
// in the "Include" mode. With a NOT, the specified paths will not be included,
@ -134,10 +133,10 @@ private abstract class BaseMediaStoreExtractor(
// Specifying the paths to filter is version-specific, delegate to the concrete
// implementations.
for (i in dirs.dirs.indices) {
if (addDirToSelector(dirs.dirs[i], args)) {
for (i in constraints.musicDirs.dirs.indices) {
if (addDirToSelector(constraints.musicDirs.dirs[i], args)) {
selector +=
if (i < dirs.dirs.lastIndex) {
if (i < constraints.musicDirs.dirs.lastIndex) {
"$dirSelectorTemplate OR "
} else {
dirSelectorTemplate
@ -362,8 +361,7 @@ private abstract class BaseMediaStoreExtractor(
// Note: The separation between version-specific backends may not be the cleanest. To preserve
// speed, we only want to add redundancy on known issues, not with possible issues.
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
BaseMediaStoreExtractor(context, musicSettings) {
private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtractor(context) {
override val projection: Array<String>
get() =
super.projection +
@ -447,10 +445,8 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
* @author Alexander Capehart (OxygenCobalt)
*/
@RequiresApi(Build.VERSION_CODES.Q)
private abstract class BaseApi29MediaStoreExtractor(
context: Context,
musicSettings: MusicSettings
) : BaseMediaStoreExtractor(context, musicSettings) {
private abstract class BaseApi29MediaStoreExtractor(context: Context) :
BaseMediaStoreExtractor(context) {
override val projection: Array<String>
get() =
super.projection +
@ -512,8 +508,7 @@ private abstract class BaseApi29MediaStoreExtractor(
* @author Alexander Capehart (OxygenCobalt)
*/
@RequiresApi(Build.VERSION_CODES.Q)
private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
BaseApi29MediaStoreExtractor(context, musicSettings) {
private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
override val projection: Array<String>
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
@ -553,8 +548,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet
* @author Alexander Capehart (OxygenCobalt)
*/
@RequiresApi(Build.VERSION_CODES.R)
private class Api30MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
BaseApi29MediaStoreExtractor(context, musicSettings) {
private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
override val projection: Array<String>
get() =
super.projection +

View file

@ -23,12 +23,11 @@ import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import java.text.CollationKey
import java.text.Collator
import org.oxycblt.auxio.music.MusicSettings
/**
* The name of a music item.
*
* This class automatically implements
* This class automatically implements advanced sorting heuristics for music naming,
*
* @author Alexander Capehart
*/
@ -80,7 +79,7 @@ sealed interface Name : Comparable<Name> {
is Unknown -> 1
}
interface Factory {
sealed interface Factory {
/**
* Create a new instance of [Name.Known]
*
@ -88,22 +87,16 @@ sealed interface Name : Comparable<Name> {
* @param sort The raw sort name obtained from the music item
*/
fun parse(raw: String, sort: String?): Known
}
companion object {
/**
* Creates a new instance from the **current state** of the given [MusicSettings]'s
* user-defined name configuration.
*
* @param settings The [MusicSettings] to use.
* @return A [Factory] instance reflecting the configuration state.
*/
fun from(settings: MusicSettings) =
if (settings.intelligentSorting) {
IntelligentKnownName.Factory
} else {
SimpleKnownName.Factory
}
/** Produces a simple [Known] with basic sorting heuristics that are locale-independent. */
data object SimpleFactory : Factory {
override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort)
}
/** Produces an intelligent [Known] with advanced, but more fragile heuristics. */
data object IntelligentFactory : Factory {
override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort)
}
}
@ -137,7 +130,6 @@ private val punctRegex by lazy { Regex("[\\p{Punct}+]") }
*
* @author Alexander Capehart (OxygenCobalt)
*/
@VisibleForTesting
data class SimpleKnownName(override val raw: String, override val sort: String?) : Name.Known() {
override val sortTokens = listOf(parseToken(sort ?: raw))
@ -148,10 +140,6 @@ data class SimpleKnownName(override val raw: String, override val sort: String?)
// Always use lexicographic mode since we aren't parsing any numeric components
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
}
data object Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort)
}
}
/**
@ -159,7 +147,6 @@ data class SimpleKnownName(override val raw: String, override val sort: String?)
*
* @author Alexander Capehart (OxygenCobalt)
*/
@VisibleForTesting
data class IntelligentKnownName(override val raw: String, override val sort: String?) :
Name.Known() {
override val sortTokens = parseTokens(sort ?: raw)
@ -208,10 +195,6 @@ data class IntelligentKnownName(override val raw: String, override val sort: Str
}
}
data object Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort)
}
companion object {
private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") }
}

View file

@ -18,9 +18,6 @@
package org.oxycblt.auxio.music.metadata
import androidx.annotation.VisibleForTesting
import org.oxycblt.auxio.music.MusicSettings
/**
* Defines the user-specified parsing of multi-value tags. This should be used to parse any tags
* that may be delimited with a separator character.
@ -45,15 +42,12 @@ interface Separators {
const val AND = '&'
/**
* Creates a new instance from the **current state** of the given [MusicSettings]'s
* user-defined separator configuration.
* Creates a new instance from a string of separator characters to use.
*
* @param settings The [MusicSettings] to use.
* @return A new [Separators] instance reflecting the configuration state.
* @param chars The separator characters to use. Each character in the string will be
* checked for when splitting a string list.
* @return A new [Separators] instance reflecting the separators.
*/
fun from(settings: MusicSettings) = from(settings.separators)
@VisibleForTesting
fun from(chars: String) =
if (chars.isNotEmpty()) {
CharSeparators(chars.toSet())

View file

@ -22,7 +22,6 @@ import java.lang.Exception
import javax.inject.Inject
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary
@ -82,7 +81,8 @@ interface UserLibrary {
*/
suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary
deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
): MutableUserLibrary
}
}
@ -139,9 +139,7 @@ interface MutableUserLibrary : UserLibrary {
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean
}
class UserLibraryFactoryImpl
@Inject
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: PlaylistDao) :
UserLibrary.Factory {
override suspend fun query() =
try {
@ -155,22 +153,22 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
override suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary
deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
): MutableUserLibrary {
val nameFactory = Name.Known.Factory.from(musicSettings)
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) {
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory)
playlistMap[playlistImpl.uid] = playlistImpl
}
return UserLibraryImpl(playlistDao, playlistMap, musicSettings)
return UserLibraryImpl(playlistDao, playlistMap, nameFactory)
}
}
private class UserLibraryImpl(
private val playlistDao: PlaylistDao,
private val playlistMap: MutableMap<Music.UID, PlaylistImpl>,
private val musicSettings: MusicSettings
private val nameFactory: Name.Known.Factory
) : MutableUserLibrary {
override fun hashCode() = playlistMap.hashCode()
@ -186,7 +184,7 @@ private class UserLibraryImpl(
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? {
val playlistImpl = PlaylistImpl.from(name, songs, Name.Known.Factory.from(musicSettings))
val playlistImpl = PlaylistImpl.from(name, songs, nameFactory)
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist =
RawPlaylist(
@ -209,9 +207,7 @@ private class UserLibraryImpl(
val playlistImpl =
synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
.also {
playlistMap[it.uid] = it.edit(name, Name.Known.Factory.from(musicSettings))
}
.also { playlistMap[it.uid] = it.edit(name, nameFactory) }
}
return try {

View file

@ -18,30 +18,14 @@
package org.oxycblt.auxio.music.info
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.oxycblt.auxio.music.MusicSettings
class NameTest {
@Test
fun name_simple_from_settings() {
val musicSettings = mockk<MusicSettings> { every { intelligentSorting } returns false }
assertTrue(Name.Known.Factory.from(musicSettings) is SimpleKnownName.Factory)
}
@Test
fun name_intelligent_from_settings() {
val musicSettings = mockk<MusicSettings> { every { intelligentSorting } returns true }
assertTrue(Name.Known.Factory.from(musicSettings) is IntelligentKnownName.Factory)
}
@Test
fun name_simple_withoutPunct() {
val name = SimpleKnownName("Loveless", null)
val name = Name.Known.SimpleFactory.parse("Loveless", null)
assertEquals("Loveless", name.raw)
assertEquals(null, name.sort)
assertEquals("L", name.thumb)
@ -52,7 +36,7 @@ class NameTest {
@Test
fun name_simple_withPunct() {
val name = SimpleKnownName("alt-J", null)
val name = Name.Known.SimpleFactory.parse("alt-J", null)
assertEquals("alt-J", name.raw)
assertEquals(null, name.sort)
assertEquals("A", name.thumb)
@ -63,7 +47,7 @@ class NameTest {
@Test
fun name_simple_oopsAllPunct() {
val name = SimpleKnownName("!!!", null)
val name = Name.Known.SimpleFactory.parse("!!!", null)
assertEquals("!!!", name.raw)
assertEquals(null, name.sort)
assertEquals("!", name.thumb)
@ -74,7 +58,7 @@ class NameTest {
@Test
fun name_simple_spacedPunct() {
val name = SimpleKnownName("& Yet & Yet", null)
val name = Name.Known.SimpleFactory.parse("& Yet & Yet", null)
assertEquals("& Yet & Yet", name.raw)
assertEquals(null, name.sort)
assertEquals("Y", name.thumb)
@ -85,7 +69,7 @@ class NameTest {
@Test
fun name_simple_withSort() {
val name = SimpleKnownName("The Smile", "Smile")
val name = Name.Known.SimpleFactory.parse("The Smile", "Smile")
assertEquals("The Smile", name.raw)
assertEquals("Smile", name.sort)
assertEquals("S", name.thumb)
@ -96,7 +80,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("Loveless", null)
val name = Name.Known.IntelligentFactory.parse("Loveless", null)
assertEquals("Loveless", name.raw)
assertEquals(null, name.sort)
assertEquals("L", name.thumb)
@ -107,7 +91,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() {
val name = IntelligentKnownName("15 Step", null)
val name = Name.Known.IntelligentFactory.parse("15 Step", null)
assertEquals("15 Step", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
@ -121,7 +105,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() {
val name = IntelligentKnownName("23Kid", null)
val name = Name.Known.IntelligentFactory.parse("23Kid", null)
assertEquals("23Kid", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
@ -135,7 +119,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() {
val name = IntelligentKnownName("Foo 1 2 Bar", null)
val name = Name.Known.IntelligentFactory.parse("Foo 1 2 Bar", null)
assertEquals("Foo 1 2 Bar", name.raw)
assertEquals(null, name.sort)
assertEquals("F", name.thumb)
@ -158,7 +142,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() {
val name = IntelligentKnownName("Foo12Bar", null)
val name = Name.Known.IntelligentFactory.parse("Foo12Bar", null)
assertEquals("Foo12Bar", name.raw)
assertEquals(null, name.sort)
assertEquals("F", name.thumb)
@ -175,7 +159,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() {
val name = IntelligentKnownName("Foo 1", null)
val name = Name.Known.IntelligentFactory.parse("Foo 1", null)
assertEquals("Foo 1", name.raw)
assertEquals(null, name.sort)
assertEquals("F", name.thumb)
@ -189,7 +173,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() {
val name = IntelligentKnownName("Error404", null)
val name = Name.Known.IntelligentFactory.parse("Error404", null)
assertEquals("Error404", name.raw)
assertEquals(null, name.sort)
assertEquals("E", name.thumb)
@ -203,7 +187,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withThe_withoutNumerics() {
val name = IntelligentKnownName("The National Anthem", null)
val name = Name.Known.IntelligentFactory.parse("The National Anthem", null)
assertEquals("The National Anthem", name.raw)
assertEquals(null, name.sort)
assertEquals("N", name.thumb)
@ -214,7 +198,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withAn_withoutNumerics() {
val name = IntelligentKnownName("An Eagle in Your Mind", null)
val name = Name.Known.IntelligentFactory.parse("An Eagle in Your Mind", null)
assertEquals("An Eagle in Your Mind", name.raw)
assertEquals(null, name.sort)
assertEquals("E", name.thumb)
@ -225,7 +209,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_withA_withoutNumerics() {
val name = IntelligentKnownName("A Song For Our Fathers", null)
val name = Name.Known.IntelligentFactory.parse("A Song For Our Fathers", null)
assertEquals("A Song For Our Fathers", name.raw)
assertEquals(null, name.sort)
assertEquals("S", name.thumb)
@ -236,7 +220,7 @@ class NameTest {
@Test
fun name_intelligent_withPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("alt-J", null)
val name = Name.Known.IntelligentFactory.parse("alt-J", null)
assertEquals("alt-J", name.raw)
assertEquals(null, name.sort)
assertEquals("A", name.thumb)
@ -247,7 +231,7 @@ class NameTest {
@Test
fun name_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("!!!", null)
val name = Name.Known.IntelligentFactory.parse("!!!", null)
assertEquals("!!!", name.raw)
assertEquals(null, name.sort)
assertEquals("!", name.thumb)
@ -258,7 +242,7 @@ class NameTest {
@Test
fun name_intelligent_withoutPunct_shortArticle_withNumerics() {
val name = IntelligentKnownName("the 1", null)
val name = Name.Known.IntelligentFactory.parse("the 1", null)
assertEquals("the 1", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
@ -269,7 +253,7 @@ class NameTest {
@Test
fun name_intelligent_spacedPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("& Yet & Yet", null)
val name = Name.Known.IntelligentFactory.parse("& Yet & Yet", null)
assertEquals("& Yet & Yet", name.raw)
assertEquals(null, name.sort)
assertEquals("Y", name.thumb)
@ -280,7 +264,7 @@ class NameTest {
@Test
fun name_intelligent_withPunct_withoutArticle_withNumerics() {
val name = IntelligentKnownName("Design : 2 : 3", null)
val name = Name.Known.IntelligentFactory.parse("Design : 2 : 3", null)
assertEquals("Design : 2 : 3", name.raw)
assertEquals(null, name.sort)
assertEquals("D", name.thumb)
@ -300,7 +284,7 @@ class NameTest {
@Test
fun name_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() {
val name = IntelligentKnownName("2 + 2 = 5", null)
val name = Name.Known.IntelligentFactory.parse("2 + 2 = 5", null)
assertEquals("2 + 2 = 5", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
@ -323,7 +307,7 @@ class NameTest {
@Test
fun name_intelligent_withSort() {
val name = IntelligentKnownName("The Smile", "Smile")
val name = Name.Known.IntelligentFactory.parse("The Smile", "Smile")
assertEquals("The Smile", name.raw)
assertEquals("Smile", name.sort)
assertEquals("S", name.thumb)
@ -334,40 +318,40 @@ class NameTest {
@Test
fun name_equals_simple() {
val a = SimpleKnownName("The Same", "Same")
val b = SimpleKnownName("The Same", "Same")
val a = Name.Known.SimpleFactory.parse("The Same", "Same")
val b = Name.Known.SimpleFactory.parse("The Same", "Same")
assertEquals(a, b)
}
@Test
fun name_equals_differentSort() {
val a = SimpleKnownName("The Same", "Same")
val b = SimpleKnownName("The Same", null)
val a = Name.Known.SimpleFactory.parse("The Same", "Same")
val b = Name.Known.SimpleFactory.parse("The Same", null)
assertNotEquals(a, b)
assertNotEquals(a.hashCode(), b.hashCode())
}
@Test
fun name_equals_intelligent_differentTokens() {
val a = IntelligentKnownName("The Same", "Same")
val b = IntelligentKnownName("Same", "Same")
val a = Name.Known.IntelligentFactory.parse("The Same", "Same")
val b = Name.Known.IntelligentFactory.parse("Same", "Same")
assertNotEquals(a, b)
assertNotEquals(a.hashCode(), b.hashCode())
}
@Test
fun name_compareTo_simple_withoutSort_withoutArticle_withoutNumeric() {
val a = SimpleKnownName("A", null)
val b = SimpleKnownName("B", null)
val a = Name.Known.SimpleFactory.parse("A", null)
val b = Name.Known.SimpleFactory.parse("B", null)
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_compareTo_simple_withoutSort_withArticle_withoutNumeric() {
val a = SimpleKnownName("A Brain in a Bottle", null)
val b = SimpleKnownName("Acid Rain", null)
val c = SimpleKnownName("Boralis / Contrastellar", null)
val d = SimpleKnownName("Breathe In", null)
val a = Name.Known.SimpleFactory.parse("A Brain in a Bottle", null)
val b = Name.Known.SimpleFactory.parse("Acid Rain", null)
val c = Name.Known.SimpleFactory.parse("Boralis / Contrastellar", null)
val d = Name.Known.SimpleFactory.parse("Breathe In", null)
assertEquals(-1, a.compareTo(b))
assertEquals(-1, a.compareTo(c))
assertEquals(-1, a.compareTo(d))
@ -375,40 +359,40 @@ class NameTest {
@Test
fun name_compareTo_simple_withSort_withoutArticle_withNumeric() {
val a = SimpleKnownName("15 Step", null)
val b = SimpleKnownName("128 Harps", null)
val c = SimpleKnownName("1969", null)
val a = Name.Known.SimpleFactory.parse("15 Step", null)
val b = Name.Known.SimpleFactory.parse("128 Harps", null)
val c = Name.Known.SimpleFactory.parse("1969", null)
assertEquals(1, a.compareTo(b))
assertEquals(-1, a.compareTo(c))
}
@Test
fun name_compareTo_simple_withPartialSort() {
val a = SimpleKnownName("A", "C")
val b = SimpleKnownName("B", null)
val a = Name.Known.SimpleFactory.parse("A", "C")
val b = Name.Known.SimpleFactory.parse("B", null)
assertEquals(1, a.compareTo(b))
}
@Test
fun name_compareTo_simple_withSort() {
val a = SimpleKnownName("D", "A")
val b = SimpleKnownName("C", "B")
val a = Name.Known.SimpleFactory.parse("D", "A")
val b = Name.Known.SimpleFactory.parse("C", "B")
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_compareTo_intelligent_withoutSort_withoutArticle_withoutNumeric() {
val a = IntelligentKnownName("A", null)
val b = IntelligentKnownName("B", null)
val a = Name.Known.IntelligentFactory.parse("A", null)
val b = Name.Known.IntelligentFactory.parse("B", null)
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_compareTo_intelligent_withoutSort_withArticle_withoutNumeric() {
val a = IntelligentKnownName("A Brain in a Bottle", null)
val b = IntelligentKnownName("Acid Rain", null)
val c = IntelligentKnownName("Boralis / Contrastellar", null)
val d = IntelligentKnownName("Breathe In", null)
val a = Name.Known.IntelligentFactory.parse("A Brain in a Bottle", null)
val b = Name.Known.IntelligentFactory.parse("Acid Rain", null)
val c = Name.Known.IntelligentFactory.parse("Boralis / Contrastellar", null)
val d = Name.Known.IntelligentFactory.parse("Breathe In", null)
assertEquals(1, a.compareTo(b))
assertEquals(1, a.compareTo(c))
assertEquals(-1, a.compareTo(d))
@ -416,9 +400,9 @@ class NameTest {
@Test
fun name_compareTo_intelligent_withoutSort_withoutArticle_withNumeric() {
val a = IntelligentKnownName("15 Step", null)
val b = IntelligentKnownName("128 Harps", null)
val c = IntelligentKnownName("1969", null)
val a = Name.Known.IntelligentFactory.parse("15 Step", null)
val b = Name.Known.IntelligentFactory.parse("128 Harps", null)
val c = Name.Known.IntelligentFactory.parse("1969", null)
assertEquals(-1, a.compareTo(b))
assertEquals(-1, b.compareTo(c))
assertEquals(-2, a.compareTo(c))
@ -426,15 +410,15 @@ class NameTest {
@Test
fun name_compareTo_intelligent_withPartialSort_withoutArticle_withoutNumeric() {
val a = SimpleKnownName("A", "C")
val b = SimpleKnownName("B", null)
val a = Name.Known.SimpleFactory.parse("A", "C")
val b = Name.Known.SimpleFactory.parse("B", null)
assertEquals(1, a.compareTo(b))
}
@Test
fun name_compareTo_intelligent_withSort_withoutArticle_withoutNumeric() {
val a = IntelligentKnownName("D", "A")
val b = IntelligentKnownName("C", "B")
val a = Name.Known.IntelligentFactory.parse("D", "A")
val b = Name.Known.IntelligentFactory.parse("C", "B")
assertEquals(-1, a.compareTo(b))
}
@ -447,7 +431,7 @@ class NameTest {
@Test
fun name_compareTo_mixed() {
val a = Name.Unknown(0)
val b = IntelligentKnownName("A", null)
val b = Name.Known.IntelligentFactory.parse("A", null)
assertEquals(-1, a.compareTo(b))
}
}