Julia에서 String은 문자가 순서대로 나열된 시퀀스예요. 매뉴얼에서도 지적하듯이, "물론 정말 골치 아픈 문제는 문자란 무엇인가를 따지기 시작할 때 생겨요."
이런 복잡한 부분은 대부분 Chars 개념에서 더 자세히 다루지만, 간단히 말하면 이래요:
일반적으로 문자열은 큰따옴표 " "로 감싸요.
작은따옴표 ' '는 Chars에만 사용해요.
큰따옴표 세 개 """ """를 사용할 수도 있는데, 이렇게 하면 문자열 안에서 "를 이스케이프하지 않고 쓸 수 있어요.
더 중요한 점은, 여러 줄 문자열에서 불필요하게 들어간 여백이 제거된다는 거예요.
julia> """
Multiline
String
"""
"Multiline\nString\n"
C 이후의 대부분 언어와 마찬가지로, Julia는 이스케이프에 백슬래시 \를 사용해요:
\n처럼 출력되지 않는 문자를 입력할 때.\$처럼 그렇지 않으면 특별한 의미를 갖게 될 문자를 보호할 때(아래 참고).문자열은 반복 가능한 컬렉션이라서, 문자 벡터로 변환할 필요 없이 for ... in 루프가 그대로 동작해요.
julia> s = "Julia"
"Julia"
# admittedly, this is a silly example
julia> for c in s; print(c, "; "); end
J; u; l; i; a;
이렇게 반복하면 Julia가 ASCII가 아닌 문자열에서도 문자 경계를 올바르게 처리해 준다는 장점이 있어요. 이것이 왜 중요한지는 다음 절에서 더 분명해져요.
원칙적으로 문자열은 벡터와 똑같이 인덱싱할 수 있어요:
julia> s[1], s[5]
('J', 'a')
"Julia" 같은 ASCII 문자열에는 분명히 잘 동작하지만, 그러면 세계 대부분의 언어는 무시하게 돼요.
약 15세기 전 고대 영어로 쓰인 베오울프에 나오는 문자 하나를 생각해 봐요.
"Hrōðgār"는 분명히 7글자지만, 그중 모두가 ASCII인 것은 아니에요.
julia> king = "Hrōðgār"
"Hrōðgār"
julia> length(king)
7
julia> isascii(king)
false
# collect() converts to a Char vector:
julia> collect(king)
7-element Vector{Char}:
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
'ō': Unicode U+014D (category Ll: Letter, lowercase)
'ð': Unicode U+00F0 (category Ll: Letter, lowercase)
'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
'ā': Unicode U+0101 (category Ll: Letter, lowercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
이런 문자열을 인덱싱하는 것은 처음에는 잘 되지만, 어느 순간 무너져요:
julia> king[3]
'ō': Unicode U+014D (category Ll: Letter, lowercase)
julia> king[4]
ERROR: StringIndexError: invalid index [4], valid nearby indices [3]=>'ō', [5]=>'ð'
문제는 이런 인덱싱이 _바이트_를 세는데, ASCII가 아닌 문자열에서는 바이트가 문자와 1:1 관계가 아니라는 점이에요.
유니코드/UTF-8 문자 하나를 표현하는 데 최대 4바이트가 필요할 수 있고, 이 부분은 Chars 개념에서 더 살펴봐요.
많은 언어에서 +를 문자열 연결 연산자로 쓰지만, Julia는 아니에요.
그래도 오류 메시지는 친절한 편이에요.
julia> "s1 " + "s2"
ERROR: MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with *
julia> "s1 " * "s2"
"s1 s2"
string() 함수는 임의의 개수만큼 문자열을 연결해요.
julia> string("lots ", "of ", "bits")
"lots of bits"
join() 함수도 같은 일을 하는데, 문자열 벡터를 받아서 주어진 구분자로 연결할 수도 있어요(기본값은 "").
julia> join(["lots", "of", "bits"], "_")
"lots_of_bits"
문자열은 불변이고, 연결에는 복사가 따른다는 점에 주의해요. 따라서 루프 안에서 반복해서 연결하는 것은 비효율적이고, 조각들을 벡터에 모아 두었다가 마지막에 한꺼번에 이어 붙이는 게 더 좋아요.
간단해요:
julia> repeat("Abc", 5)
"AbcAbcAbcAbcAbc"
많은 언어에 계산된 값을 문자열에 삽입하는 방법이 있지만, 어떤 문법이 최선인지에 대해서는 엄청난 의견 차이가 있어요.
Julia에서는 보간할 값 앞에 $를 붙여요.
julia> myvar = 42
42
julia> "myvar evaluates to $myvar"
"myvar evaluates to 42"
julia> r = 5.3
5.3
julia> "A circle of radius $r has area $(π * r^2)"
"A circle of radius 5.3 has area 88.24733763933729"
괄호를 생략하면 모호해질 때는 괄호가 필요하지만, 공백이 뒤따르는 단일 식별자에는 괄호가 선택 사항이에요.
문자열에 $를 넣으려면 \$로 이스케이프해요.
현재 보간으로는 표시할 소수 자릿수 같은 서식을 지정할 수 없다는 제약이 있어요.
우회 방법으로는 이런 것들이 있어요:
round() 함수로 감싸요.@sprintf() 매크로는 아래에서 설명해요.@sprintf()문자열 조립을 더 세밀하게 제어하기 위해, Julia는 C(그리고 이후의 여러 언어: 위키백과에 약 30개가 정리되어 있어요)에서 sprintf 함수를 가져왔어요.
Julia에서는 Printf 모듈 안의 macro로 구현되어 있어서 @ 접두사가 붙어요.
julia> using Printf
julia> r = 5.3
5.3
julia> @sprintf("A circle of radius %.1f has area %.2f", r, π * r^2)
"A circle of radius 5.3 has area 88.25"
간단히 하기 위해, 이번 절에서는 새로운 문자열을 반환하고 입력은 그대로 두는 함수들에 집중할게요.
많은 함수에는 입력을 제자리에서 수정하는, ! 접미사가 붙은 대응 함수가 있어요.
문자열은 얼마나 길까요? 때로는 무엇을 세느냐에 따라 달라요.
간단한 경우는 ASCII 문자열로, 모든 문자가 1바이트를 차지해요.
julia> string_asc = "Julia"
"Julia"
julia> length(string_asc)
5
julia> sizeof(string_asc)
5
length() 함수는 문자 수를 반환하고, sizeof() 함수는 바이트 수를 반환해요.
이 구분은 다른 문자 집합에서 중요해져요:
julia> king = "Hrōðgār"
"Hrōðgār"
julia> length(king)
7
julia> sizeof(king)
10
프로그래밍에서 흔한 작업은 긴 문자열을 하나 이상의 문자로 된 구분자 문자열을 기준으로 조각조각 나누는 것이에요.
가장 일반적으로는 split() 함수를 사용해요.
구분자는 기본값이 (모든) 공백이지만, 다른 것을 지정할 수도 있어요.
julia> split("my little string")
3-element Vector{SubString{String}}:
"my"
"little"
"string"
julia> split("My_snake_case_string", "_")
4-element Vector{SubString{String}}:
"My"
"snake"
"case"
"string"
이 예시들은 문자열을 더 작은 _문자열_로 나눠요. 문자열을 _문자_로 나누는 것은 다른 작업이에요.
julia> collect("input string")
12-element Vector{Char}:
'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
'p': ASCII/Unicode U+0070 (category Ll: Letter, lowercase)
'u': ASCII/Unicode U+0075 (category Ll: Letter, lowercase)
't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
's': ASCII/Unicode U+0073 (category Ll: Letter, lowercase)
't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
문자열에 지정한 부분 문자열이 들어 있을까요?
이상하게도 이를 확인하는 함수가 두 개 있는데, occursin()과 contains()는 인자의 순서만 다를 뿐이에요.
julia> needle = "Julia"
"Julia"
julia> haystack = "Python, Julia and R"
"Python, Julia and R"
julia> occursin(needle, haystack)
true
julia> contains(haystack, needle)
true
위치를 더 구체적으로 따지려면:
julia> startswith(haystack, "Python")
true
julia> endswith(haystack, "Python")
false
부분 문자열의 시작:끝 인덱스를 얻으려면 여러 함수가 있어요:
julia> findfirst(needle, haystack)
9:13
부분 문자열을 바꾸려면, x => y 문법으로 변경 사항을 나열해요.
julia> replace("abc", "a"=>"b", "b"=>"c", "c"=>"a")
"bca"
Julia 1.7부터는 replace 함수가 임의의 개수만큼 변경을 받을 수 있고, 바꾸는 동안 어떤 문자도 두 번 이상 바뀌지 않는다는 것이 보장돼요.
x => y 문법은 pair라고 하는데, Julia에서 중요한 타입이고 Dicts and Pairs 개념에서 더 자세히 다뤄요.
참고: 간단히 하기 위해 위의 모든 예시는 문자열 리터럴을 사용해요. 많은 함수가 더 일반적이라 정규 표현식과도 동작해요. 이 내용은 나중 개념에서 다시 다뤄요.
문자열의 앞쪽, 뒤쪽, 또는 양쪽 공백을 제거해야 할 때가 많아요.
이를 위한 함수는 lstrip()(왼쪽), rstrip()(오른쪽), strip()(양쪽)이에요.
julia> strip("\n\t useful_bit\n\n")
"useful_bit"
이 함수들은 이름만 봐도 거의 알 수 있어요:
julia> s = repeat("aBcD ", 5)
"aBcD aBcD aBcD aBcD aBcD "
julia> uppercase(s)
"ABCD ABCD ABCD ABCD ABCD "
julia> lowercase(s)
"abcd abcd abcd abcd abcd "
julia> titlecase(s) # Initial letter of each word uppercase
"Abcd Abcd Abcd Abcd Abcd "
숫자와 그 문자열 표현을 서로 변환하는 것은 자주 유용해요.
숫자를 문자열로 바꾸는 것은 간단하지만, 기본값인 10이 아닌 진법을 쓰려면 매개변수를 하나 더 넘겨야 해요:
julia> string(42)
"42"
julia> string(42, base=16)
"2a"
julia> string(42, base=2)
"101010"
문자열을 파싱해서 숫자 값을 얻는 것은 조금 더 복잡하고 오류 가능성도 커요. 목표 숫자 타입을 지정하는 것은 필수예요. 진법 지정은 선택 사항이고 기본값은 10이에요.
julia> parse(Int, "42")
42
julia> parse(Float64, "42")
42.0
julia> parse(Int, "42?")
ERROR: ArgumentError: invalid base 10 digit '?' in "42?"
julia> parse(Int, "42 ") # whitespace may be tolerated
42
때로는 문자열 안의 일반적인 보간과 이스케이프를 막는 것이 유용해요.
이럴 때는 여는 따옴표 앞에 raw를 붙여요.
julia> x = 10
10
julia> "x = $x if we use \$ for interpolation"
"x = 10 if we use \$ for interpolation"
julia> raw"x = $x if we use \$ for interpolation"
"x = \$x if we use \\\$ for interpolation"
정규 표현식은 나름의 복잡한 문법이 있는데, Julia는 두 가지 유용한 문자열 타입을 제공해요: 정규식용 r" "와 캡처한 조각을 다루기 위한 s" "예요.
다양한 다른 특수 문자열들은 Exercism 스타일 프로그래밍에서는 덜 흔하지만, 라이브러리를 만들 때는 널리 쓰여요.
v"x.y.z"는 소프트웨어 릴리스의 major.minor.patch 번호를 다뤄요.b" "은 유니코드 문자 경계를 건너뛰고 바이트 수준에서 동작해요.