From 9f1c6ba4b726307ca55d3f9966d4ef061bc433e7 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 27 Mar 2026 16:35:57 +0600 Subject: [PATCH 1/3] fix: show "N/A" user rating in search results with 0 rating apps Centralize app user rating formatting in AppUserRatingFormatter so Compose and non-Compose UI use the same raw rating rules. Remove the old wrapper/viewmodel pass-through methods and cover the shared formatter with tests. --- .../components/SearchResultsContentTest.kt | 9 ++-- .../java/foundation/e/apps/data/Constants.kt | 2 - .../e/apps/data/application/data/Ratings.kt | 1 - .../e/apps/data/install/AppManagerWrapper.kt | 13 ------ .../e/apps/ui/MainActivityViewModel.kt | 4 -- .../ui/application/ApplicationFragment.kt | 3 +- .../ui/application/ApplicationViewModel.kt | 5 -- .../ApplicationListRVAdapter.kt | 3 +- .../components/SearchResultsContent.kt | 11 ++--- .../e/apps/ui/utils/AppUserRatingFormatter.kt | 35 ++++++++++++++ .../ui/utils/AppUserRatingFormatterTest.kt | 46 +++++++++++++++++++ 11 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt create mode 100644 app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt diff --git a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt index d97dc5625..e5b3dd700 100644 --- a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt +++ b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt @@ -53,7 +53,6 @@ import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallButtonState import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType -import java.util.Locale @RunWith(AndroidJUnit4::class) class SearchResultsContentTest { @@ -137,8 +136,8 @@ class SearchResultsContentTest { fun applicationMapping_setsAuthorRatingAndPrimaryAction() { val notAvailable = composeRule.activity.getString(R.string.not_available) val openLabel = composeRule.activity.getString(R.string.open) - val expectedRating = String.format(Locale.getDefault(), "%.1f", 4.4) - val unexpectedRating = String.format(Locale.getDefault(), "%.1f", 4.9) + val expectedRating = "4" + val hiddenRating = "4.9" renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), @@ -150,7 +149,7 @@ class SearchResultsContentTest { author = "", package_name = "com.example.rated", source = Source.PLAY_STORE, - ratings = Ratings(usageQualityScore = 4.4), + ratings = Ratings(usageQualityScore = 4.0), status = Status.INSTALLED, ), Application( @@ -184,7 +183,7 @@ class SearchResultsContentTest { composeRule.onNodeWithText(expectedRating).assertIsDisplayed() composeRule.onNodeWithText(openLabel).assertIsDisplayed() composeRule.onNodeWithText(notAvailable).assertIsDisplayed() - composeRule.onAllNodesWithText(unexpectedRating).assertCountEquals(0) + composeRule.onAllNodesWithText(hiddenRating).assertCountEquals(0) } @Test diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index a06cc5737..63fb6553e 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -37,6 +37,4 @@ object Constants { "${BuildConfig.PACKAGE_NAME_PARENTAL_CONTROL}.action.APP_LOUNGE_LOGIN" const val REQUEST_GPLAY_LOGIN = "request_gplay_login" - - const val MIN_VALID_RATING = 0.1 } diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Ratings.kt b/app/src/main/java/foundation/e/apps/data/application/data/Ratings.kt index 5205b8241..5e7353ffc 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Ratings.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Ratings.kt @@ -19,6 +19,5 @@ package foundation.e.apps.data.application.data data class Ratings( - val privacyScore: Double = -1.0, val usageQualityScore: Double = -1.0 ) diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt index 8fd79c474..87f9bc4ea 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt @@ -22,7 +22,6 @@ import android.content.Context import androidx.lifecycle.LiveData import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting -import foundation.e.apps.data.Constants.MIN_VALID_RATING import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.install.download.data.DownloadProgress @@ -171,18 +170,6 @@ class AppManagerWrapper @Inject constructor( return percent } - fun handleRatingFormat(rating: Double): String? { - return if (rating >= MIN_VALID_RATING) { - if (rating % 1 == 0.0) { - rating.toInt().toString() - } else { - rating.toString() - } - } else { - null - } - } - suspend fun getCalculateProgressWithTotalSize( application: Application?, progress: DownloadProgress diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index 236a1ef01..b7e2cd50d 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -397,8 +397,4 @@ class MainActivityViewModel @Inject constructor( fun launchPwa(homeApp: ApplicationDomain) { launchPwa(homeApp.toApplication()) } - - fun handleRatingFormat(rating: Double): String? { - return appManagerWrapper.handleRatingFormat(rating) - } } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index 047850460..26875e538 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -75,6 +75,7 @@ import foundation.e.apps.ui.application.ShareButtonVisibilityState.Visible import foundation.e.apps.ui.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.ui.utils.AppUserRatingFormatter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -357,7 +358,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun updateAppRating(it: Application) { binding.ratingsInclude.apply { - val formattedRating = applicationViewModel.handleRatingFormat(it.ratings.usageQualityScore) + val formattedRating = AppUserRatingFormatter.format(it.ratings.usageQualityScore) if (formattedRating != null) { appRating.text = getString(R.string.rating_out_of, formattedRating) appRating.setCompoundDrawablesWithIntrinsicBounds( diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt index 8c12820cb..de36a579a 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt @@ -213,11 +213,6 @@ class ApplicationViewModel @Inject constructor( fun getApplication(): Application? { return applicationLiveData.value?.first } - - fun handleRatingFormat(rating: Double): String? { - return appManagerWrapper.handleRatingFormat(rating) - } - suspend fun calculateProgress(progress: DownloadProgress): Pair { return appManagerWrapper.getCalculateProgressWithTotalSize( applicationLiveData.value?.first, diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 62a0b19f1..68e92959b 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -53,6 +53,7 @@ import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.applicationlist.diffUtils.ConciseAppDiffUtils import foundation.e.apps.ui.search.SearchFragmentDirections import foundation.e.apps.ui.updates.UpdatesFragmentDirections +import foundation.e.apps.ui.utils.AppUserRatingFormatter import foundation.e.apps.ui.utils.disableInstallButton import foundation.e.apps.ui.utils.enableInstallButton import kotlinx.coroutines.launch @@ -211,7 +212,7 @@ class ApplicationListRVAdapter( appRating.isVisible = false return } - val formattedRating = mainActivityViewModel.handleRatingFormat(searchApp.ratings.usageQualityScore) + val formattedRating = AppUserRatingFormatter.format(searchApp.ratings.usageQualityScore) appRating.text = formattedRating ?: root.context.getString(R.string.not_available) } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt index 0c7b428b7..7ec728af2 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt @@ -58,9 +58,9 @@ import foundation.e.apps.ui.compose.state.InstallButtonState import foundation.e.apps.ui.compose.state.InstallButtonStyle import foundation.e.apps.ui.search.v2.ScrollPosition import foundation.e.apps.ui.search.v2.SearchTabType +import foundation.e.apps.ui.utils.AppUserRatingFormatter import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import java.util.Locale @Composable fun SearchResultsContent( @@ -555,13 +555,8 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState): val ratingText = when { source == Source.OPEN_SOURCE || source == Source.PWA || isSystemApp -> "" - ratings.usageQualityScore >= 0 -> String.format( - Locale.getDefault(), - "%.1f", - ratings.usageQualityScore - ) - - else -> stringResource(id = R.string.not_available) + else -> AppUserRatingFormatter.format(ratings.usageQualityScore) + ?: stringResource(R.string.not_available) } return SearchResultListItemState( diff --git a/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt b/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt new file mode 100644 index 000000000..952c2f5c5 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 e Foundation + * + * 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 foundation.e.apps.ui.utils + +object AppUserRatingFormatter { + + private const val MIN_VALID_RATING = 0.1 + + fun format(rating: Double?): String? { + if (rating == null || rating < MIN_VALID_RATING) { + return null + } + + return if (rating % 1 == 0.0) { + rating.toInt().toString() + } else { + rating.toString() + } + } +} diff --git a/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt b/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt new file mode 100644 index 000000000..ca68d1d4a --- /dev/null +++ b/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2026 e Foundation + * + * 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 foundation.e.apps.ui.utils + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class AppUserRatingFormatterTest { + + @Test + fun format_returnsNull_whenRatingIsNull() { + assertNull(AppUserRatingFormatter.format(null)) + } + + @Test + fun format_returnsNull_whenRatingIsBelowMinimum() { + assertNull(AppUserRatingFormatter.format(-1.0)) + assertNull(AppUserRatingFormatter.format(0.0)) + } + + @Test + fun format_returnsIntegerString_whenRatingIsWholeNumber() { + assertEquals("4", AppUserRatingFormatter.format(4.0)) + } + + @Test + fun format_returnsDecimalString_whenRatingHasFraction() { + assertEquals("4.4", AppUserRatingFormatter.format(4.4)) + } +} -- GitLab From e2328ff8a8c0a4a759270ff3192b39a3fd8884e2 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 27 Mar 2026 17:02:13 +0600 Subject: [PATCH 2/3] refactor: improve UI in search results list item Rating icon and text are vertically centered now. --- .../e/apps/ui/compose/components/SearchResultListItem.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt index 31d77299d..ef323db77 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -163,14 +164,18 @@ private fun AppIcon( private fun RatingChip(ratingText: String) { Row(verticalAlignment = Alignment.CenterVertically) { Image( + modifier = Modifier.padding(bottom = 2.dp), painter = painterResource(R.drawable.ic_star), contentDescription = stringResource(id = R.string.rating), - modifier = Modifier.size(16.dp), ) Spacer(modifier = Modifier.width(4.dp)) Text( text = ratingText, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium.copy( + fontSize = 13.sp, + lineHeight = 13.sp, + platformStyle = PlatformTextStyle(includeFontPadding = false), + ), color = MaterialTheme.colorScheme.onBackground, ) } -- GitLab From 42e1fff9cd830e5341ac39d120029b04b5592922 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 27 Mar 2026 18:59:30 +0600 Subject: [PATCH 3/3] refactor: add locale-aware formatting for user rating --- .../components/SearchResultsContentTest.kt | 12 ++++--- .../ui/application/ApplicationFragment.kt | 9 +++-- .../ApplicationListRVAdapter.kt | 3 +- .../components/SearchResultsContent.kt | 4 ++- .../e/apps/ui/utils/AppUserRatingFormatter.kt | 15 ++++---- .../ui/utils/AppUserRatingFormatterTest.kt | 35 +++++++++++++++---- 6 files changed, 53 insertions(+), 25 deletions(-) diff --git a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt index e5b3dd700..3db6387b5 100644 --- a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt +++ b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt @@ -53,6 +53,7 @@ import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallButtonState import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType +import foundation.e.apps.ui.utils.AppUserRatingFormatter @RunWith(AndroidJUnit4::class) class SearchResultsContentTest { @@ -136,8 +137,9 @@ class SearchResultsContentTest { fun applicationMapping_setsAuthorRatingAndPrimaryAction() { val notAvailable = composeRule.activity.getString(R.string.not_available) val openLabel = composeRule.activity.getString(R.string.open) - val expectedRating = "4" - val hiddenRating = "4.9" + val locale = composeRule.activity.resources.configuration.locales[0] + val expectedRating = AppUserRatingFormatter.format(4.4, locale) + val hiddenRating = AppUserRatingFormatter.format(4.9, locale) renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), @@ -149,7 +151,7 @@ class SearchResultsContentTest { author = "", package_name = "com.example.rated", source = Source.PLAY_STORE, - ratings = Ratings(usageQualityScore = 4.0), + ratings = Ratings(usageQualityScore = 4.4), status = Status.INSTALLED, ), Application( @@ -180,10 +182,10 @@ class SearchResultsContentTest { ) composeRule.onNodeWithText("com.example.rated").assertIsDisplayed() - composeRule.onNodeWithText(expectedRating).assertIsDisplayed() + composeRule.onNodeWithText(expectedRating ?: "").assertIsDisplayed() composeRule.onNodeWithText(openLabel).assertIsDisplayed() composeRule.onNodeWithText(notAvailable).assertIsDisplayed() - composeRule.onAllNodesWithText(hiddenRating).assertCountEquals(0) + composeRule.onAllNodesWithText(hiddenRating ?: "").assertCountEquals(0) } @Test diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index 26875e538..df1a74499 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -358,13 +358,14 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun updateAppRating(it: Application) { binding.ratingsInclude.apply { - val formattedRating = AppUserRatingFormatter.format(it.ratings.usageQualityScore) + val locale = resources.configuration.locales[0] + val formattedRating = AppUserRatingFormatter.format(it.ratings.usageQualityScore, locale) if (formattedRating != null) { appRating.text = getString(R.string.rating_out_of, formattedRating) appRating.setCompoundDrawablesWithIntrinsicBounds( ContextCompat.getDrawable(requireContext(), R.drawable.ic_star_blank), null, - getRatingDrawable(formattedRating), + getRatingDrawable(it.ratings.usageQualityScore), null ) appRating.compoundDrawablePadding = DRAWABLE_PADDING @@ -1076,9 +1077,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { return applyDotAccent(dotColor) } - private fun getRatingDrawable(reviewRating: String): Drawable? { - val rating = reviewRating.toDouble() - + private fun getRatingDrawable(rating: Double): Drawable? { var dotColor = ContextCompat.getColor(requireContext(), R.color.colorGreen) if (rating <= LOW_REVIEW_THRESHOLD) { dotColor = ContextCompat.getColor(requireContext(), R.color.colorRed) diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 68e92959b..161924bde 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -212,7 +212,8 @@ class ApplicationListRVAdapter( appRating.isVisible = false return } - val formattedRating = AppUserRatingFormatter.format(searchApp.ratings.usageQualityScore) + val locale = root.context.resources.configuration.locales[0] + val formattedRating = AppUserRatingFormatter.format(searchApp.ratings.usageQualityScore, locale) appRating.text = formattedRating ?: root.context.getString(R.string.not_available) } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt index 7ec728af2..458f3e0a2 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt @@ -42,6 +42,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -553,9 +554,10 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState): ) } + val locale = LocalConfiguration.current.locales[0] val ratingText = when { source == Source.OPEN_SOURCE || source == Source.PWA || isSystemApp -> "" - else -> AppUserRatingFormatter.format(ratings.usageQualityScore) + else -> AppUserRatingFormatter.format(ratings.usageQualityScore, locale) ?: stringResource(R.string.not_available) } diff --git a/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt b/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt index 952c2f5c5..06b9ca7d9 100644 --- a/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt +++ b/app/src/main/java/foundation/e/apps/ui/utils/AppUserRatingFormatter.kt @@ -17,19 +17,22 @@ package foundation.e.apps.ui.utils +import java.text.NumberFormat +import java.util.Locale + object AppUserRatingFormatter { private const val MIN_VALID_RATING = 0.1 - fun format(rating: Double?): String? { + fun format(rating: Double?, locale: Locale): String? { if (rating == null || rating < MIN_VALID_RATING) { return null } - return if (rating % 1 == 0.0) { - rating.toInt().toString() - } else { - rating.toString() - } + return NumberFormat.getNumberInstance(locale).apply { + minimumFractionDigits = 0 + maximumFractionDigits = 1 + isGroupingUsed = false + }.format(rating) } } diff --git a/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt b/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt index ca68d1d4a..28c0d933c 100644 --- a/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt +++ b/app/src/test/java/foundation/e/apps/ui/utils/AppUserRatingFormatterTest.kt @@ -20,27 +20,48 @@ package foundation.e.apps.ui.utils import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test +import java.util.Locale class AppUserRatingFormatterTest { + private val usLocale = Locale.US + private val franceLocale = Locale.FRANCE + @Test fun format_returnsNull_whenRatingIsNull() { - assertNull(AppUserRatingFormatter.format(null)) + assertNull(AppUserRatingFormatter.format(null, usLocale)) } @Test fun format_returnsNull_whenRatingIsBelowMinimum() { - assertNull(AppUserRatingFormatter.format(-1.0)) - assertNull(AppUserRatingFormatter.format(0.0)) + assertNull(AppUserRatingFormatter.format(-1.0, usLocale)) + assertNull(AppUserRatingFormatter.format(0.0, usLocale)) + } + + @Test + fun format_returnsWholeNumberWithoutFraction_forUsLocale() { + assertEquals("4", AppUserRatingFormatter.format(4.0, usLocale)) + } + + @Test + fun format_returnsDecimalWithDot_forUsLocale() { + assertEquals("4.4", AppUserRatingFormatter.format(4.4, usLocale)) + } + + @Test + fun format_returnsDecimalWithComma_forFrenchLocale() { + assertEquals("4,4", AppUserRatingFormatter.format(4.4, franceLocale)) } @Test - fun format_returnsIntegerString_whenRatingIsWholeNumber() { - assertEquals("4", AppUserRatingFormatter.format(4.0)) + fun format_roundsUnexpectedPrecision_forUsLocale() { + assertEquals("4.3", AppUserRatingFormatter.format(4.34, usLocale)) + assertEquals("4.4", AppUserRatingFormatter.format(4.36, usLocale)) } @Test - fun format_returnsDecimalString_whenRatingHasFraction() { - assertEquals("4.4", AppUserRatingFormatter.format(4.4)) + fun format_roundsUnexpectedPrecision_forFrenchLocale() { + assertEquals("4,3", AppUserRatingFormatter.format(4.34, franceLocale)) + assertEquals("4,4", AppUserRatingFormatter.format(4.36, franceLocale)) } } -- GitLab