mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
parent
c843732e0e
commit
93404c1fac
@ -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"))
|
||||||
}
|
}
|
||||||
|
@ -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.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,
|
||||||
|
@ -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(),
|
||||||
|
@ -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() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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") }
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user