문자열

문자열 (Strings)

문자열은 문자들의 유한한 나열이에요. 물론 진짜 문제는 "문자가 무엇인가"라고 물을 때 생기죠. 영어 사용자가 익숙한 문자는 A, B, C 같은 글자들과 숫자, 흔한 문장 부호들이에요. 이 문자들은 ASCII 표준에 의해 0~127 사이의 정수 값에 매핑되는 형태로 표준화되어 있어요. 물론 영어가 아닌 언어에는 훨씬 많은 다른 문자들이 있어요. 악센트나 다른 변형이 붙은 ASCII 문자 변형, 키릴 문자나 그리스 문자 같은 관련 문자 체계, 그리고 아랍어·중국어·히브리어·힌디어·일본어·한국어를 포함한 ASCII·영어와 전혀 무관한 문자 체계까지요. Unicode 표준은 "문자가 정확히 무엇인가"라는 복잡함을 다루며, 이 문제를 해결하는 결정적 표준으로 일반적으로 받아들여져요. 필요에 따라, 이 복잡함을 전부 무시하고 ASCII 문자만 존재하는 척할 수도 있고, 비-ASCII 텍스트를 다룰 때 마주칠 수 있는 어떤 문자나 인코딩이든 처리할 수 있는 코드를 작성할 수도 있어요. Julia는 평범한 ASCII 텍스트를 다루는 것을 간단하고 효율적으로 만들고, Unicode를 다루는 것도 가능한 한 간단하고 효율적으로 만들어요. 특히 C 스타일의 문자열 코드로 ASCII 문자열을 처리할 수 있고, 성능과 의미론 모두에서 기대한 대로 동작해요. 그런 코드가 비-ASCII 텍스트를 만나면, 조용히 손상된 결과를 만들지 않고 명확한 오류 메시지로 우아하게 실패해요. 그런 일이 생기면, 비-ASCII 데이터를 다루도록 코드를 수정하는 건 간단해요.

Julia 문자열에는 주목할 만한 몇 가지 고수준 특징이 있어요.

  • Julia에서 문자열(그리고 문자열 리터럴)에 쓰는 내장 구체 타입은 String이에요. 이 타입은 UTF-8 인코딩으로 Unicode 문자의 전체 범위를 지원해요. (다른 Unicode 인코딩과의 변환을 위해 transcode 함수가 제공돼요.)
  • 모든 문자열 타입은 추상 타입 AbstractString의 하위 타입이며, 외부 패키지들이 추가 AbstractString 하위 타입을 정의해요(예: 다른 인코딩용). 문자열 인자를 기대하는 함수를 정의한다면, 어떤 문자열 타입이든 받아들이도록 타입을 AbstractString으로 선언해야 해요.
  • C나 Java처럼, 그리고 대부분의 동적 언어와 달리, Julia는 단일 문자를 나타내는 일급 타입을 가지며, 이를 AbstractChar라고 불러요. AbstractChar의 내장 하위 타입인 Char는 어떤 Unicode 문자든 나타낼 수 있는 32비트 primitive 타입이에요(그리고 UTF-8 인코딩에 기반해요).
  • Java처럼, 문자열은 불변(immutable)이에요. AbstractString 객체의 값은 바꿀 수 없어요. 다른 문자열 값을 만들려면 다른 문자열의 일부분들로 새 문자열을 구성해야 해요.
  • 개념적으로, 문자열은 인덱스에서 문자로 가는 *부분 함수(partial function)*예요. 일부 인덱스 값에 대해서는 문자 값이 반환되지 않고 대신 예외가 던져져요. 이를 통해 문자열을 문자 인덱스가 아니라 인코딩된 표현의 바이트 인덱스로 효율적으로 인덱싱할 수 있어요. 가변 폭 인코딩의 Unicode 문자열에서는 문자 인덱스 방식을 효율적이면서도 단순하게 구현할 수 없거든요.

출처: julia 공식 메뉴얼 — Strings

본문

문자 (Characters)

Char 값은 단일 문자를 나타내요. 특별한 리터럴 표현과 적절한 산술 동작을 가진 32비트 primitive 타입일 뿐이며, 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+0000U+D7FFU+E000U+10FFFF예요. 이 값들 모두가 아직 의미 있게 할당된 건 아니고, 애플리케이션이 해석할 수 있다고 보장되지도 않지만, 이 값들 전부는 유효한 Unicode 문자로 간주돼요.

