Регулярно возникают задачи проверять, что пользователь вводит в поля и сообщать ему если он что-то сделал не правильно.
Ничего в этом сложного нет, напишем парочку регулярных выражений
так
const val SNILS_PATTERN = "[0-9]{3}-[0-9]{3}-[0-9]{3}\s[0-9]{2}"
и так
const val SPEC_SYMBOLS = "—−–„““”‘’„”«»"
const val UPPER_RUS_LETTERS = "А-ЯЁЙ"
const val LOWER_RUS_LETTERS = "а-яёй"
const val RUS_LETTERS = "$UPPER_RUS_LETTERS$LOWER_RUS_LETTERS"
const val RUS_NAME_PATTERN = "[${RUS_LETTERS}IVX0-9\-`'.()\s]*"
const val RUS_NAME_PATTERN_WITH_COMMA = "[${RUS_LETTERS}IVX0-9\-`'.,()\s]*"
const val LATIN_LETTERS = "A-Za-z"
еще добавим маски
const val MASK_MOBILE_PHONE = "+7 [000] [000]-[00]-[00]"
const val SNILS_MASK = "[000]-[000]-[000] [00]"
и будет норм...
Если вы тоже так считаете, то дальше можно не читать
Мы пойдём другим путём....
Теоретическая часть
Для обработки текста введенного в поле будем применять такую концепцию.Значение введенное в поле проходит последовательно три этапа:
-
фильтрация – удаляем все недопустимые символы
-
валидация – проверяем соответствует ли значение определенным правилам
-
форматирование – форматируем значение для вывода
Проверим как это работает, например, на поле для ввода СНИЛС.
СНИЛС - это
-
11 цифр
-
две последние - контрольная сумма
-
при выводе они форматируются вот так ХХХ-ХХХ-ХХХ ХХ

