Swift :: NSScanner 사용예제

NSScanner로 문자열로부터 특정한 값 뽑아내기

이 문서는 Swift 4.2를 기준으로 수정되었습니다. NSScanner에 대한 Swift Foundation 대응 클래스인 Scanner 클래스가 새로 정의되었으며, CharacterSet에 대한 몇 가지 사용 방법이 변경되었습니다

  1. 스캔할 문자열을 넘겨서 Scanner 객체를 생성한다.
  2. 스캐너가 무시하고 지나가야 할 문자들이 있으면 CharacterSet 타입의 값으로 만들어서 charactersToBeSkipped 속성으로 지정한다.
  3. 자, 이제 스캔을 시작하지.

이때, 스캐너의 API는 예전 Objective-C API에서 크게 바뀌지 않은 관계로 포인터를 인자로 받고, 넘겨 받은 포인터가 가리키는 객체에 스캔한 값을 쓰게 된다.

예를 들어 “123,456,789,100” 이라는 문자열에서 컴마로 구분된 각각의 정수값을 뽑아내고 싶다면 다음과 같이 처리할 수 있겠다.

let source = "123,456,789,100"
let scanner = Scanner(string:source)
scanner.charactersToBeSkipped = .punctuationCharacters

var a:Int = 0
while scanner.scanInteger(&a) {
    print(a)
}

콤마로 구분된 일련의 숫자들이 있는 문자열에서 각각의 정수값들을 뽑아내는 일을 하기 위해서 먼저 Scanner 객체를 생성한다. CharacterSet.punctuationCharacters는 문장부호들의 집합으로 미리 정의되어 있는 글자셋이다. 여기에 콤마가 포함되어 있으므로 이 글자를 무시하도록 하자.

  1. 첫 시도에서 스캐너의 스캔 위치는 0이며 이는 첫번째 글자 앞이다.
  2. 스캐너는 외부 변수에 값을 덤프해주려하기 때문에 별도의 변수 a 가 필요하다.
  3. scanInteger(&a) 가 실행되면 스캐너는 글자를 하나씩 읽어서 , 앞에서 멈춘다. 왜냐하면 ,는 정수로 번역할 수 없는 글자이기 때문이다. “123”까지 읽은 후 이를 정수 123으로 변환해서 변수 a의 값으로 만든다.
  4. 이 때 결과는 성공으로 true가 반환되기 때문에 while 루프 내부로 들어가서 a 값을 출력한다.
  5. 두 번째 시도에서 스캔위치는 3일 것이다.
  6. 다시 스캔을 시도할 때, “,“는 무시하고 지나간다. (toBeSkippedCharacters에 포함되어 있는 문자이므로)
  7. 마찬가지 방식으로 a 는 계속 값이 변하고 출력된다.
  8. 더 이상 스캔할 수 없게되면 while 루프가 종료될 것이다.
  1. 첫 시도에서 정수 123을 스캔한다. 여기서 커서?는 ,의 앞에 있다.
  2. 두 번째 시도에서 ,를 읽지만 스킵한다, 그 뒤 정수 456을 스캔한다.
  3. 하나의 값을 스캔하였으므로 “,”는 더이상 스킵하지 않는다. 커서는 6의 위치에 있다.
  4. 2~3의 과정을 반복한다

문제는 이러한 API가 딱히 Swift 친화적이지는 않다는 점이다. 이렇게 포인터를 넘겨 받는 함수들은 기존의 C 문법의 한계 때문에 이런 방식으로 디자인 된 것들이 많다. 스캔해야 하는 값과 스캔 성공 여부를 모두 리턴하고 싶은데, C 문법에서는 이런 동작이 불가능하기 때문이다.

하지만 Swift 라면 이야기가 다르다. Swift는 튜플을 이용해서 두 개의 값을 동시에 리턴할 수도 있고, 또 옵셔널이라는 대안이 존재하기 때문이다. 즉 스캔에 성공했다면 스캔한 값을 리턴하고 그렇지 않다면 nil을 리턴하는 것으로 실패여부까지 한 번에 전달하는 것이 가능하다.

따라서 다음과 같이 스캐너의 API를 확장하여 코드를 좀 더 간결하게 만들 수 있다.

import Foundation

extension Scanner 
{
  func scanDecimal() -> Decimal? {
    var a: Decimal = 0
    if scanDecimal(&a) { return a }
    return nil
  }
}

let str = "123,456,789"
let scanner = Scanner(string: str)
scanner.charactersToBeSkipped = .punctuationCharacters
while let a = scannner.scanDecimal() {
  print(a)
}


확인해볼 문제

-scanUpToCharactersFromSet:intoString: 에서 스캔이 끝나면 스캔 위치는 어디인지?

답 : 끝나야 하는 문자셋이나 문자열 직전까지 스캔했으며, 해당 문자열에서 계속 읽을 준비를 하게 된다.

let tobesplit = CharacterSet(charactersIn:"1")
let source = "2341234"
var s:NSString?

let e = Scanner(string:source)

e.scanUpToCharacters(from:tobesplit, into:&s) // "1"앞까지 읽음. 스캐너의 위치는 1 바로 앞에 있다.
print(s!) // 234
print(e.scanLocation) // 3 (1 바로 앞)

var i = 0
e.scanInt(&i)
print(i) // 1234