고민의 흐름대로 작성함 (GitHub)
https://school.programmers.co.kr/learn/courses/30/lessons/12930 |
---|
func solution(_ s:String) -> String {
return ''
}
문자열을 쪼개는 메서드는 여러가지가 있다. 그 중에서 components()
메서드를 사용하기로 했다.
입력값에서 각 단어들을 공백 기준으로 나눠서 각각 처리한 후, 다시 하나의 String으로 합쳐 반환하려고 하였다.
let words = str.components(separatedBy: .whitespacesAndNewlines)
var processedWords: [String] = []
for word in words {
var processedWord = ""
for (index, char) in word.enumerated() {
if index % 2 == 0 {
processedWord.append(char.uppercased())
} else {
processedWord.append(char.lowercased())
}
}
processedWords.append(processedWord)
}
↓정제: map() / enumerated() / joined() / 삼항연산자, 메서드 체이닝 활용
let words = str.components(separatedBy: .whitespacesAndNewlines)
let processedWords = words.map {
$0.enumerated()
.map({ (index, char) in index % 2 == 0 ? char.uppercased() : char.lowercased() })
.joined()
}
그런데 막상 다시 String으로 돌려보내려니 막막하였다.
기존의 공백을 유지할 수 없었기 때문이다.
입력값 | 기대되는 반환값 |
---|---|
"try hello world" | "TrY HeLlO WoRlD" |
"whats up dog" | "WhAtS Up DoG" |
단어의 구분 기준이 공백이 " "
하나로만 고정되어있다면 문제 없지만, 문제에서 제공하는 입력값의 문자열은 하나 '이상'의 공백문자로 단어가 구분되어 있다고 했다. components(separatedBy: .whitespacesAndNewlines)
에서 " "
가 아닌 .whitespacesAndNewlines
를 넣은 것은 그런 연유에서이다.
이를 해결하기 위해 입력값의 문자열에서 각 단어를 range(of:options:)
를 사용해 범위를 찾고 그 범위를 대체하려고 했으나, 이론상 가능하겠지만 그다지 효율적이지 않다는 생각이 들었다. 기존의 위치 정보를 보존하고 특정 부분만 떼어내어 처리 후 돌려주고 싶은데..
그럴바에야 제일 처음 단어를 추출할 때 해당 단어의 range
를 함께 찾아주는 게 좋다고 판단했다.
1) range()
메서드는 Range<String.Index>?
타입의 값을 반환한다.
정규식 \\S+
를 사용해 공백이 아닌 문자열을 찾고, 그 문자열을 Range<String.Index>
값과 함께 꺼내면 될 것 같다.
2) 어라? 근데 range()
메서드를 쓰는 시점에서, 그 자리에서 해당 문자열을 Substring
으로 꺼내서 바꾸고 다시 넣어줄 순 없을까? 하지만 range()
는 조건에 맞는 첫 번째 결과만 검색한다. NSRegularExpression
을 쓰면 다 찾을 수 있다고 하는데 공부가 너무 길어질 것 같다.. 일단 보류! 앞에서 한 단어씩 추출해야겠다.
하나씩 단어를 추출해 처리하는 식을 쓰다보니 작은 애니메이션이 상상되었다.
마치 기다란 줄이 함수를 통과하면서 머리부터 나오는 그림이다.
굳이 문자로 표현해보자면 이 표처럼..
반복횟수 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
input | "aaaa bbbb cccc" | "bbbb cccc" | "cccc" | "" |
output | "" | "AaAa " | "AaAa BbBb " | "AaAa BbBb CcCc" |
기존의 문자열을 머리부터 넣어서, 요구하는 조건에 맞춰 단어는 변환해주고, 그대로 새로운 문자열을 이어서 뽑아내는 것이다.
3) 하지만 이 방식대로 함수를 짜다보니 복잡해보였다.
일단 Range<String.Index> 값을 다루는 것 자체가 너무 어색하다.
배운 내용을 억지로 쓰려고 하고 있는 건 아닌가? 문자열 인덱스는 다루기 복잡하니, 정수형 인덱스로 다루고 싶은데..
문자열을 "단어 부분"과 "공백 부분"으로 나눠서 배열로 치환하자.
그 다음 단어 부분만 바꿔서 붙이면 될 것이다.
단어 부분과 공백 부분으로 나누려면 어떻게 해야하지?
그런데 검색해보니 공부를 보류하기로 한 NSRegularExpression
가 또 나왔다...;;
이래저래 생각해봐도 문자열에 접근할 때 단어 단위로 접근하려고 하니 복잡해진 것 같다.
String.Element
, 즉 Character
단위로 접근해서 문자를 변환해가는 알고리즘을 만드는 게 낫겠다고 판단했다.
반복횟수 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... |
---|---|---|---|---|---|---|---|---|
input | "aaaa bbbb cccc" | "aaa bbbb cccc" | "aa bbbb cccc" | "a bbbb cccc" | " bbbb cccc" | "bbbb cccc" | "bbb cccc" | ... |
output | "" | "A" | "Aa" | "AaA" | "AaAa" | "AaAa " | "AaAa B" | ... |
func solution(_ str: String) -> String {
var mutableString = ""
var shouldCapitalize = true
for char in str.lowercased() {
if char.isWhitespace {
mutableString.append(char)
shouldCapitalize = true
} else {
shouldCapitalize ? mutableString.append(char.uppercased()) : mutableString.append(char)
shouldCapitalize.toggle()
}
}
return mutableString
}
막상 그렇게 접근하니 간단히 정리되었다.
나름 생각도 많이 하고 복잡했는데.. 그래서 글도 이렇게 써가면서 했는데.. 좀 허무하다.
하지만 처음인데 헤매는 게 당연하다고 생각하기로 했다.
여러가지 고민을 하면서 각 메서드의 한계점이나 다른 방법들의 가능성을 검토할 수 있었다.
NSRegularExpression
을 사용하면 정규식을 통해 많은 단어를 동시에 처리할 수 있고, 정규식 엔진이 알아서 최적화를 해준다고 한다.
문자열이 특정 패턴의 단어 단위로 정말 길어진다면 초기화 비용을 감수해도 좋을지 모른다.
아무튼 이에 대한 공부/삽질은 여기까지 해야겠다!! 머리 아프다. xP
Substring
은 Swift에서 String
의 일부를 나타내는 타입이다. String
에서 특정 부분을 추출할 때 생성되며, 원본 String
의 메모리를 공유한다. 이는 성능 최적화를 위해 설계된 것으로, 메모리 복사를 최소화하여 문자열 작업을 효율적으로 수행할 수 있다.
Substring
은 원본 String
의 일부를 참조하며, 새로운 메모리를 할당하지 않는다.Substring
은 보통 짧은 기간 동안 사용된다. 원본 String
이 해제되면 Substring
도 유효하지 않게 된다.String
으로 변환해야 한다.String
과 Substring
의 차이점특징 | String | Substring |
---|---|---|
목적 | 독립적인 문자열 관리 | 원본 문자열의 일부를 효율적으로 참조 |
메모리 관리 | 별도의 메모리 공간을 할당 | 원본 String 의 메모리를 참조 |
사용 수명 | 독립적이며 장기간 사용 가능 | 원본 String 이 해제되면 유효하지 않음 |
변환 필요성 | 불필요 | 장기간 사용 시 String 으로 변환 필요 |
성능 | 더 많은 메모리 소비 | 메모리 복사를 피하므로 더 효율적 |
변경 가능 여부 | String 은 불변이지만 복사 후 수정 가능 | 자체 수정은 불가능하며, 수정하려면 변환 필요 |
// 1. Substring 사용 예시
let fullString = "Hello, Swift!"
if let commaIndex = fullString.firstIndex(of: ",") {
let substring = fullString[..<commaIndex] // "Hello"
print(substring) // 출력: "Hello"
}
// 2. Substring을 String으로 변환
let stringVersion = String(substring)
print(stringVersion) // "Hello"
String
과 Substring
을 사용해야 할까?Substring
사용이 적합한 경우:
String
사용이 적합한 경우:
Substring
을 독립적으로 다루어야 하는 경우(메모리 공유를 피하고 싶을 때).split(separator:)
let sentence = "Swift is fun"
let words = sentence.split(separator: " ")
print(words) // ["Swift", "is", "fun"]
Substring
배열로 반환된다.components(separatedBy:)
let sentence = "Swift,is,fun"
let words = sentence.components(separatedBy: ",")
print(words) // ["Swift", "is", "fun"]
String
배열을 반환한다.prefix(while:)
/ suffix(while:)
let sentence = "Swift is fun"
let prefix = sentence.prefix(while: { $0 != " " })
print(prefix) // "Swift"
Substring
을 반환한다.enumerateSubstrings(in:options:_:)
string.enumerateSubstrings(in: string.startIndex..<string.endIndex, options: .byWords) { (substring, _, _, _) in
print(substring)
}
let sentence = "Swift is fun."
sentence.enumerateSubstrings(in: sentence.startIndex..<sentence.endIndex, options: .byWords) { (substring, _, _, _) in
print(substring ?? "")
}
// 출력: "Swift", "is", "fun."
.byWords
: 단어 단위로 분리..bySentences
: 문장 단위로 분리..byLines
: 줄 단위로 분리..byComposedCharacterSequences
: 개별 문자(유니코드 시퀀스) 단위.Substring?
)을 하나씩 전달한다.replacingOccurrences(of:with:)
let sentence = "Hello World"
let modified = sentence.replacingOccurrences(of: " ", with: ",")
print(modified) // "Hello,World"
String
을 반환한다.range(of:options:)
사용if let range = string.range(of: "\\s+", options: .regularExpression) {
// range를 이용하여 문자열을 나눔
}
let sentence = "Swift is fun"
if let range = sentence.range(of: "\\s+", options: .regularExpression) {
print("First match:", sentence[range]) // " " (첫 번째 공백 패턴)
}
let words = sentence.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
print(words) // ["Swift", "is", "fun"]
range(of:options:)
는 문자열 내에서 특정 텍스트나 정규식 패턴을 검색하여 첫 번째 일치 범위를 반환하는 유용한 메서드이다. 다양한 검색 옵션을 통해 유연한 문자열 매칭이 가능하다.
func range(
of searchString: String,
options mask: String.CompareOptions = [],
range searchRange: Range<String.Index>? = nil,
locale: Locale? = nil
) -> Range<String.Index>?
of searchString
"Swift"
, "\\d+"
(정규식)options mask
(기본값: []
)
range searchRange
(기본값: nil
)
locale
(기본값: nil
)
nil
일 경우, 사용자의 기본 로케일을 사용.Range<String.Index>?
:Range<String.Index>
)를 반환.nil
반환.String.CompareOptions
)다양한 검색 방식을 제어할 수 있다.
옵션 | 설명 |
---|---|
.caseInsensitive | 대소문자를 구분하지 않고 검색. |
.literal | 문자열을 리터럴로 처리 (특수 문자도 그대로). |
.backwards | 뒤에서부터 검색. |
.anchored | 문자열의 시작 또는 끝에서 고정된 위치만 검색. |
.numeric | 숫자를 숫자로 취급하여 비교. |
.diacriticInsensitive | 발음 구별 기호(é vs e)를 무시하고 비교. |
.widthInsensitive | 폭이 다른 문자(全 vs 全: 일반 vs 좁은 폭)를 동일하게 취급. |
.regularExpression | 정규식 패턴으로 검색. |
// 1. 단순 텍스트 검색
let text = "Swift is amazing!"
if let range = text.range(of: "Swift") {
print("Matched:", text[range]) // "Swift"
}
// 2. 대소문자 구분 없이 검색
let text = "Swift is amazing!"
if let range = text.range(of: "swift", options: .caseInsensitive) {
print("Matched:", text[range]) // "Swift"
}
// 3. 문자열 끝에서부터 검색
let text = "Hello, Hello, Hello!"
if let range = text.range(of: "Hello", options: .backwards) {
print("Last Match:", text[range]) // "Hello" (마지막 "Hello")
}
// 4. 정규식 사용
let text = "I have 123 apples and 45 bananas."
if let range = text.range(of: "\\d+", options: .regularExpression) {
print("Matched:", text[range]) // "123" (첫 번째 숫자)
}
// 5. 특정 범위에서 검색
let text = "Swift programming is fun!"
let rangeToSearch = text.startIndex..<text.index(text.endIndex, offsetBy: -4)
if let range = text.range(of: "is", range: rangeToSearch) {
print("Matched in range:", text[range]) // "is"
}
// 6. 발음 구별 기호 무시 검색
let text = "Café"
if let range = text.range(of: "Cafe", options: .diacriticInsensitive) {
print("Matched:", text[range]) // "Café"
}
일치 여부만 확인:
반환값이 nil
인지 아닌지로 일치 여부를 확인할 수 있다.
if text.range(of: "target") != nil {
print("Found!")
}
일치한 문자열 추출:
반환된 Range
를 이용해 부분 문자열을 추출할 수 있다.
let match = text[range] // 일치한 부분만 추출
정규식을 활용한 복잡한 패턴 검색:
if let range = text.range(of: "\\b\\w+@\\w+\\.com\\b", options: .regularExpression) {
print("Matched Email:", text[range])
}
range(of:options:)
는 문자열 검색과 정규식 매칭에서 매우 강력한 메서드이다.
주요 특징은 다음과 같다:
Range
를 활용하여 문자열의 특정 부분을 쉽게 추출할 수 있음.정규식(Regular Expressions)은 문자열에서 특정 패턴을 검색, 매칭, 변환하는 데 사용되는 강력한 도구이다. Swift를 포함한 대부분의 프로그래밍 언어에서 사용되며, 복잡한 문자열 작업을 간단하게 처리할 수 있다.
정규식이란?
문자열에서 특정 패턴을 정의하고, 이를 이용해 해당 패턴과 일치하는 부분을 찾거나 변환하는 데 사용된다.
주요 용도:
표현식 | 의미 | 예시 |
---|---|---|
. | 임의의 한 문자 (줄 바꿈 제외) | a.c → "abc", "adc" 등 |
^ | 문자열 시작 | ^Hello → "Hello"로 시작 |
$ | 문자열 끝 | world$ → "world"로 끝 |
* | 0회 이상 반복 | a* → "", "a", "aaa" |
+ | 1회 이상 반복 | a+ → "a", "aaa" |
? | 0회 또는 1회 | a? → "", "a" |
{n} | 정확히 n번 반복 | a{3} → "aaa" |
{n,} | 최소 n번 반복 | a{2,} → "aa", "aaa" |
{n,m} | 최소 n번, 최대 m번 반복 | a{2,4} → "aa", "aaa", "aaaa" |
[abc] | a, b, c 중 하나 | [abc] → "a", "b", "c" |
[^abc] | a, b, c가 아닌 문자 | [^abc] → "d", "e" |
[a-z] | 소문자 알파벳 범위 | [a-z] → "a", "b", ... "z" |
\\d | 숫자 (0-9) | \\d+ → "123" |
\\D | 숫자가 아닌 문자 | \\D+ → "abc" |
\\w | 단어 문자 (알파벳, 숫자, _) | \\w+ → "abc123" |
\\W | 단어 문자가 아닌 문자 | \\W+ → "@" |
\\s | 공백 문자 (스페이스, 탭, 줄바꿈) | \\s+ → 공백 |
\\S | 공백이 아닌 문자 | \\S+ → "Hello" |
| | OR 연산자 | cat|dog → "cat" 또는 "dog" |
( ) | 그룹 | (ab)+ → "abab" |
(?: ) | 비캡처 그룹 (그룹화는 하지만 캡처하지 않음) | (?:ab)+ → "abab" |
특정 패턴이 문자열에 포함되어 있는지 확인.
let text = "The price is 100 dollars."
if let range = text.range(of: "\\d+", options: .regularExpression) {
print("Matched:", text[range]) // "100"
}
입력된 문자열이 특정 형식(예: 이메일, 전화번호)을 따르는지 확인.
let email = "test@example.com"
let emailPattern = "[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}"
let isValid = email.range(of: emailPattern, options: [.regularExpression, .caseInsensitive]) != nil
print(isValid) // true
특정 패턴을 다른 문자열로 변환.
let sentence = "Swift 5 is amazing."
let result = sentence.replacingOccurrences(of: "\\d", with: "X", options: .regularExpression)
print(result) // "Swift X is amazing."
특정 패턴 기준으로 문자열 나누기.
let sentence = "apple, banana; orange grape"
let components = sentence.components(separatedBy: "[,;\\s]+".regularExpression)
print(components) // ["apple", "banana", "orange", "grape"]
range(of:options:)
replacingOccurrences(of:with:options:)
components(separatedBy:)
NSRegularExpression
클래스복잡한 정규식 매칭과 변환 작업을 위해 사용.
matches(in:options:range:)
: 모든 매칭 결과 반환.stringByReplacingMatches(in:options:range:withTemplate:)
: 정규식을 기반으로 문자열 변환.import Foundation
let text = "User123 has logged in with ID456."
let regex = try NSRegularExpression(pattern: "\\d+")
let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
for match in matches {
if let range = Range(match.range, in: text) {
print("Matched:", text[range]) // "123", "456"
}
}
range(of:options:)
, replacingOccurrences(of:with:options:)
등의 메서드와 함께 사용.NSRegularExpression
을 사용하여 처리 가능.
문제 하나에서 정말 깊고 다양하게 파고 들어가시네요!!
궁금한 거 여기 벨로그에 검색하면 되겠어요