Регулярно возникают задачи проверять, что пользователь вводит в поля и сообщать ему если он что-то сделал не правильно.
Ничего в этом сложного нет, напишем парочку регулярных выражений
так
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 символов
*/
class FilterMaxLength(private val maxLength: Int) : Filter {
override fun filter(data: String): String =
data.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)
}
Принципиально ничего не меняется, только теперь пользователь сам должен вводить точки в нужных местах , а в первом варианте за него это делал форматтер. И у пользователя появилось гораздо больше возможностей для ввода неправильного значения, например .........
.
Поэтому я выбираю начальный вариант..
На сегодня, точно всё.
До следующей части.
