Документация Engee

Текстовые строки

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

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

  • Встроенным конкретным типом, используемым для строк (и строковых литералов) в Julia, является тип String. Он поддерживает полный набор символов Unicode через кодировку UTF-8. (Для преобразования в другие кодировки Unicode и из них существует функция transcode.)

  • Все строковые типы являются подтипами абстрактного типа 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. Проще говоря, при использовании макроса @views в блоке кода все фрагменты строк преобразуются в подстроки. Пример:

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

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

julia> typeof(substr)
SubString{String}

julia> @views typeof(str[1:4]) # @views преобразует фрагменты в подстроки
SubString{String}

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

Unicode и UTF-8

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

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

Отображение этих символов Unicode в виде escape-символов или специальных символов зависит от настроек языкового стандарта терминала и его поддержки 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)) не всегда совпадает с последним индексом. Если вы выполняете итерацию по индексам с первого по последний (lastindex(s)) и индексируете s, последовательность символов, возвращаемая при отсутствии ошибок, будет последовательностью символов, составляющих строку s. Таким образом, length(s) <= lastindex(s), поскольку каждый символ в строке должен иметь свой индекс. Ниже приведен неэффективный и перегруженный способ итерации символов строки s.

julia> for i = firstindex(s):lastindex(s)
           try
               println(s[i])
           catch
               # Игнорировать ошибку индекса
           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 = string(a, b)
"∀"

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

julia> length.([a, b, c])
3-element Vector{Int64}:
 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 в строку, используйте явный escape-символ \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('o', "xylophone")
4

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

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

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

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

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

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

julia> findnext('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 есть нестандартные строковые литералы. Нестандартный строковый литерал выглядит как обычный строковый литерал, заключенный в двойные кавычки, но имеет префикс-идентификатор. Его поведение может отличаться от действия обычного строкового литерала.

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

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

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

В Julia доступны совместимые с Perl регулярные выражения (regex) версии 2, предоставляемые библиотекой PCRE (описание синтаксиса PCRE2 можно найти здесь). Регулярные выражения связаны со строками двумя способами: очевидная связь заключается в том, что регулярные выражения используются для поиска регулярных шаблонов в строках; другая связь заключается в том, что регулярные выражения сами вводятся как строки, которые анализируются в конечный автомат, который может быть использован для эффективного поиска шаблонов в строках. В 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, как объясняется в этой выдержке со страницы руководства по регулярным выражениям Perl.

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 '#символ ' также
    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[\") ]")  # Интерполяция значения имени
r"[\"( ]\QJon\E[\") ]"

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

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

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

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

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

  • Символы ASCII и escape-символы ASCII создают один байт.

  • Символы \x и восьмеричные escape-последовательности создают байт, соответствующий escape-значению.

  • Escape-последовательности Unicode создают последовательность байтов, кодирующих данный код символа в UTF-8.

Эти правила частично дублируют друг друга, так как поведение \x и восьмеричных escape-последовательностей меньше 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. Escape-последовательность 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: CanonicalIndexError: 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 каждого кода символа — это всего лишь один байт, создаваемый соответствующим escape-символом \x, поэтому это различие можно смело игнорировать. Однако между escape-символами \x80--\xff и \u80--\uff есть существенная разница: первые кодируют одиночные байты, которые (если за ними не следуют очень специфические байты продолжения) не образуют допустимые данные UTF-8, тогда как последние представляют коды символов Unicode с двухбайтовыми кодировками.

Если все это совершенно непонятно, попробуйте прочитать документ The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (Абсолютный минимум, который должен знать о Unicode и наборах символов каждый разработчик программного обеспечения). Это отличный водный материал о Unicode и 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-"
    # Выполнение чего-то конкретного для серии выпуска 0.2
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"...". Литералы необработанных строк создают обычные объекты String, которые содержат вложенное содержимое именно в том виде, в котором оно было введено, без интерполяции или экранирования. Это хорошо подходит для строк, содержащих код или разметку на других языках, которые используют $ или \ в качестве специальных символов.

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

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

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