Added more date difference modes

closes: #129
This commit is contained in:
Sad Ellie 2023-11-25 19:12:19 +03:00
parent c843732e0e
commit 93404c1fac
11 changed files with 433 additions and 157 deletions

View File

@ -31,6 +31,7 @@ android {
dependencies { dependencies {
testImplementation(libs.junit.junit) testImplementation(libs.junit.junit)
implementation(project(":data:common"))
implementation(project(":data:model")) implementation(project(":data:model"))
implementation(project(":data:userprefs")) implementation(project(":data:userprefs"))
} }

View File

@ -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)
}

View File

@ -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.common.textfield.deleteTokens
import com.sadellie.unitto.core.ui.isPortrait import com.sadellie.unitto.core.ui.isPortrait
import com.sadellie.unitto.core.ui.showToast 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.AddSubtractKeyboard
import com.sadellie.unitto.feature.datecalculator.components.DateTimeDialogs import com.sadellie.unitto.feature.datecalculator.components.DateTimeDialogs
import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock
@ -193,7 +194,7 @@ private fun AddSubtractView(
containerColor = MaterialTheme.colorScheme.secondaryContainer, containerColor = MaterialTheme.colorScheme.secondaryContainer,
title = stringResource(R.string.date_calculator_start), title = stringResource(R.string.date_calculator_start),
dateTime = uiState.start, dateTime = uiState.start,
onLongClick = { updateStart(ZonedDateTime.now()) }, onLongClick = { updateStart(ZonedDateTimeUtils.nowWithMinutes()) },
onClick = { dialogState = DialogState.FROM }, onClick = { dialogState = DialogState.FROM },
onTimeClick = { dialogState = DialogState.FROM_TIME }, onTimeClick = { dialogState = DialogState.FROM_TIME },
onDateClick = { dialogState = DialogState.FROM_DATE }, onDateClick = { dialogState = DialogState.FROM_DATE },
@ -402,8 +403,8 @@ fun AddSubtractViewPreview() {
AddSubtractView( AddSubtractView(
uiState = AddSubtractState( uiState = AddSubtractState(
years = TextFieldValue("12"), years = TextFieldValue("12"),
start = ZonedDateTime.now(), start = ZonedDateTimeUtils.nowWithMinutes(),
result = ZonedDateTime.now().plusSeconds(1) result = ZonedDateTimeUtils.nowWithMinutes().plusSeconds(1)
), ),
toggleTopBar = {}, toggleTopBar = {},
showKeyboard = false, showKeyboard = false,

View File

@ -20,11 +20,12 @@ package com.sadellie.unitto.feature.datecalculator.addsubtract
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
import java.time.ZonedDateTime import java.time.ZonedDateTime
internal data class AddSubtractState( internal data class AddSubtractState(
val start: ZonedDateTime = ZonedDateTime.now(), val start: ZonedDateTime = ZonedDateTimeUtils.nowWithMinutes(),
val result: ZonedDateTime = ZonedDateTime.now(), val result: ZonedDateTime = ZonedDateTimeUtils.nowWithMinutes(),
val years: TextFieldValue = TextFieldValue(), val years: TextFieldValue = TextFieldValue(),
val months: TextFieldValue = TextFieldValue(), val months: TextFieldValue = TextFieldValue(),
val days: TextFieldValue = TextFieldValue(), val days: TextFieldValue = TextFieldValue(),

View File

@ -19,112 +19,155 @@
package com.sadellie.unitto.feature.datecalculator.components package com.sadellie.unitto.feature.datecalculator.components
import androidx.annotation.StringRes 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.selection.SelectionContainer
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.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview 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.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 com.sadellie.unitto.feature.datecalculator.difference.ZonedDateTimeDifference
import java.math.BigDecimal
@Composable @Composable
internal fun DateTimeResultBlock( internal fun DateTimeResultBlock(
modifier: Modifier = Modifier, 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) // Months
val months = zonedDateTimeDifference.months.formatDateTimeValue(R.string.date_calculator_months) if (diff.months > 0) {
val days = zonedDateTimeDifference.days.formatDateTimeValue(R.string.date_calculator_days) DateText(R.string.date_calculator_months, diff.months.toBigDecimal(), format)
val hours = zonedDateTimeDifference.hours.formatDateTimeValue(R.string.date_calculator_hours) }
val minutes = zonedDateTimeDifference.minutes.formatDateTimeValue(R.string.date_calculator_minutes)
val texts = listOf(years, months, days, hours, minutes) // Days
if (diff.days > 0) {
DateText(R.string.date_calculator_days, diff.days.toBigDecimal(), format)
}
Column( // Hours
modifier = modifier if (diff.hours > 0) {
.squashable( DateText(R.string.date_calculator_hours, diff.hours.toBigDecimal(), format)
onClick = {}, }
interactionSource = remember { MutableInteractionSource() },
cornerRadiusRange = 8.dp..32.dp, // Minutes
) if (diff.minutes > 0) {
.background(MaterialTheme.colorScheme.tertiaryContainer) DateText(R.string.date_calculator_minutes, diff.minutes.toBigDecimal(), format)
.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(" "))
)
} }
) {
Icon(Icons.Default.ContentCopy, null)
} }
}
texts.forEach { 1 -> {
AnimatedVisibility( Text(
visible = it.isNotEmpty(), text = stringResource(R.string.date_calculator_years),
enter = expandVertically(), style = MaterialTheme.typography.labelMedium
exit = shrinkVertically() )
) { SelectionContainer {
Text(it, style = MaterialTheme.typography.displaySmall) 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 @Composable
@ReadOnlyComposable private fun DateText(
private fun Long.formatDateTimeValue(@StringRes id: Int): String { @StringRes id: Int,
if (this <= 0) return "" 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 @Preview
@Composable @Composable
private fun DateTimeResultBlockPreview() { private fun DateTimeResultBlockPreview() {
DateTimeResultBlock( DateTimeResultBlock(
modifier = Modifier, modifier = Modifier,
zonedDateTimeDifference = ZonedDateTimeDifference.Default( diff = ZonedDateTimeDifference.Default(
years = 0,
months = 1, months = 1,
days = 2, days = 1,
hours = 3, hours = 0,
minutes = 4 minutes = 0,
) sumYears = BigDecimal.ZERO,
sumMonths = BigDecimal.ZERO,
sumDays = BigDecimal.ZERO,
sumHours = BigDecimal.ZERO,
sumMinutes = BigDecimal("46080"),
),
format = { it.toPlainString() }
) )
} }

View File

@ -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.formatDateWeekDayMonthYear
import com.sadellie.unitto.core.ui.datetime.formatTimeAmPm import com.sadellie.unitto.core.ui.datetime.formatTimeAmPm
import com.sadellie.unitto.core.ui.datetime.formatTimeShort import com.sadellie.unitto.core.ui.datetime.formatTimeShort
import com.sadellie.unitto.feature.datecalculator.ZonedDateTimeUtils
import java.time.ZonedDateTime import java.time.ZonedDateTime
@Composable @Composable
@ -162,6 +163,6 @@ fun DateTimeSelectorBlockPreview() {
.width(224.dp), .width(224.dp),
containerColor = MaterialTheme.colorScheme.secondaryContainer, containerColor = MaterialTheme.colorScheme.secondaryContainer,
title = "End", title = "End",
dateTime = ZonedDateTime.now(), dateTime = ZonedDateTimeUtils.nowWithMinutes(),
) )
} }

View File

@ -18,9 +18,7 @@
package com.sadellie.unitto.feature.datecalculator.difference package com.sadellie.unitto.feature.datecalculator.difference
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow 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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.R 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.DateTimeDialogs
import com.sadellie.unitto.feature.datecalculator.components.DateTimeResultBlock import com.sadellie.unitto.feature.datecalculator.components.DateTimeResultBlock
import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock
import com.sadellie.unitto.feature.datecalculator.components.DialogState import com.sadellie.unitto.feature.datecalculator.components.DialogState
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
@Composable @Composable
internal fun DateDifferencePage( internal fun DateDifferencePage(
viewModel: DateDifferenceViewModel = hiltViewModel(), viewModel: DateDifferenceViewModel = hiltViewModel(),
) { ) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle() when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) {
DifferenceUIState.Loading -> Unit
DateDifferenceView( is DifferenceUIState.Ready -> DateDifferenceView(
uiState = uiState.value, uiState = uiState,
setStartDate = viewModel::setStartDate, setStartDate = viewModel::setStartDate,
setEndDate = viewModel::setEndDate setEndDate = viewModel::setEndDate
) )
}
} }
@Composable @Composable
private fun DateDifferenceView( private fun DateDifferenceView(
uiState: DifferenceUIState, uiState: DifferenceUIState.Ready,
setStartDate: (ZonedDateTime) -> Unit, setStartDate: (ZonedDateTime) -> Unit,
setEndDate: (ZonedDateTime) -> Unit, setEndDate: (ZonedDateTime) -> Unit,
) { ) {
@ -83,7 +88,7 @@ private fun DateDifferenceView(
title = stringResource(R.string.date_calculator_start), title = stringResource(R.string.date_calculator_start),
dateTime = uiState.start, dateTime = uiState.start,
onClick = { dialogState = DialogState.FROM }, onClick = { dialogState = DialogState.FROM },
onLongClick = { setStartDate(ZonedDateTime.now()) }, onLongClick = { setStartDate(ZonedDateTimeUtils.nowWithMinutes()) },
onTimeClick = { dialogState = DialogState.FROM_TIME }, onTimeClick = { dialogState = DialogState.FROM_TIME },
onDateClick = { dialogState = DialogState.FROM_DATE }, onDateClick = { dialogState = DialogState.FROM_DATE },
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer
@ -97,23 +102,29 @@ private fun DateDifferenceView(
title = stringResource(R.string.date_calculator_end), title = stringResource(R.string.date_calculator_end),
dateTime = uiState.end, dateTime = uiState.end,
onClick = { dialogState = DialogState.TO }, onClick = { dialogState = DialogState.TO },
onLongClick = { setStartDate(ZonedDateTime.now()) }, onLongClick = { setEndDate(ZonedDateTimeUtils.nowWithMinutes()) },
onTimeClick = { dialogState = DialogState.TO_TIME }, onTimeClick = { dialogState = DialogState.TO_TIME },
onDateClick = { dialogState = DialogState.TO_DATE }, onDateClick = { dialogState = DialogState.TO_DATE },
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer
) )
AnimatedVisibility( AnimatedContent(
visible = uiState.result is ZonedDateTimeDifference.Default, targetState = uiState.result,
enter = expandVertically(), modifier = Modifier.weight(2f)
exit = shrinkVertically() ) { result ->
) { when (result) {
DateTimeResultBlock( is ZonedDateTimeDifference.Default -> {
modifier = Modifier DateTimeResultBlock(
.weight(2f) modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(), diff = result,
zonedDateTimeDifference = uiState.result, format = {
) it.format(uiState.precision, uiState.outputFormat)
.formatExpression(uiState.formatterSymbols)
}
)
}
ZonedDateTimeDifference.Zero -> Unit
}
} }
} }
@ -143,7 +154,14 @@ private fun DateDifferenceView(
@Composable @Composable
fun DateDifferenceViewPreview() { fun DateDifferenceViewPreview() {
DateDifferenceView( 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 = {}, setStartDate = {},
setEndDate = {}, setEndDate = {},
) )

View File

@ -20,32 +20,70 @@ package com.sadellie.unitto.feature.datecalculator.difference
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.ZonedDateTime import java.time.ZonedDateTime
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class DateDifferenceViewModel @Inject constructor() : ViewModel() { internal class DateDifferenceViewModel @Inject constructor(
private val _uiState = MutableStateFlow(DifferenceUIState()) 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 val uiState: StateFlow<DifferenceUIState> = combine(
.onEach { updateResult() } 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( .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) { private fun updateResult(
_uiState.update { ui -> ui.copy(result = ui.start - ui.end) } start: ZonedDateTime,
end: ZonedDateTime
) = viewModelScope.launch(Dispatchers.Default) {
_result.update {
try {
start - end
} catch (e: Exception) {
ZonedDateTimeDifference.Zero
}
}
} }
} }

View File

@ -18,10 +18,18 @@
package com.sadellie.unitto.feature.datecalculator.difference package com.sadellie.unitto.feature.datecalculator.difference
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import java.time.ZonedDateTime import java.time.ZonedDateTime
internal data class DifferenceUIState( internal sealed class DifferenceUIState {
val start: ZonedDateTime = ZonedDateTime.now(), data object Loading : DifferenceUIState()
val end: ZonedDateTime = ZonedDateTime.now(),
val result: ZonedDateTimeDifference = ZonedDateTimeDifference.Zero data class Ready(
) val start: ZonedDateTime,
val end: ZonedDateTime,
val result: ZonedDateTimeDifference,
val precision: Int,
val outputFormat: Int,
val formatterSymbols: FormatterSymbols,
) : DifferenceUIState()
}

View File

@ -18,43 +18,65 @@
package com.sadellie.unitto.feature.datecalculator.difference 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.ZonedDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
internal sealed class ZonedDateTimeDifference( 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,
) {
data class Default( data class Default(
override val years: Long = 0, val years: Long,
override val months: Long = 0, val months: Long,
override val days: Long = 0, val days: Long,
override val hours: Long = 0, val hours: Long,
override val minutes: Long = 0, val minutes: Long,
) : ZonedDateTimeDifference( val sumYears: BigDecimal,
years = years, val sumMonths: BigDecimal,
months = months, val sumDays: BigDecimal,
days = days, val sumHours: BigDecimal,
hours = hours, val sumMinutes: BigDecimal,
minutes = minutes, ) : ZonedDateTimeDifference()
)
data object Zero : ZonedDateTimeDifference() data object Zero : ZonedDateTimeDifference()
} }
// https://stackoverflow.com/a/25760725 /**
internal infix operator fun ZonedDateTime.minus(localDateTime: ZonedDateTime): ZonedDateTimeDifference { * Same as other [ZonedDateTime.minus] but `MAX_PRECISION` passed as scale.
if (this == localDateTime) return ZonedDateTimeDifference.Zero *
* @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 fromDateTime: ZonedDateTime = this
var toDateTime: ZonedDateTime = localDateTime var toDateTime: ZonedDateTime = zonedDateTime
val epSeconds: BigDecimal = (this.toEpochSecond() - zonedDateTime.toEpochSecond()).toBigDecimal().abs()
// Swap to avoid negative // Swap to avoid negative
if (this > localDateTime) { if (this > zonedDateTime) {
fromDateTime = localDateTime fromDateTime = zonedDateTime
toDateTime = this 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 if (listOf(years, months, days, hours, minutes).sum() == 0L) return ZonedDateTimeDifference.Zero
return ZonedDateTimeDifference.Default( 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") }

View File

@ -20,32 +20,128 @@ package com.sadellie.unitto.feature.datecalculator.difference
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.math.BigDecimal
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
class ZonedDateTimeDifferenceKtTest { class ZonedDateTimeDifferenceKtTest {
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME 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 @Test
fun `same dates`() { 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 @Test
fun `positive difference dates one day`() { fun `positive difference one day`() {
assertEquals(ZonedDateTimeDifference.Default(days = 1), `may 1 2023` - `may 2 2023`) 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 @Test
fun `positive difference dates one minth`() { fun `positive difference one month`() {
assertEquals(ZonedDateTimeDifference.Default(months = 1), `may 1 2023` - `june 1 2023`) 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 @Test
fun `negative difference dates one day`() { fun `negative difference one day`() {
assertEquals(ZonedDateTimeDifference.Default(days = 1), `may 2 2023` - `may 1 2023`) 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)
)
} }
} }