music: use factory to create known names

Implement a new Name.Known.Factory instance that replaces the usage of
Name.Known.from.

This again allows songs to be differentiated on tag interpretation and
is generally easier to test.
This commit is contained in:
Alexander Capehart 2023-08-18 15:27:45 -06:00
parent c1655a9eca
commit fcffb56021
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 263 additions and 207 deletions

View file

@ -223,7 +223,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>()

View file

@ -32,6 +32,7 @@ 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
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -108,7 +109,7 @@ interface DeviceLibrary {
*/
suspend fun create(
rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>
processedSongs: Channel<RawSong>,
): DeviceLibraryImpl
}
}
@ -119,7 +120,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>
): DeviceLibraryImpl {
val separators = Separators.from(musicSettings.separators)
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>>()
@ -130,7 +132,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
// All music information is grouped as it is indexed by other components.
for (rawSong in rawSongs) {
val song = SongImpl(rawSong, musicSettings, separators)
val song = SongImpl(rawSong, nameFactory, separators)
// At times the indexer produces duplicate songs, try to filter these. Comparing by
// UID is sufficient for something like this, and also prevents collisions from
// causing severe issues elsewhere.
@ -210,7 +212,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
// Now that all songs are processed, also process albums and group them into their
// respective artists.
val albums = albumGrouping.values.mapTo(mutableSetOf()) { AlbumImpl(it, musicSettings) }
val albums = albumGrouping.values.mapTo(mutableSetOf()) { AlbumImpl(it, nameFactory) }
for (album in albums) {
for (rawArtist in album.rawArtists) {
val key = RawArtist.Key(rawArtist)
@ -246,8 +248,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
}
// Artists and genres do not need to be grouped and can be processed immediately.
val artists = artistGrouping.values.mapTo(mutableSetOf()) { ArtistImpl(it, musicSettings) }
val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, musicSettings) }
val artists = artistGrouping.values.mapTo(mutableSetOf()) { ArtistImpl(it, nameFactory) }
val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, nameFactory) }
return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres)
}

View file

