Spec-Zone.ru › Julia 1.7

Строки

Строки — это конечные последовательности символов. Конечно, настоящие проблемы возникают, когда задается вопрос о том, что такое символ. Символы, с которыми знакомы говорящие по-английски, — это буквы A, B, C, и так далее, вместе с цифрами и общеупотребительными знаками препинания. Эти символы стандартизованы вместе с сопоставлением целым значениям от 0 до 127 стандартом ASCII. Конечно, существует множество других символов, используемых в языках, отличных от английского, включая варианты символов ASCII с диакритическими знаками и другими модификациями, связанные письмена, такие как кириллица и греческий алфавит, и письмена, совершенно не связанные с ASCII и английским, включая арабский, китайский, иврит, хинди, японский и корейский. Стандарт Unicode решает сложности определения символа, и он, как правило, признан окончательным стандартом, решающим эту проблему. В зависимости от ваших потребностей, вы можете либо полностью игнорировать эти сложности и просто предположить, что существуют только символы ASCII, либо вы можете написать код, который может обрабатывать любые символы или кодировки, которые могут встретиться при работе с текстом, отличным от ASCII. Julia делает работу с обычным текстом ASCII простой и эффективной, а обработка Unicode — максимально простой и эффективной. В частности, вы можете написать код для обработки строк в стиле C, и он будет работать как ожидается, как с точки зрения производительности, так и с точки зрения семантики. Если такой код столкнется с текстом, отличным от ASCII, он изящно завершится с ясным сообщением об ошибке, а не тихо приведет к искажённым результатам. В этом случае изменение кода для обработки данных, отличных от ASCII, будет простым.

Есть несколько примечательных функций высокого уровня в строках Julia:

  • Встроенный конкретный тип, используемый для строк (и строковых литералов) в Julia, — это String. Он поддерживает весь диапазон символов Unicode через кодировку UTF-8. (Предоставляется функция transcode для преобразования в/из других кодировок Unicode.)
  • Все типы строк являются подтипами абстрактного типа AbstractString, а внешние пакеты определяют дополнительные AbstractString подтипы (например, для других кодировок). Если вы определяете функцию, ожидающую строковый аргумент, вы должны объявить тип как AbstractString для принятия любого типа строки.
  • Как в C и Java, но в отличие от большинства динамических языков, в Julia существует базовый тип для представления одного символа, называемый AbstractChar. Встроенный подтип Char типа AbstractChar — это 32-битный примитивный тип, который может представлять любой символ Unicode (и который основан на кодировке UTF-8).
  • Как и в Java, строки являются неизменяемыми: значение объекта AbstractString изменить нельзя. Чтобы создать другое строковое значение, вы создаёте новую строку из частей других строк.
  • Концептуально, строка — это частичная функция от индексов к символам: для некоторых значений индексов не возвращается значение символа, а вместо этого выбрасывается исключение. Это позволяет эффективно индексировать строки по байтовому индексу кодированного представления, а не по индексу символа, что невозможно реализовать как эффективно, так и просто для кодировок с переменной длиной строк Unicode.

Символы

Значение Char представляет собой отдельный символ: это просто 32-битный примитивный тип со специальным представлением литералов и соответствующим поведением арифметических операций, который может быть преобразован в числовое значение, представляющее код символа Unicode. (Пакеты Julia могут определять другие подтипы AbstractChar, например, для оптимизации операций для других кодировок символов.) Вот как вводятся и отображаются значения Char.

julia> c = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> typeof(c)
Char

Вы можете легко преобразовать Char в его целое значение, т. е. код символа:

julia> c = Int('x')
120

julia> typeof(c)
Int64

На 32-битных архитектурах, typeof(c) будет Int32. Вы можете так же легко преобразовать целое значение обратно в Char:

julia> Char(120)
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

Не все целые значения являются допустимыми кодами Unicode, но для повышения производительности преобразование Char не проверяет, что каждое значение символа является допустимым. Если вы хотите проверить, что каждое преобразованное значение является допустимым кодом символа, используйте функцию isvalid:

julia> Char(0x110000)
'\U110000': Unicode U+110000 (category In: Invalid, too high)

julia> isvalid(Char, 0x110000)
false

На момент написания этой документации допустимыми кодами Unicode являются U+0000 по U+D7FF и U+E000 по U+10FFFF. Всем из них ещё не присвоено понятное значение, и приложения не обязательно интерпретируют их, но все эти значения считаются допустимыми символами Unicode.

Вы можете ввести любой символ Unicode в одинарных кавычках, используя \u за которым следуют до четырёх шестнадцатеричных цифр или \U за которым следуют до восьми шестнадцатеричных цифр (наиболее длинное допустимое значение требует только шести):

julia> '\u0'
'\0': ASCII/Unicode U+0000 (category Cc: Other, control)