в поле значение 123-45, пользователь нажимает цифру 7
-
фильтрация – т.к. СИНЛС это только цифры то удаляем все не цифровые символы. получаем 123457. передаем его на валидацию
-
валидация – СНИЛС это 11 цифр значит значение не валидное. передаем дальше «Error(123457)»
-
форматирование – после форматирования получим «Error(123-457)»
после всех этих манипуляций, отображаем в поле 123-457. Отображать ошибку или нет для этого поля решает «логика отображения». В данном случае, пока фокус в поле ввода, ошибку не отображаем.
Теперь перейдём к написанию кода
Этап 1. Фильтрация
Создадим интерфейс Filter. У него один метод filter: String -> String
interface Filter {
fun filter(data: String): String
}
И чтобы не возвращаться, сразу сделаем тривиальную реализацию фильтра. Эта реализация возвращает данные без изменения.
object SimpleFilter : Filter {
override fun filter(data: String) = data
}
и тут в голову приходит мысль, скорее всего нужно иметь возможность соединять фильтры в цепочку.для этого сделаем ComplexFilter
open class ComplexFilter private constructor(private val filters: List<Filter>)
: Filter {
override fun filter(data: String): String =
filters.fold(data) { res, filter -> filter.filter(res) }
companion object {
fun build(filters: List<Filter>): ComplexFilter {
return ComplexFilter(filters)
}
}
}
И сразу напишем DSL для построения фильтров
class ComplexFilterBuilder {
private val filters: MutableList<Filter> = mutableListOf()
fun build(): ComplexFilter {
return ComplexFilter.build(filters)
}
operator fun Filter.unaryPlus(): ComplexFilterBuilder {
filters.add(this)
return this@ComplexFilterBuilder
}
}
fun filter(lambda: ComplexFilterBuilder.() -> ComplexFilterBuilder): ComplexFilter {
return ComplexFilterBuilder().lambda().build()
}
теперь фильтр для поля СНИЛС будет выглядеть вот так
val snilsFilter = filter {
+FilterOnlyDigits
+FilterMaxLength(11)
}
Напишем несколько фильтров
/**
* Удаляет из строки все символы пробела
*/
object FilterSpacesSymbols: Filter {
override fun filter(data: String): String = data.filterNot { it.isWhitespace() }
}
/**
* Удаляет из строки все символы из кирииллицы
*/
object FilterNonLatinsSymbols: Filter {
override fun filter(data: String): String = data.filterNot { it.isCyrillic() }
}
/**
* Удаляет из строки указанные символы
*/
class FilterSymbols(private val filteredSymbols: String): Filter {
override fun filter(data: String): String = data.filterNot { it in filteredSymbols }
}
/**
* Оставляет строку длиной не более maxLength символов
*/
object FilterMaxLength(private val maxLength: Int) : Filter {
override fun filter(data: String): String =
date.take(maxLength)
}
/**
* Оставляет в строке только цифры
*/
object FilterOnlyDigits : Filter {
override fun filter(data: String): String = data.filter { it.isDigit() }
}
И теперь проверим, что snilsFilter ведёт себя правильно
тесты для snilsFilter
class SnilsFilterTest : FunSpec({
context("Snils filter") {
val snilsFilter = filter {
+FilterOnlyDigits
+FilterMaxLength(11)
}
withData(
listOf(
" " to "",
"sd fasdf as fsd a fas asd f" to "",
"s6d84f65sd46s5d4f" to "684654654",
"123-456-789" to "123456789",
"123-456-789 11" to "12345678911",
"123-456" to "123456",
"123-456-789-123-456-789" to "12345678912",
"123456789123456789" to "12345678912",
)
) { (data, res) ->
snilsFilter.filter(data) should be(res)
}
}
})
c фильтрами закончили, теперь перейдем к валидаторам
Этап 2. Валидация
С валидацией чуть сложнее.
Создадим интерфейс Validator. Для него нужен метод, который принимает String возвращает ValidationResult (результат валидации)
interface Validator {
fun validate(data: String): ValidationResult
}
ValidationResult это sealed класс.
-
У него может быть два варианта
ValidиError -
ValidиErrorсодержать строку с данными -
Errorдополнительно содержит список ошибок :List<ValidationError> -
ValidationError- базовый интерфейс для ошибок валидации
interface ValidationError
sealed class ValidationResult {
abstract val data: String
abstract fun isValid(): Boolean
class Valid(override val data: String) : ValidationResult() {
override fun isValid(): Boolean = true
}
class Error(override val data: String, val errors: List<ValidationError>) : ValidationResult() {
override fun isValid(): Boolean = false
}
companion object {
fun valid(value: String): ValidationResult = Valid(value)
fun invalid(value: String, errors: List<ValidationError>): ValidationResult {
assert(errors.isNotEmpty())
return Error(value, errors)
}
fun invalid(value: String, error: ValidationError): ValidationResult {
return Error(value, listOf(error))
}
}
}
fun String.asValid() : ValidationResult = ValidationResult.valid(this)
Тривиальный валидатор будет выглядеть так. Он всегда считает данные валидными
class SimpleValidator: Validator {
override fun validate(data: String): ValidationResult = data.asValid()
}
и снова надо соединять валидаторы в цепочки, т.е. прогонять строку через несколько валидаций для этого
open class ComplexValidator private constructor(private val validators: List<Validator>) :
Validator {
override fun validate(data: String) =
validators.fold(valid(data)) { res, validator -> res.andThen(validator) }
companion object {
fun build(validators: List<Validator>): ComplexValidator {
return ComplexValidator(validators)
}
}
}
для того чтобы последовательно применять валидации добавим несколько методов в ValidationResult
fun bind(anotherValidationFunction: (String) -> ValidationResult): ValidationResult {
return when (this) {
is Error -> {
when(val res = anotherValidationFunction(data)) {
is Error -> invalid(res.data, this.errors + res.errors)
is Valid -> invalid(res.data, this.errors)
}
}
is Valid -> anotherValidationFunction(data)
}
}
fun andThen(anotherValidator: Validator): ValidationResult =
bind { str: String -> anotherValidator.validate(str) }
И DSL для построения валидаторов
class ComplexValidatorBuilder() {
private val validators: MutableList<Validator> = mutableListOf()
fun build(): ComplexValidator {
return ComplexValidator.build(validators)
}
operator fun Validator.unaryPlus(): ComplexValidatorBuilder {
validators.add(this)
return this@ComplexValidatorBuilder
}
}
fun validator(lambda: ComplexValidatorBuilder.() -> ComplexValidatorBuilder): ComplexValidator {
return ComplexValidatorBuilder().lambda().build()
}
Допишем недостающие валидаторы
object OnlyDigitsValidationError: ValidationError
object OnlyDigitsValidator: Validator {
override fun validate(data: String): ValidationResult =
if(data.all { it.isDigit() })
valid(data)
else
invalid(data, OnlyDigitsValidationError)
}
object ExactLengthValidationError : ValidationError
object ExactLengthValidator(val exactLength: Int) : Validator {
override fun validate(data: String): ValidationResult =
if (data.length == exactLength)
valid(data)
else
invalid(data, ExactLengthValidationError)
}
object SnilsCheckSumValidatorError : ValidationError
object SnilsCheckSumValidator : Validator {
override fun validate(data: String): ValidationResult {
if (data.length != 11)
return ValidationResult.invalid(data, SnilsCheckSumValidatorError)
try {
val part1 = data.substring(0, 9)
val part2 = data.substring(9, 11)
val checkSum = part1
.reversed()
.mapIndexed { index, c -> c.digitToInt() * (index + 1) }
.sum()
.mod(101)
.toString()
.padStart(2, '0')
.takeLast(2)
return if (checkSum == part2)
ValidationResult.valid(data)
else
ValidationResult.invalid(data, SnilsCheckSumValidatorError)
} catch (e: Exception) {
return ValidationResult.invalid(data, SnilsCheckSumValidatorError)
}
}
}
Теперь можно написать валидатор для СНИЛС
val snilsValidator = validator {
+ExactLengthValidator(11) // 11 символов
+OnlyDigitsValidator // только цифры
+SnilsCheckSumValidator // проверка контрольной суммы СНИЛС
}
И тесты для snilsValidator
class SnilsValidatorTest: FunSpec( {
context("valid snils") {
withData(
listOf(
"11223344595",
"12345678964",
"98765432183",
"11111111145",
"45645645617",
"91919191943",
"77777777712",
"95195195147")
){ data ->
snilsValidator.validate(data) should beValid()
}
}
context("wrong snils") {
withData(
listOf(
"777777777777777777" to SnilsCheckSumValidatorError,
"77777777713" to SnilsCheckSumValidatorError,
"7777777" to ExactLengthValidationError,
"7sdfsdf7" to OnlyDigitsValidationError,
"" to ExactLengthValidationError
)
) { (data, error) ->
with(snilsValidator.validate(data)) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain error
}
}
}
})
Этап 3. Форматирование
Создадим интерфейс Formatter
interface Formatter {
fun format(data: String): String
}
Тривиальный форматтер будет такой. Он всегда возвращает данные без форматирования
class SimpleFormatter: Formatter {
override fun format(data: String) = data
}
Немного поразмыслив, приходим к выводу что, скорее всего не надо выполнять несколько форматирований, для одного поля. Поэтому ComplexFormatter не будем делать.
напишем форматтер для СНИЛСа
class SnilsFormatter: Formatter {
override fun format(data: String): String =
buildString {
data.forEachIndexed { index, c ->
when (index) {
0, 1, 2 -> append(c)
3 -> append('-').append(c)
4, 5 -> append(c)
6 -> append('-').append(c)
7, 8, -> append(c)
9 -> append(' ').append(c)
10 -> append(c)
else -> append(c)
}
}
}
}
тесты для SnilsFormatter
class SnilsFormatterTest : FunSpec({
context("Snils formatter") {
withData(
listOf(
"asdfgh" to "asd-fgh",
"" to "",
"1" to "1",
"12" to "12",
"123" to "123",
"1234" to "123-4",
"12345" to "123-45",
"123456" to "123-456",
"1234567" to "123-456-7",
"12345678" to "123-456-78",
"123456789" to "123-456-789",
"1234567891" to "123-456-789 1",
"12345678900" to "123-456-789 00",
"123456789111111111" to "123-456-789 11",
)
) { (data, res) ->
SnilsFormatter.format(data) should be(res)
}
}
})
Соберём всё вместе.
добавим такую сущность FormField
-
у нее есть список фильтров
-
список валидаторов
-
форматтер (по умолчанию -
SimpleFormatter) -
поле может быть обязательным или не обязательным
class FormField private constructor(
private val filters: List<Filter> = emptyList(),
private val validators: List<Validator> = emptyList(),
private val formatter: Formatter = SimpleFormatter(),
val isOptional: Boolean = false
)
у FormField всего один метод process Алгоритм у него простой:
-
входящую строку прогоняет через фильтры
-
если поле не обязательное и полученное значение пустое, то возвращаем
ValidationResult.Valid -
то что осталось, прогоняем через валидаторы,
-
в полученном
ValidationResultформатирует текст.
fun process(data: String): ValidationResult {
val filtered = filters.fold(data) { res, filter -> filter.filter(res) }
return if (filtered.isEmpty() && isOptional) {
filtered.asValid()
} else {
validators
.fold(ValidationResult.valid(filtered)) { res, validator ->
res.andThen(validator)
}
.map {
formatter.format(it)
}
}
}
Для построения FormField напишем метод build. Для того чтобы обрабатывать "обязательность" поля в этом методе, добавляем в список валидаторов NotEmptyValidator
companion object {
fun build(
filters: List<Filter>,
validators: List<Validator>,
formatter: Formatter,
isOptional: Boolean
): FormField =
if (isOptional == true)
FormField(filters, validators, formatter, true)
else
FormField(filters, listOf(NotEmptyValidator) + validators, formatter, false)
}
добавим билдер для DSL.
И окончательный результат будет такой
class FormField private constructor(
private val filters: List<Filter> = emptyList(),
private val validators: List<Validator> = emptyList(),
private val formatter: Formatter = SimpleFormatter,
val isOptional: Boolean = false
) {
fun process(data: String): ValidationResult {
val filtered = filters.fold(data) { res, filter -> filter.filter(res) }
return if (filtered.isEmpty() && isOptional) {
filtered.asValid()
} else {
validators
.fold(ValidationResult.valid(filtered)) { res, validator ->
res.andThen(validator)
}
.map {
formatter.format(it)
}
}
}
companion object {
fun build(
filters: List<Filter>,
validators: List<Validator>,
formatter: Formatter,
isOptional: Boolean
): FormField =
if (isOptional == true)
FormField(filters, validators, formatter, true)
else
FormField(filters, listOf(NotEmptyValidator) + validators, formatter, false)
}
}
class FieldBuilder(private val isOptional: Boolean = false) {
private val filters: MutableList<Filter> = mutableListOf()
private val validators: MutableList<Validator> = mutableListOf()
private var formatter: Formatter = SimpleFormatter
fun build(): FormField {
return FormField.build(filters, validators, formatter, isOptional)
}
operator fun Filter.unaryPlus(): FieldBuilder {
filters.add(this)
return this@FieldBuilder
}
operator fun Validator.unaryPlus(): FieldBuilder {
validators.add(this)
return this@FieldBuilder
}
operator fun Formatter.unaryPlus(): FieldBuilder {
formatter = this
return this@FieldBuilder
}
}
/**
* возвращает обязательное поле
* Example
* val f = formField {
* +FilterLength(10)
* +SnilsValidator()
* +SnilsFormatter()
* }
*/
fun formField(lambda: FieldBuilder.() -> FieldBuilder): FormField {
return FieldBuilder().lambda().build()
}
/**
* возвращает не обязательное поле. т.е. если значение в поле пустое,
* то валидация не происходит и поле считается валидным,
* если поле не пустое, то проводятся валидации
*/
fun optionalFormField(lambda: FieldBuilder.() -> FieldBuilder): FormField {
return FieldBuilder(isOptional = true).lambda().build()
}
Теперь поле для ввода СНИЛС можно описать так
val snilsField = formField {
+snilsFilter
+snilsValidator
+SnilsFormatter()
}
или так
val snilsField = formField {
+OnlyDigitsFilter()
+MaxLengthFilter(11)
+ExactLengthValidator(11)
+OnlyDigitsValidator()
+SnilsCheckSumValidator()
+SnilsFormatter()
}
тесты для snilsField
class SnilsFormFieldTest : FunSpec({
context("required snils field") {
val f = formField {
+FilterOnlyDigits
+FilterMaxLength(11)
+OnlyDigitsValidator
+ExactLengthValidator(11)
+SnilsCheckSumValidator
+SnilsFormatter
}
test("empty string") {
with(f.process("")) {
this shouldNot beValid()
(this as ValidationResult.Error) shouldNot beValid()
}
}
test("spaces") {
with(f.process(" ")) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain NotEmptyValidationError
this.errors shouldContain ExactLengthValidationError
this.errors shouldContain SnilsCheckSumValidatorError
}
}
test("123-456d") {
with(f.process("123-456d")) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain ExactLengthValidationError
this.errors shouldContain SnilsCheckSumValidatorError
this.data should be("123-456")
}
}
test("123-456-789 64") {
with(f.process("123-456-789 64")) {
this should beValid()
this.data should be("123-456-789 64")
}
}
}
context("optional snils field") {
val f = optionalFormField {
+FilterOnlyDigits
+FilterMaxLength(11)
+OnlyDigitsValidator
+SnilsCheckSumValidator
+SnilsFormatter
}
test("empty string") {
with(f.process("")) {
this should beValid()
this.data should be("")
}
}
test("spaces") {
with(f.process(" ")) {
this should beValid()
this.data should be("")
}
}
test("some not digit symbols") {
with(f.process("asdfasdfasdf")) {
this should beValid()
this.data should be("")
}
}
test("123-456d") {
with(f.process("123-456d")) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain SnilsCheckSumValidatorError
this.data should be("123-456")
}
}
test("123-456-789 64") {
with(f.process("123-456-789 64")) {
this should beValid()
this.data should be("123-456-789 64")
}
}
}
})
Внимательный читатель задаст вопрос, "Есть OnlyDigitstFilter и OnlyDigitsValidator. Выглядит это как что-то избыточное и повторяющееся...тоже самое и с длиной FilterMaxLength и ExactLengthValidator"