작은따옴표 안에서 \u 뒤에 최대 4개의 16진수 숫자, 또는 \U 뒤에 최대 8개의 16진수 숫자(가장 긴 유효 값은 6자리면 충분해요)로 어떤 Unicode 문자든 입력할 수 있어요.

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)

문자열 기초 (String Basics)

문자열 리터럴은 큰따옴표나 삼중 큰따옴표로 구분돼요 (작은따옴표가 아니라).

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)로 반환돼요. beginend 키워드는 인덱싱 연산 안에서 주어진 차원에 따라 각각 첫 번째와 마지막 인덱스의 줄임말로 쓸 수 있어요. 문자열 인덱싱은 Julia의 대부분의 인덱싱처럼 1-기반이에요. firstindex는 모든 AbstractString에 대해 항상 1을 반환해요. 하지만 아래에서 보겠지만, lastindex(str)는 일반적으로 문자열의 length(str)과 같지 않아요. 일부 Unicode 문자가 여러 "코드 유닛(code unit)"을 차지할 수 있기 때문이에요.

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]
[...]

범위 인덱싱으로 부분 문자열(substring)을 추출할 수도 있어요.

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 타입을 사용해 문자열에 대한 view를 만들 수도 있어요. 더 간단하게는 코드 블록에 @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 converts slices to SubStrings
SubString{String}

chop, chomp, strip 같은 몇 가지 표준 함수는 SubString을 반환해요.

Unicode와 UTF-8 (Unicode and 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 이상의 코드 포인트는 문자당 최대 4바이트까지 여러 바이트로 인코딩돼요.

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)