julia> '\u78'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> '\u2200'
'∀': Unicode U+2200 (category Sm: Symbol, math)

julia> '\U10ffff'
'\U10ffff': Unicode U+10FFFF (category Cn: Other, not assigned)

Julia использует настройки локальной и языковой среды вашей системы для определения того, какие символы могут быть напечатаны как есть, а какие должны быть выведены с использованием универсальных, экранированных \u или \U форм ввода. В дополнение к этим формам экранирования Unicode могут также использоваться все традиционные формы экранирования ввода языка C .

julia> Int('\0')
0

julia> Int('\t')
9

julia> Int('\n')
10

julia> Int('\e')
27

julia> Int('\x7f')
127

julia> Int('\177')
127

Вы можете выполнять сравнения и ограниченные арифметические операции со значениями Char:

julia> 'A' < 'a'
true

julia> 'A' <= 'a' <= 'Z'
false

julia> 'A' <= 'X' <= 'Z'
true

julia> 'x' - 'a'
23

julia> 'A' + 1
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)

Основы строк

Строковые литералы заключены в двойные кавычки или тройные двойные кавычки:

julia> str = "Hello, world.\n"
"Hello, world.\n"

julia> """Contains "quote" characters"""
"Contains \"quote\" characters"

Длинные строки в строках могут быть разделены путём предшествования новой строки обратной косой чертой (\):

julia> "This is a long \
       line"
"This is a long line"

Если вы хотите извлечь символ из строки, вы индексируете её:

julia> str[begin]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

julia> str[1]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

julia> str[6]
',': ASCII/Unicode U+002C (category Po: Punctuation, other)

julia> str[end]
'\n': ASCII/Unicode U+000A (category Cc: Other, control)

Многие объекты Julia, включая строки, могут быть индексированы целыми числами. Индекс первого элемента (первого символа строки) возвращается функцией firstindex(str), а индекс последнего элемента (символа) — функцией lastindex(str). Ключевые слова begin и end могут использоваться внутри операции индексирования в качестве сокращения для первого и последнего индексов соответственно вдоль заданного измерения. Индексирование строк, как и большинство индексирования в Julia, основано на 1: firstindex всегда возвращает 1 для любого AbstractString. Однако, как мы увидим ниже, lastindex(str) в общем случае не то же самое, что length(str) для строки, так как некоторые символы Unicode могут занимать несколько «единиц кода».

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

julia> str[end-1]
'.': ASCII/Unicode U+002E (category Po: Punctuation, other)

julia> str[end÷2]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

Использование индекса меньше, чем begin (1) или больше, чем end вызывает ошибку:

julia> str[begin-1]
ERROR: BoundsError: attempt to access 14-codeunit String at index [0]
[...]

julia> str[end+1]
ERROR: BoundsError: attempt to access 14-codeunit String at index [15]
[...]

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

julia> str[4:9]
"lo, wo"

Обратите внимание, что выражения str[k] и str[k:k] не дают одинакового результата:

julia> str[6]
',': ASCII/Unicode U+002C (category Po: Punctuation, other)

julia> str[6:6]
","

Первое — это одиночное значение символа типа Char, а второе — строковое значение, которое, случаем, содержит только один символ. В Julia это разные вещи.

Индексирование диапазоном создаёт копию выбранной части исходной строки. В качестве альтернативы, возможно создать представление строки с помощью типа SubString, например:

julia> str = "long string"
"long string"

julia> substr = SubString(str, 1, 4)
"long"

julia> typeof(substr)
SubString{String}

Некоторые стандартные функции, такие как chop, chomp или strip возвращают SubString.

Unicode и UTF-8

Julia полностью поддерживает символы и строки Unicode. Как обсуждалось выше, в литералах символов коды Unicode могут быть представлены с помощью последовательностей экранирования Unicode \u и \U, а также всех стандартных последовательностей экранирования языка C. Они также могут использоваться для написания строковых литералов:

julia> s = "\u2200 x \u2203 y"
"∀ x ∃ y"

Будут ли эти символы Unicode отображаться как экранированные или как специальные символы, зависит от настроек локальной среды вашего терминала и его поддержки Unicode. Строковые литералы кодируются с использованием кодировки UTF-8. UTF-8 — это кодировка с переменной длиной, что означает, что не все символы кодируются одинаковым количеством байтов («единиц кода»). В UTF-8 символы ASCII — то есть те, у которых коды символов меньше 0x80 (128) — кодируются так же, как в ASCII, с использованием одного байта, а коды символов 0x80 и выше кодируются с использованием нескольких байтов — до четырёх на один символ.

Индексы строк в Julia относятся к единицам кода (= байтам для UTF-8), блокам с фиксированной шириной, которые используются для кодирования произвольных символов (кодов символов). Это означает, что не каждый индекс в String обязательно является допустимым индексом для символа. Если вы индексируете строку по такому недопустимому байтовому индексу, будет выброшена ошибка:

