Android Plurals for float values
Asked Answered
Q

6

6

I would like to use plurals for my Android project. However, the values I provide can be float values.

So for instance, when setting 1.5 stars, I want this to understand, it's not 1 star but 1.5 stars.

<plurals name="stars">
  <item quantity="one">%d star</item>
  <item quantity="other">%d stars</item>
</plurals>

However, the Android system seems to use integer values (%d) only.

The method looks like this:

String getQuantityString(@PluralsRes int id, int quantity, Object... formatArgs)

where quantity is defined as Int.

Is there any solution for this?

Quennie answered 26/2, 2019 at 10:6 Comment(1)
I think ultimately you have to pass an int so you can only pass int part of rating and it will covered in other . Or maybe i did not understand the question..Corydalis
Q
2

After doing further research, it appears there is no good solution for this.

As also seen in the other answers, they always require a lot of "manual processing" to it requiring no different workflow than creating separate string resources.

The general suggestion seems to be rounding / processing the float values manually (e.g checking whether the float value matches 1.0) and then using apropriate Int values for the plurals call.

But aside from not really using plurals then this comes with the problem of other languages (e.g. I have no clue if 1.5 stars would also be plural in another language as it is in English) and thus these rounding options may not apply universally.

So the answer is: there seems to be no perfect solution (meaning solved "automatically" by the Android system).

What I actually do therefore is to simply pick exceptions and use different Strings there. So the (pseudo code) way of doing currently looks like

// optionally wrap different languages around
// if language == English

   when (amountStars) {
    is 1.0 -> getString(R.string.stars_singular, 1) 
    ... ->
    else -> getString(R.string.stars_plural, amountStars)
   }
// if language == Chinese ...

where additional cases have to be "hard coded". So for example you have to decide whether 0 means

"0 stars" (plural string) or

"no star" (singular string)

But there seems no real benefit of using plurals over separate string resources with common placeholders. On the other hand this (at last for me) gives more flexibility for formatting options. For example, one may create a text like "1 star and a half" where it becomes singular again (even though numerically we would write 1.5 stars).

Quennie answered 5/3, 2019 at 15:52 Comment(0)
F
1

Don't use plurals for fractional numbers. Just stick with basic string resources and use a placeholder:

<string name="fractional_stars">%1$s stars</string>

getString(R.string.fractional_stars, 0.5F.toString())

or

<string name="fractional_stars">% stars</string>

getString(R.string.half_a_star).replace("%", 0.5F.toString())
Focus answered 5/5, 2022 at 7:18 Comment(0)
S
0

Simply do this:

getQuantityString(R.plurals.stars, quantity > 1f ? 2 : 1, quantity):

And replace the %d in your strings with %f.

Sneaker answered 26/2, 2019 at 16:54 Comment(3)
However, I'm not sure its simply that. There might be languages that have issues with 2 or 0. For instance 0.5 stars is also different than "no star" of having a quantity of 0.Quennie
That is true. However, I think the point is, that the quantity parameter of getQuantityString() is mapped to the values of the quantity attribute of the plural items ("zero", "one", "other"), whilest formatArgs are used for the placeholder(s) in the strings themselves. Instead of my simple example you can also create a separate method that maps the given float to an int representing "zero", "one", "other" ... but you can still use the original float value as in formatArgs.Sneaker
Well, the whole idea was to avoid manual separation. If I have to process them "manually" anyway, I can just create some String resources and do the logic myself. So there is no real use in using plurals in the first place. But thanks for the suggestion.Quennie
F
0

getQuantityString takes a quantity of type Int and Object... formatArgs If you round the quantity to Int you would make sure that any value in 1.0 -> 1.99 is a single item and other than that is a plural

 resources.getQuantityString(
            R.plurals.products_left_in_stock_message_plural,
            leftInStock.toInt(), leftInStock.toString()
        )

So you only round it the quantity but pass the actually value as an argument

<plurals name="products_left_in_stock_message_plural">
    <item quantity="one">Only one item is available from this product</item>
    <item quantity="other">There are only %s item is available from this product</item>
</plurals>
Futile answered 20/12, 2022 at 12:17 Comment(0)
B
0

you can do this:

data class PluralInfo(
    @StringRes val talkBackOne: Int,    //singular plural form
    @StringRes val talkBackTwo: Int,    //dual plural form.
    @StringRes val talkBackFew: Int,    //paucal plural form (russian 2-4)
    @StringRes val talkBackMany: Int,   //russian 5-19, arabic 11-99...
    @StringRes val talkBackZero: Int,   //zero plural form
    @StringRes val talkBackOther: Int,  //default plural form (float values here)
) {
    fun getQuantityString(context: Context, quantity: Double): String {
        val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            context.resources.configuration.locales[0];
        } else {
            //noinspection deprecation
            context.resources.configuration.locale;
        }

        val rules = PluralRules.forLocale(locale, PluralRules.PluralType.CARDINAL)
        val resId = when (rules.select(quantity)) {
            PluralRules.KEYWORD_ZERO -> talkBackMany
            PluralRules.KEYWORD_ONE -> talkBackOne
            PluralRules.KEYWORD_TWO -> talkBackFew
            PluralRules.KEYWORD_FEW -> talkBackFew
            PluralRules.KEYWORD_MANY -> talkBackMany
            PluralRules.KEYWORD_OTHER -> talkBackOther
            else -> talkBackOther
        }
        return context.getString(resId)
    }
}
Bedside answered 27/6, 2023 at 9:28 Comment(0)
H
0

You should be able to do something like this to get this to work as long as you are 24++

fun Resources.getQuantityString(@PluralsRes id: Int, quantity: Double): String {
    return getQuantityString(id, intSampleForDecimalQuantity(quantity))
}


fun Resources.getQuantityString(@PluralsRes id: Int, quantity: Double, vararg formatArgs: String): String {
    return getQuantityString(id, intSampleForDecimalQuantity(quantity), *formatArgs)
}


fun Resources.getQuantityText(@PluralsRes id: Int, quantity: Double): CharSequence {
    return getQuantityText(id, intSampleForDecimalQuantity(quantity))
}


private fun Resources.intSampleForDecimalQuantity(quantity: Double): Int {
    val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        configuration.locales[0];
    } else {
        //noinspection deprecation
        configuration.locale;
    }
    val rules = PluralRules.forLocale(locale, PluralRules.PluralType.CARDINAL)
    val intSamples = rules.getSamples(rules.select(quantity)).map { it.toInt() }
    return intSamples.first()
}

This use android.icu.text that does support decimal for plural quantities to get an integer sample of the selected quantitative group and then use that to go to the default Android api to get the correct plural. This should work for all locales.

Hereditable answered 15/3 at 23:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.