Рядок тексту (англ. string) - це фрагмент тексту між лапками.
mystring <- "some text"
mystring
#> [1] "some text"
typeof(mystring)
#> [1] "character"
На жаль, ця термінологія може заплутати програмістів, знайомих з іншими мовами.
Хоча type тут character, R не розрізняє одну літеру (у багатьох інших мовах її називають char, але не в R) і довгий рядок тексту.
І те, і те є одним елементом, і його можна брати як в одинарні ' ', так і в подвійні " " лапки, без жодної різниці.
Посібник зі стилю радить використовувати подвійні лапки, крім випадків, коли всередині рядка тексту потрібні подвійні лапки без екранування.
str2 <- 'String with "quotes"' # convenience format
str2
#> [1] "String with \"quotes\"" # standard format
Отже, character vector - це вектор рядків тексту, де кожен рядок є окремим елементом.
s <- c("some", "strings")
s
#> [1] "some" "strings"
length(s)
#> [1] 2
length(s[1]) # the number of vector elements, not the string length
#> [1] 1
nchar(s[1]) # the number of characters in the string "some"
#> [1] 4
У наведеному вище прикладі length(s[1]) рівнозначно length("some"): ідеться про кількість елементів вектора, а не про довжину рядка тексту.
Маніпулювати окремим рядком тексту, звісно, можна, як у прикладі з nchar() вище, але для цього потрібні спеціальні функції, про які йтиметься далі.
Як ми бачили вище, деякі розділові символи потрібно «екранувати», поставивши перед ними зворотну скісну риску \.
Цей список навряд чи здивує досвідчених програмістів, адже ця тема досить стандартизована в різних мовах.
Крім лапок (як показано вище), до списку входять символи нового рядка \n, табуляції \t, зворотна скісна риска \\ і символи Unicode, що починаються з \U або \u.
Щоб не використовувати багато екранованих символів, можна визначати необроблені рядки всередині r"( )":
r"(raw "string")"
#> [1] "raw \"string\""
Для необроблених рядків, що містять дужки ( ), є багато інших варіантів синтаксису.
Будь-яка сучасна підтримувана версія R кодує рядки тексту в UTF-8, тож більшість мов світу можна підтримувати.
"आधुनिक मानक हिन्दी"
#> [1] "आधुनिक मानक हिन्दी"
Крім того, багато функцій R зважають на locale і намагаються дотримуватися місцевих узвичаєнь щодо сортування рядків тексту, перетворення на верхній чи нижній регістр, дат і часу тощо.
У цьому силабусі приклади переважно для американської англійської: і тому, що це поширений типовий варіант, і тому, що автори силабусу живуть у США.
Локалі - складна тема, тож варто пошукати документацію під власну ситуацію.
У базовому R можливо майже все, але багато програмістів вважають підтримку роботи з рядками тексту досить обмеженою й заплутаною.
На щастя, є пакети tidyverse, значно інтуїтивніші, гнучкіші та самоузгодженіші.
Досі існує багато застарілого коду, що використовує функції для рядків старого зразка. Це стосується й більшості рішень спільноти на Exercism.
Крім того, для невеликих завдань буває корисно використати вбудовані функції замість імпорту великої бібліотеки. Супровідники треку R схильні робити саме так, коли пишуть зразкові рішення для перевірки вправ у нашому репозиторії на GitHub.
Тож певне знання старих підходів усе ще варте вивчення.
Наведені нижче функції показано в найпростішій формі, але більшість із них має ще й необовʼязкові аргументи. Деталі можна знайти в документації.
ncharПовертає кількість символів у рядку тексту.
nchar("string")
#> [1] 6
nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7
Тема того, що означає «символ» (складна!), виходить за межі нашої розповіді. Звернімо увагу лише на кілька моментів у прикладах вище:
\0 (це додало б 1 до довжини).Будьмо уважні до того, що саме передаємо в nchar.
Результати все ще можуть бути непередбачуваними для значень не типу character, хоча в останніх версіях R це покращилося.
grepШукає зразок у векторі рядків тексту й повертає індекси збігів.
grep("Jav", c("Java", "Python", "Javascript"))
#> [1] 1 3
paste і paste0
Поєднує довільну кількість рядків тексту в один рядок.
За бажанням для paste можна вказати роздільник: типово це пробіл.
paste0(v) рівнозначно paste(v, sep = "").
paste("bits", "of", "a", "string")
#> [1] "bits of a string"
paste0("bits", "of", "a", "string")
#> [1] "bitsofastring"
sprintfДля тоншого контролю над складанням рядків тексту R запозичує функцію sprintf із C (і з кількох пізніших мов: Вікіпедія налічує близько 30).
r <- 5.3
pi <- 3.14159
sprintf("A circle of radius %.1f has area %.2f", r, pi * r^2)
#> [1] "A circle of radius 5.3 has area 88.25"
trimwsПровідні та/або кінцеві пробільні символи можна прибрати за допомогою trimws():
s <- " messy string "
trimws(s)
#> [1] "messy string"
Є параметр which = "left" (або "right"), щоб не обрізати обидва кінці.
substrПрацює з одним рядком тексту й повертає підрядок між двома межами (включно).
substr("Exercism", 5, 7)
#> [1] "cis"
strsplitРозбиває рядок тексту на список R за рядком-роздільником. Роздільник обовʼязковий (без типового значення), але може бути порожнім рядком.
strsplit("R, Python, Julia", ", ")
[[1]]
#> [1] "R" "Python" "Julia"
strsplit("Exercism", "")
[[1]]
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
Списки розглянемо в окремому концепті.
Якщо ми ще не дійшли до цієї частини силабусу, достатньо поки знати, що функція unlist() (у цьому випадку) перетворить список на вектор.
unlist(strsplit("Exercism", ""))
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
regexpr і regexpr
Це скорочення від «regular expression» (регулярний вираз): ці функції шукають входження зразка в рядку тексту й повертають список (потенційно) корисних даних. Їх згадано лише для повноти: є кращі варіанти!
regexpr повертає лише перше входження, gregexpr повертає всі входження («g» означає «global», тобто глобальний пошук).
Як видно з назв, зразок може бути регулярним виразом, про який ітиметься в пізнішому концепті.
Чому ці старі функції для рядків тексту схожі на команди оболонки Unix?
Хоча R створили 1993 року, його задумали значною мірою зворотно сумісним із мовою S.
Мову S розробили в Bell Labs, уперше вона зʼявилася 1976 року. Bell Labs також відповідає за Unix (з 1969 року) і мову C (1972).
Оце так команда з тієї значно давнішої епохи! Залишається лише припустити, що вони спілкувалися між собою й обмінювалися ідеями.
Якщо бути поблажливими, можна сказати, що старі функції для рядків тексту - не найкраще, що може запропонувати R (є й менш поблажливі думки).
Щоб виправити становище, бібліотеку stringr розробляють із 2009 року, і тепер вона є ключовою частиною збірки Tidyverse.
Імпортуймо її у свій простір імен за допомогою library(stringr) або library(tidyverse).
Серед ключових можливостей:
str_ і намагаються зробити призначення зрозумілим.Як і всі пакети Tidyverse, stringr має добру документацію, яку зазвичай пишуть автори пакета.
У stringr доступні десятки функцій, і вони часто мають різноманітні необовʼязкові аргументи.
Спершу це може трохи перевантажувати, тож документація - наш друг.
Подумаймо, що хочемо подати на вхід (один рядок тексту? вектор? щось складніше?) і що хочемо отримати на виході.
Імовірно, є простий спосіб зробити те, що потрібно, якщо знайти правильну функцію та правильні аргументи.
Використання конвеєрів |> для зʼєднання функцій докладніше розглянемо в інших частинах силабусу.
Поки що корисно знати, що:
Наведений нижче приклад ілюструє синтаксис. Окремі функції розглянемо пізніше.
library(stringr)
"Monday-25-February" |>
str_split("-") |>
unlist() |> # [1] "Monday" "25" "February"
str_sub(1, 3) |> # [1] "Mon" "25" "Feb"
str_to_upper() |> # [1] "MON" "25" "FEB"
str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
str_length рахує кодові точки (подібно до nchars).
Різні інші функції працюють із пробільними символами, зокрема str_trim() і str_squish().
Це часто важливе завдання очищення на початку будь-якого проєкту з науки про дані.
# The first 3 kernels in JuPyteR notebooks:
str_length(c("Julia", "Python", "R")) #> [1] 1 6 5
#> [1] 5 6 1
str_pad(c("Julia", "Python", "R"), 8, "right") # add spaces if necessary
#> [1] "Julia " "Python " "R "
str_trim("Julia Python R ") # remove leading & trailing whitespace
#> [1] "Julia Python R"
str_squish("Julia Python R ") # trim, collapse multiple spaces
#> [1] "Julia Python R"
Функція str_sub(string, start, end) повертає підрядок від start до end (включно), а str_sub_all() може працювати з кількома рядками тексту.
Тут багато гнучкості.
start і end типово вказують на перший і останній символ відповідно.
Вони можуть бути векторами індексів.
На відміну від індексації векторів (і це приємно), відʼємні значення відраховуються від кінця рядка тексту, як у Python.
s <- "abcdefgh"
str_sub(s, 2, 4)
#> [1] "bcd"
str_sub(s, 5)
#> [1] "efgh"
str_sub(s, -3, -2)
#> [1] "fg"
str_sub(s, c(1, 4), c(2, 5))
#> [1] "ab" "de"
str_sub(s, c(1, 4, 6), c(2, 5, -1))
#> [1] "ab" "de" "fgh"
s |> str_sub(2, 4) # these functions are designed for pipes
#> [1] "bcd"
# str_sub_all takes vector input, gives list output
str_sub_all(c(s, "ijklmnop"), c(1, 4, 6), c(2, 5, -1))
[[1]]
#> [1] "ab" "de" "fgh"
[[2]]
#> [1] "ij" "lm" "nop"
str_flatten перетворює вектор character на один рядок тексту.
Аргумент collapse вставляється між кожним елементом і типово дорівнює "".
c("R", "Julia", "Python") |> str_flatten()
#> [1] "RJuliaPython"
c("R", "Julia", "Python") |> str_flatten(collapse = " - ")
#> [1] "R - Julia - Python"
str_c перетворює кілька векторів character на один вектор character, за потреби повторюючи елементи.
str_c(LETTERS[1:8], 1:8, sep = ":")
#> [1] "A:1" "B:2" "C:3" "D:4" "E:5" "F:6" "G:7" "H:8"
str_glue приносить у R просту інтерполяцію рядків тексту.
Вирази всередині фігурних дужок { } обчислюються, а замість них підставляється їхній текстовий вигляд.
Якщо подати кілька рядків із виразами, їх також зʼєднають, як у str_flatten.
Рядки тексту також можна за бажанням очистити від провідних і кінцевих пробільних символів.
r <- 5.3
pi <- 3.14159
str_glue("A circle of radius {r} has area {pi * r^2}")
A circle of radius 5.3 has area 88.2472631
Хоча str_glue гнучка (і доволі складна), ще ширші можливості дає бібліотека glue, на якій вона ґрунтується.
Дуже поширене застосування R - приводити неохайні дані до однорідного формату для аналізу. Щоб це полегшити, є кілька способів розбити текст на підрядки.
str_split_1 найпростіша: перетворює один рядок тексту на вектор частин, розбиваючи його за вказаним зразком (типового зразка немає).
s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R" "Julia" "Python"
str_split_i приймає вектор character та індекс n і повертає вектор із n-м підрядком кожного вхідного рядка тексту.
s2 <- c(s, "C Kotlin, F#") #> [1] "R Julia Python" "C Kotlin, F#"
str_split_i(s2, " ", 2) # 2nd element of each
#> [1] "Julia" "Kotlin,"
str_split приймає вектор character і повертає список векторів.
У нашому силабусі є концепт списків, який пояснює цей результат.
str_split(s2, " ")
[[1]]
#> [1] "R" "Julia" "Python"
[[2]]
#> [1] "C" "Kotlin," "F#"
Є також str_split_fixed, яка повертає матрицю.
Матриці розглянемо в пізнішому концепті.
Щоб зіставити весь рядок тексту, str_equal() може використовувати різні визначення «рівності».
Найпростіше: типово враховується регістр, але це можна змінити.
Локалі та Unicode відкривають значно більше можливостей.
str_equal("Odin", "odin")
#> [1] FALSE
str_equal("Odin", "odin", ignore_case = TRUE)
#> [1] TRUE
Є багато інших функцій, про які докладніше йтиметься в концепті регулярних виразів.
Деякі мови (переважно європейські) розрізняють великі та малі літери.
У stringr є набір функцій для взаємного перетворення регістрів, з урахуванням локалі:
str_to_lower()str_to_upper()str_to_title() : перша літера кожного слова - великаstr_to_sentence() : перша літера речення - великаstr_to_upper("elixir") # defaults to locale = "en"
#> [1] "ELIXIR"
str_to_upper("ελληνικά") # infers Greek
#> [1] "ΕΛΛΗΝΙΚΆ"
str_to_title("the cat sat on the mat")
#> [1] "The Cat Sat On The Mat"
str_to_sentence("the cat sat on the mat")
#> [1] "The cat sat on the mat"
Для заміни підрядка на основі зіставлення зі зразком str_replace() змінює перше входження зразка, а str_replace_all() змінює всі входження.
Для заміни підрядка на основі індексації символів є форма присвоєння для str_sub().
s <- "abcde"
str_sub(s, 3, 4) <- "yz"
s
#> [1] "abyze"
str_dup створює дублікати, а str_unique прибирає дублікати.
Типово str_dup не має роздільника, але його можна вказати.
str_dup("xyz", 4, sep = ", ")
#> [1] "xyz, xyz, xyz, xyz"
str_unique - розширення unique().
Обидві прибирають повторювані елементи з вектора, але функція з stringr уміє ще й ураховувати регістр і різні локальні зіставлення.
inp <- c("some", "strings", "Some", "Strings")
str_unique(inp)
#> [1] "some" "strings" "Some" "Strings"
str_unique(inp, ignore_case = TRUE)
#> [1] "some" "strings"
Бібліотека stringr ґрунтується на бібліотеці stringi із функцій нижчого рівня.
Якщо є конкретні потреби, не розглянуті вище, тоді: