Wireframe

NSScanner

http://nshipster.com/nsscanner/

문자열은 모든 곳에 있고 컴퓨팅의 여러 부분을 담당하고 있다. 이메일, 에세이, 시, 소설, 이 글도 그렇고 심지어 블로그의 코드도 모두 문자열로 되어 있다. 문자열을 쪼개고 특정한 부분을 추출해 내는 능력은 프로그래밍에서 강력한 무기가 될 수 있다. 코코아는 문자열을 쪼개고 합치는 다양한 방법을 제공하는데, 그 중 몇 가지 유명한 예를 들자면,


NSScanner는 문자열의 래퍼처럼 사용하고, 내부의 문자열을 탐색해서 효과적으로 부분문자열 집합이나 숫자값등을 추출해 낼 수 있다. 이 동작을 제어하는 몇 가지 프로퍼티를 살펴보자면,

숫자값을 추출하기

스캐너 객체의 존재의 이유는 큰 문자열로부터 특정한 데이터를 검색하여 뽑아내는데 있다. 이를 위한 열 개가 넘는 메소드가 존재하는데, 이들은 공통된 패턴을 가진다. 즉 찾은 값을 반환할 포인터를 인자로 받고, 성공 여부를 리턴한다.

다음 예를 살펴보자.

import Foundation
var skip = CharacterSet.whitespacesAndNewlines
skip.formUnion(.punctuationCharacters)
// skip 은 공백 및 문장부호들의 합집합
// `합집합`이라는 표현을 썼다고 해서
// CharacterSet이 Set<Character>는 아니다!!!
let scanner = Scanner(string:"John & Paul & Ringo & George.")
scanner.charactersToBeSkipped = skip
var name: NSString?
while scanner.scanUpToCharacters(from:skip, into: &name) {
    print(name!)
}

문자열 스캐너는 다음과 같은 동작을 수행할 수 있는데, 기본적으로 문자열을 탐색할 수 있다.

숫자도 탐색 가능하다. 숫자 포맷에 따라서 비슷한 원리로 동작되는 메소드들을 가진다.

문자열 지역화와 NSScanner

NSScanner도 코코아 프레임워크의 일부이고, 이 프레임워크는 지역화를 지원하므로, NSScanner도 지역화를 지원할 수 있다. 이는 지역화된 스캐너를 생성하는 +localizedScannerWithString: 을 통해서 생성하거나 locale 프로퍼티를 설정하는 것으로 만들 수 있다.

double price;
NSScanner *gasPriceScanner = [[NSScanner alloc] initWithString:@"2.09 per callon"];
[gasPriceScanner scanDouble:&price];
// price --> 2.09
NSScanner *benzinPriceScanner = [[NSScanner alloc] initWithString:@"1,38 pro Liter"];
[benzinPriceScanner setLocale:[NSLocale localeWithLocaleIdentifier:@"de-DE"]];
[benzinPriceScanner scanDouble:&price];

예제, SVG 데이터 파싱하기

NSScanner를 활용할 수 있는 예로 SVG데이터를 예로 들어보겠다. SVG데이터는 각 좌표에서 다음 좌표로 이어지는 방법을 알리는 접두어(M, L, C)와 각 점의 좌표의 연속으로 이루어지며, 각 숫자값의 구분은 기본적으로 컴마이긴한데, 뒤의 숫자가 음수인 경우 컴마가 생략되기도 한다.

var svgPathData = "M28.2,971.4c-10,0.5-19.1,13.3-28.2,2.1c0,15.1,23.7,30.5,39.8,16.3c16,14.1,39.8-1.3,39.8-16.3c-12.5,15.4-25-14.4-39.8,4.5C35.8,972.7,31.9,971.2,28.2,971.4z"

좌표의 이동을 나타내기 위해 offset() 메소드를 CGPoint에 확장해보겠다.

extension CGPoint {
    func offset(p:CGPoint) -> CGPoint {
        return CGPoint(x: x+p.x, y:y+p.y)
    }
}

이 데이터의 규칙성을 찾기가 힘들다. x, y 좌표값은 주로 컴마로 구분되지만 간혹 그렇지 않은 경우도 있다. 이를 정규식으로 구분하는 것은 꽤 어려울 수 있다. 대신에 NSScanner를 사용하는 것은 조금 더 쉬울 수 있다. 아래 함수는 이런 데이터를 파싱하여 UIBezierPath를 생성할 수 있도록 한다.

func bezierPathFromSVGPath(str:String) -> UIBezierPath {
    let scanner = NSScanner(string: str)
    // 콤마랑 공백문자는 패스하도록 설정한다.
    let skipChars = NSMutableCharacterSet(charactersInString:",")
    skipChars.formUnionWithCharactersSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
    scanner.charactersToBeSkipped = skipChars
    var path = UIBesizerPath()
    let instructionSet = NSCharacterSet(charactersInString:"MCSQTAmcsqta") //데이터에 들어있을 컨트롤 문자들
    var instruction:NSString?
    while scanner.scanCharactersFromSet(instructionSet, intoString:&instruction) { // 먼저 컨트롤 문자를 뽑아내고,
        var x = 0.0, y = 0.0
        var points:[CGPoint] = []
        while scanner.scanDouble(&x) && scanner.scanDouble(&y) { //좌표값이 될 숫자 두 개를 뽑는다.
            points.append(CGPoint(x:x, y:y))
        }
        switch instruction ?? "" { // ?? 는 옵셔널 값이 nil 이면 뒤에 오는 값을, 아니면 해당 값을 언래핑한다.
        case "M":
            path.moveToPoint(point[0]) // M 인 경우, 선을 그리지 않고 해당 포인트로 이동
        case "C":
        // C인 경우 커브포인트이므로, 도착지점과 두 개의 컨트롤 포인트가 된다.
            path.addCurveToPoint(Points[2], controlPoint1: points[0], controlPoint2: points[1])
        case "c": // c 인 경우, 상대좌표를 이용하여 포인트 및 컨트롤 포인트를 만들어서 추가한다.
            path.addCurveToPoint(path.currentPoint.offset(points[2]),
                controlPoint1: path.currentPoint.offset(points[0]),
                controlPoint2: path.currentPoint.offset(points[1])
            )
        default:
            break
        }
    }
    return path;
}

아래 gist 코드는 간단하게 각 데이터 포맷을 읽어서 그 값을 옵셔널로 리턴하거나 nil을 리턴하도록 하는 확장이다.

이 확장을 이용하면 위 while 문을 좀 더 swifty 하게 변경할 수 있다.

while let instruction = scanner.scanCharactersFromSet(instructionSet) {
    var points:[CGPoint] = []
    while let x = scanner.scanDouble(), y = scanner.scanDouble() {
        points.append(CGPoint(x:x, y:y))
    }
    ...
}
Exit mobile version