Регулярные выражения в Kotlin

1. Введение

Мы можем найти применение (или злоупотребление) регулярными выражениями практически в любом программном обеспечении, от быстрых скриптов до невероятно сложных приложений.

В этой статье мы рассмотрим, как использовать регулярные выражения в Kotlin.

Мы не будем обсуждать синтаксис регулярных выражений; для адекватного понимания статьи требуется знакомство с регулярными выражениями в целом, и рекомендуется знание синтаксиса Java Pattern в частности.

1. Введение  Мы можем найти применение (или злоупотребление) регулярными выражениями практически в любом программном обеспечении, от быстрых скриптов до невероятно сложных приложений.

2. Настройка

Хотя регулярные выражения не являются частью языка Kotlin, они входят в его стандартную библиотеку.

Вероятно, они уже есть в нашем проекте в качестве зависимого компонента:

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.2.21</version>
</dependency>

3. Создание объекта регулярного выражения

Регулярные выражения - это экземпляры класса kotlin.text.Regex. Мы можем создать его несколькими способами.

Возможно, вызвать конструктор регулярных выражений:

Regex("a[bc]+d?")

или мы можем вызвать метод to Regex для строки:

"a[bc]+d?".toRegex()

Наконец, мы можем использовать статический фабричный метод:

Regex.fromLiteral("a[bc]+d?")

За исключением различий, описанных в следующем разделе, эти варианты эквивалентны и зависят от личных предпочтений. Просто помните о необходимости быть последовательными!

Совет: регулярные выражения часто содержат символы, которые интерпретируются как escape-последовательности в строковых литералах. Таким образом, мы можем использовать необработанные строки, чтобы забыть о нескольких уровнях экранирования:

"""a[bc]+d?\W""".toRegex()

3.1. Параметры подбора

Как конструктор Regex, так и метод toRegex позволяют нам указать один дополнительный параметр или набор:

Regex("a(b|c)+d?", CANON_EQ)
Regex("a(b|c)+d?", setOf(DOT_MATCHES_ALL, COMMENTS))
"a(b|c)+d?".toRegex(MULTILINE)
"a(b|c)+d?".toRegex(setOf(IGNORE_CASE, COMMENTS, UNIX_LINES))

Параметры перечислены в классе Regex Option, который мы статически импортировали в приведенном выше примере:

  • IGNORE_CASE – включает сопоставление без учета регистра
  • MULTILINE – изменяет значение ^ и $
  • LITERAL – приводит к тому, что метасимволам или управляющим последовательностям в шаблоне не придается особого значения
  • UNIX_LINES – в этом режиме только символ \n распознается как символ окончания строки
  • COMMENTS – разрешает использование пробелов и комментариев в шаблоне
  • DOT_MATCHES_ALL – приводит к тому, что точка совпадает с любым символом, включая символ окончания строки
  • CANON_EQ – обеспечивает эквивалентность с помощью канонической декомпозиции

4. Соответствие

Мы используем регулярные выражения в основном для сопоставления входных строк, а иногда и для извлечения или замены их частей.

Теперь мы подробно рассмотрим методы, предлагаемые классом Regex в Kotlin для сопоставления строк.

4.1. Проверка частичных или полных совпадений

В этих случаях нам интересно знать, удовлетворяет ли строка или часть строки нашему регулярному выражению.

Если нам нужно только частичное совпадение, мы можем использовать contains Match в:

val regex = """a([bc]+)d?""".toRegex()

assertTrue(regex.containsMatchIn("xabcdy"))

Если мы хотим, чтобы вместо этого совпадала вся строка, мы используем совпадения:

assertTrue(regex.matches("abcd"))

Обратите внимание, что мы также можем использовать совпадения в качестве инфиксного оператора:

assertFalse(regex matches "xabcdy")

4.2. Извлечение соответствующих компонентов

В этих случаях мы хотим сопоставить строку с регулярным выражением и извлечь части строки.

Возможно, нам потребуется сопоставить всю строку целиком:

val matchResult = regex.matchEntire("abbccbbd")

Или мы могли бы захотеть найти первую подстроку, которая соответствует:

val matchResult = regex.find("abcbabbd")

Или, может быть, найти все совпадающие подстроки сразу, в виде набора:

val matchResults = regex.findAll("abcb abbd")

В любом случае, если поиск завершен успешно, результатом будет один или несколько экземпляров класса Match Result. В следующем разделе мы увидим, как его использовать.

Если поиск не завершен успешно, эти методы возвращают значение null или пустой набор в случае find All.

4.3. MatchResult Class

Экземпляры класса Match Result представляют собой успешные совпадения некоторой входной строки с регулярным выражением; либо полные, либо частичные совпадения (см. предыдущий раздел).

Как таковые, они имеют значение, которое является совпадающей строкой или подстрокой:

val regex = """a([bc]+)d?""".toRegex()
val matchResult = regex.find("abcb abbd")

assertEquals("abcb", matchResult.value)

И у них есть ряд индексов, указывающих, какая часть входных данных была сопоставлена:

assertEquals(IntRange(0, 3), matchResult.range)

