mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 16:55:26 +02:00
parent
c843732e0e
commit
93404c1fac
@ -31,6 +31,7 @@ android {
|
||||
dependencies {
|
||||
testImplementation(libs.junit.junit)
|
||||
|
||||
implementation(project(":data:common"))
|
||||
implementation(project(":data:model"))
|
||||
implementation(project(":data:userprefs"))
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.datecalculator
|
||||
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
internal object ZonedDateTimeUtils {
|
||||
/**
|
||||
* Get current [ZonedDateTime] with seconds and nanoseconds set to zero. Used in date calculator
|
||||
* module.
|
||||
*
|
||||
* @return [ZonedDateTime] with units less than minutes zeroed out.
|
||||
*/
|
||||
fun nowWithMinutes(): ZonedDateTime = ZonedDateTime.now().truncatedTo(ChronoUnit.MINUTES)
|
||||
}
|
@ -80,6 +80,7 @@ import com.sadellie.unitto.core.ui.common.textfield.addTokens
|
||||
import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
|
||||
import com.sadellie.unitto.core.ui.isPortrait
|
||||
import com.sadellie.unitto.core.ui.showToast
|
||||
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
|
||||
import com.sadellie.unitto.feature.datecalculator.components.AddSubtractKeyboard
|
||||
import com.sadellie.unitto.feature.datecalculator.components.DateTimeDialogs
|
||||
import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock
|
||||
@ -193,7 +194,7 @@ private fun AddSubtractView(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
title = stringResource(R.string.date_calculator_start),
|
||||
dateTime = uiState.start,
|
||||
onLongClick = { updateStart(ZonedDateTime.now()) },
|
||||
onLongClick = { updateStart(ZonedDateTimeUtils.nowWithMinutes()) },
|
||||
onClick = { dialogState = DialogState.FROM },
|
||||
onTimeClick = { dialogState = DialogState.FROM_TIME },
|
||||
onDateClick = { dialogState = DialogState.FROM_DATE },
|
||||
@ -402,8 +403,8 @@ fun AddSubtractViewPreview() {
|
||||
AddSubtractView(
|
||||
uiState = AddSubtractState(
|
||||
years = TextFieldValue("12"),
|
||||
start = ZonedDateTime.now(),
|
||||
result = ZonedDateTime.now().plusSeconds(1)
|
||||
start = ZonedDateTimeUtils.nowWithMinutes(),
|
||||
result = ZonedDateTimeUtils.nowWithMinutes().plusSeconds(1)
|
||||
),
|
||||
toggleTopBar = {},
|
||||
showKeyboard = false,
|
||||
|
@ -20,11 +20,12 @@ package com.sadellie.unitto.feature.datecalculator.addsubtract
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
|
||||
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
internal data class AddSubtractState(
|
||||
val start: ZonedDateTime = ZonedDateTime.now(),
|
||||
val result: ZonedDateTime = ZonedDateTime.now(),
|
||||
val start: ZonedDateTime = ZonedDateTimeUtils.nowWithMinutes(),
|
||||
val result: ZonedDateTime = ZonedDateTimeUtils.nowWithMinutes(),
|
||||
val years: TextFieldValue = TextFieldValue(),
|
||||
val months: TextFieldValue = TextFieldValue(),
|
||||
val days: TextFieldValue = TextFieldValue(),
|
||||
|
@ -19,112 +19,155 @@
|
||||
package com.sadellie.unitto.feature.datecalculator.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sadellie.unitto.core.base.R
|
||||
import com.sadellie.unitto.core.ui.common.squashable
|
||||
import com.sadellie.unitto.core.ui.common.PagedIsland
|
||||
import com.sadellie.unitto.feature.datecalculator.difference.ZonedDateTimeDifference
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
internal fun DateTimeResultBlock(
|
||||
modifier: Modifier = Modifier,
|
||||
zonedDateTimeDifference: ZonedDateTimeDifference
|
||||
diff: ZonedDateTimeDifference.Default,
|
||||
format: (BigDecimal) -> String
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
PagedIsland(
|
||||
modifier = modifier,
|
||||
pagesCount = 6,
|
||||
backgroundColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||
) { currentPage ->
|
||||
when(currentPage) {
|
||||
0 -> {
|
||||
Text(
|
||||
text = stringResource(R.string.date_calculator_difference),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
SelectionContainer {
|
||||
Column {
|
||||
// Years
|
||||
if (diff.years > 0) {
|
||||
DateText(R.string.date_calculator_years, diff.years.toBigDecimal(), format)
|
||||
}
|
||||
|
||||
val years = zonedDateTimeDifference.years.formatDateTimeValue(R.string.date_calculator_years)
|
||||
val months = zonedDateTimeDifference.months.formatDateTimeValue(R.string.date_calculator_months)
|
||||
val days = zonedDateTimeDifference.days.formatDateTimeValue(R.string.date_calculator_days)
|
||||
val hours = zonedDateTimeDifference.hours.formatDateTimeValue(R.string.date_calculator_hours)
|
||||
val minutes = zonedDateTimeDifference.minutes.formatDateTimeValue(R.string.date_calculator_minutes)
|
||||
// Months
|
||||
if (diff.months > 0) {
|
||||
DateText(R.string.date_calculator_months, diff.months.toBigDecimal(), format)
|
||||
}
|
||||
|
||||
val texts = listOf(years, months, days, hours, minutes)
|
||||
// Days
|
||||
if (diff.days > 0) {
|
||||
DateText(R.string.date_calculator_days, diff.days.toBigDecimal(), format)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.squashable(
|
||||
onClick = {},
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
cornerRadiusRange = 8.dp..32.dp,
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.tertiaryContainer)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.date_calculator_difference),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(texts.filter { it.isNotEmpty() }.joinToString(" "))
|
||||
)
|
||||
// Hours
|
||||
if (diff.hours > 0) {
|
||||
DateText(R.string.date_calculator_hours, diff.hours.toBigDecimal(), format)
|
||||
}
|
||||
|
||||
// Minutes
|
||||
if (diff.minutes > 0) {
|
||||
DateText(R.string.date_calculator_minutes, diff.minutes.toBigDecimal(), format)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.ContentCopy, null)
|
||||
}
|
||||
}
|
||||
|
||||
texts.forEach {
|
||||
AnimatedVisibility(
|
||||
visible = it.isNotEmpty(),
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
Text(it, style = MaterialTheme.typography.displaySmall)
|
||||
1 -> {
|
||||
Text(
|
||||
text = stringResource(R.string.date_calculator_years),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
SelectionContainer {
|
||||
DateText(diff.sumYears, format)
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
Text(
|
||||
text = stringResource(R.string.date_calculator_months),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
SelectionContainer {
|
||||
DateText(diff.sumMonths, format)
|
||||
}
|
||||
}
|
||||
|
||||
3 -> {
|
||||
Text(
|
||||
text = stringResource(R.string.date_calculator_days),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
SelectionContainer {
|
||||
DateText(diff.sumDays, format)
|
||||
}
|
||||
}
|
||||
|
||||
4 -> {
|
||||
Text(
|
||||
text = stringResource(R.string.date_calculator_hours),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
SelectionContainer {
|
||||
DateText(diff.sumHours, format)
|
||||
}
|
||||
}
|
||||
|
||||
5 -> {
|
||||
Text(
|
||||
text = stringResource(R.string.date_calculator_minutes),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
SelectionContainer {
|
||||
DateText(diff.sumMinutes, format)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun Long.formatDateTimeValue(@StringRes id: Int): String {
|
||||
if (this <= 0) return ""
|
||||
private fun DateText(
|
||||
@StringRes id: Int,
|
||||
value: BigDecimal,
|
||||
format: (BigDecimal) -> String,
|
||||
) = Text(
|
||||
text = "${stringResource(id)}: ${format(value)}",
|
||||
style = MaterialTheme.typography.displaySmall
|
||||
)
|
||||
|
||||
return "${stringResource(id)}: $this"
|
||||
}
|
||||
@Composable
|
||||
private fun DateText(
|
||||
value: BigDecimal,
|
||||
format: (BigDecimal) -> String,
|
||||
) = Text(
|
||||
text = format(value),
|
||||
style = MaterialTheme.typography.displaySmall
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DateTimeResultBlockPreview() {
|
||||
DateTimeResultBlock(
|
||||
modifier = Modifier,
|
||||
zonedDateTimeDifference = ZonedDateTimeDifference.Default(
|
||||
diff = ZonedDateTimeDifference.Default(
|
||||
years = 0,
|
||||
months = 1,
|
||||
days = 2,
|
||||
hours = 3,
|
||||
minutes = 4
|
||||
)
|
||||
days = 1,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
sumYears = BigDecimal.ZERO,
|
||||
sumMonths = BigDecimal.ZERO,
|
||||
sumDays = BigDecimal.ZERO,
|
||||
sumHours = BigDecimal.ZERO,
|
||||
sumMinutes = BigDecimal("46080"),
|
||||
),
|
||||
format = { it.toPlainString() }
|
||||
)
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ import com.sadellie.unitto.core.ui.common.squashable
|
||||
import com.sadellie.unitto.core.ui.datetime.formatDateWeekDayMonthYear
|
||||
import com.sadellie.unitto.core.ui.datetime.formatTimeAmPm
|
||||
import com.sadellie.unitto.core.ui.datetime.formatTimeShort
|
||||
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
@ -162,6 +163,6 @@ fun DateTimeSelectorBlockPreview() {
|
||||
.width(224.dp),
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
title = "End",
|
||||
dateTime = ZonedDateTime.now(),
|
||||
dateTime = ZonedDateTimeUtils.nowWithMinutes(),
|
||||
)
|
||||
}
|
||||
|
@ -18,9 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.feature.datecalculator.difference
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
@ -39,29 +37,36 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.sadellie.unitto.core.base.OutputFormat
|
||||
import com.sadellie.unitto.core.base.R
|
||||
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
|
||||
import com.sadellie.unitto.core.ui.common.textfield.formatExpression
|
||||
import com.sadellie.unitto.data.common.format
|
||||
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
|
||||
import com.sadellie.unitto.feature.datecalculator.components.DateTimeDialogs
|
||||
import com.sadellie.unitto.feature.datecalculator.components.DateTimeResultBlock
|
||||
import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock
|
||||
import com.sadellie.unitto.feature.datecalculator.components.DialogState
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Composable
|
||||
internal fun DateDifferencePage(
|
||||
viewModel: DateDifferenceViewModel = hiltViewModel(),
|
||||
) {
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
DateDifferenceView(
|
||||
uiState = uiState.value,
|
||||
setStartDate = viewModel::setStartDate,
|
||||
setEndDate = viewModel::setEndDate
|
||||
)
|
||||
when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) {
|
||||
DifferenceUIState.Loading -> Unit
|
||||
is DifferenceUIState.Ready -> DateDifferenceView(
|
||||
uiState = uiState,
|
||||
setStartDate = viewModel::setStartDate,
|
||||
setEndDate = viewModel::setEndDate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateDifferenceView(
|
||||
uiState: DifferenceUIState,
|
||||
uiState: DifferenceUIState.Ready,
|
||||
setStartDate: (ZonedDateTime) -> Unit,
|
||||
setEndDate: (ZonedDateTime) -> Unit,
|
||||
) {
|
||||
@ -83,7 +88,7 @@ private fun DateDifferenceView(
|
||||
title = stringResource(R.string.date_calculator_start),
|
||||
dateTime = uiState.start,
|
||||
onClick = { dialogState = DialogState.FROM },
|
||||
onLongClick = { setStartDate(ZonedDateTime.now()) },
|
||||
onLongClick = { setStartDate(ZonedDateTimeUtils.nowWithMinutes()) },
|
||||
onTimeClick = { dialogState = DialogState.FROM_TIME },
|
||||
onDateClick = { dialogState = DialogState.FROM_DATE },
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
@ -97,23 +102,29 @@ private fun DateDifferenceView(
|
||||
title = stringResource(R.string.date_calculator_end),
|
||||
dateTime = uiState.end,
|
||||
onClick = { dialogState = DialogState.TO },
|
||||
onLongClick = { setStartDate(ZonedDateTime.now()) },
|
||||
onLongClick = { setEndDate(ZonedDateTimeUtils.nowWithMinutes()) },
|
||||
onTimeClick = { dialogState = DialogState.TO_TIME },
|
||||
onDateClick = { dialogState = DialogState.TO_DATE },
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = uiState.result is ZonedDateTimeDifference.Default,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
DateTimeResultBlock(
|
||||
modifier = Modifier
|
||||
.weight(2f)
|
||||
.fillMaxWidth(),
|
||||
zonedDateTimeDifference = uiState.result,
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = uiState.result,
|
||||
modifier = Modifier.weight(2f)
|
||||
) { result ->
|
||||
when (result) {
|
||||
is ZonedDateTimeDifference.Default -> {
|
||||
DateTimeResultBlock(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
diff = result,
|
||||
format = {
|
||||
it.format(uiState.precision, uiState.outputFormat)
|
||||
.formatExpression(uiState.formatterSymbols)
|
||||
}
|
||||
)
|
||||
}
|
||||
ZonedDateTimeDifference.Zero -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +154,14 @@ private fun DateDifferenceView(
|
||||
@Composable
|
||||
fun DateDifferenceViewPreview() {
|
||||
DateDifferenceView(
|
||||
uiState = DifferenceUIState(),
|
||||
uiState = DifferenceUIState.Ready(
|
||||
start = ZonedDateTimeUtils.nowWithMinutes(),
|
||||
end = ZonedDateTimeUtils.nowWithMinutes().truncatedTo(ChronoUnit.MINUTES),
|
||||
result = ZonedDateTimeDifference.Zero,
|
||||
precision = 3,
|
||||
outputFormat = OutputFormat.PLAIN,
|
||||
formatterSymbols = FormatterSymbols.Spaces
|
||||
),
|
||||
setStartDate = {},
|
||||
setEndDate = {},
|
||||
)
|
||||
|
@ -20,32 +20,70 @@ package com.sadellie.unitto.feature.datecalculator.difference
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
|
||||
import com.sadellie.unitto.data.common.stateIn
|
||||
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
|
||||
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class DateDifferenceViewModel @Inject constructor() : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(DifferenceUIState())
|
||||
internal class DateDifferenceViewModel @Inject constructor(
|
||||
userPrefsRepository: UserPreferencesRepository,
|
||||
) : ViewModel() {
|
||||
private val _start = MutableStateFlow(ZonedDateTimeUtils.nowWithMinutes())
|
||||
private val _end = MutableStateFlow(ZonedDateTimeUtils.nowWithMinutes())
|
||||
private val _result = MutableStateFlow<ZonedDateTimeDifference>(ZonedDateTimeDifference.Zero)
|
||||
|
||||
val uiState = _uiState
|
||||
.onEach { updateResult() }
|
||||
val uiState: StateFlow<DifferenceUIState> = combine(
|
||||
userPrefsRepository.formattingPrefs,
|
||||
_start,
|
||||
_end,
|
||||
_result
|
||||
) { prefs, start, end, result ->
|
||||
return@combine DifferenceUIState.Ready(
|
||||
start = start,
|
||||
end = end,
|
||||
result = result,
|
||||
precision = prefs.digitsPrecision,
|
||||
outputFormat = prefs.outputFormat,
|
||||
formatterSymbols = AllFormatterSymbols.getById(prefs.separator)
|
||||
)
|
||||
}
|
||||
.mapLatest { ui ->
|
||||
updateResult(
|
||||
start = ui.start,
|
||||
end = ui.end
|
||||
)
|
||||
|
||||
ui
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5000L), DifferenceUIState()
|
||||
viewModelScope, DifferenceUIState.Loading
|
||||
)
|
||||
|
||||
fun setStartDate(newValue: ZonedDateTime) = _uiState.update { it.copy(start = newValue) }
|
||||
fun setStartDate(newValue: ZonedDateTime) = _start.update { newValue }
|
||||
|
||||
fun setEndDate(newValue: ZonedDateTime) = _uiState.update { it.copy(end = newValue) }
|
||||
fun setEndDate(newValue: ZonedDateTime) = _end.update { newValue }
|
||||
|
||||
private fun updateResult() = viewModelScope.launch(Dispatchers.Default) {
|
||||
_uiState.update { ui -> ui.copy(result = ui.start - ui.end) }
|
||||
private fun updateResult(
|
||||
start: ZonedDateTime,
|
||||
end: ZonedDateTime
|
||||
) = viewModelScope.launch(Dispatchers.Default) {
|
||||
_result.update {
|
||||
try {
|
||||
start - end
|
||||
} catch (e: Exception) {
|
||||
ZonedDateTimeDifference.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,18 @@
|
||||
|
||||
package com.sadellie.unitto.feature.datecalculator.difference
|
||||
|
||||
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
internal data class DifferenceUIState(
|
||||
val start: ZonedDateTime = ZonedDateTime.now(),
|
||||
val end: ZonedDateTime = ZonedDateTime.now(),
|
||||
val result: ZonedDateTimeDifference = ZonedDateTimeDifference.Zero
|
||||
)
|
||||
internal sealed class DifferenceUIState {
|
||||
data object Loading : DifferenceUIState()
|
||||
|
||||
data class Ready(
|
||||
val start: ZonedDateTime,
|
||||
val end: ZonedDateTime,
|
||||
val result: ZonedDateTimeDifference,
|
||||
val precision: Int,
|
||||
val outputFormat: Int,
|
||||
val formatterSymbols: FormatterSymbols,
|
||||
) : DifferenceUIState()
|
||||
}
|
||||
|
@ -18,43 +18,65 @@
|
||||
|
||||
package com.sadellie.unitto.feature.datecalculator.difference
|
||||
|
||||
import com.sadellie.unitto.core.base.MAX_PRECISION
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
internal sealed class ZonedDateTimeDifference(
|
||||
open val years: Long = 0,
|
||||
open val months: Long = 0,
|
||||
open val days: Long = 0,
|
||||
open val hours: Long = 0,
|
||||
open val minutes: Long = 0,
|
||||
) {
|
||||
internal sealed class ZonedDateTimeDifference {
|
||||
data class Default(
|
||||
override val years: Long = 0,
|
||||
override val months: Long = 0,
|
||||
override val days: Long = 0,
|
||||
override val hours: Long = 0,
|
||||
override val minutes: Long = 0,
|
||||
) : ZonedDateTimeDifference(
|
||||
years = years,
|
||||
months = months,
|
||||
days = days,
|
||||
hours = hours,
|
||||
minutes = minutes,
|
||||
)
|
||||
val years: Long,
|
||||
val months: Long,
|
||||
val days: Long,
|
||||
val hours: Long,
|
||||
val minutes: Long,
|
||||
val sumYears: BigDecimal,
|
||||
val sumMonths: BigDecimal,
|
||||
val sumDays: BigDecimal,
|
||||
val sumHours: BigDecimal,
|
||||
val sumMinutes: BigDecimal,
|
||||
) : ZonedDateTimeDifference()
|
||||
|
||||
data object Zero : ZonedDateTimeDifference()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/25760725
|
||||
internal infix operator fun ZonedDateTime.minus(localDateTime: ZonedDateTime): ZonedDateTimeDifference {
|
||||
if (this == localDateTime) return ZonedDateTimeDifference.Zero
|
||||
/**
|
||||
* Same as other [ZonedDateTime.minus] but `MAX_PRECISION` passed as scale.
|
||||
*
|
||||
* @receiver First [ZonedDateTime].
|
||||
* @param zonedDateTime Second [ZonedDateTime].
|
||||
* @return [ZonedDateTimeDifference.Default] (_always positive_) or [ZonedDateTimeDifference.Zero]
|
||||
*/
|
||||
internal infix operator fun ZonedDateTime.minus(
|
||||
zonedDateTime: ZonedDateTime
|
||||
): ZonedDateTimeDifference = this.minus(zonedDateTime = zonedDateTime, scale = MAX_PRECISION)
|
||||
|
||||
/**
|
||||
* Calculate difference between [this] and [zonedDateTime]. Return absolute value, order of operands
|
||||
* doesn't matter.
|
||||
*
|
||||
* @receiver First [ZonedDateTime].
|
||||
* @param zonedDateTime Second [ZonedDateTime].
|
||||
* @param scale Scale that will be used to calculate [ZonedDateTimeDifference.Default.sumDays] and
|
||||
* others summed values.
|
||||
* @return [ZonedDateTimeDifference.Default] (_always positive_) or [ZonedDateTimeDifference.Zero]
|
||||
*/
|
||||
internal fun ZonedDateTime.minus(
|
||||
zonedDateTime: ZonedDateTime,
|
||||
scale: Int
|
||||
): ZonedDateTimeDifference {
|
||||
// https://stackoverflow.com/a/25760725
|
||||
|
||||
if (this == zonedDateTime) return ZonedDateTimeDifference.Zero
|
||||
|
||||
var fromDateTime: ZonedDateTime = this
|
||||
var toDateTime: ZonedDateTime = localDateTime
|
||||
var toDateTime: ZonedDateTime = zonedDateTime
|
||||
val epSeconds: BigDecimal = (this.toEpochSecond() - zonedDateTime.toEpochSecond()).toBigDecimal().abs()
|
||||
|
||||
// Swap to avoid negative
|
||||
if (this > localDateTime) {
|
||||
fromDateTime = localDateTime
|
||||
if (this > zonedDateTime) {
|
||||
fromDateTime = zonedDateTime
|
||||
toDateTime = this
|
||||
}
|
||||
|
||||
@ -77,6 +99,21 @@ internal infix operator fun ZonedDateTime.minus(localDateTime: ZonedDateTime): Z
|
||||
if (listOf(years, months, days, hours, minutes).sum() == 0L) return ZonedDateTimeDifference.Zero
|
||||
|
||||
return ZonedDateTimeDifference.Default(
|
||||
years = years, months = months, days = days, hours = hours, minutes = minutes
|
||||
years = years,
|
||||
months = months,
|
||||
days = days,
|
||||
hours = hours,
|
||||
minutes = minutes,
|
||||
sumYears = epSeconds.divide(yearInSeconds, scale, RoundingMode.HALF_EVEN),
|
||||
sumMonths = epSeconds.divide(monthsInSeconds, scale, RoundingMode.HALF_EVEN),
|
||||
sumDays = epSeconds.divide(dayInSeconds, scale, RoundingMode.HALF_EVEN),
|
||||
sumHours = epSeconds.divide(hourInSeconds, scale, RoundingMode.HALF_EVEN),
|
||||
sumMinutes = epSeconds.divide(minuteInSeconds, scale, RoundingMode.HALF_EVEN),
|
||||
)
|
||||
}
|
||||
|
||||
private val yearInSeconds by lazy { BigDecimal("31104000") }
|
||||
private val monthsInSeconds by lazy { BigDecimal("2592000") }
|
||||
private val dayInSeconds by lazy { BigDecimal("86400") }
|
||||
private val hourInSeconds by lazy { BigDecimal("3600") }
|
||||
private val minuteInSeconds by lazy { BigDecimal("60") }
|
||||
|
@ -20,32 +20,128 @@ package com.sadellie.unitto.feature.datecalculator.difference
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class ZonedDateTimeDifferenceKtTest {
|
||||
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME
|
||||
private val `may 1 2023`: ZonedDateTime = ZonedDateTime.parse("2023-05-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
private val `may 2 2023`: ZonedDateTime = ZonedDateTime.parse("2023-05-02T12:00+01:00[Europe/Paris]", formatter)
|
||||
private val `june 1 2023`: ZonedDateTime = ZonedDateTime.parse("2023-06-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
@Test
|
||||
fun `same dates`() {
|
||||
assertEquals(ZonedDateTimeDifference.Zero, `may 1 2023` - `may 1 2023`)
|
||||
val date1: ZonedDateTime = ZonedDateTime.parse("2023-05-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
assertEquals(ZonedDateTimeDifference.Zero, date1 - date1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `positive difference dates one day`() {
|
||||
assertEquals(ZonedDateTimeDifference.Default(days = 1), `may 1 2023` - `may 2 2023`)
|
||||
fun `positive difference one day`() {
|
||||
val date1: ZonedDateTime = ZonedDateTime.parse("2023-05-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
val date2: ZonedDateTime = ZonedDateTime.parse("2023-05-02T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
assertEquals(
|
||||
ZonedDateTimeDifference.
|
||||
Default(
|
||||
years = 0,
|
||||
months = 0,
|
||||
days = 1,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
sumYears = BigDecimal("0.003"),
|
||||
sumMonths = BigDecimal("0.033"),
|
||||
sumDays = BigDecimal("1.000"),
|
||||
sumHours = BigDecimal("24.000"),
|
||||
sumMinutes = BigDecimal("1440.000"),
|
||||
),
|
||||
date1.minus(date2, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `positive difference dates one minth`() {
|
||||
assertEquals(ZonedDateTimeDifference.Default(months = 1), `may 1 2023` - `june 1 2023`)
|
||||
fun `positive difference one month`() {
|
||||
val date1: ZonedDateTime = ZonedDateTime.parse("2023-05-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
val date2: ZonedDateTime = ZonedDateTime.parse("2023-06-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
assertEquals(
|
||||
ZonedDateTimeDifference.Default(
|
||||
years = 0,
|
||||
months = 1,
|
||||
days = 0,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
sumYears = BigDecimal("0.086"),
|
||||
sumMonths = BigDecimal("1.033"),
|
||||
sumDays = BigDecimal("31.000"),
|
||||
sumHours = BigDecimal("744.000"),
|
||||
sumMinutes = BigDecimal("44640.000"),
|
||||
),
|
||||
date1.minus(date2, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `negative difference dates one day`() {
|
||||
assertEquals(ZonedDateTimeDifference.Default(days = 1), `may 2 2023` - `may 1 2023`)
|
||||
fun `negative difference one day`() {
|
||||
val date1: ZonedDateTime = ZonedDateTime.parse("2023-05-02T12:00+01:00[Europe/Paris]", formatter)
|
||||
val date2: ZonedDateTime = ZonedDateTime.parse("2023-05-01T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
assertEquals(
|
||||
ZonedDateTimeDifference.Default(
|
||||
years = 0,
|
||||
months = 0,
|
||||
days = 1,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
sumYears = BigDecimal("0.003"),
|
||||
sumMonths = BigDecimal("0.033"),
|
||||
sumDays = BigDecimal("1.000"),
|
||||
sumHours = BigDecimal("24.000"),
|
||||
sumMinutes = BigDecimal("1440.000"),
|
||||
),
|
||||
date1.minus(date2, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `positive big difference`() {
|
||||
val date1: ZonedDateTime = ZonedDateTime.parse("2023-10-25T12:00+01:00[Europe/Paris]", formatter)
|
||||
val date2: ZonedDateTime = ZonedDateTime.parse("2023-11-25T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
assertEquals(
|
||||
ZonedDateTimeDifference.Default(
|
||||
years = 0,
|
||||
months = 0,
|
||||
days = 30,
|
||||
hours = 23,
|
||||
minutes = 0,
|
||||
sumYears = BigDecimal("0.086"),
|
||||
sumMonths = BigDecimal("1.033"),
|
||||
sumDays = BigDecimal("31.000"),
|
||||
sumHours = BigDecimal("744.000"),
|
||||
sumMinutes = BigDecimal("44640.000"),
|
||||
),
|
||||
date1.minus(date2, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `positive big difference 2`() {
|
||||
val date1: ZonedDateTime = ZonedDateTime.parse("2023-11-25T12:00+01:00[Europe/Paris]", formatter)
|
||||
val date2: ZonedDateTime = ZonedDateTime.parse("2023-12-25T12:00+01:00[Europe/Paris]", formatter)
|
||||
|
||||
assertEquals(
|
||||
ZonedDateTimeDifference.Default(
|
||||
years = 0,
|
||||
months = 1,
|
||||
days = 0,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
sumYears = BigDecimal("0.083"),
|
||||
sumMonths = BigDecimal("1.000"),
|
||||
sumDays = BigDecimal("30.000"),
|
||||
sumHours = BigDecimal("720.000"),
|
||||
sumMinutes = BigDecimal("43200.000"),
|
||||
),
|
||||
date1.minus(date2, 3)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user