Spec-Zone.ru › Julia 1.6

Строки

Строки представляют собой конечные последовательности символов. Конечно, настоящие проблемы начинаются, когда возникает вопрос о том, что такое символ. Символы, с которыми знакомы говорящие по-английски, — это буквы 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> 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."

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

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

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

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

julia> typeof(re)
Regex

Чтобы проверить, соответствует ли regex строке, используйте occursin:

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

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

Как можно видеть здесь, occursin просто возвращает true или false, указывая, существует ли соответствие заданному regex в строке. Однако часто требуется не только знать, соответствует ли строка, но и *как* она соответствует. Чтобы получить эту информацию о соответствии, используйте функцию 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

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

julia> first, second, third = m.captures; 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 manpage:

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

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

Конструктор Regex() может использоваться для создания корректной строки regex программно. Это позволяет использовать содержимое строковых переменных и другие операции над строками при построении строки regex. Любой из кодов 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("[\"( ]$name[\") ]")  # interpolate value of name
r"[\"( ]Jon[\") ]"

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

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

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

Еще один полезный нестандартный строковый литерал — это литерал массива байтов: 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 по \xff в сравнении с \u80 по \uff есть существенная разница: первые экранирования все кодируют один байт, которые — если не следуют очень специфическими байтами продолжения — не образуют корректные данные UTF-8, в то время как последние экранирования все представляют коды символов с двухбайтовым кодированием.

END_OF_DOCUMENT_MARKER ```

Если всё это чрезвычайно запутанно, попробуйте прочитать «Абсолютный минимум, который каждый разработчик ПО должен знать о 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-"
    # 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"...". Необработанные строковые литералы создают обычные объекты String, которые содержат заключённое содержимое точно так, как оно было введено, без интерполяции или разбора. Это полезно для строк, содержащих код или разметку на других языках, в которых $ или \ используются в качестве специальных символов.

Исключением является то, что кавычки всё же должны быть экранированы, например, 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.6.0/manual/strings/

Spec-Zone.ru

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