이미 기본은 알고 있어요. \d나 [A-Z] 같은 문자 클래스, {4}나 + 같은 수량자, 그리고 matches?, all-matching-subseqs, re-replace 같은 단어들요. 이번 연습 문제에서는 정규 표현식 가운데 구조와 문맥을 표현하는 부분을 다뤄요. 이들은 같은 regexp 어휘에 속하고, 같은 R/ / 리터럴로 작성해요.
앵커는 문자 대신 위치를 매칭해요.
^ ! the start of the string
$ ! the end of the string
\b ! a word boundary
$는 문자열의 끝을 의미하므로, 달러 기호 자체를 나타낼 때는 \$라고 써요.
단어 경계(\b)는 단어 문자가 단어 문자가 아닌 것과 만나는 지점이에요. 단어 문자는 글자와 숫자, 밑줄이고, 그 밖의 모든 것(공백, 구두점, 문자열의 양 끝)은 경계로 쳐요. 그래서 \bcat\b는 cat이 단독으로 있을 때만 매칭해요.
USING: regexp ;
"cat nap" R/ \bcat\b/ re-contains? . ! => t
"scatter" R/ \bcat\b/ re-contains? . ! => f (cat sits inside a word)
소괄호 ( )는 패턴의 일부를 묶어서, 수량자나 |("이것 또는 저것")가 그룹 전체에 적용되도록 해요.
USING: regexp ;
"abab" R/ (ab)+/ matches? . ! => t (+ applies to "ab")
"dog" R/ cat|dog/ matches? . ! => t (either word)
"$100.50" R/ \$\d+(\.\d{2})?/ matches? . ! => t
"$100" R/ \$\d+(\.\d{2})?/ matches? . ! => t (the group is optional)
마지막 패턴은 "달러 기호, 하나 이상의 숫자, 그다음 마침표와 두 자리 숫자로 이루어진 선택적 그룹"이라고 읽어요.
참고: Factor에서 그룹은 패턴을 구성할 뿐이에요. 일부 정규식 도구와 달리, 그룹이 매칭한 텍스트를 다시 꺼낼 수 없어요. 캡처 그룹 추출이 없고,
\1같은 역참조도 지원하지 않아요. 어떤 기준점 주변의 텍스트를 뽑아내려면, 대신 룩어라운드(아래)를 사용해요.
룩어라운드는 현재 위치 바로 앞이나 바로 뒤에 오는 것을 확인하지만, 그 문자들을 매칭의 일부로 만들지 않아요. "너비가 0"이기 때문에, 검사하는 내용이 결과에 포함되지 않아요.
(?=...) ! lookahead: ... must follow
(?!...) ! negative lookahead: ... must NOT follow
(?<=...) ! lookbehind: ... must come before
(?<!...) ! negative lookbehind: ... must NOT come before
룩어라운드는 선택적이에요. 같은 문자라도 옆에 무엇이 있는지에 따라 매칭되기도 하고 안 되기도 해요. 평범한 \d+는 모든 숫자를 잡아요. 룩비하인드와 룩어헤드는 알맞은 이웃을 가진 것만 남겨요.
USING: regexp ;
"3 for $10 or 10 for $30" R/ \d+/ all-matching-subseqs .
! => { "3" "10" "10" "30" } (every number)
"3 for $10 or 10 for $30" R/ (?<=\$)\d+/ all-matching-subseqs .
! => { "10" "30" } (only the prices — the bare 3 and 10 drop)
"buy 2 at 15% off, 4 at 30% off" R/ \d+(?=%)/ all-matching-subseqs .
! => { "15" "30" } (only the discounts — the bare 2 and 4 drop)
닫는 / 뒤에 문자를 붙이면 패턴 전체의 동작이 바뀌어요. i는 대소문자를 구분하지 않게 해요.
USING: regexp ;
"REFUND" R/ refund/i matches? . ! => t
"Refund" R/ refund/i matches? . ! => t
all-matching-subseqs는 매칭된 텍스트를 그대로 줘요. map-matches는 한 걸음 더 나아가, 각 매칭에 대해 콰테이션을 실행하고 결과를 모아요. 콰테이션은 매칭의 start 인덱스, end 인덱스, 그리고 전체 문자열을 받아요. subseq가 이 세 가지를 매칭된 텍스트로 바꿔 주고, 그다음 변환하면 돼요.
map-matches ( string regexp quot: ( start end string -- obj ) -- seq )
USING: math regexp sequences ;
"a1 b22 c333" R/ \d+/ [ subseq length ] map-matches .
! => { 1 2 3 } (the length of each run of digits)
부수 효과만을 위한 사촌 격인 each-match는 아무것도 모으지 않고 각 매칭에 대해 콰테이션을 실행해요.
작은 회사의 장부를 관리하고 있는데, 매일 거래 내역이 정리되지 않은 텍스트 줄로 들어와요. 정규 표현식으로 금액이 올바른지 검증하고, 숫자를 표시하는 기호를 기준으로 값을 뽑아내고, 자세히 살펴봐야 할 줄에 표시를 달 거예요.
올바른 형식의 금액은 $와 하나 이상의 숫자, 그리고 선택 사항인 소수 부분으로 이루어져요. 소수 부분은 점 뒤에 정확히 두 자리 숫자가 오는 형태예요. $100 또는 $100.50은 올바른 금액이지만, $100.5는 아니에요. valid-amount?를 정의해서, 전체 문자열이 올바른 형식의 금액일 때 t를 반환하게 해요.
"$100.50" valid-amount? . ! => t
"$100" valid-amount? . ! => t
"$100.5" valid-amount? . ! => f
dollar-amounts를 정의해서, 줄에서 $ 바로 뒤에 오는 모든 숫자를 순서대로 반환하게 해요. 이때 $는 빼고 숫자만 반환해요.
"spent $100 and $25 today" dollar-amounts .
! => { "100" "25" }
percentages를 정의해서, 바로 뒤에 %가 오는 모든 숫자를 반환하게 해요.
"up 5% then down 12%" percentages .
! => { "5" "12" }
flagged?를 정의해서, 대소문자가 어떻게 섞여 있든 줄에 refund나 chargeback이 나오면 t를 반환하게 해요.
"Issued a REFUND today" flagged? . ! => t
"a normal sale" flagged? . ! => f
Exercism에 가입하고 Factor 트랙을 개념 47개연습 문제 163개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.