Skip to content

Commit

Permalink
feat: added strings topic (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
y9vad9 authored Nov 22, 2022
1 parent 218622f commit eb05892
Show file tree
Hide file tree
Showing 4 changed files with 359 additions and 1 deletion.
345 changes: 345 additions & 0 deletions docs/kotlin/data-structures/strings.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
---
keywords: [kotlin string, strings, рядки, строка, isUpperCase, slice, substring, indexOf, для новачків, для початківців, туторіал]
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';

# Рядки та Символи
Ми вже розглядали рядки як вбудований тип даних, тепер же прийшов час розібрати їх більш детально. Що ж таке рядок?
:::info
**Рядок** – це структура, що має деяку послідовність (набір) символів, які можуть бути буквами, числами, знаками, тощо.
:::
Тобто за аналогією з [діапазонами](../../kotlin/basics/cycles-and-recursions#for), де інтервал – це по суті набір чисел
між вказаною відстанню чисел, рядок – це деякий набір (список, послідовність) символів, які утворюють рядок. Тобто:
```kotlin
val string: String = "abcdef123"
```
Це набір (послідовність) з наступних символів: *a*, *b*, *c*, *d*, *e*, *f*, *1*, *2*, *3*.

:::tip Варто знати
До речі, для символів існує окремий тип – і це `Char`. Взагалі, він може існувати й без рядкаЖ
```kotlin
val character: Char = 'a'
```
На відміну від рядка, вказуємо ми символ за допомогою одинарних лапок `'`.
:::

Але навіщо це вам? Вирішім наступне завдання:
:::info завдання
Користувач вводить своє ім'я, перевірте, чи перший буква з великої та виведіть відповідний результат.
:::
І тут постає два питання: як нам перевірити саме перший символ та за допомогою чого. Відповідаю на ваші запитання.

## Index оператор
Для того, щоб отримати нам деякий конкретний символ з рядка, ми використовуємо index-оператор. Але що це таке?
:::info Термінологія
**Індекс оператор** – це оператор, що отримує з вказаної послідовності (набору, списку) певний елемент за
його порядковим номером (переважно його називають індексом).

Використання:
```kotlin
val string = "123"
println(string[0]) // виведе 1
```
:::
Тобто, у нас є послідовність деяких символів, що ввів користувач, і за допомогою цього оператору `послідовність[індекс]`
ми отримуємо відповідний індексу елемент.
:::danger увага
Важливо враховувати, що індекс (тобто порядковий номер) будь-якої послідовності завжди **починається з нуля**.
Будь-те обережні, щоб запобігти помилок, де ви отримуєте елемент, що не існує:
```kotlin
val string = "123"
// This will error
println(string[3]) // такого елемента не існує
```
У даному прикладі, ми намагаємось отримати четвертий елемент нашої послідовності, хоча його не існує.
:::
Тож, щоб отримати першу букву з введеного користувачем ім'я ми робимо наступне:
```kotlin
fun main() {
println("Введіть ім'я: ")
val name = readln()
println(name[0])
}
```
І при введенні ми отримаємо потрібний нам результат:
```text
Введіть ім'я:
> василь
в
```
Але як же нам перевірити чи є перший символ великою буквою?
## Вбудовані функції
### isUpperCase()
Для того, щоб перевірити, чи є символ великою буквою, ми можемо скористатись наступною функцією – `Char.isUpperCase()`.
Тобто, наступним чином:
```kotlin
fun main() {
println("Введіть ім'я: ")
val name = readln()
println(
if(name[0].isUpperCase())
"Перша буква з великої"
else "Перша буква з маленької"
)
}
```
Нам виведе:
```text
Введіть ім'я:
> василь
Перша буква з маленької
```
### slice, substring, indexOf
Розберім ще не менш важливі функції: `slice` та `indexOf`. Вирішім наступне завдання:
:::info Завдання
Користувач вводить своє ім'я та прізвище через пробіл. Виведіть окремо ім'я та прізвище.
:::
Щоб вирішити це завдання, перш за все, нам потрібно зрозуміти – а як саме нам отримати з одного рядка ім'я та прізвище?

Якщо обходитись без цих функцій, ми можемо придумати наступне: а зробім лінійний (один за одним)
перебір символів за їх індексом. Тобто наступне буде для ім'я (не лякайтесь та розберіться,
тут все не так складно, як може здатись, на перший погляд):
```kotlin
fun main() {
println(getFirstName("Вадим Ярощук"))
}

/**
* Функція, що отримує ім'я з рядка.
* Параметр [string] – рядок з якого отримуємо ім'я.
*
*/
fun getFirstName(string: String): String {
return collectString(string, toIndex = indexOf(string, ' '))
}

/**
* Отримує індекс першого елемента з [string], який відповідає [symbol].
* Параметр [fromIndex] – вказує, з якого індекса починати перевірку на відповідність.
*
* З кожним викликом функції, якщо елемент за індексом [fromIndex] не відповідає [symbol],
* [fromIndex] збільшується на один, щоб перевірити наступний елемент при наступному повторенні.
*/
fun indexOf(string: String, symbol: Char, fromIndex: Int = 0): Int {
val nextIndex = fromIndex + 1

return if(string[fromIndex] == symbol)
fromIndex - 1
else indexOf(string, symbol, nextIndex)
}

/**
* Отримує рядок з рядка [string] з початкового елемента за індекcом [fromIndex]
* до елемента за індексом [toIndex].
* Отримується за допомогою рекурсії.
*
* Параметр [string] – це оригінальний рядок, з якого будемо отримувати елементи (символи).
* Параметр [tempString] – це тимчасовий рядок задіяний в рекурсії, який збирає символи
* при кожному виклику функції (отримує рядок з [fromIndex], який збільшується з кожним викликом)
* Параметр [fromIndex] – початковий індекс, з якого будемо отримувати рядок
* Параметр [toIndex] – кінцевий індекс, до якого буде продовжуватись рекурсія.
*/
fun collectString(string: String, tempString: String = "", fromIndex: Int = 0, toIndex: Int): String {
val result = tempString + string[fromIndex]

return if(fromIndex == toIndex)
result
else collectString(string, result, fromIndex + 1, toIndex)
}
```
:::tip Корисно знати
У даному прикладі, аргументи (параметри) функцій мають параметри за замовчуванням. Щоб зробити подібне,
все що вам потрібно, це до параметра додати дорівнює та значення яке вам потрібне за замовчуванням.

Також використовуються іменовані параметри (коли при виклику функції, ти вказуєш ім'я параметру), тобто:
`foo(parameterName = "smth")`. Використовується, наприклад, коли є параметри за замовчуванням, щоб їх пропускати (
бо якщо продовжувати перелік, нам знадобиться задати навіть параметер за замовчуванням).
:::
Наш алгоритм отримання індексу пробіла наступний – беремо елемент за його індексом (початковий у нас 0) →
перевіряємо, чи цей елемент (символ) за цим індексом є пробілом → якщо ні, йдемо далі (індекс + 1),
якщо так, закінчуємо.

Після чого переходимо до отримання всіх символів до отриманого індексу, за наступним алгоритмом: отримуємо
елемент за вказаним початковим індексом `fromIndex` (початковий – це нуль) → перевіряємо чи цей індекс не більше
кінцевого індексу `toIndex` → якщо так, то повертаємо `tempString` (цей аргумент з кожним повторенням поповнювався
по одному символу), якщо ні, то викликаємо всередині ту ж функцію додаючи поточний елемент (символ) у `tempString`.

Можете погратись з цим кодом [тут](https://pl.kotl.in/gJLhX-ZBe).

Але, розбивати це на рекурсію трішки заскладно, чи не так? Згадуючи тему про [цикли](../../kotlin/basics/cycles-and-recursions#for)
ми можемо переробити це на цикли. Наприклад, переробимо `indexOf`:
```kotlin
fun indexOf(string: String, symbol: Char) {
for(index in 0..name.length) {
if(name[index] == symbol)
return index
}

return -1 // немає такого символа в рядку
}
```
Виглядає більш простіше, чи не так? Насправді це можна ще спростити за допомогою наступного факту:
:::tip Важливо знати
**Рядок**, як і діапазони (прогресії) **також має ітератор**. Що дозволяє нам зробити наступне:
```kotlin
fun indexOf(string: String, symbol: Char) {
for(char in string) {
if(char == symbol)
return index
}

return -1 // немає такого символа в рядку
}
```
Тобто, тепер замість числа в змінній перед `in` у нас число (забігаючи наперед, там будуть різні дані в залежності
типів).
:::
Як домашнє завдання залишаю вам переробити `сollectString` на цикл самотужки.

Але, взагалі, подібні функції вже існують в Kotlin:
#### indexOf
Щоб отримати порядковий номер (індекс) потрібного нам елементу, ми використовуємо вже вбудовану в мову функцію `indexOf`:
```kotlin
fun main() {
val name = "Vadim Yaroschuk"
println("Індекс пробілу: ${name.indexOf(' ')}")
}
```
У нас виведе наступне:
```text
Індекс пробілу: 5
```
Таким чином ми вже знайшли механізм того, як знайти пробіл, тепер дізнаймось про функцію яка «обріже» наш рядок
до потрібного нам.
#### slice / substring
Для того, щоб обрізати рядок ми використовуємо функцію `slice` або `substring`. Ці функції відрізняються лише тим,
що slice копіює рядок у вказаному діапазоні, а `substring` використовує вже створений рядок за допомогою внутрішнього
механізму.

Тож буде так:
<Tabs>
<TabItem value="slice" default>

```kotlin
val fullName = "Vadim Yaroschuk"
val firstName = fullName.slice(0..fullName.indexOf(" ") - 1)
println("Ім'я користувача: $firstName")
```

</TabItem>
<TabItem value="substring">

```kotlin
val fullName = "Vadim Yaroschuk"
val firstName = fullName.substring(0, fullName.indexOf(" ") - 1)
println("Ім'я користувача: $firstName")
```

</TabItem>
</Tabs>

До речі, для `substring` є деякі функції спрощення, наприклад:
- `substringAfter(Char)` – обрізає рядок від першого Сhar (не включно) до кінця рядка
- `substringBefore(Char)` – обрізає рядок до першого відповідного Сhar (не включно)
- `substringAfterLast(Char)` – обрізає рядок від останнього відповідного Сhar (не включно) до кінця рядка
- `substringBeforeLast(Char)` – обрізає рядок до останнього відповідного Сhar (не включно)

Тобто, в нашому випадку це буде так:
```kotlin
val fullName = "Vadim Yaroschuk"
val firstName = fullName.substringBefore(' ')
val surname = fullName.substringAfter(' ')
println("Ім'я користувача: $firstName")
println("Прізвище користувача: $surname")
```
Виведе наступне:
```text
Ім'я користувача: Vadim
Прізвище користувача: Yaroschuk
```
### startsWith, endsWith
Перейдімо до також важливих функцій перевірки рядка: `startsWith` та `endsWith`. Щоб зрозуміти для чого вони
використовуються, вирішимо наступне завдання:
:::info завдання
Користувач вводить посилання на будь-який файл. Визначте, чи є посилання безпечним (перевіривши чи є з'єднання
`https://`) та чи є файл веб-сторінкою (`.html`).
Наприклад:
```text
> http://foo.bar/index.html
З'єднання не є безпечним.
Файл є веб-сторінкою.
```
Для цього нам знадобляться ці функції. Як вже зрозуміло за назвою: `startsWith` – перевіряє рядок, чи рядок починається
на якийсь довільний рядок, а `endsWith` – чи рядок закінчується на довільний рядок.
:::
Тож, для вирішення нашої задачі, використаємо ці функції:
```kotlin
fun main() {
val link = readln()

if(link.startsWith("https://"))
println("З'єднання є безпечним.")
else println("З'єднання не є безпечним.")

if(link.endsWith(".html"))
println("Файл є веб-сторінкою.")
println("Файл не є веб-сторінкою")
}
```
Все дуже просто!
## Оператор `in` (contains)
А тепер перейдім до не менш важливого оператору `in` (contains, містить українською) – якщо попередні дві функції
перевіряли початок та кінець рядка, то даний оператор перевіряє увесь рядок на присутність вказаного підрядка. Тобто:
```kotlin
fun main() {
val bio = "I am Kotlin developer"
if("Kotlin" in bio)
println("Kotlin присутній в рядку")
else println("Kotlin не присутній в рядку")
}
```
:::caution
Ви маєте бути обережними та не переплутати порядок:
```kotlin
fun main() {
val bio = "I am Kotlin developer"
// This will error
if(bio in "Kotlin")
// This will error
println("Kotlin присутній в рядку")
...
// This is correct
if("Kotlin" in bio)
// This is correct
println("Kotlin присутній в рядку")
...
```
:::
Щоб не переплутати, ви можете використовувати функцію-відповідник до цього оператору (якщо ви пам'ятаєте,
в темі про [оператори](../../kotlin/basics/operators.md), я розповідав, що функції можуть виражатись через функцію,
і як раз для подібних речей іноді їх використовують):
```kotlin
fun main() {
val bio = "I am Kotlin developer"
if(bio.contains("Kotlin"))
println("Kotlin присутній в рядку")
else println("Kotlin не присутній в рядку")
}
```
Що буде трішки очевидніше, мабуть. Чітких правил щодо використань немає, хоча я зазвичай використовую `in`.
## Завдання
Для того, щоб закріпити матеріл, пропоную наступні для вирішення завдання:
:::info завдання №1
Створіть функції відповідники до substring (substringAfter, subStringAfterLast, substringBefore, substringBeforeLast)
та за допомогою них дізнайтесь з повного користувацького ПІБ окремо ім'я, прізвище, та ім'я по-батькові.
:::
:::info завдання №2
Користувач вводить рядок з довільним текстом. Перевірте рядок на присутність будь-якої лайки (на ваш розсуд).
:::
:::info завдання №3
Користувач вводить рядок з розташуванням файлу. Дізнайтесь, який саме тип файлу знаходиться там (картинка, відео, тощо),
враховуйте різні формати одного типу файла.
:::
Loading

0 comments on commit eb05892

Please sign in to comment.