String у Julia - це впорядкована послідовність символів. Як зазначає посібник: «Звісно, справжні труднощі починаються тоді, коли постає питання, що таке символ».
Багато з цієї складності ми докладніше розглянемо в концепції Chars, а поки що коротко:
Загалом рядки тексту беруть у подвійні лапки " ".
Одинарні лапки ' ' використовують лише для Chars.
Також можна використовувати потрійні подвійні лапки """ """, що дає змогу вживати " всередині рядка без екранування.
Що важливіше, у багаторядкових рядках прибирають зайвий відступ.
julia> """
Multiline
String
"""
"Multiline\nString\n"
Як і більшість мов, починаючи з C, Julia використовує зворотну скісну риску \ для екранування:
\n у попередньому прикладі.\$ (див. нижче).Рядок тексту - це колекція, яку можна перебирати, тож цикл for ... in працюватиме без потреби перетворювати його на вектор символів.
julia> s = "Julia"
"Julia"
# admittedly, this is a silly example
julia> for c in s; print(c, "; "); end
J; u; l; i; a;
Такий перебір має ту перевагу, що Julia правильно визначає межі символів, навіть для не-ASCII рядків. Значення цього стане зрозумілішим у наступному розділі.
У принципі, рядки тексту можна індексувати точно так само, як вектори:
julia> s[1], s[5]
('J', 'a')
Це, звісно, працює для ASCII-рядка на кшталт «Julia», але так ми нехтуємо більшістю мов світу.
Погляньмо на один із символів у творі Beowulf, написаному давньоанглійською близько 15 століть тому.
«Hrōðgār» - це явно 7 символів, але не всі з них належать до ASCII.
julia> king = "Hrōðgār"
"Hrōðgār"
julia> length(king)
7
julia> isascii(king)
false
# collect() converts to a Char vector:
julia> collect(king)
7-element Vector{Char}:
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
'ō': Unicode U+014D (category Ll: Letter, lowercase)
'ð': Unicode U+00F0 (category Ll: Letter, lowercase)
'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
'ā': Unicode U+0101 (category Ll: Letter, lowercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
Індексування такого рядка починається добре, але згодом ламається:
julia> king[3]
'ō': Unicode U+014D (category Ll: Letter, lowercase)
julia> king[4]
ERROR: StringIndexError: invalid index [4], valid nearby indices [3]=>'ō', [5]=>'ð'
Проблема в тому, що таке індексування рахує байти, які в не-ASCII рядках не мають співвідношення 1:1 із символами.
Символ Unicode/UTF-8 може потребувати до 4 байтів для свого подання, і концепція Chars розгляне це докладніше.
Багато мов використовують + як оператор конкатенації рядків, але не Julia.
Принаймні повідомлення про помилку корисне.
julia> "s1 " + "s2"
ERROR: MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with *
julia> "s1 " * "s2"
"s1 s2"
Функція string() зʼєднає довільну кількість рядків.
julia> string("lots ", "of ", "bits")
"lots of bits"
Функція join() зробить те саме, а також візьме вектор рядків і зʼєднає їх заданим роздільником (із типовим значенням "").
julia> join(["lots", "of", "bits"], "_")
"lots_of_bits"
Зауважмо, що рядки тексту незмінні, а конкатенація передбачає копіювання. Тож повторювані конкатенації всередині циклу неефективні, і краще зібрати фрагменти у вектор, а потім зʼєднати їх усі наприкінці.
Просто:
julia> repeat("Abc", 5)
"AbcAbcAbcAbcAbc"
У багатьох мовах є спосіб вставляти обчислені значення в рядки, хоч щодо найкращого синтаксису для цього точаться неабиякі суперечки.
У Julia перед інтерпольованим значенням стоїть $.
julia> myvar = 42
42
julia> "myvar evaluates to $myvar"
"myvar evaluates to 42"
julia> r = 5.3
5.3
julia> "A circle of radius $r has area $(π * r^2)"
"A circle of radius 5.3 has area 88.24733763933729"
Дужки потрібні, коли їх пропуск створив би неоднозначність, але необовʼязкові для одного ідентифікатора, за яким іде пробіл.
Щоб уставити $ у рядок, його екранують як \$.
Зауважмо на обмеження: інтерполяція наразі не дає змоги форматувати, наприклад, кількість десяткових знаків для показу.
Обхідні шляхи такі:
round().@sprintf() описано нижче.@sprintf()Для тоншого контролю над складанням рядків Julia запозичує функцію sprintf із C (і з кількох пізніших мов: Вікіпедія перелічує близько 30).
У Julia це реалізовано як macro у модулі Printf, звідси префікс @.
julia> using Printf
julia> r = 5.3
5.3
julia> @sprintf("A circle of radius %.1f has area %.2f", r, π * r^2)
"A circle of radius 5.3 has area 88.25"
Для простоти цей розділ зосередиться на функціях, які повертають новий рядок і не змінюють вхідні дані.
Багато з них мають відповідник із суфіксом !, який змінює вхідні дані на місці.
Яка довжина рядка тексту? Іноді все залежить від того, як ми рахуємо.
Найпростіший випадок - ASCII-рядки, де кожен символ займає 1 байт.
julia> string_asc = "Julia"
"Julia"
julia> length(string_asc)
5
julia> sizeof(string_asc)
5
Функція length() повертає кількість символів, а функція sizeof() - кількість байтів.
Ця відмінність стає важливою для інших наборів символів:
julia> king = "Hrōðgār"
"Hrōðgār"
julia> length(king)
7
julia> sizeof(king)
10
Поширена операція в програмуванні - це взяти довгий рядок і розбити його на частини за рядком-роздільником з одного або кількох символів.
Найзагальніше ми використовуємо функцію split().
Типовим роздільником є (будь-який) пробільний символ, але можна вказати й інші.
julia> split("my little string")
3-element Vector{SubString{String}}:
"my"
"little"
"string"
julia> split("My_snake_case_string", "_")
4-element Vector{SubString{String}}:
"My"
"snake"
"case"
"string"
Ці приклади розбивають рядок на менші рядки тексту. Розбиття рядка на символи - це інша операція.
julia> collect("input string")
12-element Vector{Char}:
'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
'p': ASCII/Unicode U+0070 (category Ll: Letter, lowercase)
'u': ASCII/Unicode U+0075 (category Ll: Letter, lowercase)
't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
's': ASCII/Unicode U+0073 (category Ll: Letter, lowercase)
't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
Чи містить рядок заданий підрядок?
Як не дивно, для цього є дві функції: occursin() і contains() відрізняються лише порядком своїх аргументів.
julia> needle = "Julia"
"Julia"
julia> haystack = "Python, Julia and R"
"Python, Julia and R"
julia> occursin(needle, haystack)
true
julia> contains(haystack, needle)
true
Щоб точніше визначити позицію:
julia> startswith(haystack, "Python")
true
julia> endswith(haystack, "Python")
false
Щоб отримати індекси початку й кінця підрядка, у нас є кілька функцій:
julia> findfirst(needle, haystack)
9:13
Щоб замінити підрядки, перелічуємо зміни синтаксисом x => y.
julia> replace("abc", "a"=>"b", "b"=>"c", "c"=>"a")
"bca"
Починаючи з Julia 1.7, функція replace може приймати довільну кількість змін, із гарантією, що жоден символ не буде змінено більше ніж один раз під час заміни.
Синтаксис x => y називають pair; це важливий тип у Julia, про який докладніше йдеться в концепції Dicts and Pairs.
Зауваження: для простоти всі приклади вище використовують рядкові літерали. Багато з цих функцій загальніші й працюватимуть також із регулярними виразами. Ми повернемося до цього в одній із наступних концепцій.
Часто нам потрібно прибрати провідні й кінцеві пробільні символи з рядка.
Для цього є функції lstrip() (зліва), rstrip() (справа), strip() (з обох боків).
julia> strip("\n\t useful_bit\n\n")
"useful_bit"
Назви цих функцій досить промовисті:
julia> s = repeat("aBcD ", 5)
"aBcD aBcD aBcD aBcD aBcD "
julia> uppercase(s)
"ABCD ABCD ABCD ABCD ABCD "
julia> lowercase(s)
"abcd abcd abcd abcd abcd "
julia> titlecase(s) # Initial letter of each word uppercase
"Abcd Abcd Abcd Abcd Abcd "
Взаємне перетворення чисел та їхнього рядкового подання часто буває корисним.
Перетворити число на рядок просто, хоч основа, відмінна від типової 10, потребує додаткового параметра:
julia> string(42)
"42"
julia> string(42, base=16)
"2a"
julia> string(42, base=2)
"101010"
Розбір рядка для отримання числового значення трохи складніший і має більше можливостей для помилки. Указати цільовий числовий тип обовʼязково. Указувати основу необовʼязково, а її типове значення - 10.
julia> parse(Int, "42")
42
julia> parse(Float64, "42")
42.0
julia> parse(Int, "42?")
ERROR: ArgumentError: invalid base 10 digit '?' in "42?"
julia> parse(Int, "42 ") # whitespace may be tolerated
42
Іноді корисно вимкнути звичайну інтерполяцію та екранування всередині рядків.
Для цього перед відкривною лапкою ставлять raw.
julia> x = 10
10
julia> "x = $x if we use \$ for interpolation"
"x = 10 if we use \$ for interpolation"
julia> raw"x = $x if we use \$ for interpolation"
"x = \$x if we use \\\$ for interpolation"
Регулярні вирази мають власний складний синтаксис, і Julia надає два корисні типи рядків: r" " для Regex і s" " для роботи із захопленими фрагментами.
Різні інші особливі рядки рідше трапляються в програмуванні у стилі Exercism, хоч і широко використовуються під час створення бібліотек.
v"x.y.z" описують нумерацію major.minor.patch для релізів програмного забезпечення.b" " обходять межі символів Unicode і працюють на рівні байтів.