@ -25,7 +25,6 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.MimeType
@ -48,12 +47,15 @@ import org.oxycblt.auxio.util.update
* Library-backed implementation of [Song].
*
* @param rawSong The [RawSong] to derive the member data from.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @param separators The [Separators] to parse multi-value tags with.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @author Alexander Capehart (OxygenCobalt)
*/
class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings, separators: Separators) :
Song {
class SongImpl(
private val rawSong: RawSong,
nameFactory: Name.Known.Factory,
separators: Separators
) : Song {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
@ -72,10 +74,8 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings, separ
update(rawSong.albumArtistNames)
}
override val name =
Name.Known.from(
requireNotNull(rawSong.name) { "Invalid raw: No title" },
rawSong.sortName,
musicSettings)
nameFactory.parse(
requireNotNull(rawSong.name) { "Invalid raw: No title" }, rawSong.sortName)
override val track = rawSong.track
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
@ -256,13 +256,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings, separ
* Library-backed implementation of [Album].
*
* @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumImpl(
grouping: Grouping<RawAlbum, SongImpl>,
musicSettings: MusicSettings,
) : Album {
class AlbumImpl(grouping: Grouping<RawAlbum, SongImpl>, nameFactory: Name.Known.Factory) : Album {
private val rawAlbum = grouping.raw.inner
override val uid =
@ -275,7 +272,7 @@ class AlbumImpl(
update(rawAlbum.name)
update(rawAlbum.rawArtists.map { it.name })
}
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings)
override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName)
override val dates: Date.Range?
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri)
@ -376,10 +373,10 @@ class AlbumImpl(
* Library-backed implementation of [Artist].
*
* @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSettings) : Artist {
class ArtistImpl(grouping: Grouping<RawArtist, Music>, nameFactory: Name.Known.Factory) : Artist {
private val rawArtist = grouping.raw.inner
override val uid =
@ -387,7 +384,7 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) }
?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) }
override val name =
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
rawArtist.name?.let { nameFactory.parse(it, rawArtist.sortName) }
?: Name.Unknown(R.string.def_artist)
override val songs: Set<Song>
@ -473,15 +470,15 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
* Library-backed implementation of [Genre].
*
* @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSettings) : Genre {
class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, nameFactory: Name.Known.Factory) : Genre {
private val rawGenre = grouping.raw.inner
override val uid = Music.UID.auxio(MusicType.GENRES) { update(rawGenre.name) }
override val name =
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
rawGenre.name?.let { nameFactory.parse(it, rawGenre.name) }
?: Name.Unknown(R.string.def_genre)
override val songs: Set<Song>

View file

@ -57,36 +57,6 @@ sealed interface Name : Comparable<Name> {
/** A tokenized version of the name that will be compared. */
@VisibleForTesting(VisibleForTesting.PROTECTED) abstract val sortTokens: List<SortToken>
/** An individual part of a name string that can be compared intelligently. */
@VisibleForTesting(VisibleForTesting.PROTECTED)
data class SortToken(val collationKey: CollationKey, val type: Type) :
Comparable<SortToken> {
override fun compareTo(other: SortToken): Int {
// Numeric tokens should always be lower than lexicographic tokens.
val modeComp = type.compareTo(other.type)
if (modeComp != 0) {
return modeComp
}
// Numeric strings must be ordered by magnitude, thus immediately short-circuit
// the comparison if the lengths do not match.
if (type == Type.NUMERIC &&
collationKey.sourceString.length != other.collationKey.sourceString.length) {
return collationKey.sourceString.length - other.collationKey.sourceString.length
}
return collationKey.compareTo(other.collationKey)
}
/** Denotes the type of comparison to be performed with this token. */
enum class Type {
/** Compare as a digit string, like "65". */
NUMERIC,
/** Compare as a standard alphanumeric string, like "65daysofstatic" */
LEXICOGRAPHIC
}
}
final override val thumb: String
get() =
// TODO: Remove these safety checks once you have real unit testing
@ -110,20 +80,30 @@ sealed interface Name : Comparable<Name> {
is Unknown -> 1
}
companion object {
interface Factory {
/**
* Create a new instance of [Name.Known]
*
* @param raw The raw name obtained from the music item
* @param sort The raw sort name obtained from the music item
* @param musicSettings [MusicSettings] required for name configuration.
*/
fun from(raw: String, sort: String?, musicSettings: MusicSettings): Known =
if (musicSettings.intelligentSorting) {
IntelligentKnownName(raw, sort)
} else {
SimpleKnownName(raw, sort)
}
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 new [Factory] instance reflecting the configuration state.
*/
fun from(settings: MusicSettings) =
if (settings.intelligentSorting) {
IntelligentKnownName.Factory()
} else {
SimpleKnownName.Factory()
}
}
}
}
@ -157,8 +137,8 @@ private val punctRegex by lazy { Regex("[\\p{Punct}+]") }
*
* @author Alexander Capehart (OxygenCobalt)
*/
private data class SimpleKnownName(override val raw: String, override val sort: String?) :
Name.Known() {
@VisibleForTesting
data class SimpleKnownName(override val raw: String, override val sort: String?) : Name.Known() {
override val sortTokens = listOf(parseToken(sort ?: raw))
private fun parseToken(name: String): SortToken {
@ -168,6 +148,10 @@ private data class SimpleKnownName(override val raw: String, override val sort:
// Always use lexicographic mode since we aren't parsing any numeric components
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
}
class Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort)
}
}
/**
@ -175,7 +159,8 @@ private data class SimpleKnownName(override val raw: String, override val sort:
*
* @author Alexander Capehart (OxygenCobalt)
*/
private data class IntelligentKnownName(override val raw: String, override val sort: String?) :
@VisibleForTesting
data class IntelligentKnownName(override val raw: String, override val sort: String?) :
Name.Known() {
override val sortTokens = parseTokens(sort ?: raw)
@ -223,7 +208,40 @@ private data class IntelligentKnownName(override val raw: String, override val s
}
}
class 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+)") }
}
}
/** An individual part of a name string that can be compared intelligently. */
@VisibleForTesting(VisibleForTesting.PROTECTED)
data class SortToken(val collationKey: CollationKey, val type: Type) : Comparable<SortToken> {
override fun compareTo(other: SortToken): Int {
// Numeric tokens should always be lower than lexicographic tokens.
val modeComp = type.compareTo(other.type)
if (modeComp != 0) {
return modeComp
}
// Numeric strings must be ordered by magnitude, thus immediately short-circuit
// the comparison if the lengths do not match.
if (type == Type.NUMERIC &&
collationKey.sourceString.length != other.collationKey.sourceString.length) {
return collationKey.sourceString.length - other.collationKey.sourceString.length
}
return collationKey.compareTo(other.collationKey)
}
/** Denotes the type of comparison to be performed with this token. */
enum class Type {
/** Compare as a digit string, like "65". */
NUMERIC,
/** Compare as a standard alphanumeric string, like "65daysofstatic" */
LEXICOGRAPHIC
}
}