julia> s[1]
'∀': Unicode U+2200 (category Sm: Symbol, math)

julia> s[2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[3]
ERROR: StringIndexError: invalid index [3], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[4]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

В этом случае символ ∀ — символ из трёх байтов, поэтому индексы 2 и 3 недопустимы, а индекс следующего символа равен 4; этот следующий допустимый индекс можно вычислить с помощью nextind(s,1), а следующий индекс после этого — с помощью nextind(s,4) и так далее.

Поскольку end всегда является последним допустимым индексом в коллекции, end-1 ссылается на недопустимый байтовый индекс, если предпоследний символ — многобайтовый.

julia> s[end-1]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

julia> s[end-2]
ERROR: StringIndexError: invalid index [9], valid nearby indices [7]=>'∃', [10]=>' '
Stacktrace:
[...]

julia> s[prevind(s, end, 2)]
'∃': Unicode U+2203 (category Sm: Symbol, math)

Первый случай работает, потому что последний символ y и пробел — символы по одному байту, а end-2 индексирует середину ∃ многобайтового представления. Правильный способ в этом случае — использовать prevind(s, lastindex(s), 2) или, если вы используете это значение для индексирования s, вы можете написать s[prevind(s, end, 2)] и end расширяется до lastindex(s).

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

julia> s[1:1]
"∀"

julia> s[1:2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[1:4]
"∀ "

Из-за кодировок переменной длины количество символов в строке (указанное функцией length(s)) не всегда совпадает с последним индексом. Если вы итерируетесь по индексам от 1 до lastindex(s) и обращаетесь к s, то последовательность символов, возвращаемая без исключений, — это последовательность символов, составляющих строку s. Таким образом length(s) <= lastindex(s), так как каждый символ в строке должен иметь свой собственный индекс. Ниже приведён неэффективный и громоздкий способ итерирования по символам строки s:

julia> for i = firstindex(s):lastindex(s)
           try
               println(s[i])
           catch
               # ignore the index error
           end
       end
∀

x

∃

y

Пустые строки на самом деле содержат пробелы. К счастью, приведенный выше неуклюжий способ излишний для итерирования по символам строки, так как вы можете использовать строку как итерируемый объект, без обработки исключений:

julia> for c in s
           println(c)
       end
∀

x

∃

y

Если вам нужно получить допустимые индексы для строки, вы можете использовать функции nextind и prevind для инкремента/декремента до следующего/предыдущего допустимого индекса, как указано выше. Вы также можете использовать функцию eachindex для итерирования по допустимым индексам символов:

julia> collect(eachindex(s))
7-element Vector{Int64}:
  1
  4
  5
  6
  7
 10
 11

Для доступа к исходным кодовым единицам (байтам для UTF-8) кодирования можно использовать функцию codeunit(s,i), где индекс i последовательно изменяется от 1 до ncodeunits(s). Функция codeunits(s) возвращает обёртку AbstractVector{UInt8}, которая позволяет получить доступ к этим исходным кодовым единицам (байтам) как к массиву.

Строки в Julia могут содержать недопустимые последовательности кодовых единиц UTF-8. Эта соглашение позволяет рассматривать любую последовательность байтов как String. В таких ситуациях правило состоит в том, что при разборе последовательности кодовых единиц слева направо символы образуются самой длинной последовательностью 8-битных кодовых единиц, которая соответствует началу одного из следующих битовых шаблонов (каждый x может быть 0 или 1):

  • 0xxxxxxx;
  • 110xxxxx 10xxxxxx;
  • 1110xxxx 10xxxxxx 10xxxxxx;
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx;
  • 10xxxxxx;
  • 11111xxx.

В частности, это означает, что избыточные и слишком высокие последовательности кодовых единиц и их префиксы обрабатываются как один неверный символ, а не как несколько неверных символов. Это правило лучше всего объяснить на примере:

julia> s = "\xc0\xa0\xe2\x88\xe2|"
"\xc0\xa0\xe2\x88\xe2|"

julia> foreach(display, s)
'\xc0\xa0': [overlong] ASCII/Unicode U+0020 (category Zs: Separator, space)
'\xe2\x88': Malformed UTF-8 (category Ma: Malformed, bad data)
'\xe2': Malformed UTF-8 (category Ma: Malformed, bad data)
'|': ASCII/Unicode U+007C (category Sm: Symbol, math)

julia> isvalid.(collect(s))
4-element BitArray{1}:
 0
 0
 0
 1

julia> s2 = "\xf7\xbf\xbf\xbf"
"\U1fffff"

julia> foreach(display, s2)
'\U1fffff': Unicode U+1FFFFF (category In: Invalid, too high)

Мы видим, что первые две кодовые единицы в строке s образуют избыточное кодирование пробела. Оно неверно, но принимается в строке как один символ. Следующие две кодовые единицы образуют действительное начало последовательности из трёх байтов UTF-8. Однако пятая кодовая единица \xe2 не является её действительным продолжением. Следовательно, кодовые единицы 3 и 4 также интерпретируются как неверные символы в этой строке. Аналогично кодовая единица 5 образует неверный символ, потому что | не является действительным продолжением для неё. Наконец, строка s2 содержит слишком большой код символа.

Julia по умолчанию использует кодировку UTF-8, а поддержку новых кодировок можно добавить с помощью пакетов. Например, пакет LegacyStrings.jl реализует типы UTF16String и UTF32String. Дополнительное обсуждение других кодировок и способы реализации поддержки для них выходит за рамки данной документации на текущий момент. Для дальнейшего обсуждения проблем кодирования UTF-8 см. раздел ниже о литералах массивов байтов. Функция transcode предназначена для преобразования данных между различными кодировками UTF-xx, в основном для работы с внешними данными и библиотеками.

Конкатенация

Одной из наиболее распространённых и полезных операций со строками является конкатенация:

julia> greet = "Hello"
"Hello"

julia> whom = "world"
"world"

julia> string(greet, ", ", whom, ".\n")
"Hello, world.\n"

Важно помнить о потенциально опасных ситуациях, таких как конкатенация недопустимых строк UTF-8. Результирующая строка может содержать другие символы, чем входные строки, и её количество символов может быть меньше суммы количества символов конкатенированных строк, например:

julia> a, b = "\xe2\x88", "\x80"
("\xe2\x88", "\x80")

julia> c = a*b
"∀"

julia> collect.([a, b, c])
3-element Array{Array{Char,1},1}:
 ['\xe2\x88']
 ['\x80']
 ['∀']

julia> length.([a, b, c])
3-element Array{Int64,1}:
 1
 1
 1

Эта ситуация может возникнуть только для недопустимых строк UTF-8. Для допустимых строк UTF-8 конкатенация сохраняет все символы в строках и сохраняет аддитивность длин строк.

Julia также предоставляет * для конкатенации строк:

julia> greet * ", " * whom * ".\n"
"Hello, world.\n"

Хотя * может показаться неожиданным выбором для пользователей языков, которые предоставляют + для конкатенации строк, такое использование * имеет прецедент в математике, особенно в абстрактной алгебре.

В математике + обычно обозначает коммутативную операцию, где порядок операндов не имеет значения. Примером этого является сложение матриц, где A + B == B + A для любых матриц A и B одинаковой формы. В противоположность этому, * обычно обозначает некоммутативную операцию, где порядок операндов имеет значение. Примером этого является умножение матриц, где вообще A * B != B * A. Как и в случае с умножением матриц, конкатенация строк некоммутативна: greet * whom != whom * greet. Поэтому * является более естественным выбором для инфиксного оператора конкатенации строк, соответствующим общепринятому математическому использованию.

Более точно, множество всех конечных строк S вместе с оператором конкатенации строк * образует свободную полугруппу (S, *). Единичным элементом этого множества является пустая строка "". В тех случаях, когда свободная полугруппа не коммутативна, операция обычно обозначается как \cdot, *, или подобный символ, а не +, который, как уже говорилось, обычно подразумевает коммутативность.

Интерполяция строк

Создание строк с помощью конкатенации может стать довольно утомительным. Чтобы уменьшить необходимость этих многословных вызовов string или многократных умножений, Julia позволяет вставлять значения в строковые литералы с использованием $, как в Perl:

julia> "$greet, $whom.\n"
"Hello, world.\n"

Это более удобочитаемо и удобно и эквивалентно приведенной выше конкатенации строк — система переписывает этот, казалось бы, единственный строковый литерал в вызов string(greet, ", ", whom, ".\n").

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

julia> "1 + 2 = $(1 + 2)"
"1 + 2 = 3"

Как конкатенация, так и интерполяция строк вызывают string для преобразования объектов в строковую форму. Однако string на самом деле просто возвращает вывод print, поэтому новые типы должны добавлять методы к print или show вместо string.

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

julia> v = [1,2,3]
3-element Vector{Int64}:
 1
 2
 3

julia> "v: $v"
"v: [1, 2, 3]"

string является тождеством для AbstractString и AbstractChar значений, поэтому они вставляются в строки как есть, без кавычек и экранирования:

julia> c = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> "hi, $c"
"hi, x"

Чтобы включить литеральный $ в строковый литерал, экранируйте его обратной косой чертой:

julia> print("I have \$100 in my account.\n")
I have $100 in my account.

Строковые литералы с тройными кавычками

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

Во-первых, строки с тройными кавычками также отступы до уровня самой левой строки. Это полезно для определения строк внутри кода, который отступает. Например:

julia> str = """
           Hello,
           world.
         """
"  Hello,\n  world.\n"

В этом случае последняя (пустая) строка перед закрывающей """ устанавливает уровень отступа.

Уровень отступа определяется как самая длинная общая начальная последовательность пробелов или табуляций во всех строках, исключая строку, следующую за открывающей """ и строки, содержащие только пробелы или табуляции (строка, содержащая закрывающую """ всегда включается). Затем для всех строк, исключая текст после открывающей """, общая начальная последовательность удаляется (включая строки, содержащие только пробелы и табуляции, если они начинаются с этой последовательности), например:

julia> """    This
         is
           a test"""
"    This\nis\n  a test"

Далее, если открывающая """ следует за новой строкой, новая строка удаляется из результирующей строки.

"""hello"""

эквивалентно

"""
hello"""

но

"""

hello"""

будет содержать литеральную новую строку в начале.

Удаление новой строки выполняется после отступа. Например:

julia> """
         Hello,
         world."""
"Hello,\nworld."

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

julia> """
         Averylong\
         word"""
"Averylongword"

Конечные пробелы остаются неизменными.

Строковые литералы с тройными кавычками могут содержать " символы без экранирования.

Обратите внимание, что символы перевода строки в строковых литералах, как с одинарными, так и с тройными кавычками, приводят к символу перевода строки (LF) \n в строке, даже если ваш редактор использует комбинацию возврата каретки \r (CR) или CRLF для завершения строк. Чтобы включить CR в строку, используйте явное экранирование \r; например, вы можете ввести литеральную строку "a CRLF line ending\r\n".

Общие операции

Вы можете лексикографически сравнивать строки с помощью стандартных операторов сравнения:

julia> "abracadabra" < "xylophone"
true

julia> "abracadabra" == "xylophone"
false

julia> "Hello, world." != "Goodbye, world."
true

julia> "1 + 2 = 3" == "1 + 2 = $(1 + 2)"
true

Вы можете искать индекс конкретного символа с помощью функций findfirst и findlast:

julia> findfirst(isequal('o'), "xylophone")
4

julia> findlast(isequal('o'), "xylophone")
7

julia> findfirst(isequal('z'), "xylophone")

Вы можете начать поиск символа с заданного смещения, используя функции findnext и findprev:

julia> findnext(isequal('o'), "xylophone", 1)
4

julia> findnext(isequal('o'), "xylophone", 5)
7

julia> findprev(isequal('o'), "xylophone", 5)
4

julia> findnext(isequal('o'), "xylophone", 8)

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

julia> occursin("world", "Hello, world.")
true

julia> occursin("o", "Xylophon")
true

julia> occursin("a", "Xylophon")
false

julia> occursin('o', "Xylophon")
true

Последний пример показывает, что occursin также может искать литерал символа.

Ещё две полезные функции для строк — repeat и join:

julia> repeat(".:Z:.", 10)
".:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:."

julia> join(["apples", "bananas", "pineapples"], ", ", " and ")
"apples, bananas and pineapples"

Некоторые другие полезные функции:

  • firstindex(str) возвращает минимальный (байтовый) индекс, который можно использовать для индексирования в str (всегда 1 для строк, но не обязательно для других контейнеров).
  • lastindex(str) возвращает максимальный (байтовый) индекс, который можно использовать для индексирования в str.
  • length(str) количество символов в str.
  • length(str, i, j) количество допустимых индексов символов в str от i до j.
  • ncodeunits(str) количество кодовых единиц в строке.
  • codeunit(str, i) возвращает значение кодовой единицы в строке str по индексу i.
  • thisind(str, i) для заданного произвольного индекса в строке находит первый индекс символа, на который указывает индекс.
  • nextind(str, i, n=1) находит начало n-го символа, начиная после индекса i.
  • prevind(str, i, n=1) находит начало n-го символа, начиная перед индексом i.

Нестандартные строковые литералы

Иногда требуется создать строку или использовать семантику строк, но поведение стандартного способа создания строк не подходит. В таких случаях Julia предоставляет нестандартные строковые литералы. Нестандартный строковый литерал выглядит как обычный строковый литерал в двойных кавычках, но сразу после него идёт идентификатор, и он может вести себя иначе, чем обычный строковый литерал.

Регулярные выражения, литералы массивов байтов и литералы номеров версий, как описано ниже, являются примерами нестандартных строковых литералов. Пользователи и пакеты также могут определять новые нестандартные строковые литералы. Более подробная документация приведена в разделе Метапрограммирование.

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

В Julia используются регулярные выражения (regex) совместимые с Perl, предоставляемые библиотекой PCRE (описание синтаксиса можно найти здесь). Регулярные выражения связаны со строками двумя способами: очевидная связь заключается в том, что регулярные выражения используются для поиска регулярных шаблонов в строках; другая связь заключается в том, что регулярные выражения сами вводятся как строки, которые анализируются в состояние машины, которое можно использовать для эффективного поиска шаблонов в строках. В Julia регулярные выражения вводятся с помощью нестандартных строковых литералов, предваряемых различными идентификаторами, начинающимися с r. Самый базовый литерал регулярного выражения без включённых опций просто использует r"...":

julia> re = r"^\s*(?:#|$)"
r"^\s*(?:#|$)"

julia> typeof(re)
Regex

Для проверки соответствия регулярного выражения строке используйте occursin:

julia> occursin(r"^\s*(?:#|$)", "not a comment")
false

julia> occursin(r"^\s*(?:#|$)", "# a comment")
true

Как можно увидеть здесь, occursin просто возвращает true или false, указывая, есть ли совпадение заданного регулярного выражения в строке. Однако, часто нужно знать не только совпадение, но и как оно произошло. Для захвата этой информации о совпадении используйте функцию match:

julia> match(r"^\s*(?:#|$)", "not a comment")

julia> match(r"^\s*(?:#|$)", "# a comment")
RegexMatch("#")

Если регулярное выражение не соответствует заданной строке, match возвращает nothing — специальное значение, которое ничего не печатает в интерактивном запросе. Помимо того, что оно не печатает, это совершенно обычное значение, и вы можете проверить его программно:

m = match(r"^\s*(?:#|$)", line)
if m === nothing
    println("not a comment")
else
    println("blank or comment")
end

Если регулярное выражение соответствует строке, возвращаемое значение match — это объект RegexMatch. Эти объекты записывают, как выражение совпадает, включая подстроку, к которой соответствует шаблон, и любые захваченные подстроки, если таковые имеются. Этот пример захватывает только часть подстроки, которая соответствует, но, возможно, мы хотим захватить любой непустой текст после символа комментария. Мы можем сделать следующее:

julia> m = match(r"^\s*(?:#\s*(.*?)\s*$|$)", "# a comment ")
RegexMatch("# a comment ", 1="a comment")

При вызове match вы можете указать индекс, с которого следует начать поиск. Например:

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",1)
RegexMatch("1")

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",6)
RegexMatch("2")

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",11)
RegexMatch("3")

Вы можете извлечь следующую информацию из объекта RegexMatch:

  • всю сопоставленную подстроку: m.match
  • захваченные подстроки в виде массива строк: m.captures
  • смещение, с которого начинается всё совпадение: m.offset
  • смещения захваченных подстрок в виде вектора: m.offsets

Если какое-то из совпадений не найдено, вместо подстроки m.captures содержит nothing в этой позиции, и m.offsets имеет смещение ноль (вспомните, что индексы в Julia имеют основание 1, поэтому смещение нуль в строке недопустимо). Вот пара несколько придуманных примеров:

julia> m = match(r"(a|b)(c)?(d)", "acd")
RegexMatch("acd", 1="a", 2="c", 3="d")

julia> m.match
"acd"

julia> m.captures
3-element Vector{Union{Nothing, SubString{String}}}:
 "a"
 "c"
 "d"

julia> m.offset
1

julia> m.offsets
3-element Vector{Int64}:
 1
 2
 3

julia> m = match(r"(a|b)(c)?(d)", "ad")
RegexMatch("ad", 1="a", 2=nothing, 3="d")

julia> m.match
"ad"

julia> m.captures
3-element Vector{Union{Nothing, SubString{String}}}:
 "a"
 nothing
 "d"

julia> m.offset
1

julia> m.offsets
3-element Vector{Int64}:
 1
 0
 2

Удобно, что захваченные подстроки возвращаются в виде массива, так что можно использовать синтаксис деструктуризации для связывания их с локальными переменными. Для удобства объект RegexMatch реализует методы итератора, которые передаются в поле captures , поэтому вы можете деструктурировать объект соответствия непосредственно:

julia> first, second, third = m; first
"a"

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

julia> m=match(r"(?<hour>\d+):(?<minute>\d+)","12:45")
RegexMatch("12:45", hour="12", minute="45")

julia> m[:minute]
"45"

julia> m[2]
"45"

К захваченным подстрокам можно обратиться в строке подстановки, используя replace, используя \n для ссылки на n-ю группу захвата и предваряя строку подстановки префиксом s. Группа захвата 0 ссылается на весь объект соответствия. К именованным группам захвата можно обратиться в строке подстановки с помощью \g<groupname>. Например:

julia> replace("first second", r"(\w+) (?<agroup>\w+)" => s"\g<agroup> \1")
"second first"

К пронумерованным группам захвата также можно обратиться как \g<n> для устранения неоднозначности, как в:

julia> replace("a", r"." => s"\g<0>1")
"a1"

Можно изменить поведение регулярных выражений, используя комбинацию флагов i, m, s, и x после закрывающей двойной кавычки. Эти флаги имеют то же значение, что и в Perl, как описано в этом фрагменте из страницы руководства perlre:

i   Do case-insensitive pattern matching.

    If locale matching rules are in effect, the case map is taken
    from the current locale for code points less than 255, and
    from Unicode rules for larger code points. However, matches
    that would cross the Unicode rules/non-Unicode rules boundary
    (ords 255/256) will not succeed.

m   Treat string as multiple lines.  That is, change "^" and "$"
    from matching the start or end of the string to matching the
    start or end of any line anywhere within the string.

s   Treat string as single line.  That is, change "." to match any
    character whatsoever, even a newline, which normally it would
    not match.

    Used together, as r""ms, they let the "." match any character
    whatsoever, while still allowing "^" and "$" to match,
    respectively, just after and just before newlines within the
    string.

x   Tells the regular expression parser to ignore most whitespace
    that is neither backslashed nor within a character class. You
    can use this to break up your regular expression into
    (slightly) more readable parts. The '#' character is also
    treated as a metacharacter introducing a comment, just as in
    ordinary code.

Например, следующее регулярное выражение имеет все три флага, включённые:

julia> r"a+.*b+.*?d$"ism
r"a+.*b+.*?d$"ims

julia> match(r"a+.*b+.*?d$"ism, "Goodbye,\nOh, angry,\nBad world\n")
RegexMatch("angry,\nBad world")

Литерал r"..." создаётся без интерполяции и раскомментирования (за исключением кавычек " , которые всё ещё нужно экранировать). Вот пример, демонстрирующий разницу с обычными строковыми литералами:

julia> x = 10
10

julia> r"$x"
r"$x"

julia> "$x"
"10"

julia> r"\x"
r"\x"

julia> "\x"
ERROR: syntax: invalid escape sequence

Также поддерживаются тройные кавычки для регулярных выражений в формате r"""...""", которые могут быть удобны для регулярных выражений, содержащих кавычки или символы новой строки.

Конструктор Regex() может использоваться для создания корректной строки регулярного выражения программно. Это позволяет использовать содержимое строковых переменных и другие строковые операции при создании строки регулярного выражения. Любые из вышеперечисленных кодов регулярных выражений могут быть использованы в единственном строковом аргументе для Regex(). Вот несколько примеров:

julia> using Dates

julia> d = Date(1962,7,10)
1962-07-10

julia> regex_d = Regex("Day " * string(day(d)))
r"Day 10"

julia> match(regex_d, "It happened on Day 10")
RegexMatch("Day 10")

julia> name = "Jon"
"Jon"

julia> regex_name = Regex("[\"( ]\\Q$name\\E[\") ]")  # interpolate value of name
r"[\"( ]\QJon\E[\") ]"

julia> match(regex_name, " Jon ")
RegexMatch(" Jon ")

julia> match(regex_name, "[Jon]") === nothing
true

Обратите внимание на использование последовательности экранирования \Q...\E . Все символы между \Q и \E интерпретируются как буквальные символы (после интерполяции строк). Эта последовательность экранирования может быть полезна при интерполяции, возможно, вредоносного, пользовательского ввода.

Литералы массивов байтов

Ещё один полезный нестандартный строковый литерал — это литерал массива байтов: b"...". Этот формат позволяет использовать синтаксис строк для выражения постоянных массивов байтов — т.е. массивов значений UInt8. Тип этих объектов — CodeUnits{UInt8, String}. Правила для литералов массивов байтов следующие:

  • Символы ASCII и ASCII-экранирования дают один байт.
  • \x и восьмеричные последовательности экранирования дают байт, соответствующий значению экранирования.
  • Последовательности экранирования Unicode дают последовательность байтов, кодирующих этот код символа в UTF-8.

Существует некоторое пересечение между этими правилами, так как поведение \x и восьмеричных экранирований меньше 0x80 (128) описывается и первым, и вторым правилом, но здесь эти правила согласуются. Вместе эти правила позволяют легко использовать символы ASCII, произвольные значения байтов и последовательности UTF-8 для создания массивов байтов. Вот пример, использующий все три:

julia> b"DATA\xff\u2200"
8-element Base.CodeUnits{UInt8, String}:
 0x44
 0x41
 0x54
 0x41
 0xff
 0xe2
 0x88
 0x80

Строка ASCII "DATA" соответствует байтам 68, 65, 84, 65. \xff даёт единственный байт 255. Последовательность экранирования Unicode \u2200 кодируется в UTF-8 как три байта 226, 136, 128. Обратите внимание, что полученный массив байтов не соответствует корректной строке UTF-8:

julia> isvalid("DATA\xff\u2200")
false

Как уже упоминалось, тип CodeUnits{UInt8, String} ведёт себя как массив UInt8, и если вам нужен стандартный вектор, вы можете преобразовать его, используя Vector{UInt8}:

julia> x = b"123"
3-element Base.CodeUnits{UInt8, String}:
 0x31
 0x32
 0x33

julia> x[1]
0x31

julia> x[1] = 0x32
ERROR: setindex! not defined for Base.CodeUnits{UInt8, String}
[...]

julia> Vector{UInt8}(x)
3-element Vector{UInt8}:
 0x31
 0x32
 0x33

Также обратите внимание на существенную разницу между \xff и \uff: первая последовательность экранирования кодирует байт 255, в то время как последняя последовательность экранирования представляет код символа 255, который кодируется двумя байтами в UTF-8:

julia> b"\xff"
1-element Base.CodeUnits{UInt8, String}:
 0xff

julia> b"\uff"
2-element Base.CodeUnits{UInt8, String}:
 0xc3
 0xbf

Литералы символов используют такое же поведение.

Для кодовых точек меньше чем \u80, происходит так, что кодировка UTF-8 каждой кодовой точки — это просто один байт, полученный соответствующим \x экранированием, поэтому различием можно пренебречь. Однако для экранирований \x80 по сравнению с \u80, существует существенное различие: первые экранирования кодируют только один байт, который — если за ним не следуют очень специфические продолжения байты — не образуют корректные данные UTF-8, тогда как последние экранирования представляют все кодовые точки Юникода с двухбайтовой кодировкой.

Если это всё чрезвычайно запутанно, попробуйте прочитать «Абсолютный минимум, который каждый разработчик ПО должен знать об Юникоде и наборах символов». Это отличное введение в Юникод и UTF-8 и может помочь снять некоторую путаницу по этому вопросу.

Литералы номеров версий

Номера версий легко выражаются с помощью нестандартных строковых литералов вида v"...". Литералы номеров версий создают объекты VersionNumber, которые следуют спецификациям семантического управления версиями, и, следовательно, состоят из основных, второстепенных и исправленных числовых значений, а также предварительных и сборных альфа-числовых аннотаций. Например, v"0.2.1-rc1+win64" разбивается на основную версию 0, второстепенную версию 2, исправленную версию 1, предварительную версию rc1 и сборку win64. При вводе литерала версии все, кроме основного номера версии, являются необязательными, поэтому, например, v"0.2" эквивалентно v"0.2.0" (с пустыми предварительными/сборочными аннотациями), v"2" эквивалентно v"2.0.0" и так далее.

Объекты VersionNumber в основном полезны для простого и правильного сравнения двух (или более) версий. Например, константа VERSION содержит номер версии Julia в виде объекта VersionNumber, и поэтому можно определить некоторые зависящие от версии поведения с помощью простых утверждений, как:

if v"0.2" <= VERSION < v"0.3-"
    # do something specific to 0.2 release series
end

Обратите внимание, что в приведенном выше примере используется нестандартный номер версии v"0.3-" с последующим -: эта запись является расширением Julia стандартной записи и используется для указания версии, которая ниже любой 0.3 версии, включая все ее предварительные версии. Таким образом, в приведенном выше примере код будет выполняться только с стабильными 0.2 версиями и исключит такие версии, как v"0.3.0-rc1". Для того, чтобы также разрешить нестабильные (т. е. предварительные) 0.2 версии, проверка нижней границы должна быть изменена следующим образом: v"0.2-" <= VERSION.

Другое нестандартное расширение спецификации версий позволяет использовать последующий + для выражения верхней границы версий сборки, например, VERSION > v"0.2-rc1+" можно использовать для обозначения любой версии, большей чем 0.2-rc1 и любой из ее сборок: она вернет false для версии v"0.2-rc1+win64" и true для v"0.2-rc2".

Использование таких специальных версий в сравнениях является хорошей практикой (особенно, последующий - всегда должен использоваться в верхних границах, если нет веских причин этого не делать), но их нельзя использовать в качестве фактического номера версии чего-либо, поскольку они недействительны в схеме семантического управления версиями.

Помимо использования в константе VERSION, объекты VersionNumber широко используются в модуле Pkg, для указания версий пакетов и их зависимостей.

Необработанные строковые литералы

Необработанные строки без интерполяции или экранирования могут быть выражены с помощью нестандартных строковых литералов вида raw"...". Необработанные строковые литералы создают обычные $ объекты, которые содержат заключенное содержимое точно так, как оно было введено, без интерполяции или экранирования. Это полезно для строк, содержащих код или разметку на других языках, в которых $ или \ являются специальными символами.

Исключением является то, что кавычки все равно должны быть экранированы, например, raw"\"" эквивалентно "\"". Чтобы сделать возможным выражение всех строк, обратные слэши также должны быть экранированы, но только когда они появляются непосредственно перед символом кавычки:

julia> println(raw"\\ \\\"")
\\ \"

Обратите внимание, что первые два обратных слэша отображаются дословно в выводе, так как они не предшествуют символу кавычки. Однако следующий обратный слэш экранирует следующий за ним обратный слэш, а последний обратный слэш экранирует кавычку, так как эти обратные слэши предшествуют кавычке.

© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/strings/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API