Краткий ответ - это принцип Single Responsibility доведенный до абсолюта.
Фильтры и валидаторы это разные сущности (фильтры занимаются фильтрацией, а валидаторы занимаются валидацией ), они разделены. И они обе нужны.
В данном случае OnlyDigitsValidator будет всегда возвращать Valid, т.к. OnlyDigitsFilter удалил все не цифровые символы. Но для сохранения целостной картины я оставляю OnlyDigitsValidator.
ExactLengthValidator позволяет определить какая именно ошибка случилась. т.е. если введено 5 символов из необходимых 11-ти, то в списке ошибок будет ExactLengthValidationError.
Можно использовать "оптимизированный" вариант поля снилс
val f = optionalFormField {
+FilterOnlyDigits
+FilterMaxLength(11)
+SnilsCheckSumValidator
+SnilsFormatter
}
но я предпочитаю полный вариант
val snilsField = formField {
+OnlyDigitsFilter()
+MaxLengthFilter(11)
+ExactLengthValidator(11)
+OnlyDigitsValidator()
+SnilsCheckSumValidator()
+SnilsFormatter()
}
Возможны случаи, когда не надо удалять из введенной строки неправильные символы, но при этом надо выводить сообщение об ошибке (далее, если будут следующие части, будет такой пример.) - для этого также необходимо разделение фильтров и валидаторов.
И еще один вопрос "Зачем прогонять все валидации, ведь достаточно будет прекращать проверку сразу после первой неудачной валидации".
Рассмотрим такой пример - поле для составления пароля. Это поле требует много валидаторов на различные условия (содержит цифры, содержит спецсимволы, содержит заглавные буквы и т.д.) и после того как пользователь ввел пароль (или по мере ввода) надо по каждому условию выводить или не выводить ошибку. Для этого поля надо прогонять все валидации. В итоге для универсальности, я решил, что надо в любом случае прогонять все валидации.
Что в итоге получили
-
все поведение поля описано в одном методе
-
описание, отчасти похоже на ТЗ которое пишет аналитик.
-
поведение поля можно полностью протестировать с помощью Unit-тестов
Итак, с одиночным полем ввода понятно. В следующих частях соберем эти поля в форму и прикрутим их к андроиду...
А пока можно рассмотреть ещё один пример
Представим, есть такое поле, "дата рождения".
К значению в этом поле следующие требования
-
значение должно быть правильной датой
-
дата должна быть меньше текущей
-
дата отображается в формате dd.mm.yyyy
-
разрешено вводить только цифровые символы
-
если введена не верная дата, то выводим сообщение об ошибке
-
если введена дата больше текущей, то выводим сообщение об ошибке
Приступим
Сначала создадим метод dateBeforeField(maxDate: LocalDate) , который возвращает FormField с ограничением по максимальной дате.
И метод birthDateField() который уже возращает поле для ввода дня рождения
fun dateBeforeField(maxDate: LocalDate) = formField {
+FilterOnlyDigits
+FilterMaxLength(8)
+ExactLengthValidator(8)
+DateValidator()
+DateBeforeValidator(maxDate)
+DateFormatter
}
fun birthDateField() = dateBeforeField(currentDate())
fun currentDate() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
Необходимые фильтры уже написаны, Написать DateFormatter не сложно
DateValidator
DateValidator - проверяет, что введена правильная (существующая) дата. Написать его совсем не сложно (хорошо, что есть kotlinx.datetime) Строка после фильтров приходит в виде DDMMYYYY, для этого сделаем DDMMYYYYformat - нужный нам формат даты, остальное очевидно...
val DDMMYYYYformat = LocalDate.Format {
dayOfMonth()
monthNumber()
year()
}
class DateValidator(val format: DateTimeFormat<LocalDate> = DDMMYYYYformat) : Validator {
override fun validate(data: String): ValidationResult =
try {
LocalDate.parse(data, format)
data.asValid()
} catch (e: Exception) {
ValidationResult.invalid(data, DateValidationError)
}
}
DateBeforeValidator
Тут тоже ничего сложного, Дополнительно потребуется параметр includeBorder - включать или не включать границу в разрешенные значения
object DateBeforeValidationError : ValidationError
class DateBeforeValidator(
val maxDate: LocalDate,
val includeBorder: Boolean = false,
val format: DateTimeFormat<LocalDate> = DDMMYYYYformat
) : Validator {
override fun validate(data: String): ValidationResult =
try {
val localDate = LocalDate.parse(data, format)
when {
localDate < maxDate -> data.asValid()
localDate == maxDate && includeBorder -> data.asValid()
else -> ValidationResult.invalid(data, DateBeforeValidationError)
}
} catch (e: Exception) {
ValidationResult.invalid(data, DateBeforeValidationError)
}
}
и окончательно проверим, что поле ведет себя как предполагается (не получается вставить спойлер в спойлер)
class BirthDayFieldTest : FunSpec({
val maxDate = LocalDate(2024,10,10)
context("required birthday field") {
val f = dateBeforeField(maxDate)
"".let {
test("$it must be invalid") {
with(f.process(it)) {
this shouldNot beValid()
(this as ValidationResult.Error) shouldNot beValid()
}
}
}
" ".let {
test("$it must be invalid") {
with(f.process(it)) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain NotEmptyValidationError
this.errors shouldContain ExactLengthValidationError
this.errors shouldContain DateValidationError
}
}
}
"sadfasdf".let {
test("$it must be invalid") {
with(f.process(it)) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain NotEmptyValidationError
this.errors shouldContain ExactLengthValidationError
this.errors shouldContain DateValidationError
}
}
}
"1234".let {
test("$it must be invalid") {
with(f.process(it)) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain ExactLengthValidationError
this.errors shouldContain DateValidationError
this.data should be("12.34")
}
}
}
context("valid dates") {
listOf("12.12.2002", "29.02.2024", "09.10.2024", "01.01.2024").forEach {
test("$it must be valid") {
with(f.process(it)) {
this should beValid()
this.data should be(it)
}
}
}
}
context("dates after max date") {
listOf("12.12.2025", "28.02.2025", "10.10.2024", "10.10.2025").forEach {
test("$it must be invalid") {
with(f.process(it)) {
this shouldNot beValid()
(this as ValidationResult.Error).errors shouldContain DateBeforeValidationError
}
}
}
}
}
context("optional birthday field") {
val f = optionalFormField {
+FilterOnlyDigits
+FilterMaxLength(8)
+DateValidator()
+DateBeforeValidator(maxDate = maxDate)
+DateFormatter
}
"".let {
test("$it must be valid") {
with(f.process(it)) {
this should beValid()
}
}
}
" ".let {
test("$it must be valid") {
with(f.process(it)) {
this should beValid()
}
}
}
"sadfasdf".let {
test("$it must be valid") {
with(f.process(it)) {
this should beValid()
}
}
}
}
})
Пока я писал тесты (а вы их смотрели), появилась мысль, что возможно описать это поле по-другому
Изменим фильтры - разрешим вводить не только цифры но и точки, изменим формат с которым работают валидаторы и тогда не нужен будет форматтер.
val format = LocalDate.Format {
dayOfMonth()
char('.')
monthNumber()
char('.')
year()
}
formField {
+FilterOnlyDigitsAndDots
+FilterMaxLength(10)
+ExactLengthValidator(10)
+DateValidator(format = format)
+DateBeforeValidator(maxDate, format = format)
}
Принципиально ничего не меняется, только теперь пользователь сам должен вводить точки в нужных местах , а в первом варианте за него это делал форматтер. И у пользователя появилось гораздо больше возможностей для ввода неправильного значения, например ......... .
Поэтому я выбираю начальный вариант..
На сегодня, точно всё.
До следующей части.

Автор: Skoroxod66
