mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
Added formatter for time
This commit is contained in:
parent
d5cc855b3d
commit
542a95acf7
@ -37,6 +37,8 @@ class UnittoLibraryComposePlugin : Plugin<Project> {
|
||||
"implementation"(libs.findLibrary("androidx.compose.material3").get())
|
||||
"implementation"(libs.findLibrary("androidx.lifecycle.runtime.compose").get())
|
||||
"implementation"(libs.findLibrary("androidx.compose.material.icons.extended").get())
|
||||
"implementation"(libs.findLibrary("androidx.compose.ui.tooling").get())
|
||||
"implementation"(libs.findLibrary("androidx.compose.ui.tooling.preview").get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,10 +24,20 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.sadellie.unitto.core.ui"
|
||||
|
||||
// Workaround from https://github.com/robolectric/robolectric/pull/4736
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.org.robolectric)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
implementation(project(mapOf("path" to ":core:base")))
|
||||
}
|
||||
|
@ -18,14 +18,20 @@
|
||||
|
||||
package com.sadellie.unitto.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.sadellie.unitto.core.base.INTERNAL_DISPLAY
|
||||
import com.sadellie.unitto.core.base.KEY_0
|
||||
import com.sadellie.unitto.core.base.KEY_COMMA
|
||||
import com.sadellie.unitto.core.base.KEY_DOT
|
||||
import com.sadellie.unitto.core.base.KEY_E
|
||||
import com.sadellie.unitto.core.base.KEY_LEFT_BRACKET
|
||||
import com.sadellie.unitto.core.base.KEY_MINUS
|
||||
import com.sadellie.unitto.core.base.KEY_RIGHT_BRACKET
|
||||
import com.sadellie.unitto.core.base.OPERATORS
|
||||
import com.sadellie.unitto.core.base.Separator
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
object Formatter {
|
||||
private const val SPACE = " "
|
||||
@ -42,6 +48,19 @@ object Formatter {
|
||||
*/
|
||||
var fractional = KEY_COMMA
|
||||
|
||||
private val timeDivisions by lazy {
|
||||
mapOf(
|
||||
R.string.day_short to BigDecimal("86400000000000000000000"),
|
||||
R.string.hour_short to BigDecimal("3600000000000000000000"),
|
||||
R.string.minute_short to BigDecimal("60000000000000000000"),
|
||||
R.string.second_short to BigDecimal("1000000000000000000"),
|
||||
R.string.millisecond_short to BigDecimal("1000000000000000"),
|
||||
R.string.microsecond_short to BigDecimal("1000000000000"),
|
||||
R.string.nanosecond_short to BigDecimal("1000000000"),
|
||||
R.string.attosecond_short to BigDecimal("1"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change current separator to another [separator].
|
||||
*
|
||||
@ -118,4 +137,42 @@ object Formatter {
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes [input] and [basicUnit] of the unit to format it to be more human readable.
|
||||
*
|
||||
* @return String like "1d 12h 12s".
|
||||
*/
|
||||
@Composable
|
||||
fun formatTime(input: String, basicUnit: BigDecimal?): String {
|
||||
if (basicUnit == null) return KEY_0
|
||||
|
||||
try {
|
||||
// Don't need magic if the input is zero
|
||||
if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return KEY_0
|
||||
} catch (e: NumberFormatException) {
|
||||
// For case such as "10-" and "("
|
||||
return KEY_0
|
||||
}
|
||||
// Attoseconds don't need "magic"
|
||||
if (basicUnit.compareTo(BigDecimal.ONE) == 0) return formatNumber(input)
|
||||
|
||||
var result = if (input.startsWith(KEY_MINUS)) KEY_MINUS else ""
|
||||
var remainingSeconds = BigDecimal(input)
|
||||
.abs()
|
||||
.multiply(basicUnit)
|
||||
.setScale(0, RoundingMode.HALF_EVEN)
|
||||
|
||||
if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return KEY_0
|
||||
|
||||
timeDivisions.forEach { (timeStr, divider) ->
|
||||
val division = remainingSeconds.divideAndRemainder(divider)
|
||||
val time = division.component1()
|
||||
remainingSeconds = division.component2()
|
||||
if (time.compareTo(BigDecimal.ZERO) == 1) {
|
||||
result += "${formatNumber(time.toPlainString())}${stringResource(timeStr)} "
|
||||
}
|
||||
}
|
||||
return result.trimEnd()
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,14 @@
|
||||
|
||||
package com.sadellie.unitto.core.ui
|
||||
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import com.sadellie.unitto.core.base.Separator
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.math.BigDecimal
|
||||
|
||||
private val formatter = Formatter
|
||||
|
||||
@ -33,8 +38,12 @@ private const val INCOMPLETE_EXPR = "50+123456÷8×0.8–12+"
|
||||
private const val COMPLETE_EXPR = "50+123456÷8×0.8–12+0-√9*4^9+2×(9+8×7)"
|
||||
private const val SOME_BRACKETS = "(((((((("
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class FormatterTest {
|
||||
|
||||
@get: Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun setSeparatorSpaces() {
|
||||
formatter.setSeparator(Separator.SPACES)
|
||||
@ -77,4 +86,101 @@ class FormatterTest {
|
||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatTimeTest() {
|
||||
formatter.setSeparator(Separator.SPACES)
|
||||
composeTestRule.setContent {
|
||||
var basicValue = BigDecimal.valueOf(1)
|
||||
assertEquals("-28", Formatter.formatTime("-28", basicValue))
|
||||
assertEquals("-0.05", Formatter.formatTime("-0.05", basicValue))
|
||||
assertEquals("0", Formatter.formatTime("0", basicValue))
|
||||
assertEquals("0", Formatter.formatTime("-0", basicValue))
|
||||
|
||||
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
|
||||
assertEquals("-28d", Formatter.formatTime("-28", basicValue))
|
||||
assertEquals("-1h 12m", Formatter.formatTime("-0.05", basicValue))
|
||||
assertEquals("0", Formatter.formatTime("0", basicValue))
|
||||
assertEquals("0", Formatter.formatTime("-0", basicValue))
|
||||
|
||||
// DAYS
|
||||
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
|
||||
assertEquals("12h", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("1h 12m", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("7m 12s", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28d", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("90d", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("90d 12h", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("90d 7m 12s", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// HOURS
|
||||
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
|
||||
assertEquals("30m", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("3m", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("18s", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("1d 4h", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("3d 18h", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("3d 18h 30m", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("3d 18h 18s", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// MINUTES
|
||||
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
|
||||
assertEquals("30s", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("3s", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("300ms", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28m", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("1h 30m", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("1h 30m 30s", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("1h 30m 300ms", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// SECONDS
|
||||
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
|
||||
assertEquals("500ms", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("50ms", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("5ms", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28s", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("1m 30s", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("1m 30s 500ms", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("1m 30s 5ms", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// MILLISECONDS
|
||||
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
|
||||
assertEquals("500µs", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("50µs", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("5µs", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28ms", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("90ms", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("90ms 500µs", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("90ms 5µs", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// MICROSECONDS
|
||||
basicValue = BigDecimal.valueOf(1_000_000_000_000)
|
||||
assertEquals("500ns", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("50ns", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("5ns", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28µs", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("90µs", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("90µs 500ns", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("90µs 5ns", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// NANOSECONDS
|
||||
basicValue = BigDecimal.valueOf(1_000_000_000)
|
||||
assertEquals("500 000 000as", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("50 000 000as", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("5 000 000as", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28ns", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("90ns", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("90ns 500 000 000as", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("90ns 5 000 000as", Formatter.formatTime("90.005", basicValue))
|
||||
|
||||
// ATTOSECONDS
|
||||
basicValue = BigDecimal.valueOf(1)
|
||||
assertEquals("0.5", Formatter.formatTime("0.5", basicValue))
|
||||
assertEquals("0.05", Formatter.formatTime("0.05", basicValue))
|
||||
assertEquals("0.005", Formatter.formatTime("0.005", basicValue))
|
||||
assertEquals("28", Formatter.formatTime("28", basicValue))
|
||||
assertEquals("90", Formatter.formatTime("90", basicValue))
|
||||
assertEquals("90.5", Formatter.formatTime("90.5", basicValue))
|
||||
assertEquals("90.005", Formatter.formatTime("90.005", basicValue))
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ package com.sadellie.unitto.data
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
|
||||
fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
|
||||
flow: Flow<T1>,
|
||||
flow2: Flow<T2>,
|
||||
flow3: Flow<T3>,
|
||||
@ -29,9 +29,10 @@ fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
|
||||
flow5: Flow<T5>,
|
||||
flow6: Flow<T6>,
|
||||
flow7: Flow<T7>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
|
||||
flow8: Flow<T8>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
|
||||
): Flow<R> =
|
||||
kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
|
||||
kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { args: Array<*> ->
|
||||
transform(
|
||||
args[0] as T1,
|
||||
args[1] as T2,
|
||||
@ -40,5 +41,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
|
||||
args[4] as T5,
|
||||
args[5] as T6,
|
||||
args[6] as T7,
|
||||
args[7] as T8,
|
||||
)
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ internal fun MainScreen(
|
||||
processInput = { viewModel.processInput(it) },
|
||||
deleteDigit = { viewModel.deleteDigit() },
|
||||
clearInput = { viewModel.clearInput() },
|
||||
onOutputTextFieldClick = { viewModel.toggleFormatTime() }
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -108,6 +109,7 @@ private fun MainScreenContent(
|
||||
processInput: (String) -> Unit = {},
|
||||
deleteDigit: () -> Unit = {},
|
||||
clearInput: () -> Unit = {},
|
||||
onOutputTextFieldClick: () -> Unit
|
||||
) {
|
||||
PortraitLandscape(
|
||||
modifier = modifier,
|
||||
@ -125,6 +127,8 @@ private fun MainScreenContent(
|
||||
navigateToRightScreen = navigateToRightScreen,
|
||||
swapUnits = swapMeasurements,
|
||||
converterMode = mainScreenUIState.mode,
|
||||
formatTime = mainScreenUIState.formatTime,
|
||||
onOutputTextFieldClick = onOutputTextFieldClick
|
||||
)
|
||||
},
|
||||
content2 = {
|
||||
|
@ -33,6 +33,7 @@ import com.sadellie.unitto.data.units.AbstractUnit
|
||||
* @property unitFrom Unit on the left.
|
||||
* @property unitTo Unit on the right.
|
||||
* @property mode
|
||||
* @property formatTime If true will format output when converting time.
|
||||
*/
|
||||
data class MainScreenUIState(
|
||||
val inputValue: String = KEY_0,
|
||||
@ -43,6 +44,7 @@ data class MainScreenUIState(
|
||||
val unitFrom: AbstractUnit? = null,
|
||||
val unitTo: AbstractUnit? = null,
|
||||
val mode: ConverterMode = ConverterMode.DEFAULT,
|
||||
val formatTime: Boolean = true
|
||||
)
|
||||
|
||||
enum class ConverterMode {
|
||||
|
@ -129,6 +129,8 @@ class MainViewModel @Inject constructor(
|
||||
*/
|
||||
private val _showError: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
private val _formatTime: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
/**
|
||||
* Current state of UI.
|
||||
*/
|
||||
@ -139,8 +141,9 @@ class MainViewModel @Inject constructor(
|
||||
_calculated,
|
||||
_result,
|
||||
_showLoading,
|
||||
_showError
|
||||
) { inputValue, unitFromValue, unitToValue, calculatedValue, resultValue, showLoadingValue, showErrorValue ->
|
||||
_showError,
|
||||
_formatTime
|
||||
) { inputValue, unitFromValue, unitToValue, calculatedValue, resultValue, showLoadingValue, showErrorValue, formatTime ->
|
||||
return@combine MainScreenUIState(
|
||||
inputValue = inputValue,
|
||||
calculatedValue = calculatedValue,
|
||||
@ -153,7 +156,8 @@ class MainViewModel @Inject constructor(
|
||||
* If there will be more modes, this should be a separate value which we update when
|
||||
* changing units.
|
||||
*/
|
||||
mode = if (_unitFrom.value is NumberBaseUnit) ConverterMode.BASE else ConverterMode.DEFAULT
|
||||
mode = if (_unitFrom.value is NumberBaseUnit) ConverterMode.BASE else ConverterMode.DEFAULT,
|
||||
formatTime = formatTime
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
@ -389,6 +393,10 @@ class MainViewModel @Inject constructor(
|
||||
return _calculated.value ?: _input.value
|
||||
}
|
||||
|
||||
fun toggleFormatTime() {
|
||||
_formatTime.update { !it }
|
||||
}
|
||||
|
||||
private suspend fun convertInput() {
|
||||
// Loading don't do anything
|
||||
if ((_unitFrom.value == null) or (_unitTo.value == null)) return
|
||||
|
@ -21,6 +21,7 @@ package com.sadellie.unitto.feature.converter.components
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -43,6 +44,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.sadellie.unitto.core.ui.Formatter
|
||||
import com.sadellie.unitto.data.units.AbstractUnit
|
||||
import com.sadellie.unitto.core.ui.R
|
||||
import com.sadellie.unitto.data.units.UnitGroup
|
||||
import com.sadellie.unitto.feature.converter.ConverterMode
|
||||
|
||||
/**
|
||||
@ -62,6 +64,8 @@ import com.sadellie.unitto.feature.converter.ConverterMode
|
||||
* @param navigateToRightScreen Function that is called when clicking right unit selection button.
|
||||
* @param swapUnits Method to swap units.
|
||||
* @param converterMode [ConverterMode.BASE] doesn't use formatting for input/output.
|
||||
* @param formatTime If True will use [Formatter.formatTime].
|
||||
* @param onOutputTextFieldClick Action to be called when user clicks on output text field.
|
||||
*/
|
||||
@Composable
|
||||
internal fun TopScreenPart(
|
||||
@ -77,6 +81,8 @@ internal fun TopScreenPart(
|
||||
navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit,
|
||||
swapUnits: () -> Unit,
|
||||
converterMode: ConverterMode,
|
||||
formatTime: Boolean,
|
||||
onOutputTextFieldClick: () -> Unit
|
||||
) {
|
||||
var swapped by remember { mutableStateOf(false) }
|
||||
val swapButtonRotation: Float by animateFloatAsState(
|
||||
@ -101,12 +107,20 @@ internal fun TopScreenPart(
|
||||
textToCopy = calculatedValue ?: inputValue,
|
||||
)
|
||||
MyTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onOutputTextFieldClick),
|
||||
primaryText = {
|
||||
when {
|
||||
networkLoading -> stringResource(R.string.loading_label)
|
||||
networkError -> stringResource(R.string.error_label)
|
||||
converterMode == ConverterMode.BASE -> outputValue.uppercase()
|
||||
formatTime and (unitTo?.group == UnitGroup.TIME) -> {
|
||||
Formatter.formatTime(
|
||||
input = calculatedValue ?: inputValue,
|
||||
basicUnit = unitFrom?.basicUnit
|
||||
)
|
||||
}
|
||||
else -> Formatter.format(outputValue)
|
||||
}
|
||||
},
|
||||
|
@ -40,6 +40,8 @@ org-jetbrains-kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxComposeUi" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidxComposeUi" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxComposeUi" }
|
||||
androidx-compose-ui-test-junit4 = { group= "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxCompose" }
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidxCompose" }
|
||||
androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
|
||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycleRuntimeCompose" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user