Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I format a time duration to a string with localized time units on Android?

I would like to format a time duration (e.g. in seconds) in a String which would contain the localized names of time units. For example for input 8692 I would get string "2 h 24 min 52 sec" with the time units correctly localized for each language. Is there some class in Android that could do this? Does anyone know some external opensource library for this? Thank you.

like image 887
Pavel S. Avatar asked Jul 02 '15 11:07

Pavel S.


3 Answers

I just came across this same issue when trying to print a Kotlin Duration type with localized formatting and because I couldn't find a good solution, I wrote one myself. It is based on APIs provided starting in Android 9 (for localized units), but with a fallback to English units for lower Android versions so it can be used with lower targeting apps.

Here's how it looks like on the usage side (see Kotlin Duration type to understand 1st line):

val duration = 5.days.plus(3.hours).plus(2.minutes).plus(214.milliseconds)

DurationFormat().format(duration) // "5day 3hour 2min"
DurationFormat(Locale.GERMANY).format(duration) // "5T 3Std. 2Min."
DurationFormat(Locale.forLanguageTag("ar").format(duration) // "٥يوم ٣ساعة ٢د"
DurationFormat().format(duration, smallestUnit = DurationFormat.Unit.HOUR) // "5day 3hour"
DurationFormat().format(15.minutes) // "15min"
DurationFormat().format(0.hours) // "0sec"

As you can see, you can specify a custom locale to the DurationFormat type. By default it uses Locale.getDefault(). Languages that have different symbols for number than romanic are also supported (via NumberFormat). Also, you can specify a custom smallestUnit, by default it is set to SECOND, so milliseconds will not be shown. Note that any unit with a value of 0 will be ignored and if the entire number is 0, the smallest unit will be used with the value 0.

This is the full DurationFormat type, feel free to copy (also available as a GitHub gist incl. unit tests):

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"
        }
    }
}
like image 192
Jeehut Avatar answered Sep 18 '22 04:09

Jeehut


I don't know about any open source library, but if I understand the problem correctly, it shouldn't be too hard to do it by hand:

public static String foo(int duration) {
    int hours = duration/3600;
    int minutes = (duration%3600)/60;
    int seconds = duration%60;
    return hours + getResources().getString(R.string.hours) +
           minutes + getResources().getString(R.string.minutes) +
           seconds + getResources().getString(R.string.seconds);
}

Then you define those strings in your strings.xml and localise them as you normally would. (See http://developer.android.com/guide/topics/resources/localization.html)

As for Android, there's the Duration class (http://developer.android.com/reference/javax/xml/datatype/Duration.html), which you could use, but its toString() method does not exactly do what you want.

Edit just for completeness: I now understand your problem better, you don't want to localise all the time units by hand. Unicode Common Locale Data Repository has some (well, a LOT of) interesting data to offer. They provide XML data on pretty much anything you could possibly want to localise, e.g. de.xml (German):

<unit type="duration-hour">
    <displayName>Stunden</displayName>
    <unitPattern count="one">{0} Stunde</unitPattern>
    <unitPattern count="other">{0} Stunden</unitPattern>
    <perUnitPattern>{0} pro Stunde</perUnitPattern>
</unit>

I assume that this is overkill, but as I said: for completeness.

like image 2
icke Avatar answered Oct 16 '22 20:10

icke


I did it like that.

val minuteString = systemResources.getString(systemResources.getIdentifier("minute", "string", "android"))
val secondString = systemResources.getString(systemResources.getIdentifier("second", "string", "android"))
val hourString = systemResources.getString(systemResources.getIdentifier("hour", "string", "android"))
val dayString = systemResources.getString(systemResources.getIdentifier("day", "string", "android")

In this file you look up other time unit resources https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/strings.xml

like image 2
mobo Avatar answered Oct 16 '22 18:10

mobo