Last active
August 21, 2024 15:13
-
-
Save Jeehut/78534c27b24d78f14a3cbd3eebead861 to your computer and use it in GitHub Desktop.
Localized duration formatting in Kotlin using APIs in Android 9+ with fallback to English on Android 8 and lower.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.icu.text.MeasureFormat | |
import android.icu.text.NumberFormat | |
import android.icu.util.MeasureUnit | |
import android.os.Build | |
import java.util.Locale | |
import kotlin.time.Duration | |
import kotlin.time.ExperimentalTime | |
import kotlin.time.days | |
import kotlin.time.hours | |
import kotlin.time.milliseconds | |
import kotlin.time.minutes | |
import kotlin.time.seconds | |
@ExperimentalTime | |
data class DurationFormat(val locale: Locale = Locale.getDefault()) { | |
enum class Unit { | |
DAY, HOUR, MINUTE, SECOND, MILLISECOND | |
} | |
fun format(duration: kotlin.time.Duration, smallestUnit: Unit = Unit.SECOND): String { | |
var formattedStringComponents = mutableListOf<String>() | |
var remainder = duration | |
for (unit in Unit.values()) { | |
val component = calculateComponent(unit, remainder) | |
remainder = when (unit) { | |
Unit.DAY -> remainder - component.days | |
Unit.HOUR -> remainder - component.hours | |
Unit.MINUTE -> remainder - component.minutes | |
Unit.SECOND -> remainder - component.seconds | |
Unit.MILLISECOND -> remainder - component.milliseconds | |
} | |
val unitDisplayName = unitDisplayName(unit) | |
if (component > 0) { | |
val formattedComponent = NumberFormat.getInstance(locale).format(component) | |
formattedStringComponents.add("$formattedComponent$unitDisplayName") | |
} | |
if (unit == smallestUnit) { | |
val formattedZero = NumberFormat.getInstance(locale).format(0) | |
if (formattedStringComponents.isEmpty()) formattedStringComponents.add("$formattedZero$unitDisplayName") | |
break | |
} | |
} | |
return formattedStringComponents.joinToString(" ") | |
} | |
private fun calculateComponent(unit: Unit, remainder: Duration) = when (unit) { | |
Unit.DAY -> remainder.inDays.toLong() | |
Unit.HOUR -> remainder.inHours.toLong() | |
Unit.MINUTE -> remainder.inMinutes.toLong() | |
Unit.SECOND -> remainder.inSeconds.toLong() | |
Unit.MILLISECOND -> remainder.inMilliseconds.toLong() | |
} | |
private fun unitDisplayName(unit: Unit) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { | |
val measureFormat = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.NARROW) | |
when (unit) { | |
DurationFormat.Unit.DAY -> measureFormat.getUnitDisplayName(MeasureUnit.DAY) | |
DurationFormat.Unit.HOUR -> measureFormat.getUnitDisplayName(MeasureUnit.HOUR) | |
DurationFormat.Unit.MINUTE -> measureFormat.getUnitDisplayName(MeasureUnit.MINUTE) | |
DurationFormat.Unit.SECOND -> measureFormat.getUnitDisplayName(MeasureUnit.SECOND) | |
DurationFormat.Unit.MILLISECOND -> measureFormat.getUnitDisplayName(MeasureUnit.MILLISECOND) | |
} | |
} else { | |
when (unit) { | |
Unit.DAY -> "day" | |
Unit.HOUR -> "hour" | |
Unit.MINUTE -> "min" | |
Unit.SECOND -> "sec" | |
Unit.MILLISECOND -> "msec" | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.test.ext.junit.runners.AndroidJUnit4 | |
import org.junit.Assert.assertEquals | |
import org.junit.Test | |
import org.junit.runner.RunWith | |
import java.util.Locale | |
import kotlin.time.ExperimentalTime | |
import kotlin.time.days | |
import kotlin.time.hours | |
import kotlin.time.milliseconds | |
import kotlin.time.minutes | |
import kotlin.time.seconds | |
@ExperimentalTime | |
@RunWith(AndroidJUnit4::class) | |
class DurationFormatTest { | |
@Test | |
fun formatShouldRespondWithLocalizedDurationString() { | |
val combinedDuration = 5.days.plus(2.hours).plus(62.minutes).plus(214.milliseconds) | |
// default locale (English) | |
assertEquals("10sec", DurationFormat().format(10.seconds)) | |
assertEquals("2hour", DurationFormat().format(2.hours)) | |
assertEquals("5day 3hour 2min", DurationFormat().format(combinedDuration)) | |
assertEquals("0sec", DurationFormat().format(0.seconds)) | |
// custom smallest unit | |
assertEquals("0hour", DurationFormat().format(10.seconds, smallestUnit = DurationFormat.Unit.HOUR)) | |
assertEquals("2hour", DurationFormat().format(2.hours, smallestUnit = DurationFormat.Unit.HOUR)) | |
assertEquals("5day 3hour", DurationFormat().format(combinedDuration, smallestUnit = DurationFormat.Unit.HOUR)) | |
assertEquals("5day 3hour 2min 214msec", DurationFormat().format(combinedDuration, smallestUnit = DurationFormat.Unit.MILLISECOND)) | |
assertEquals("0hour", DurationFormat().format(0.seconds, smallestUnit = DurationFormat.Unit.HOUR)) | |
// locale set to German | |
val germanLocale = Locale.GERMAN | |
assertEquals("10Sek.", DurationFormat(germanLocale).format(10.seconds)) | |
assertEquals("2Std.", DurationFormat(germanLocale).format(2.hours)) | |
assertEquals("5T 3Std. 2Min.", DurationFormat(germanLocale).format(combinedDuration)) | |
assertEquals("0Sek.", DurationFormat(germanLocale).format(0.seconds)) | |
// locale set to Arabic | |
val arabicLocale = Locale.forLanguageTag("ar") | |
assertEquals("١٠ث", DurationFormat(arabicLocale).format(10.seconds)) | |
assertEquals("٢ساعة", DurationFormat(arabicLocale).format(2.hours)) | |
assertEquals("٥يوم ٣ساعة ٢د", DurationFormat(arabicLocale).format(combinedDuration)) | |
assertEquals("٠ث", DurationFormat(arabicLocale).format(0.seconds)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is close to what I'm looking for, but not quite.
Firstly, having both "seconds" and "milliseconds" in the same result is unhelpful. Generally "milliseconds" or "microseconds" should only be shown for very short durations, with "seconds" otherwise, even if it's fractional.
Secondly, I need rounding rather than truncation to whatever the smallest unit is. So if the smallest unit is "minutes", 91 seconds should be reported as TWO minutes.
Thirdly, I need a compact version. In an English locale I want something like
3w2d14h6m5s
or3h4s
.I imagine that other languages might write:
3w2t14st6m5s
(DE)3sem2j14h6m5s
(FR)3ти2д14г6х5с
(UK)3周2天14时6分5秒
(ZH)3주2일14시6분5초
(KR)مضى: 3أ2ي14س6د5ث
or3as2ay14s6d5t
(in latin script)The problem is, I have trouble finding comparable abbreviations in even a single language, let alone a table with multiple languages.