diff --git a/app/src/main/java/org/oxycblt/auxio/music/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/Date.kt
new file mode 100644
index 000000000..11a2378bd
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/music/Date.kt
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2022 Auxio Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.music
+
+import android.content.Context
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import kotlin.math.max
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.util.inRangeOrNull
+import org.oxycblt.auxio.util.nonZeroOrNull
+
+/**
+ * An ISO-8601/RFC 3339 Date.
+ *
+ * This class only encodes the timestamp spec and it's conversion to a human-readable date, without
+ * any other time management or validation. In general, this should only be used for display.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+class Date private constructor(private val tokens: List) : Comparable {
+ private val year = tokens[0]
+ private val month = tokens.getOrNull(1)
+ private val day = tokens.getOrNull(2)
+ private val hour = tokens.getOrNull(3)
+ private val minute = tokens.getOrNull(4)
+ private val second = tokens.getOrNull(5)
+
+ /**
+ * Resolve this instance into a human-readable date.
+ * @param context [Context] required to get human-readable names.
+ * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan
+ * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
+ * be properly localized.
+ */
+ fun resolveDate(context: Context): String {
+ if (month != null) {
+ // Parse a date format from an ISO-ish format
+ val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
+ format.applyPattern("yyyy-MM")
+ val date =
+ try {
+ format.parse("$year-$month")
+ } catch (e: ParseException) {
+ null
+ }
+
+ if (date != null) {
+ // Reformat as a readable month and year
+ format.applyPattern("MMM yyyy")
+ return format.format(date)
+ }
+ }
+
+ // Unable to create fine-grained date, just format as a year.
+ return context.getString(R.string.fmt_number, year)
+ }
+
+ override fun hashCode() = tokens.hashCode()
+
+ override fun equals(other: Any?) = other is Date && tokens == other.tokens
+
+ override fun compareTo(other: Date): Int {
+ for (i in 0 until max(tokens.size, other.tokens.size)) {
+ val ai = tokens.getOrNull(i)
+ val bi = other.tokens.getOrNull(i)
+ when {
+ ai != null && bi != null -> {
+ val result = ai.compareTo(bi)
+ if (result != 0) {
+ return result
+ }
+ }
+ ai == null && bi != null -> return -1 // a < b
+ ai == null && bi == null -> return 0 // a = b
+ else -> return 1 // a < b
+ }
+ }
+
+ return 0
+ }
+
+ override fun toString() = StringBuilder().appendDate().toString()
+
+ private fun StringBuilder.appendDate(): StringBuilder {
+ // Construct an ISO-8601 date, dropping precision that doesn't exist.
+ append(year.toStringFixed(4))
+ append("-${(month ?: return this).toStringFixed(2)}")
+ append("-${(day ?: return this).toStringFixed(2)}")
+ append("T${(hour ?: return this).toStringFixed(2)}")
+ append(":${(minute ?: return this.append('Z')).toStringFixed(2)}")
+ append(":${(second ?: return this.append('Z')).toStringFixed(2)}")
+ return this.append('Z')
+ }
+
+ private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len)
+
+ /**
+ * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from
+ * several sub-items and thus can have a "range" of release dates.
+ * @param min The earliest [Date] in the range.
+ * @param max The latest [Date] in the range. May be the same as [min].
+ * @author Alexander Capehart
+ */
+ class Range private constructor(val min: Date, val max: Date) : Comparable {
+
+ /**
+ * Resolve this instance into a human-readable date range.
+ * @param context [Context] required to get human-readable names.
+ * @return If the date has a maximum value, then a `min - max` formatted string will be
+ * returned with the formatted [Date]s of the minimum and maximum dates respectively.
+ * Otherwise, the formatted name of the minimum [Date] will be returned.
+ */
+ fun resolveDate(context: Context) =
+ if (min != max) {
+ context.getString(
+ R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context))
+ } else {
+ min.resolveDate(context)
+ }
+
+ override fun compareTo(other: Range): Int {
+ return min.compareTo(other.min)
+ }
+
+ companion object {
+ /**
+ * Create a [Range] from the given list of [Date]s.
+ * @param dates The [Date]s to use.
+ * @return A [Range] based on the minimum and maximum [Date]s. If there are no [Date]s,
+ * null is returned.
+ */
+ fun from(dates: List): Range? {
+ if (dates.isEmpty()) {
+ // Nothing to do.
+ return null
+ }
+ // Simultaneously find the minimum and maximum values in the given range.
+ // If this list has only one item, then that one date is the minimum and maximum.
+ var min = dates.first()
+ var max = min
+ for (i in 1..dates.lastIndex) {
+ if (dates[i] < min) {
+ min = dates[i]
+ }
+ if (dates[i] > max) {
+ max = dates[i]
+ }
+ }
+ return Range(min, max)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from
+ * https://github.com/quodlibet/mutagen
+ */
+ private val ISO8601_REGEX =
+ Regex(
+ """^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""")
+
+ /**
+ * Create a [Date] from a year component.
+ * @param year The year component.
+ * @return A new [Date] of the given component, or null if the component is invalid.
+ */
+ fun from(year: Int) = fromTokens(listOf(year))
+
+ /**
+ * Create a [Date] from a date component.
+ * @param year The year component.
+ * @param month The month component.
+ * @param day The day component.
+ * @return A new [Date] consisting of the given components. May have reduced precision if
+ * the components were partially invalid, and will be null if all components are invalid.
+ */
+ fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day))
+
+ /**
+ * Create [Date] from a datetime component.
+ * @param year The year component.
+ * @param month The month component.
+ * @param day The day component.
+ * @param hour The hour component
+ * @return A new [Date] consisting of the given components. May have reduced precision if
+ * the components were partially invalid, and will be null if all components are invalid.
+ */
+ fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
+ fromTokens(listOf(year, month, day, hour, minute))
+
+ /**
+ * Create a [Date] from a [String] timestamp.
+ * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision.
+ * @return A new [Date] consisting of the given components. May have reduced precision if
+ * the components were partially invalid, and will be null if all components are invalid or
+ * if the timestamp is invalid.
+ */
+ fun from(timestamp: String): Date? {
+ val tokens =
+ // Match the input with the timestamp regex
+ (ISO8601_REGEX.matchEntire(timestamp) ?: return null)
+ .groupValues
+ // Filter to the specific tokens we want and convert them to integer tokens.
+ .mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
+ return fromTokens(tokens)
+ }
+
+ /**
+ * Create a [Date] from the given non-validated tokens.
+ * @param tokens The tokens to use for each date component, in order of precision.
+ * @return A new [Date] consisting of the given components. May have reduced precision if
+ * the components were partially invalid, and will be null if all components are invalid.
+ */
+ private fun fromTokens(tokens: List): Date? {
+ val validated = mutableListOf()
+ validateTokens(tokens, validated)
+ if (validated.isEmpty()) {
+ // No token was valid, return null.
+ return null
+ }
+ return Date(validated)
+ }
+
+ /**
+ * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop
+ * as soon as an invalid token is found.
+ * @param src The input tokens to validate.
+ * @param dst The destination list to add valid tokens to.
+ */
+ private fun validateTokens(src: List, dst: MutableList) {
+ dst.add(src.getOrNull(0)?.nonZeroOrNull() ?: return)
+ dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return)
+ dst.add(src.getOrNull(2)?.inRangeOrNull(1..31) ?: return)
+ dst.add(src.getOrNull(3)?.inRangeOrNull(0..23) ?: return)
+ dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return)
+ dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return)
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt
index 299e5c60b..528ff3531 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt
@@ -24,8 +24,6 @@ import android.os.Parcelable
import java.security.MessageDigest
import java.text.CollationKey
import java.text.Collator
-import java.text.ParseException
-import java.text.SimpleDateFormat
import java.util.UUID
import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel
@@ -37,7 +35,6 @@ import org.oxycblt.auxio.music.extractor.parseMultiValue
import org.oxycblt.auxio.music.extractor.toUuidOrNull
import org.oxycblt.auxio.music.storage.*
import org.oxycblt.auxio.settings.Settings
-import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull
@@ -610,8 +607,8 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent(
override val collationKey = makeCollationKeyImpl()
override fun resolveName(context: Context) = rawName
- /** The [DateRange] that [Song]s in the [Album] were released. */
- val dates: DateRange? = DateRange.from(songs.mapNotNull { it.date })
+ /** The [Date.Range] that [Song]s in the [Album] were released. */
+ val dates = Date.Range.from(songs.mapNotNull { it.date })
/**
* The [Type] of this album, signifying the type of release it actually is. Defaults to
@@ -1196,236 +1193,6 @@ class Genre constructor(private val raw: Raw, override val songs: List) :
}
}
-/**
- * An ISO-8601/RFC 3339 Date.
- *
- * This class only encodes the timestamp spec and it's conversion to a human-readable date, without
- * any other time management or validation. In general, this should only be used for display.
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-class Date private constructor(private val tokens: List) : Comparable {
- private val year = tokens[0]
- private val month = tokens.getOrNull(1)
- private val day = tokens.getOrNull(2)
- private val hour = tokens.getOrNull(3)
- private val minute = tokens.getOrNull(4)
- private val second = tokens.getOrNull(5)
-
- /**
- * Resolve this instance into a human-readable date.
- * @param context [Context] required to get human-readable names.
- * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan
- * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
- * be properly localized.
- */
- fun resolveDate(context: Context): String {
- if (month != null) {
- // Parse a date format from an ISO-ish format
- val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
- format.applyPattern("yyyy-MM")
- val date =
- try {
- format.parse("$year-$month")
- } catch (e: ParseException) {
- null
- }
-
- if (date != null) {
- // Reformat as a readable month and year
- format.applyPattern("MMM yyyy")
- return format.format(date)
- }
- }
-
- // Unable to create fine-grained date, just format as a year.
- return context.getString(R.string.fmt_number, year)
- }
-
- override fun hashCode() = tokens.hashCode()
-
- override fun equals(other: Any?) = other is Date && tokens == other.tokens
-
- override fun compareTo(other: Date): Int {
- for (i in 0 until max(tokens.size, other.tokens.size)) {
- val ai = tokens.getOrNull(i)
- val bi = other.tokens.getOrNull(i)
- when {
- ai != null && bi != null -> {
- val result = ai.compareTo(bi)
- if (result != 0) {
- return result
- }
- }
- ai == null && bi != null -> return -1 // a < b
- ai == null && bi == null -> return 0 // a = b
- else -> return 1 // a < b
- }
- }
-
- return 0
- }
-
- override fun toString() = StringBuilder().appendDate().toString()
-
- private fun StringBuilder.appendDate(): StringBuilder {
- // Construct an ISO-8601 date, dropping precision that doesn't exist.
- append(year.toStringFixed(4))
- append("-${(month ?: return this).toStringFixed(2)}")
- append("-${(day ?: return this).toStringFixed(2)}")
- append("T${(hour ?: return this).toStringFixed(2)}")
- append(":${(minute ?: return this.append('Z')).toStringFixed(2)}")
- append(":${(second ?: return this.append('Z')).toStringFixed(2)}")
- return this.append('Z')
- }
-
- private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len)
-
- companion object {
- /**
- * A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from
- * https://github.com/quodlibet/mutagen
- */
- private val ISO8601_REGEX =
- Regex(
- """^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""")
-
- /**
- * Create a [Date] from a year component.
- * @param year The year component.
- * @return A new [Date] of the given component, or null if the component is invalid.
- */
- fun from(year: Int) = fromTokens(listOf(year))
-
- /**
- * Create a [Date] from a date component.
- * @param year The year component.
- * @param month The month component.
- * @param day The day component.
- * @return A new [Date] consisting of the given components. May have reduced precision if
- * the components were partially invalid, and will be null if all components are invalid.
- */
- fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day))
-
- /**
- * Create [Date] from a datetime component.
- * @param year The year component.
- * @param month The month component.
- * @param day The day component.
- * @param hour The hour component
- * @return A new [Date] consisting of the given components. May have reduced precision if
- * the components were partially invalid, and will be null if all components are invalid.
- */
- fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
- fromTokens(listOf(year, month, day, hour, minute))
-
- /**
- * Create a [Date] from a [String] timestamp.
- * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision.
- * @return A new [Date] consisting of the given components. May have reduced precision if
- * the components were partially invalid, and will be null if all components are invalid or
- * if the timestamp is invalid.
- */
- fun from(timestamp: String): Date? {
- val tokens =
- // Match the input with the timestamp regex
- (ISO8601_REGEX.matchEntire(timestamp) ?: return null)
- .groupValues
- // Filter to the specific tokens we want and convert them to integer tokens.
- .mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
- return fromTokens(tokens)
- }
-
- /**
- * Create a [Date] from the given non-validated tokens.
- * @param tokens The tokens to use for each date component, in order of precision.
- * @return A new [Date] consisting of the given components. May have reduced precision if
- * the components were partially invalid, and will be null if all components are invalid.
- */
- private fun fromTokens(tokens: List): Date? {
- val validated = mutableListOf()
- validateTokens(tokens, validated)
- if (validated.isEmpty()) {
- // No token was valid, return null.
- return null
- }
- return Date(validated)
- }
-
- /**
- * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop
- * as soon as an invalid token is found.
- * @param src The input tokens to validate.
- * @param dst The destination list to add valid tokens to.
- */
- private fun validateTokens(src: List, dst: MutableList) {
- dst.add(src.getOrNull(0)?.nonZeroOrNull() ?: return)
- dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return)
- dst.add(src.getOrNull(2)?.inRangeOrNull(1..31) ?: return)
- dst.add(src.getOrNull(3)?.inRangeOrNull(0..23) ?: return)
- dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return)
- dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return)
- }
- }
-}
-
-/**
- * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from several
- * sub-items and thus can have a "range" of release dates.
- * @param min The earliest [Date] in the range.
- * @param max The latest [Date] in the range. May be the same as [min].
- * @author Alexander Capehart
- */
-class DateRange private constructor(val min: Date, val max: Date) : Comparable {
-
- /**
- * Resolve this instance into a human-readable date range.
- * @param context [Context] required to get human-readable names.
- * @return If the date has a maximum value, then a `min - max` formatted string will be returned
- * with the formatted [Date]s of the minimum and maximum dates respectively. Otherwise, the
- * formatted name of the minimum [Date] will be returned.
- */
- fun resolveDate(context: Context) =
- if (min != max) {
- context.getString(
- R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context))
- } else {
- min.resolveDate(context)
- }
-
- override fun compareTo(other: DateRange): Int {
- return min.compareTo(other.min)
- }
-
- companion object {
- /**
- * Create a [DateRange] from the given list of [Date]s.
- * @param dates The [Date]s to use.
- * @return A [DateRange] based on the minimum and maximum [Date]s from [dates], or a
- * [DateRange] with a single minimum. If no [Date]s were given, null is returned.
- */
- fun from(dates: List): DateRange? {
- if (dates.isEmpty()) {
- // Nothing to do.
- return null
- }
- // Simultaneously find the minimum and maximum values in the given range.
- // If this list has only one item, then that one date is the minimum and maximum.
- var min = dates.first()
- var max = min
- for (i in 1..dates.lastIndex) {
- if (dates[i] < min) {
- min = dates[i]
- }
- if (dates[i] > max) {
- max = dates[i]
- }
- }
- return DateRange(min, max)
- }
- }
-}
-
// --- MUSIC UID CREATION UTILITIES ---
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt
index 4f8b9c2d7..5c8fa818f 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt
@@ -543,10 +543,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
val INT = NullableComparator()
/** A re-usable instance configured for [Long]s. */
val LONG = NullableComparator()
- /** A re-usable instance configured for [Date]s. */
- val DATE = NullableComparator()
- /** A re-usable instance configured for [DateRange]s. */
- val DATE_RANGE = NullableComparator()
+ /** A re-usable instance configured for [Date.Range]s. */
+ val DATE_RANGE = NullableComparator()
}
}