이 경우 문자 는 3바이트 문자라 인덱스 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와 공백이 1바이트 문자이기 때문이에요. 반면 end-2 멀티바이트 표현의 중간을 가리켜요. 이 경우 올바른 방법은 prevind(s, lastindex(s), 2)를 쓰는 거예요. 혹은 그 값을 s에 인덱싱하는 데 쓴다면 s[prevind(s, end, 2)]라고 쓸 수 있고, endlastindex(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

문자열에 대한 유효 인덱스가 필요하다면, nextindprevind 함수로 다음/이전 유효 인덱스로 증감시킬 수 있어요(위에서 언급했듯이). 또한 eachindex 함수로 유효한 문자 인덱스를 순회할 수 있어요.

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

인코딩의 원시 코드 유닛(UTF-8의 경우 바이트)에 접근하려면 codeunit(s,i) 함수를 사용할 수 있는데, 인덱스 i1부터 ncodeunits(s)까지 연속적으로 진행돼요. codeunits(s) 함수는 이런 원시 코드유닛(바이트)을 배열로 접근하게 해 주는 AbstractVector{UInt8} 래퍼를 반환해요.

Julia의 문자열은 잘못된 UTF-8 코드 유닛 시퀀스를 포함할 수 있어요. 이 관례 덕분에 어떤 바이트 시퀀스든 String으로 취급할 수 있어요. 이런 상황에서 규칙은 이래요. 코드 유닛 시퀀스를 왼쪽에서 오른쪽으로 파싱할 때, 다음 비트 패턴 중 하나의 시작과 일치하는 최장의 8비트 코드 유닛 시퀀스로 문자가 형성돼요 (각 x0이나 1이 될 수 있어요):

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

특히 이는 과도하게 긴(overlong) 코드 유닛 시퀀스와 너무 높은 코드 유닛 시퀀스, 그리고 그것들의 접두사가 여러 개의 잘못된 문자로 처리되지 않고 하나의 잘못된 문자로 처리된다는 뜻이에요. 이 규칙은 예로 설명하는 게 제일 좋아요.

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의 처음 두 코드 유닛이 공백 문자의 과도하게 긴 인코딩을 형성한다는 걸 볼 수 있어요. 유효하지 않지만, 문자열에서 단일 문자로 받아들여져요. 다음 두 코드 유닛은 3바이트 UTF-8 시퀀스의 유효한 시작을 형성해요. 하지만 다섯 번째 코드 유닛 \xe2는 그 유효한 연속(continuation)이 아니에요. 따라서 코드 유닛 3과 4도 이 문자열에서 잘못된 문자로 해석돼요. 마찬가지로 코드 유닛 5는, |가 그 유효한 연속이 아니므로, 잘못된 문자를 형성해요. 마지막으로 문자열 s2는 너무 높은 코드 포인트 하나를 담고 있어요.

Julia는 기본적으로 UTF-8 인코딩을 사용하며, 새 인코딩에 대한 지원은 패키지로 추가할 수 있어요. 예를 들어 LegacyStrings.jl 패키지는 UTF16StringUTF32String 타입을 구현해요. 다른 인코딩과 지원 구현 방법에 대한 추가 논의는 현재로서는 이 문서의 범위를 벗어나요. UTF-8 인코딩 이슈에 대한 더 자세한 논의는 아래의 바이트 배열 리터럴 섹션을 참고하세요. 다양한 UTF-xx 인코딩 간 데이터 변환, 주로 외부 데이터와 라이브러리를 다룰 때, 위해 transcode 함수가 제공돼요.

연결 (Concatenation)

가장 흔하고 유용한 문자열 연산 중 하나는 연결(concatenation)이에요.

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 문자열에 대해서는 연결이 문자열의 모든 문자를 보존하고 문자열 길이의 가산성(additivity)도 유지해요.

Julia는 문자열 연결에 [*](../../base/math/#Base.:*-Tuple{Any, Vararg{Any}})도 제공해요.

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

문자열 연결에 +를 제공하는 언어에 익숙한 사용자에게 *는 놀라운 선택처럼 보일 수 있지만, 수학, 특히 추상대수학에서 *의 이런 용법에는 선례가 있어요.

수학에서 +는 보통 가환(commutative) 연산을 나타내며, 피연산자의 순서가 중요하지 않아요. 행렬 덧셈이 그 예인데, 모양이 같은 어떤 행렬 AB에 대해서도 A + B == B + A예요. 반대로 *는 보통 비가환(noncommutative) 연산을 나타내며, 피연산자의 순서가 중요해요. 행렬 곱셈이 그 예인데, 일반적으로 A * B != B * A예요. 행렬 곱셈처럼 문자열 연결도 비가환이에요: greet * whom != whom * greet. 따라서 *가 중위 문자열 연결 연산자로 더 자연스러운 선택이며, 일반적인 수학 용법과 일치해요.

더 정확히 말하면, 모든 유한 길이 문자열의 집합 S와 문자열 연결 연산자 *자유 모노이드(free monoid) (S, *)를 형성해요. 이 집합의 항등원은 빈 문자열 ""이에요. 자유 모노이드가 가환이 아닐 때는 연산을 보통 \cdot, *, 또는 비슷한 기호로 나타내며, 언급했듯이 보통 가환을 뜻하는 +는 쓰지 않아요.

보간 (Interpolation)

연결을 사용해 문자열을 구성하는 건 다소 번거로워질 수 있어요. string에 대한 장황한 호출이나 반복된 곱셈의 필요를 줄이기 위해, Julia는 Perl에서처럼 $를 사용해 문자열 리터럴에 보간(interpolation)을 허용해요.

julia> greet = "Hello"; whom = "world";

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

이건 더 읽기 쉽고 편리하며, 위의 문자열 연결과 동등해요. 시스템은 이 하나로 보이는 문자열 리터럴을 string(greet, ", ", whom, ".\n") 호출로 재작성해요.

$ 다음의 가장 짧은 완전한 표현식이 문자열에 보간될 값을 가진 표현식으로 취급돼요. 따라서 괄호를 사용해 어떤 표현식이든 문자열에 보간할 수 있어요.

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

연결과 문자열 보간 모두 객체를 문자열 형태로 변환하기 위해 string을 호출해요. 하지만 string은 실제로 print의 출력을 반환할 뿐이므로, 새 타입은 string 대신 print나 [show](../../base/io-network/#Base.show-Tuple{IO, Any})에 메서드를 추가해야 해요.

대부분의 AbstractString이 아닌 객체는 그것들이 리터럴 표현식으로 입력된 방식과 밀접하게 대응하는 문자열로 변환돼요.

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

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

stringAbstractStringAbstractChar 값에 대해 항등 함수(identity)이므로, 이들은 인용 없이 이스케이프 없이 그 자체로 문자열에 보간돼요.

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.

삼중 따옴표 문자열 리터럴 (Triple-Quoted String Literals)

삼중 따옴표("""...""")로 문자열을 만들면 더 긴 텍스트 블록을 만드는 데 유용할 수 있는 특별한 동작이 몇 가지 있어요.

먼저, 삼중 따옴표 문자열은 가장 덜 들여쓰기된 줄의 레벨로 들여쓰기가 제거(dedent)돼요. 이는 들여쓰기된 코드 안에서 문자열을 정의할 때 유용해요. 예를 들어:

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" 리터럴 문자열을 입력할 수 있어요.

일반적인 연산 (Common Operations)

표준 비교 연산자로 문자열을 사전순으로 비교할 수 있어요.

julia> "abracadabra" < "xylophone"
true

julia> "abracadabra" == "xylophone"
false

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

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

findfirstfindlast 함수로 특정 문자의 인덱스를 검색할 수 있어요.

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

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

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

[findnext](../../base/arrays/#Base.findnext-Tuple{Any, Integer})와 [findprev](../../base/arrays/#Base.findprev-Tuple{Any, Integer}) 함수로 주어진 오프셋에서 문자 검색을 시작할 수 있어요.

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이 문자 리터럴도 찾을 수 있음을 보여줘요.

유용한 문자열 함수 두 개 더로는 repeatjoin이 있어요.

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)i부터 j까지 str의 유효한 문자 인덱스 수예요.
  • ncodeunits(str)은 문자열의 코드 유닛 수예요.
  • codeunit(str, i)은 문자열 str에서 인덱스 i의 코드 유닛 값을 주어요.
  • thisind(str, i)은 문자열의 임의 인덱스가 주어졌을 때, 그 인덱스가 가리키는 문자의 첫 인덱스를 찾아요.
  • nextind(str, i, n=1)은 인덱스 i 뒤에서 시작하는 n번째 문자의 시작을 찾아요.
  • prevind(str, i, n=1)은 인덱스 i 앞에서 시작하는 n번째 문자의 시작을 찾아요.

비표준 문자열 리터럴 (Non-Standard String Literals)

문자열을 구성하거나 문자열 의미론을 쓰고 싶지만, 표준 문자열 구성의 동작이 딱 맞지 않는 상황이 있어요. 이런 종류의 상황을 위해 Julia는 비표준 문자열 리터럴을 제공해요. 비표준 문자열 리터럴은 일반 큰따옴표 문자열 리터럴처럼 보이지만 즉시 식별자가 접두사로 붙고, 일반 문자열 리터럴과 다르게 동작할 수 있어요.

아래에서 설명할 정규 표현식, 바이트 배열 리터럴, 버전 번호 리터럴이 비표준 문자열 리터럴의 몇 가지 예시예요. 사용자와 패키지는 새 비표준 문자열 리터럴을 정의할 수도 있어요. 더 자세한 문서는 Metaprogramming 섹션에 있어요.

정규 표현식 (Regular Expressions)

때로는 정확한 문자열이 아니라 특정 패턴을 찾고 있기도 해요. 예를 들어 큰 텍스트 파일에서 날짜 하나를 추출하려고 한다고 가정해볼게요. 그 날짜가 무엇인지 모르지만(그래서 찾고 있는 거예요), YYYY-MM-DD 같은 모양일 거라는 건 알고 있어요. 정규 표현식은 이런 패턴을 지정하고 검색하게 해 줘요.

Julia는 PCRE 라이브러리가 제공하는 Perl 호환 정규 표현식(regex) 버전 2를 사용해요 (더 자세한 내용은 PCRE2 문법 설명 참고). 정규 표현식은 두 가지 방식으로 문자열과 관련돼요. 명백한 연결은 정규 표현식이 문자열에서 규칙적인 패턴을 찾는 데 쓰인다는 거예요. 다른 연결은 정규 표현식 자체가 문자열로 입력되며, 문자열에서 패턴을 효율적으로 검색하는 데 쓸 수 있는 상태 머신으로 파싱된다는 거예요. 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은 단순히 주어진 regex의 매칭이 문자열에 있는지 나타내는 true나 false를 반환해요. 하지만 흔히 문자열이 매칭됐는지뿐 아니라 어떻게 매칭됐는지도 알고 싶어요. 매칭에 대한 이 정보를 포착하려면 대신 match 함수를 사용하세요.

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

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

정규 표현식이 주어진 문자열과 매칭되지 않으면 matchnothing을 반환해요. 이건 대화형 프롬프트에서 아무것도 출력하지 않는 특별한 값이에요. 출력하지 않는 것 외에는 완전히 평범한 값이며, 프로그램적으로 테스트할 수 있어요.

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는 0 오프셋을 가져요 (Julia의 인덱스는 1-기반이므로 문자열에 대한 0 오프셋은 유효하지 않다는 걸 기억하세요). 여기 다소 인위적인 예시 두 개가 있어요.

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

캡처를 배열로 반환받는 게 편리해서, destructuring 문법으로 로컬 변수에 묶을 수 있어요. 편의상 RegexMatch 객체는 captures 필드로 전달되는 반복자 메서드를 구현하므로, 매치 객체를 직접 destructure할 수 있어요.

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](../../base/collections/#Base.replace-Tuple{Any, Vararg{Pair}})를 사용할 때는 \n으로 n번째 캡처 그룹을 가리키고 치환 문자열에 s를 접두사로 붙여, 캡처를 치환 문자열에서 참조할 수 있어요. 캡처 그룹 0은 전체 매칭 객체를 가리켜요. 명명된 캡처 그룹은 치환에서 \g<name>으로 참조할 수 있어요. 예를 들어:

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.

예를 들어 다음 regex는 세 플래그를 모두 켠 거예요.

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: ParseError:
# Error @ none:1:2
"\x"
#└┘ ── invalid hex escape sequence

r"""...""" 형태의 삼중 따옴표 regex 문자열도 지원돼요 (큰따옴표나 새줄을 담은 정규 표현식에 편리할 수 있어요).

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("[\"( ]\\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 사이의 모든 문자는 리터럴 문자로 해석돼요. 이는 그렇지 않으면 regex 메타문자가 될 문자를 매칭할 때 편리해요. 다만 문자열 보간과 함께 이 기능을 쓸 때는 주의가 필요해요. 보간된 문자열 자체에 \E 시퀀스가 포함되어 의도치 않게 리터럴 매칭을 끝낼 수 있거든요. 사용자 입력은 regex에 포함하기 전에 살균(sanitize)해야 해요.

바이트 배열 리터럴 (Byte Array Literals)

또 다른 유용한 비표준 문자열 리터럴은 바이트 배열 문자열 리터럴 b"..."이에요. 이 형태는 문자열 표기법으로 읽기 전용 리터럴 바이트 배열, 즉 UInt8 값의 배열을 표현하게 해 줘요. 그 객체들의 타입은 CodeUnits{UInt8, String}이에요. 바이트 배열 리터럴의 규칙은 다음과 같아요.

  • ASCII 문자와 ASCII 이스케이프는 단일 바이트를 생성해요.
  • \x와 8진수 이스케이프 시퀀스는 이스케이프 값에 해당하는 바이트를 생성해요.
  • Unicode 이스케이프 시퀀스는 그 코드 포인트를 UTF-8로 인코딩한 바이트 시퀀스를 생성해요.

이 규칙들 사이에는 겹치는 부분이 있는데, 0x80(128) 미만의 \x와 8진수 이스케이프의 동작이 처음 두 규칙 모두에 해당하기 때문이에요. 하지만 여기서 규칙들이 일치해요. 함께 보면, 이 규칙들 덕분에 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: 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 인코딩이 해당 \x 이스케이프가 생성하는 단일 바이트와 우연히 일치하므로, 이 구분을 안전하게 무시할 수 있어요. 하지만 \x80\xff 이스케이프와 \u80\uff를 비교하면 큰 차이가 있어요. 전자 이스케이프는 모두 단일 바이트를 인코딩하는데, 아주 특정한 연속 바이트가 뒤따르지 않는 한 유효한 UTF-8 데이터를 형성하지 않아요. 반면 후자 이스케이프는 모두 2바이트 인코딩을 가진 Unicode 코드 포인트를 나타내요.

이 모든 게 극도로 혼란스럽다면, 모든 소프트웨어 개발자가 반드시 알아야 할 Unicode와 문자 집합에 대한 최소한의 지식을 읽어보는 걸 추천해요. Unicode와 UTF-8에 대한 훌륭한 입문서이고, 이 주제에 대한 혼란을 완화하는 데 도움이 될 거예요.

버전 번호 리터럴 (Version Number Literals)

버전 번호는 v"..." 형태의 비표준 문자열 리터럴로 쉽게 표현할 수 있어요. 버전 번호 리터럴은 시맨틱 버저닝 2.0.0-rc2의 명세를 따르는 VersionNumber 객체를 만들며, 따라서 major, minor, patch 숫자 값 뒤에 pre-release와 build 영숫자 주석이 따라오는 구조예요. 예를 들어 v"0.2.1-rc1+win64"는 major 버전 0, minor 버전 2, patch 버전 1, pre-release rc1, build win64로 나뉘어요. 버전 리터럴을 입력할 때 major 버전 번호 외에는 모두 선택 사항이에요. 따라서 예를 들어 v"0.2"v"0.2.0"(빈 pre-release/build 주석)과 동등하고, 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 릴리스보다(그리고 그 pre-release들 전부보다도) 낮은 버전을 나타내는 데 쓰여요. 따라서 위 예제의 코드는 안정적인 0.2 버전에서만 실행되고 v"0.3.0-rc1" 같은 버전은 제외해요. 불안정한(즉 pre-release) 0.2 버전도 허용하려면 하한 검사를 v"0.2-" <= VERSION처럼 수정해야 해요.

또 다른 비표준 버전 명세 확장은 뒤에 +를 사용해 빌드 버전의 상한을 표현하게 해 줘요. 예를 들어 VERSION > v"0.2-rc1+"0.2-rc1와 그 모든 빌드보다 높은 모든 버전을 의미하는 데 쓸 수 있어요. 버전 v"0.2-rc1+win64"에 대해 false를, v"0.2-rc2"에 대해 true를 반환해요.

비교에서 이런 특별한 버전을 쓰는 것은 좋은 관례예요(특히 상한에는 특별한 이유가 없는 한 항상 뒤의 -를 써야 해요). 하지만 이것들은 시맨틱 버저닝 체계에서 유효하지 않으므로, 무엇의 실제 버전 번호로는 사용해서는 안 돼요.

VersionNumber 객체는 VERSION 상수에 사용되는 것 외에도, Pkg 모듈에서 패키지 버전과 그 의존성을 지정하는 데 널리 사용돼요.

원시 문자열 리터럴 (Raw String Literals)

보간이나 이스케이프 해제가 없는 원시 문자열은 raw"..." 형태의 비표준 문자열 리터럴로 표현할 수 있어요. 원시 문자열 리터럴은 평범한 String 객체를 만들며, 그 안의 내용을 보간이나 이스케이프 없이 입력한 그대로 담아요. 이는 $\를 특수 문자로 쓰는 다른 언어의 코드나 마크업을 담은 문자열에 유용해요.

예외는 인용 부호로, 여전히 이스케이프해야 해요. 예를 들어 raw"\"""\""와 동등해요. 모든 문자열을 표현할 수 있게 하려면 백슬래시도, 하지만 인용 문자 바로 앞에 나타날 때만, 이스케이프해야 해요.

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

처음 두 백슬래시가 그대로 출력에 나타난다는 점에 주목하세요. 인용 문자 앞에 오지 않기 때문이에요. 하지만 다음 백슬래시는 그 뒤를 따르는 백슬래시를 이스케이프하고, 마지막 백슬래시는 인용을 이스케이프해요. 이 백슬래시들이 인용 앞에 오기 때문이에요.

주석 문자열 (Annotated Strings)

참고: AnnotatedString의 API는 실험적인 것으로 간주되며 Julia 버전 간에 변경될 수 있어요.

문자열의 영역에 관련된 메타데이터를 담을 수 있는 것이 때로 유용해요. AnnotatedString은 다른 문자열을 감싸고, 그 영역에 라벨이 붙은 값(:label => value)으로 주석을 달 수 있게 해 줘요. 모든 일반 문자열 연산은 밑에 있는 문자열에 적용돼요. 하지만 가능할 때마다 스타일 정보는 보존돼요. 이는 AnnotatedString을 조작—부분 문자열을 취하고, 패딩하고, 다른 문자열과 연결하고—해도 메타데이터 주석이 "차고 따라온다"는 뜻이에요.

이 문자열 타입은 StyledStrings stdlib의 기초로, :face-라벨 주석을 사용해 스타일 정보를 담아요.

AnnotatedString을 연결할 때, 문자열 주석을 유지하고 싶다면 string 대신 annotatedstring을 사용하도록 주의하세요.

julia> str = Base.AnnotatedString("hello there",
               [(1:5, :word, :greeting), (7:11, :label, 1)])
"hello there"

julia> length(str)
11

julia> lpad(str, 14)
"   hello there"

julia> typeof(lpad(str, 7))
Base.AnnotatedString{String}

julia> str2 = Base.AnnotatedString(" julia", [(2:6, :face, :magenta)])
" julia"

julia> Base.annotatedstring(str, str2)
"hello there julia"

julia> str * str2 == Base.annotatedstring(str, str2) # *-concatenation still works
true

AnnotatedString의 주석은 annotationsannotate! 함수로 접근·수정할 수 있어요.

더 알아보기