4.4. Группировки и деструктурирование

Мы также можем извлекать группы (совпадающие подстроки) из экземпляров MatchResult.

Мы можем получить их в виде строк:

assertEquals(listOf("abcb", "bcb"), matchResult.groupValues)

Или мы также можем рассматривать их как объекты группы совпадений, состоящие из значения и диапазона:

assertEquals(IntRange(1, 3), matchResult.groups[1].range)

Группа с индексом 0 - это всегда вся совпадающая строка. Вместо этого индексы, превышающие 0, представляют группы в регулярном выражении, разделенные круглыми скобками, например ([bc]+) в нашем примере.

Мы также можем деструктурировать экземпляры результатов сопоставления в инструкции присваивания:

val regex = """([\w\s]+) is (\d+) years old""".toRegex()
val matchResult = regex.find("Mickey Mouse is 95 years old")!!
val (name, age) = matchResult.destructured

assertEquals("Mickey Mouse", name)
assertEquals("95", age)

4.5. Определяйте группы по названию

Начиная с версии 1.9.0, Kotlin поддерживает группы захвата по именам. Эта функция позволяет нам находить совпадающие группы захвата по именам.

Давайте сначала рассмотрим, как использовать эту функцию. Мы по-прежнему будем использовать наш пример с Микки Маусом:

val regex = """(?<name>[\w\s]+) is (?<age>\d+) years old""".toRegex()
val matchResult = regex.find("Mickey Mouse is 95 years old")!!

val age = matchResult.groups["age"]?.value
val name = matchResult.groups["name"]?.value

assertEquals("Mickey Mouse", name)
assertEquals("95", age)

Как мы видим, в шаблоне регулярных выражений мы можем определить именованную группу захвата, используя “(?ШАБЛОН <Имя>)“, например (?<возраст>\d+). Позже мы можем вызвать группы MatchResult[theName]?.value, чтобы получить соответствующую группу по имени, которое мы определили в шаблоне, например, MatchResult.groups[“возраст”]?.value.

Таким образом, функция “сгруппировать по имени” позволяет нам удобно извлекать из группы совпадающее значение. Нам больше не нужно подсчитывать индексы группы.

4.6. Множественные совпадения

У результата сопоставления также есть метод next, который мы можем использовать для получения следующего совпадения входной строки с регулярным выражением, если таковое имеется:

val regex = """a([bc]+)d?""".toRegex()
var matchResult = regex.find("abcb abbd")

assertEquals("abcb", matchResult!!.value)

matchResult = matchResult.next()
assertEquals("abbd", matchResult!!.value)

matchResult = matchResult.next()
assertNull(matchResult)

Как мы видим, next возвращает значение null, когда совпадений больше нет.

5. Замена

Другое распространенное применение регулярных выражений - замена совпадающих подстрок другими строками.

Для этой цели у нас есть два метода, легко доступных в стандартной библиотеке.

Один из них, replace, предназначен для замены всех вхождений совпадающей строки:

val regex = """(red|green|blue)""".toRegex()
val beautiful = "Roses are red, Violets are blue"
val grim = regex.replace(beautiful, "dark")

assertEquals("Roses are dark, Violets are dark", grim)

Другой, replaceFirst, предназначен для замены только первого вхождения:

val shiny = regex.replaceFirst(beautiful, "rainbow")

assertEquals("Roses are rainbow, Violets are blue", shiny)

5.1. Сложные замены

Для более сложных сценариев, когда мы не хотим заменять совпадения постоянными строками, но вместо этого хотим применить преобразование, регулярное выражение по-прежнему дает нам то, что нам нужно.

Введите перегрузку replace, завершая ее:

val reallyBeautiful = regex.replace(beautiful) {
m -> m.value.toUpperCase() + "!"
}

assertEquals("Roses are RED!, Violets are BLUE!", reallyBeautiful)

Как мы можем видеть, для каждого совпадения мы можем вычислить строку замены, используя это совпадение.

6. Расщепление

Наконец, возможно, нам захочется разбить строку на список подстрок в соответствии с регулярным выражением. И снова, регулярное выражение Kotlin помогло нам разобраться:

val regex = """\W+""".toRegex()
val beautiful = "Roses are red, Violets are blue"

assertEquals(listOf(
"Roses", "are", "red", "Violets", "are", "blue"), regex.split(beautiful))

В данном случае регулярное выражение соответствует одному или нескольким символам, не являющимся словами, поэтому результатом операции разделения является список слов.

Мы также можем ограничить длину результирующего списка:

assertEquals(listOf("Roses", "are", "red", "Violets are blue"), regex.split(beautiful, 4))

7. Совместимость с Java

Если нам нужно передать наше регулярное выражение в Java-код или в какой-либо другой API на языке JVM, который ожидает экземпляр java.util.regex.Pattern, мы можем просто преобразовать наше регулярное выражение:

regex.toPattern()

8. Выводы

В этой статье мы рассмотрели поддержку регулярных выражений в стандартной библиотеке Kotlin.

Оригинал статьи: https://www.baeldung.com/kotlin/regular-expressions