From 5d10a5759ecaa4f89038cc501919d05689c24902 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Fri, 5 Jul 2024 09:17:23 +0200 Subject: [PATCH] fix: [ANDROAPP-6305] Take into account invalid date formats (#270) * fix: [ANDROAPP-6305] Take into account invalid date formats * fix: [ANDROAPP-6305] ktlint --- .../ui/designsystem/component/InputAge.kt | 36 ++++++++++----- .../designsystem/component/InputDateTime.kt | 46 ++++++++----------- .../component/internal/DateTimeUtils.kt | 43 +++++++++++++++++ .../resources/values/strings_en.xml | 1 + 4 files changed, 87 insertions(+), 39 deletions(-) create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt index 4aefaff9b..d87902bfb 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt @@ -35,6 +35,8 @@ import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType.None import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues.YEARS import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation.Companion.DATE_MASK import org.hisp.dhis.mobile.ui.designsystem.component.internal.RegExValidations +import org.hisp.dhis.mobile.ui.designsystem.component.internal.dateIsInRange +import org.hisp.dhis.mobile.ui.designsystem.component.internal.isValidDate import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2LightColorScheme import org.hisp.dhis.mobile.ui.designsystem.theme.Outline @@ -321,19 +323,29 @@ private fun provideSupportingText( ): List? = (uiModel.inputType as? DateOfBirth)?.value?.text?.let { if ( - it.length == DATE_FORMAT.length && - !dateIsInRange(parseStringDateToMillis(it), selectableDates) + it.length == DATE_FORMAT.length && (!isValidDate(it) || !dateIsInRange(parseStringDateToMillis(it), selectableDates)) ) { - val dateOutOfRangeText = "${provideStringResource("date_out_of_range")} (" + - formatStringToDate(selectableDates.initialDate) + " - " + - formatStringToDate(selectableDates.endDate) + ")" - - listOf( - SupportingTextData( - text = dateOutOfRangeText, - SupportingTextState.ERROR, - ), - ).plus(uiModel.supportingText ?: listOf()) + val supportingTextErrorList: MutableList = mutableListOf() + if (!isValidDate(it)) { + val incorrectFormatText = provideStringResource("incorrect_date_format") + supportingTextErrorList.add( + SupportingTextData( + text = incorrectFormatText, + SupportingTextState.ERROR, + ), + ) + } else if (!dateIsInRange(parseStringDateToMillis(it), selectableDates)) { + val dateOutOfRangeText = "${provideStringResource("date_out_of_range")} (" + + formatStringToDate(selectableDates.initialDate) + " - " + + formatStringToDate(selectableDates.endDate) + ")" + supportingTextErrorList.add( + SupportingTextData( + text = dateOutOfRangeText, + SupportingTextState.ERROR, + ), + ) + } + supportingTextErrorList.plus(uiModel.supportingText ?: listOf()).toList() } else { uiModel.supportingText } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt index b4cda5e89..5c5b3004c 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt @@ -54,6 +54,10 @@ import androidx.compose.ui.window.DialogProperties import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTimeVisualTransformation import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation import org.hisp.dhis.mobile.ui.designsystem.component.internal.RegExValidations +import org.hisp.dhis.mobile.ui.designsystem.component.internal.dateIsInRange +import org.hisp.dhis.mobile.ui.designsystem.component.internal.isValidDate +import org.hisp.dhis.mobile.ui.designsystem.component.internal.isValidHourFormat +import org.hisp.dhis.mobile.ui.designsystem.component.internal.yearIsInRange import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2LightColorScheme import org.hisp.dhis.mobile.ui.designsystem.theme.Outline @@ -90,6 +94,7 @@ fun InputDateTime( var showDatePicker by rememberSaveable { mutableStateOf(false) } var showTimePicker by rememberSaveable { mutableStateOf(false) } var dateOutOfRangeText = uiModel.outOfRangeText ?: provideStringResource("date_out_of_range") + dateOutOfRangeText = "$dateOutOfRangeText (" + formatStringToDate( uiModel.selectableDates.initialDate, ) + " - " + @@ -99,18 +104,22 @@ fun InputDateTime( text = incorrectHourFormatText, SupportingTextState.ERROR, ) + val incorrectDateFormatItem = SupportingTextData( + text = provideStringResource("incorrect_date_format"), + SupportingTextState.ERROR, + ) val dateOutOfRangeItem = SupportingTextData( text = dateOutOfRangeText, SupportingTextState.ERROR, ) val supportingTextList = - getSupportingTextList(uiModel, dateOutOfRangeItem, incorrectHourFormatItem) + getSupportingTextList(uiModel, dateOutOfRangeItem, incorrectHourFormatItem, incorrectDateFormatItem) InputShell( modifier = modifier.testTag("INPUT_DATE_TIME") .focusRequester(focusRequester), title = uiModel.title, - state = if (supportingTextList.contains(dateOutOfRangeItem)) InputShellState.ERROR else uiModel.state, + state = if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains(incorrectDateFormatItem)) InputShellState.ERROR else uiModel.state, isRequiredField = uiModel.isRequired, onFocusChanged = uiModel.onFocusChanged, inputField = { @@ -228,7 +237,7 @@ fun InputDateTime( SupportingText( item.text, item.state, - modifier = Modifier.testTag("INPUT_DATE_TIME_SUPPORTING_TEXT"), + modifier = Modifier.testTag("INPUT_DATE_TIME_SUPPORTING_TEXT" + item.text), ) } }, @@ -373,7 +382,7 @@ fun InputDateTime( } } -fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: SupportingTextData, incorrectHourFormatItem: SupportingTextData): List { +fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: SupportingTextData, incorrectHourFormatItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData): List { val supportingTextList = mutableListOf() uiModel.supportingText?.forEach { item -> @@ -383,6 +392,8 @@ fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: Suppo val dateIsInRange: Boolean val dateIsInYearRange: Boolean val isValidHourFormat: Boolean + val isValidDateFormat: Boolean + when (uiModel.actionType) { DateTimeActionType.TIME -> { if (uiModel.inputTextFieldValue?.text!!.length == 4) { @@ -401,15 +412,19 @@ fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: Suppo ) dateIsInYearRange = yearIsInRange(uiModel.inputTextFieldValue.text, getDefaultFormat(uiModel.actionType), uiModel.yearRange) isValidHourFormat = isValidHourFormat(uiModel.inputTextFieldValue.text.substring(8, 12)) + isValidDateFormat = isValidDate(uiModel.inputTextFieldValue.text.substring(0, 8)) if (!dateIsInRange || !dateIsInYearRange) supportingTextList.add(dateOutOfRangeItem) + if (!isValidDateFormat) supportingTextList.add(incorrectDateFormatItem) if (!isValidHourFormat) supportingTextList.add(incorrectHourFormatItem) } } DateTimeActionType.DATE -> { if (uiModel.inputTextFieldValue?.text!!.length == 8) { dateIsInRange = dateIsInRange(parseStringDateToMillis(uiModel.inputTextFieldValue.text), uiModel.selectableDates, uiModel.format) + isValidDateFormat = isValidDate(uiModel.inputTextFieldValue.text) dateIsInYearRange = yearIsInRange(uiModel.inputTextFieldValue.text, getDefaultFormat(uiModel.actionType), uiModel.yearRange) if (!dateIsInRange || !dateIsInYearRange) supportingTextList.add(dateOutOfRangeItem) + if (!isValidDateFormat) supportingTextList.add(incorrectDateFormatItem) } } } @@ -564,14 +579,6 @@ fun formatStringToDate(dateString: String): String { } } -private fun isValidHourFormat(timeString: String): Boolean { - val hourRange = IntRange(0, 24) - val minuteRange = IntRange(0, 60) - - return timeString.length == 4 && hourRange.contains(timeString.substring(0, 2).toInt()) && - minuteRange.contains(timeString.substring(2, 4).toInt()) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun timePickerColors(): TimePickerColors { @@ -592,21 +599,6 @@ private fun timePickerColors(): TimePickerColors { ) } -internal fun dateIsInRange(date: Long, allowedDates: SelectableDates, format: String = "ddMMyyyy"): Boolean { - return ( - date >= parseStringDateToMillis(allowedDates.initialDate, format) && - date <= parseStringDateToMillis(allowedDates.endDate, format) - ) -} - -fun yearIsInRange(date: String, pattern: String, yearRange: IntRange): Boolean { - val cal = Calendar.getInstance() - return date.parseDate(pattern)?.let { - cal.time = it - yearRange.contains(cal.get(Calendar.YEAR)) - } ?: false -} - fun String.parseDate(pattern: String): Date? { return if (isNotEmpty() && length == pattern.length) { val sdf = SimpleDateFormat(pattern, Locale.getDefault()) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt new file mode 100644 index 000000000..1e2388bde --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt @@ -0,0 +1,43 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.internal + +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates +import org.hisp.dhis.mobile.ui.designsystem.component.parseDate +import org.hisp.dhis.mobile.ui.designsystem.component.parseStringDateToMillis +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar + +internal fun dateIsInRange(date: Long, allowedDates: SelectableDates, format: String = "ddMMyyyy"): Boolean { + return ( + date >= parseStringDateToMillis(allowedDates.initialDate, format) && + date <= parseStringDateToMillis(allowedDates.endDate, format) + ) +} + +internal fun yearIsInRange(date: String, pattern: String, yearRange: IntRange): Boolean { + val cal = Calendar.getInstance() + return date.parseDate(pattern)?.let { + cal.time = it + yearRange.contains(cal.get(Calendar.YEAR)) + } ?: false +} + +internal fun isValidHourFormat(timeString: String): Boolean { + val hourRange = IntRange(0, 24) + val minuteRange = IntRange(0, 60) + + return timeString.length == 4 && hourRange.contains(timeString.substring(0, 2).toInt()) && + minuteRange.contains(timeString.substring(2, 4).toInt()) +} + +internal fun isValidDate(text: String): Boolean { + if (text.length != 8) return false + val format = SimpleDateFormat("ddMMyyyy") + format.isLenient = false + return try { + format.parse(text) + true + } catch (e: ParseException) { + false + } +} diff --git a/designsystem/src/commonMain/resources/values/strings_en.xml b/designsystem/src/commonMain/resources/values/strings_en.xml index 4696574ef..fcfa8e63c 100644 --- a/designsystem/src/commonMain/resources/values/strings_en.xml +++ b/designsystem/src/commonMain/resources/values/strings_en.xml @@ -39,6 +39,7 @@ Cancel Select date Date out of range + Incorrect date format Incorrect time format Not all options are displayed.\n Search to see more.