From 93404c1faca0d9126634c167e0bbc86c3efe113b Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Sat, 25 Nov 2023 19:12:19 +0300 Subject: [PATCH] Added more date difference modes closes: #129 --- feature/datecalculator/build.gradle.kts | 1 + .../datecalculator/ZonedDateTimeUtils.kt | 32 +++ .../addsubtract/AddSubtractPage.kt | 7 +- .../addsubtract/AddSubtractUIState.kt | 5 +- .../components/DateTimeResultBlock.kt | 189 +++++++++++------- .../components/DateTimeSelectorBlock.kt | 3 +- .../difference/DateDifferencePage.kt | 68 ++++--- .../difference/DateDifferenceViewModel.kt | 62 ++++-- .../difference/DifferenceUIState.kt | 18 +- .../difference/ZonedDateTimeDifference.kt | 89 ++++++--- .../ZonedDateTimeDifferenceKtTest.kt | 116 ++++++++++- 11 files changed, 433 insertions(+), 157 deletions(-) create mode 100644 feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/ZonedDateTimeUtils.kt diff --git a/feature/datecalculator/build.gradle.kts b/feature/datecalculator/build.gradle.kts index 1da16426..60f20cae 100644 --- a/feature/datecalculator/build.gradle.kts +++ b/feature/datecalculator/build.gradle.kts @@ -31,6 +31,7 @@ android { dependencies { testImplementation(libs.junit.junit) + implementation(project(":data:common")) implementation(project(":data:model")) implementation(project(":data:userprefs")) } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/ZonedDateTimeUtils.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/ZonedDateTimeUtils.kt new file mode 100644 index 00000000..8861a60c --- /dev/null +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/ZonedDateTimeUtils.kt @@ -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 . + */ + +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) +} diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt index 9d032839..488fd1d0 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt @@ -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, diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt index 740830b2..037049d2 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt @@ -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(), diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeResultBlock.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeResultBlock.kt index 24a82684..3e139440 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeResultBlock.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeResultBlock.kt @@ -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() } ) } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeSelectorBlock.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeSelectorBlock.kt index 6cfbe9e7..dbace055 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeSelectorBlock.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/DateTimeSelectorBlock.kt @@ -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(), ) } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferencePage.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferencePage.kt index 398bbfd0..9734bd3b 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferencePage.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferencePage.kt @@ -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 = {}, ) diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferenceViewModel.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferenceViewModel.kt index b5e8aa51..4f37b7be 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferenceViewModel.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DateDifferenceViewModel.kt @@ -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.Zero) - val uiState = _uiState - .onEach { updateResult() } + val uiState: StateFlow = 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 + } + } } } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DifferenceUIState.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DifferenceUIState.kt index 9f998ac2..f2916e9c 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DifferenceUIState.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/DifferenceUIState.kt @@ -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() +} diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifference.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifference.kt index ab4b207b..7de5b45d 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifference.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifference.kt @@ -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") } diff --git a/feature/datecalculator/src/test/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifferenceKtTest.kt b/feature/datecalculator/src/test/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifferenceKtTest.kt index ad9fc7e6..3566430d 100644 --- a/feature/datecalculator/src/test/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifferenceKtTest.kt +++ b/feature/datecalculator/src/test/java/com/sadellie/unitto/feature/datecalculator/difference/ZonedDateTimeDifferenceKtTest.kt @@ -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) + ) } }