View file

@ -18,6 +18,9 @@
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.
@ -40,9 +43,22 @@ interface Separators {
const val SLASH = '/'
const val PLUS = '+'
const val AND = '&'
/**
* Creates a new instance from the **current state** of the given [MusicSettings]'s
* user-defined separator configuration.
*
* @param settings The [MusicSettings] to use.
* @return A new [Separators] instance reflecting the configuration state.
*/
fun from(settings: MusicSettings) = from(settings.separators)
fun from(selector: String) =
if (selector.isNotEmpty()) CharSeparators(selector.toSet()) else NoSeparators
@VisibleForTesting
fun from(chars: String) =
if (chars.isNotEmpty()) {
CharSeparators(chars.toSet())
} else {
NoSeparators
}
}
}

View file

@ -77,7 +77,6 @@ private class TagWorkerImpl(
private val rawSong: RawSong,
private val future: Future<TrackGroupArray>
) : TagWorker {
override fun poll(): RawSong? {
if (!future.isDone) {
// Not done yet, nothing to do.

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.music.user
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
@ -51,10 +50,10 @@ private constructor(
* Clone the data in this instance to a new [PlaylistImpl] with the given [name].
*
* @param name The new name to use.
* @param musicSettings [MusicSettings] required for name configuration.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/
fun edit(name: String, musicSettings: MusicSettings) =
PlaylistImpl(uid, Name.Known.from(name, null, musicSettings), songs)
fun edit(name: String, nameFactory: Name.Known.Factory) =
PlaylistImpl(uid, nameFactory.parse(name, null), songs)
/**
* Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s.
@ -76,29 +75,26 @@ private constructor(
*
* @param name The name of the playlist.
* @param songs The songs to initially populate the playlist with.
* @param musicSettings [MusicSettings] required for name configuration.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
PlaylistImpl(
Music.UID.auxio(MusicType.PLAYLISTS),
Name.Known.from(name, null, musicSettings),
songs)
fun from(name: String, songs: List<Song>, nameFactory: Name.Known.Factory) =
PlaylistImpl(Music.UID.auxio(MusicType.PLAYLISTS), nameFactory.parse(name, null), songs)
/**
* Populate a new instance from a read [RawPlaylist].
*
* @param rawPlaylist The [RawPlaylist] to read from.
* @param deviceLibrary The [DeviceLibrary] to initialize from.
* @param musicSettings [MusicSettings] required for name configuration.
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/
fun fromRaw(
rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary,
musicSettings: MusicSettings
nameFactory: Name.Known.Factory
) =
PlaylistImpl(
rawPlaylist.playlistInfo.playlistUid,
Name.Known.from(rawPlaylist.playlistInfo.name, null, musicSettings),
nameFactory.parse(rawPlaylist.playlistInfo.name, null),
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) })
}
}

View file

@ -26,6 +26,7 @@ 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
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
@ -144,7 +145,9 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
UserLibrary.Factory {
override suspend fun query() =
try {
playlistDao.readRawPlaylists()
val rawPlaylists = playlistDao.readRawPlaylists()
logD("Successfully read ${rawPlaylists.size} playlists")
rawPlaylists
} catch (e: Exception) {
logE("Unable to read playlists: $e")
listOf()
@ -154,11 +157,10 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary
): MutableUserLibrary {
logD("Successfully read ${rawPlaylists.size} playlists")
// Convert the database playlist information to actual usable playlists.
val nameFactory = Name.Known.Factory.from(musicSettings)
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) {
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, musicSettings)
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory)
playlistMap[playlistImpl.uid] = playlistImpl
}
return UserLibraryImpl(playlistDao, playlistMap, musicSettings)
@ -184,7 +186,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, musicSettings)
val playlistImpl = PlaylistImpl.from(name, songs, Name.Known.Factory.from(musicSettings))
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist =
RawPlaylist(
@ -207,7 +209,9 @@ private class UserLibraryImpl(
val playlistImpl =
synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
.also { playlistMap[it.uid] = it.edit(name, musicSettings) }
.also {
playlistMap[it.uid] = it.edit(name, Name.Known.Factory.from(musicSettings))
}
}
return try {

View file

@ -22,342 +22,352 @@ 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 {
private fun mockIntelligentSorting(enabled: Boolean) =
mockk<MusicSettings>() { every { intelligentSorting } returns enabled }
@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_from_simple_withoutPunct() {
val name = Name.Known.from("Loveless", null, mockIntelligentSorting(false))
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)
assertEquals("Loveless", name.raw)
assertEquals(null, name.sort)
assertEquals("L", name.thumb)
val only = name.sortTokens.single()
assertEquals("Loveless", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_simple_withPunct() {
val name = Name.Known.from("alt-J", null, mockIntelligentSorting(false))
fun name_simple_withPunct() {
val name = SimpleKnownName("alt-J", null)
assertEquals("alt-J", name.raw)
assertEquals(null, name.sort)
assertEquals("A", name.thumb)
val only = name.sortTokens.single()
assertEquals("altJ", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_simple_oopsAllPunct() {
val name = Name.Known.from("!!!", null, mockIntelligentSorting(false))
fun name_simple_oopsAllPunct() {
val name = SimpleKnownName("!!!", null)
assertEquals("!!!", name.raw)
assertEquals(null, name.sort)
assertEquals("!", name.thumb)
val only = name.sortTokens.single()
assertEquals("!!!", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_simple_spacedPunct() {
val name = Name.Known.from("& Yet & Yet", null, mockIntelligentSorting(false))
fun name_simple_spacedPunct() {
val name = SimpleKnownName("& Yet & Yet", null)
assertEquals("& Yet & Yet", name.raw)
assertEquals(null, name.sort)
assertEquals("Y", name.thumb)
val first = name.sortTokens[0]
assertEquals("Yet Yet", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
}
@Test
fun name_from_simple_withSort() {
val name = Name.Known.from("The Smile", "Smile", mockIntelligentSorting(false))
fun name_simple_withSort() {
val name = SimpleKnownName("The Smile", "Smile")
assertEquals("The Smile", name.raw)
assertEquals("Smile", name.sort)
assertEquals("S", name.thumb)
val only = name.sortTokens.single()
assertEquals("Smile", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withoutNumerics() {
val name = Name.Known.from("Loveless", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("Loveless", null)
assertEquals("Loveless", name.raw)
assertEquals(null, name.sort)
assertEquals("L", name.thumb)
val only = name.sortTokens.single()
assertEquals("Loveless", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() {
val name = Name.Known.from("15 Step", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() {
val name = IntelligentKnownName("15 Step", null)
assertEquals("15 Step", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
val first = name.sortTokens[0]
assertEquals("15", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, first.type)
assertEquals(SortToken.Type.NUMERIC, first.type)
val second = name.sortTokens[1]
assertEquals("Step", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, second.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() {
val name = Name.Known.from("23Kid", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() {
val name = IntelligentKnownName("23Kid", null)
assertEquals("23Kid", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
val first = name.sortTokens[0]
assertEquals("23", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, first.type)
assertEquals(SortToken.Type.NUMERIC, first.type)
val second = name.sortTokens[1]
assertEquals("Kid", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, second.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() {
val name = Name.Known.from("Foo 1 2 Bar", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() {
val name = IntelligentKnownName("Foo 1 2 Bar", null)
assertEquals("Foo 1 2 Bar", name.raw)
assertEquals(null, name.sort)
assertEquals("F", name.thumb)
val first = name.sortTokens[0]
assertEquals("Foo", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
val second = name.sortTokens[1]
assertEquals("1", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, second.type)
assertEquals(SortToken.Type.NUMERIC, second.type)
val third = name.sortTokens[2]
assertEquals(" ", third.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, third.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type)
val fourth = name.sortTokens[3]
assertEquals("2", fourth.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, fourth.type)
assertEquals(SortToken.Type.NUMERIC, fourth.type)
val fifth = name.sortTokens[4]
assertEquals("Bar", fifth.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, fifth.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, fifth.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() {
val name = Name.Known.from("Foo12Bar", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() {
val name = IntelligentKnownName("Foo12Bar", null)
assertEquals("Foo12Bar", name.raw)
assertEquals(null, name.sort)
assertEquals("F", name.thumb)
val first = name.sortTokens[0]
assertEquals("Foo", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
val second = name.sortTokens[1]
assertEquals("12", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, second.type)
assertEquals(SortToken.Type.NUMERIC, second.type)
val third = name.sortTokens[2]
assertEquals("Bar", third.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, third.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() {
val name = Name.Known.from("Foo 1", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() {
val name = IntelligentKnownName("Foo 1", null)
assertEquals("Foo 1", name.raw)
assertEquals(null, name.sort)
assertEquals("F", name.thumb)
val first = name.sortTokens[0]
assertEquals("Foo", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
val second = name.sortTokens[1]
assertEquals("1", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, second.type)
assertEquals(SortToken.Type.NUMERIC, second.type)
}
@Test
fun name_from_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() {
val name = Name.Known.from("Error404", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() {
val name = IntelligentKnownName("Error404", null)
assertEquals("Error404", name.raw)
assertEquals(null, name.sort)
assertEquals("E", name.thumb)
val first = name.sortTokens[0]
assertEquals("Error", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
val second = name.sortTokens[1]
assertEquals("404", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, second.type)
assertEquals(SortToken.Type.NUMERIC, second.type)
}
@Test
fun name_from_intelligent_withoutPunct_withThe_withoutNumerics() {
val name = Name.Known.from("The National Anthem", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withThe_withoutNumerics() {
val name = IntelligentKnownName("The National Anthem", null)
assertEquals("The National Anthem", name.raw)
assertEquals(null, name.sort)
assertEquals("N", name.thumb)
val first = name.sortTokens[0]
assertEquals("National Anthem", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
}
@Test
fun name_from_intelligent_withoutPunct_withAn_withoutNumerics() {
val name = Name.Known.from("An Eagle in Your Mind", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withAn_withoutNumerics() {
val name = IntelligentKnownName("An Eagle in Your Mind", null)
assertEquals("An Eagle in Your Mind", name.raw)
assertEquals(null, name.sort)
assertEquals("E", name.thumb)
val first = name.sortTokens[0]
assertEquals("Eagle in Your Mind", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
}
@Test
fun name_from_intelligent_withoutPunct_withA_withoutNumerics() {
val name = Name.Known.from("A Song For Our Fathers", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_withA_withoutNumerics() {
val name = IntelligentKnownName("A Song For Our Fathers", null)
assertEquals("A Song For Our Fathers", name.raw)
assertEquals(null, name.sort)
assertEquals("S", name.thumb)
val first = name.sortTokens[0]
assertEquals("Song For Our Fathers", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
}
@Test
fun name_from_intelligent_withPunct_withoutArticle_withoutNumerics() {
val name = Name.Known.from("alt-J", null, mockIntelligentSorting(true))
fun name_intelligent_withPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("alt-J", null)
assertEquals("alt-J", name.raw)
assertEquals(null, name.sort)
assertEquals("A", name.thumb)
val only = name.sortTokens.single()
assertEquals("altJ", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() {
val name = Name.Known.from("!!!", null, mockIntelligentSorting(true))
fun name_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("!!!", null)
assertEquals("!!!", name.raw)
assertEquals(null, name.sort)
assertEquals("!", name.thumb)
val only = name.sortTokens.single()
assertEquals("!!!", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_from_intelligent_withoutPunct_shortArticle_withNumerics() {
val name = Name.Known.from("the 1", null, mockIntelligentSorting(true))
fun name_intelligent_withoutPunct_shortArticle_withNumerics() {
val name = IntelligentKnownName("the 1", null)
assertEquals("the 1", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
val first = name.sortTokens[0]
assertEquals("1", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, first.type)
assertEquals(SortToken.Type.NUMERIC, first.type)
}
@Test
fun name_from_intelligent_spacedPunct_withoutArticle_withoutNumerics() {
val name = Name.Known.from("& Yet & Yet", null, mockIntelligentSorting(true))
fun name_intelligent_spacedPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("& Yet & Yet", null)
assertEquals("& Yet & Yet", name.raw)
assertEquals(null, name.sort)
assertEquals("Y", name.thumb)
val first = name.sortTokens[0]
assertEquals("Yet Yet", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
}
@Test
fun name_from_intelligent_withPunct_withoutArticle_withNumerics() {
val name = Name.Known.from("Design : 2 : 3", null, mockIntelligentSorting(true))
fun name_intelligent_withPunct_withoutArticle_withNumerics() {
val name = IntelligentKnownName("Design : 2 : 3", null)
assertEquals("Design : 2 : 3", name.raw)
assertEquals(null, name.sort)
assertEquals("D", name.thumb)
val first = name.sortTokens[0]
assertEquals("Design", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, first.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type)
val second = name.sortTokens[1]
assertEquals("2", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, second.type)
assertEquals(SortToken.Type.NUMERIC, second.type)
val third = name.sortTokens[2]
assertEquals(" ", third.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, third.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type)
val fourth = name.sortTokens[3]
assertEquals("3", fourth.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, fourth.type)
assertEquals(SortToken.Type.NUMERIC, fourth.type)
}
@Test
fun name_from_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() {
val name = Name.Known.from("2 + 2 = 5", null, mockIntelligentSorting(true))
fun name_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() {
val name = IntelligentKnownName("2 + 2 = 5", null)
assertEquals("2 + 2 = 5", name.raw)
assertEquals(null, name.sort)
assertEquals("#", name.thumb)
val first = name.sortTokens[0]
assertEquals("2", first.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, first.type)
assertEquals(SortToken.Type.NUMERIC, first.type)
val second = name.sortTokens[1]
assertEquals(" ", second.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, second.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type)
val third = name.sortTokens[2]
assertEquals("2", third.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, third.type)
assertEquals(SortToken.Type.NUMERIC, third.type)
val fourth = name.sortTokens[3]
assertEquals(" ", fourth.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, fourth.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, fourth.type)
val fifth = name.sortTokens[4]
assertEquals("5", fifth.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.NUMERIC, fifth.type)
assertEquals(SortToken.Type.NUMERIC, fifth.type)
}
@Test
fun name_from_intelligent_withSort() {
val name = Name.Known.from("The Smile", "Smile", mockIntelligentSorting(true))
fun name_intelligent_withSort() {
val name = IntelligentKnownName("The Smile", "Smile")
assertEquals("The Smile", name.raw)
assertEquals("Smile", name.sort)
assertEquals("S", name.thumb)
val only = name.sortTokens.single()
assertEquals("Smile", only.collationKey.sourceString)
assertEquals(Name.Known.SortToken.Type.LEXICOGRAPHIC, only.type)
assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type)
}
@Test
fun name_equals_simple() {
val a = Name.Known.from("The Same", "Same", mockIntelligentSorting(false))
val b = Name.Known.from("The Same", "Same", mockIntelligentSorting(false))
val a = SimpleKnownName("The Same", "Same")
val b = SimpleKnownName("The Same", "Same")
assertEquals(a, b)
}
@Test
fun name_equals_differentSort() {
val a = Name.Known.from("The Same", "Same", mockIntelligentSorting(false))
val b = Name.Known.from("The Same", null, mockIntelligentSorting(false))
val a = SimpleKnownName("The Same", "Same")
val b = SimpleKnownName("The Same", null)
assertNotEquals(a, b)
assertNotEquals(a.hashCode(), b.hashCode())
}
@Test
fun name_equals_intelligent_differentTokens() {
val a = Name.Known.from("The Same", "Same", mockIntelligentSorting(true))
val b = Name.Known.from("Same", "Same", mockIntelligentSorting(true))
val a = IntelligentKnownName("The Same", "Same")
val b = IntelligentKnownName("Same", "Same")
assertNotEquals(a, b)
assertNotEquals(a.hashCode(), b.hashCode())
}
@Test
fun name_compareTo_simple_withoutSort_withoutArticle_withoutNumeric() {
val a = Name.Known.from("A", null, mockIntelligentSorting(false))
val b = Name.Known.from("B", null, mockIntelligentSorting(false))
val a = SimpleKnownName("A", null)
val b = SimpleKnownName("B", null)
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_compareTo_simple_withoutSort_withArticle_withoutNumeric() {
val a = Name.Known.from("A Brain in a Bottle", null, mockIntelligentSorting(false))
val b = Name.Known.from("Acid Rain", null, mockIntelligentSorting(false))
val c = Name.Known.from("Boralis / Contrastellar", null, mockIntelligentSorting(false))
val d = Name.Known.from("Breathe In", null, mockIntelligentSorting(false))
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)
assertEquals(-1, a.compareTo(b))
assertEquals(-1, a.compareTo(c))
assertEquals(-1, a.compareTo(d))
@ -365,40 +375,40 @@ class NameTest {
@Test
fun name_compareTo_simple_withSort_withoutArticle_withNumeric() {
val a = Name.Known.from("15 Step", null, mockIntelligentSorting(false))
val b = Name.Known.from("128 Harps", null, mockIntelligentSorting(false))
val c = Name.Known.from("1969", null, mockIntelligentSorting(false))
val a = SimpleKnownName("15 Step", null)
val b = SimpleKnownName("128 Harps", null)
val c = SimpleKnownName("1969", null)
assertEquals(1, a.compareTo(b))
assertEquals(-1, a.compareTo(c))
}
@Test
fun name_compareTo_simple_withPartialSort() {
val a = Name.Known.from("A", "C", mockIntelligentSorting(false))
val b = Name.Known.from("B", null, mockIntelligentSorting(false))
val a = SimpleKnownName("A", "C")
val b = SimpleKnownName("B", null)
assertEquals(1, a.compareTo(b))
}
@Test
fun name_compareTo_simple_withSort() {
val a = Name.Known.from("D", "A", mockIntelligentSorting(false))
val b = Name.Known.from("C", "B", mockIntelligentSorting(false))
val a = SimpleKnownName("D", "A")
val b = SimpleKnownName("C", "B")
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_compareTo_intelligent_withoutSort_withoutArticle_withoutNumeric() {
val a = Name.Known.from("A", null, mockIntelligentSorting(true))
val b = Name.Known.from("B", null, mockIntelligentSorting(true))
val a = IntelligentKnownName("A", null)
val b = IntelligentKnownName("B", null)
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_compareTo_intelligent_withoutSort_withArticle_withoutNumeric() {
val a = Name.Known.from("A Brain in a Bottle", null, mockIntelligentSorting(true))
val b = Name.Known.from("Acid Rain", null, mockIntelligentSorting(true))
val c = Name.Known.from("Boralis / Contrastellar", null, mockIntelligentSorting(true))
val d = Name.Known.from("Breathe In", null, mockIntelligentSorting(true))
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)
assertEquals(1, a.compareTo(b))
assertEquals(1, a.compareTo(c))
assertEquals(-1, a.compareTo(d))
@ -406,9 +416,9 @@ class NameTest {
@Test
fun name_compareTo_intelligent_withoutSort_withoutArticle_withNumeric() {
val a = Name.Known.from("15 Step", null, mockIntelligentSorting(true))
val b = Name.Known.from("128 Harps", null, mockIntelligentSorting(true))
val c = Name.Known.from("1969", null, mockIntelligentSorting(true))
val a = IntelligentKnownName("15 Step", null)
val b = IntelligentKnownName("128 Harps", null)
val c = IntelligentKnownName("1969", null)
assertEquals(-1, a.compareTo(b))
assertEquals(-1, b.compareTo(c))
assertEquals(-2, a.compareTo(c))
@ -416,15 +426,28 @@ class NameTest {
@Test
fun name_compareTo_intelligent_withPartialSort_withoutArticle_withoutNumeric() {
val a = Name.Known.from("A", "C", mockIntelligentSorting(false))
val b = Name.Known.from("B", null, mockIntelligentSorting(false))
val a = SimpleKnownName("A", "C")
val b = SimpleKnownName("B", null)
assertEquals(1, a.compareTo(b))
}
@Test
fun name_compareTo_intelligent_withSort_withoutArticle_withoutNumeric() {
val a = Name.Known.from("D", "A", mockIntelligentSorting(true))
val b = Name.Known.from("C", "B", mockIntelligentSorting(true))
val a = IntelligentKnownName("D", "A")
val b = IntelligentKnownName("C", "B")
assertEquals(-1, a.compareTo(b))
}
@Test
fun name_unknown() {
val a = Name.Unknown(0)
assertEquals("?", a.thumb)
}
@Test
fun name_compareTo_mixed() {
val a = Name.Unknown(0)
val b = IntelligentKnownName("A", null)
assertEquals(-1, a.compareTo(b))
}
}