Skip to content

Instantly share code, notes, and snippets.

@Jeehut
Last active October 1, 2023 14:39
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jeehut/78534c27b24d78f14a3cbd3eebead861 to your computer and use it in GitHub Desktop.
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.
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"
}
}
}
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))
}
}
@kurahaupo
Copy link

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 or 3h4s.

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)
  • (AR) مضى: 3أ2ي14س6د5ث or 3as2ay14s6d5t (in latin script)

The problem is, I have trouble finding comparable abbreviations in even a single language, let alone a table with multiple languages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment