사이드 프로젝트 개발을 하면서 문자열 String이 숫자를 포함하고 있는지 여부를 알고 싶었습니다.
이전 글에서 실시간 버스의 도착 정보에 대한 메세지가 총 4가지 종류로 제공된다고 했습니다.
- 1️⃣ "곧 도착"
- 2️⃣ "00분00초후[0번째 전]"
- 3️⃣ "운행종료"
- 4️⃣ "출발대기"
그리고 각각의 응답 메세지 형식에 따라서 아래와 같은 enum 타입으로 분류해준다고 했었죠.
enum ArrivalStatus: Comparable {
case arriveSoon // 곧 도착
case coming(remainingSecond: Int) // 운행중(남은시간(초))
case waiting // 출발대기
case finished // 운행종료
case unknown // 알 수 없음
}
그렇기 때문에 적절한 ArrivalStatus로 변환할 수 있도록 String의 내용을 분석해줄 필요가 있었습니다.
2️⃣의 경우에는 유일하게 문자열에 숫자가 포함됩니다.
그리고 숫자가 포함된다면 문자열에서 00분00초의 패턴을 추출하여 남은 시간을 구해줘야 합니다. (.coming 케이스의 연관값으로 전달하기 위해)
저는 아래와 같이 동작하는 로직을 설계하게 되었습니다.

여기서 로직 구현이 필요한 부분은
- 숫자 포함 여부 확인
- "00분00초" → 패턴 추출
이렇게 2가지가 됩니다.
각각의 로직을 어떻게 구현했는지 설명드리겠습니다.
📌 String의 숫자 포함 여부 확인
숫자 포함 여부 확인에 저는 String의 rangeOfCharacter(from:options:range:) 메서드를 활용했습니다.
(Foundation 프레임워크를 임포트해야 사용 가능)

해당 메서드는 파라미터로 설정한 옵션에 따라 문자열을 탐색하며, 문자열의 주어진 범위 내에서 주어진 문자 집합에 해당하는 범위를 구해줍니다. 가장 먼저 발견한 곳의 range만 반환하게 되며, 반환 타입이 옵셔널이기 때문에 매칭되는 곳이 없으면 nil을 반환합니다.
파라미터는 from을 제외하고는 필수가 아니며, 저는 from 파라미터만 사용했습니다.
각각의 파라미터 용도는 이렇습니다.
- from : 검색하고자 하는 문자를 나타내는 CharacterSet
- options : 문자열 검색 옵션 (검색 방향 등을 설정할 수 있습니다.)
- range : 검색할 범위 설정

어쨋든 해당 String 내부에 지정한 CharacterSet에 매칭되는 곳이 있다면 그 부분의 range값이 반환될 것이고, 저는 반환되는 값이 있는지 없는지만 확인하면 숫자 포함 여부를 구할 수 있습니다.
from 파라미터로 숫자에 대한 CharacterSet을 넘겨줘야 하는데, 저는 타입 프로퍼티로 기본적으로 제공되는 .decimalDigits를 사용했습니다.

이런 식으로 사용할 수 있습니다.

저는 String 익스텐션에 이렇게 메서드를 작성했습니다.
rangeOfCharacter의 결과가 nil인지 아닌지만 확인하고, Bool값으로 반환해줍니다.
// String이 숫자를 포함하는지 여부를 반환하는 메서드
func isContainsNumber() -> Bool {
return self.rangeOfCharacter(from: .decimalDigits) != nil
}
📌 String에서 특정 패턴 추출
이제 숫자 포함 여부는 확인할 수 있게 되었습니다.
이번에는 문자열에서 특정 패턴을 추출해야 합니다. 저의 경우는 "00분"과 "00초"의 패턴을 찾아서 분·초 값을 숫자로 뽑아내야 했죠.
사실 먼저 ChatGPT 선생님께 도움을 요청했고, 받은 코드를 다듬어서 String 익스텐션으로 아래의 메서드를 만들었습니다.
베끼기만 하는건 도움이 되지 않으니 어떻게 동작하는지를 살펴봅시다.
// 서울시 버스 도착 정보의 BusArrival에 있는 arrivalMessage로부터 남은 시간을 구해서 Int타입의 초 값으로 반환하는 메서드
func getSeoulBusRemainingSecond() -> Int {
let minutePattern = "(\\d+)분"
let secondPattern = "(\\d+)초"
guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
var minutes = 0
var seconds = 0
// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(minuteMatch.range(at: 1), in: self) {
minutes = Int(self[range]) ?? 0
}
}
// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(secondMatch.range(at: 1), in: self) {
seconds = Int(self[range]) ?? 0
}
}
return (minutes * 60) + seconds
}
먼저 "O분"과 "O초" 에 대한 정규식 패턴을 정의해줍니다.
let minutePattern = "(\\d+)분"
let secondPattern = "(\\d+)초"
해당 패턴이 의미하는 바는 아래와 같습니다.
- \\d : "\d"는 숫자 하나(digit)를 나타내는 정규표현식입니다. "\"는 정규표현식에서 특수한 의미를 갖기 때문에 백슬래시를 2개 사용해서 이스케이핑 처리를 해줬습니다. 👉 숫자 하나
- + : 앞의 패턴이 하나 이상 반복될 수 있음을 의미합니다. 앞의 \\d패턴이 하나 이상 반복될 수 있다는 의미가 되겠죠. 👉 하나 이상의 숫자가 올 수 있음
- () : 캡처 그룹을 의미하며, 요구하는 숫자 패턴과 매칭되는 부분을 묶음처리한다고 생각해주면 됩니다.
결과적으로 이 정규표현식과 매칭되는 패턴은 이렇습니다.
- 하나 이상의 숫자 바로 뒤에 "분"이나 "초"라는 문자가 있음
- ex) 15분, 3분, 40초, 3초...
이제 정규표현식을 사용하기 위해 NSRegularExpression 객체를 생성해줍니다. 앞서 정의한 패턴을 파라미터로 넘겨주고, 잘못된 정규표현식을 넘길 경우 에러가 발생하기 때문에 NSRegularExpression 객체 생성 시에는 try문을 사용해줍니다.
guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
이제 각각의 패턴을 찾아서 분과 초를 추출합니다.
- NSRegularExpression의 firstMatch(in:options:range:) 메서드를 사용해줍니다.
- 이 메서드는 문자열에서 정규표현식 패턴과 일치하는 첫번째 문자열을 반환해줍니다.
- minuteMatch.range(at: 1)는 캡처 그룹 1에 해당하는 범위를 가져오게 되는데,
- 정규표현식 "(\\d+)분"에서 숫자 부분 \d+를 나타냅니다.
- 마지막으로 매칭된 부분을 Range 객체로 변환하고, 문자열의 서브스트링에서 Int값을 추출해줍니다.
var minutes = 0
var seconds = 0
// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(minuteMatch.range(at: 1), in: self) {
minutes = Int(self[range]) ?? 0
}
}
// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(secondMatch.range(at: 1), in: self) {
seconds = Int(self[range]) ?? 0
}
}
이제 아래처럼 사용할 수 있게 됩니다.
import Foundation
let arrivalMessage1 = "14분30초후[6번째전]"
let arrivalMessage2 = "1분4초후[1번째전]"
extension String {
func getSeoulBusRemainingSecond() -> Int {
let minutePattern = "(\\d+)분"
let secondPattern = "(\\d+)초"
guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
var minutes = 0
var seconds = 0
// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(minuteMatch.range(at: 1), in: self) {
minutes = Int(self[range]) ?? 0
}
}
// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(secondMatch.range(at: 1), in: self) {
seconds = Int(self[range]) ?? 0
}
}
return (minutes * 60) + seconds
}
}
arrivalMessage1.getSeoulBusRemainingSecond() // 870
arrivalMessage2.getSeoulBusRemainingSecond() // 64
실제 활용 코드
이제 실제로 프로젝트에서 사용한 코드를 보여드리겠습니다.
버스 도착 정보 메세지를 받아서 위의 메서드들을 활용하여 결과를 ArrivalStatus 타입으로 반환해주었습니다.
// 버스의 도착 상태를 구해서 BusArrivalStatus 타입 값을 반환하는 메서드
func getBusArrivalStatusFromSeoulBusStation(arrivalMessage: String) -> ArrivalStatus {
if arrivalMessage.contains("출발대기") {
return .waiting
} else if arrivalMessage.contains("운행종료") {
return .finished
} else if arrivalMessage.contains("곧 도착") {
return .arriveSoon
} else if arrivalMessage.isContainsNumber() {
return .coming(remainingSecond: arrivalMessage.getSeoulBusRemainingSecond())
} else {
return .unknown
}
}
레퍼런스
https://tngusmiso.tistory.com/62
https://borabong.tistory.com/32
'iOS' 카테고리의 다른 글
[ iOS ] Keychain 사용해보기 (0) | 2024.06.07 |
---|---|
[ iOS ] Comparable 프로토콜을 사용하여 열거형 값을 비교하기 (0) | 2024.05.30 |
[iOS] SwiftUI의 TextField에 Clear Button 추가하기 (0) | 2024.05.28 |
[iOS] Build input file cannot be found: '~/Info.plist' 에러 발생 시 (0) | 2024.05.05 |
[iOS] Core Location 테스트하기 (외부 의존성의 응답을 테스트하기) (0) | 2024.05.03 |
사이드 프로젝트 개발을 하면서 문자열 String이 숫자를 포함하고 있는지 여부를 알고 싶었습니다.
이전 글에서 실시간 버스의 도착 정보에 대한 메세지가 총 4가지 종류로 제공된다고 했습니다.
- 1️⃣ "곧 도착"
- 2️⃣ "00분00초후[0번째 전]"
- 3️⃣ "운행종료"
- 4️⃣ "출발대기"
그리고 각각의 응답 메세지 형식에 따라서 아래와 같은 enum 타입으로 분류해준다고 했었죠.
enum ArrivalStatus: Comparable {
case arriveSoon // 곧 도착
case coming(remainingSecond: Int) // 운행중(남은시간(초))
case waiting // 출발대기
case finished // 운행종료
case unknown // 알 수 없음
}
그렇기 때문에 적절한 ArrivalStatus로 변환할 수 있도록 String의 내용을 분석해줄 필요가 있었습니다.
2️⃣의 경우에는 유일하게 문자열에 숫자가 포함됩니다.
그리고 숫자가 포함된다면 문자열에서 00분00초의 패턴을 추출하여 남은 시간을 구해줘야 합니다. (.coming 케이스의 연관값으로 전달하기 위해)
저는 아래와 같이 동작하는 로직을 설계하게 되었습니다.

여기서 로직 구현이 필요한 부분은
- 숫자 포함 여부 확인
- "00분00초" → 패턴 추출
이렇게 2가지가 됩니다.
각각의 로직을 어떻게 구현했는지 설명드리겠습니다.
📌 String의 숫자 포함 여부 확인
숫자 포함 여부 확인에 저는 String의 rangeOfCharacter(from:options:range:) 메서드를 활용했습니다.
(Foundation 프레임워크를 임포트해야 사용 가능)

해당 메서드는 파라미터로 설정한 옵션에 따라 문자열을 탐색하며, 문자열의 주어진 범위 내에서 주어진 문자 집합에 해당하는 범위를 구해줍니다. 가장 먼저 발견한 곳의 range만 반환하게 되며, 반환 타입이 옵셔널이기 때문에 매칭되는 곳이 없으면 nil을 반환합니다.
파라미터는 from을 제외하고는 필수가 아니며, 저는 from 파라미터만 사용했습니다.
각각의 파라미터 용도는 이렇습니다.
- from : 검색하고자 하는 문자를 나타내는 CharacterSet
- options : 문자열 검색 옵션 (검색 방향 등을 설정할 수 있습니다.)
- range : 검색할 범위 설정

어쨋든 해당 String 내부에 지정한 CharacterSet에 매칭되는 곳이 있다면 그 부분의 range값이 반환될 것이고, 저는 반환되는 값이 있는지 없는지만 확인하면 숫자 포함 여부를 구할 수 있습니다.
from 파라미터로 숫자에 대한 CharacterSet을 넘겨줘야 하는데, 저는 타입 프로퍼티로 기본적으로 제공되는 .decimalDigits를 사용했습니다.

이런 식으로 사용할 수 있습니다.

저는 String 익스텐션에 이렇게 메서드를 작성했습니다.
rangeOfCharacter의 결과가 nil인지 아닌지만 확인하고, Bool값으로 반환해줍니다.
// String이 숫자를 포함하는지 여부를 반환하는 메서드
func isContainsNumber() -> Bool {
return self.rangeOfCharacter(from: .decimalDigits) != nil
}
📌 String에서 특정 패턴 추출
이제 숫자 포함 여부는 확인할 수 있게 되었습니다.
이번에는 문자열에서 특정 패턴을 추출해야 합니다. 저의 경우는 "00분"과 "00초"의 패턴을 찾아서 분·초 값을 숫자로 뽑아내야 했죠.
사실 먼저 ChatGPT 선생님께 도움을 요청했고, 받은 코드를 다듬어서 String 익스텐션으로 아래의 메서드를 만들었습니다.
베끼기만 하는건 도움이 되지 않으니 어떻게 동작하는지를 살펴봅시다.
// 서울시 버스 도착 정보의 BusArrival에 있는 arrivalMessage로부터 남은 시간을 구해서 Int타입의 초 값으로 반환하는 메서드
func getSeoulBusRemainingSecond() -> Int {
let minutePattern = "(\\d+)분"
let secondPattern = "(\\d+)초"
guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
var minutes = 0
var seconds = 0
// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(minuteMatch.range(at: 1), in: self) {
minutes = Int(self[range]) ?? 0
}
}
// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(secondMatch.range(at: 1), in: self) {
seconds = Int(self[range]) ?? 0
}
}
return (minutes * 60) + seconds
}
먼저 "O분"과 "O초" 에 대한 정규식 패턴을 정의해줍니다.
let minutePattern = "(\\d+)분"
let secondPattern = "(\\d+)초"
해당 패턴이 의미하는 바는 아래와 같습니다.
- \\d : "\d"는 숫자 하나(digit)를 나타내는 정규표현식입니다. "\"는 정규표현식에서 특수한 의미를 갖기 때문에 백슬래시를 2개 사용해서 이스케이핑 처리를 해줬습니다. 👉 숫자 하나
- + : 앞의 패턴이 하나 이상 반복될 수 있음을 의미합니다. 앞의 \\d패턴이 하나 이상 반복될 수 있다는 의미가 되겠죠. 👉 하나 이상의 숫자가 올 수 있음
- () : 캡처 그룹을 의미하며, 요구하는 숫자 패턴과 매칭되는 부분을 묶음처리한다고 생각해주면 됩니다.
결과적으로 이 정규표현식과 매칭되는 패턴은 이렇습니다.
- 하나 이상의 숫자 바로 뒤에 "분"이나 "초"라는 문자가 있음
- ex) 15분, 3분, 40초, 3초...
이제 정규표현식을 사용하기 위해 NSRegularExpression 객체를 생성해줍니다. 앞서 정의한 패턴을 파라미터로 넘겨주고, 잘못된 정규표현식을 넘길 경우 에러가 발생하기 때문에 NSRegularExpression 객체 생성 시에는 try문을 사용해줍니다.
guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
이제 각각의 패턴을 찾아서 분과 초를 추출합니다.
- NSRegularExpression의 firstMatch(in:options:range:) 메서드를 사용해줍니다.
- 이 메서드는 문자열에서 정규표현식 패턴과 일치하는 첫번째 문자열을 반환해줍니다.
- minuteMatch.range(at: 1)는 캡처 그룹 1에 해당하는 범위를 가져오게 되는데,
- 정규표현식 "(\\d+)분"에서 숫자 부분 \d+를 나타냅니다.
- 마지막으로 매칭된 부분을 Range 객체로 변환하고, 문자열의 서브스트링에서 Int값을 추출해줍니다.
var minutes = 0
var seconds = 0
// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(minuteMatch.range(at: 1), in: self) {
minutes = Int(self[range]) ?? 0
}
}
// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(secondMatch.range(at: 1), in: self) {
seconds = Int(self[range]) ?? 0
}
}
이제 아래처럼 사용할 수 있게 됩니다.
import Foundation
let arrivalMessage1 = "14분30초후[6번째전]"
let arrivalMessage2 = "1분4초후[1번째전]"
extension String {
func getSeoulBusRemainingSecond() -> Int {
let minutePattern = "(\\d+)분"
let secondPattern = "(\\d+)초"
guard let minuteRegex = try? NSRegularExpression(pattern: minutePattern) else { return 0 }
guard let secondRegex = try? NSRegularExpression(pattern: secondPattern) else { return 0 }
var minutes = 0
var seconds = 0
// 분 추출
if let minuteMatch = minuteRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(minuteMatch.range(at: 1), in: self) {
minutes = Int(self[range]) ?? 0
}
}
// 초 추출
if let secondMatch = secondRegex.firstMatch(in: self, range: NSRange(location: 0, length: count)) {
if let range = Range(secondMatch.range(at: 1), in: self) {
seconds = Int(self[range]) ?? 0
}
}
return (minutes * 60) + seconds
}
}
arrivalMessage1.getSeoulBusRemainingSecond() // 870
arrivalMessage2.getSeoulBusRemainingSecond() // 64
실제 활용 코드
이제 실제로 프로젝트에서 사용한 코드를 보여드리겠습니다.
버스 도착 정보 메세지를 받아서 위의 메서드들을 활용하여 결과를 ArrivalStatus 타입으로 반환해주었습니다.
// 버스의 도착 상태를 구해서 BusArrivalStatus 타입 값을 반환하는 메서드
func getBusArrivalStatusFromSeoulBusStation(arrivalMessage: String) -> ArrivalStatus {
if arrivalMessage.contains("출발대기") {
return .waiting
} else if arrivalMessage.contains("운행종료") {
return .finished
} else if arrivalMessage.contains("곧 도착") {
return .arriveSoon
} else if arrivalMessage.isContainsNumber() {
return .coming(remainingSecond: arrivalMessage.getSeoulBusRemainingSecond())
} else {
return .unknown
}
}
레퍼런스
https://tngusmiso.tistory.com/62
https://borabong.tistory.com/32
'iOS' 카테고리의 다른 글
[ iOS ] Keychain 사용해보기 (0) | 2024.06.07 |
---|---|
[ iOS ] Comparable 프로토콜을 사용하여 열거형 값을 비교하기 (0) | 2024.05.30 |
[iOS] SwiftUI의 TextField에 Clear Button 추가하기 (0) | 2024.05.28 |
[iOS] Build input file cannot be found: '~/Info.plist' 에러 발생 시 (0) | 2024.05.05 |
[iOS] Core Location 테스트하기 (외부 의존성의 응답을 테스트하기) (0) | 2024.05.03 |