콘텐츠로 건너뛰기
Home » (Swift) String타입의 기초 – 03. 활용

(Swift) String타입의 기초 – 03. 활용

목차

  1. 문자열 생성하기
  2. 문자열 조작하기
  3. 활용

Updated (2023-03-03) : Swift 최신 버전과 일부 다를 수 있음

기본적인 문자열 조작에 이어서 몇 가지 문자열 조작과 관련한

  • 대소문자 및 숫자 관련
  • 자르기
  • 코드 값 및 인코딩

대소문자 및 숫자 관련

문자열 전체를 대문자화, 소문자화 하는 기능으로 lowercased(), uppercased() 가 있다. 과거 분사 형태로 된 이름으로부터 이는 본래의 문자열을 변경하는 mutating 메소드가 아니라, 변환된 사본을 생성하는 함수임을 알 수 있다.

lowercased(), uppercased()의 경우에는 String 타입이 기본적으로 제공한다. 현재 로케일에서의 지역화 버전인 localizedLowercased(), localizedUppercased()는 Foundation 의 NSString으로부터 브릿징된 API도 존재한다.

문자열이 대문자나 소문자로만 이루어져 있는지를 검사하는 (파이썬의 .isupper(), .islower()에 대응하는) 메소드는 직접적으로 제공되지 않는다. 대신 Character 타입에서 isUppercase, isLowercase 프로퍼티를 제공하기 때문에 다음과 같은 함수를 만들어서 체크할 수 있다. 이전에는 Foundation의 NSCharacterSet의 기능을 사용하거나, 문자의 코드값 범위를 비교하는 귀찮은 작업을 해야했지만, 지금은 아래와 같이 깔끔하게 정리할 수 있다.

// 대문자/소문자 여부 추가

extension String {
  var isUppercase: Bool {
    // 'self.' 은 생략가능하다.
    return allSatisfy{ $0.isUppercase }
  }

  var isLowercase: Bool {
    return allSatisfy{ $0.isLowercase }
  }
}

String의 구성요소인 Character 타입이 숫자 판별 여부를 알려주는 프로퍼티인 isNumber를 가지고 있기 때문에 같은 식으로 숫자 여부를 판별할 수 있다. 주의할 점은 이 프로퍼티는 유니코드 문자에서 “숫자를 나타내는” 문자인지 여부를 판단한다는 것이다. 따라서 “7”과 같은 문자 외에도 “⅚”, “㊈”, “𝟠”의 경우에도 모두 숫자로 판단하게 되니 실제 decimal 숫자인지 여부를 판단하는데 사용되어서는 안될 것이다.

참고로 Character 타입은 정수값을 표현하는 문자인지를 판단하는 isWholeNumber 프로퍼티도 제공하는데, 여기서는 정수값을 표현하는 문자를 판단한다. “⅚”는 정수 표현이 아니지만 “千” 은 1,000 이므로 정수표현으로 인식한다. 또한 wholeNumberValue 를 사용하여 정수 표현 문자인 경우, 가리키는 정수값을 얻을 수도 있다.

따라서 decimal 숫자인지를 알아내기 위해서는 문자열의 모든 글자가 0~9 의 아스키코드인 48~57 영역에 있는지를 검사해야 한다.

extension String {
  var isDecimal: Bool { 
    return allSatisfy{ (c) in 
      if let v = c.asciiValue, (48...57) ~= v { return true }
      return false
    }
  }
}

특정한 문자셋에 포함되는 글자인지 파악하기 위해서는 CharacterSet를 사용할 수 있다. 단, CharacterSet은 NSCharacterSet의 브릿징 타입이며, 순수한 표준 라이브러리에는 포함되지 않는다. CharacterSet에는 decimalDigits 라는 데시멀 숫자만 포함된 집합이 있고, contains(_ :unicodeScalar) 라는 메소드가 있으므로 이를 이용할 수 있다. (Character를 받는 것이 아니라 유니코드 스칼라 값을 받는다)

import Foundation

func isDecimal(_ s: String) -> Bool {
  let ds = CharacterSet.decimalDigits
  return s.unicodeScalars.allSatisfy{ ds.contains($0) }
}

CharacterSet에는 영숫자, 공백문자, 부호, 공백과 개행 등 몇 가지 미리 정의된 문자집합을 제공하고 있으니, 필요에 따라 위 코드를 참고해서 사용할 수 있을 것이다.

자르기

문자열을 다룰 때 가장 많이 사용하고 중요한 동작 중 하나가 특정한 구분자를 기분으로 문자열을 문자열의 배열로 쪼개거나, 반대로 문자열의 배열을 하나의 문자열로 합치는 것인데, 이 부분에 대한 인터페이스가 초기 Swift 버전에서는 미흡했다. 현재는 여러 모로 개선이 되었다

String 타입은 현재 두 가지 버전의 split(*) 메소드를 제공한다.

  • split(separator:, masSplit:, omittingEmptySubsequences:)
  • split(maxSplit:, omittingEmtpySubSequences:, whereSeparator:)

첫번째 메소드는 특정한 Character를 기준으로 문자열을 자르며, 두 번째 메소드는 Character를 판정하는 클로저를 사용하여 문자를 자른다.

//  콤마를 기준으로 자르기

let words = someString.split(separator:",")

// 공백 및 개행 문자를 기준으로 자르기

let words = someString.split{
  CharacterSet.whitespacesAndNewlines.contains($0.unicodeScalar)
}

트리밍

파일로부터 읽어들였거나, 키보드로 입력받은 문자에서 앞뒤 공백을 제거하는 등, 문자열의 앞뒤에 필요 없는 문자를 잘라내어 버리는 것을 트리밍이라고 한다. 아쉽게도 String 타입은 그 자체로는 트리밍 기능을 제공하지 않는다. 대신에 NSString으로부터 브릿징된 trimmingCharacters(in:) 을 사용하여 이 기능을 이용할 수 있다. 공백 문자를 제거하려는 경우에는 CharacterSet.whitespacesAndNewlines 를 사용하여 필요없는 공백문자를 트리밍할 수 있다.

import Foundation

let p = "    hello    "
let q = p.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
print("|\(q)|")
// |hello|

예제 – 회문 검사

Swift 문자열은 유니코드 규격을 기반으로 설계되었다. 따라서 정수가 아닌 String.Index라는 타입의 값을 사용하여 특정 위치의 글자를 액세스한다. 유니코드의 경우 여러 글리프가 조합되어 하나의 글자로 표현되는 경우도 있어서 한 단위의 코드값이 반드시 하나의 글자가 된다는 보장이 없기 때문이다. 따라서 N번째 글자를 얻으려면 str.index(str.startIndex, offsetBy:N) 으로 N번째 글자에 대한 인덱스를 얻거나, 순차적으로 str.index(after:) 를 사용하여 다음 인덱스, 그 다음 인덱스를 얻어서 처리해야 한다.

func isPalindrome(_ str: String) -> Bool {
  let u = str.count / 2
  var x1 = str.startIndex
  var x2 = str.index(before:str.endIndex) 
  // endIndex는 문자열 바깥이다.

  while x1 < x2 {
    if str[x1] != str[x2] { return false }
    x1 = str.index(after:x1)
    x2 = str.index(before:x2)
  }
  return true
}

인코딩하여 데이터로 변환하기

네트워크 전송이나, 파일 저장등을 위해서 문자열을 인코딩하여 Data 로 변경해야 할 때가 있다. 문자열의 직렬화에는 어떤 인코딩을 사용할 것인지가 반드시 필요하다. 이 때.data(using:allowLossyConversion:=false)메소드가 사용된다.

let s = "hello world"
let d: Data? = s.data(using:.utf8) // 11bytes data
let c = String(data: d!, encoding: .utf8)

키보드로부터 입력받기

키보드(표준입력)으로부터 전달받은 데이터는 문자열로 변환되어 프로그램 내로 들어오게 되는데, 이 때 입력은 항상 성공한다는 보장이 없기 때문에 String? 타입을 전달 받게 된다. 따라서 guard let, if let을 통해서 안전하게 처리하는 것이 필요하다. 흔히 연습문제들에서는 라인당 하나 혹은 그 이상의 정수를 입력받고, 처음 입력 받은 수만큼 추가로 입력 받는처리가 많기 때문에 입력값을 분해하고 숫자로 반드는 것이 필요하다.

// 2개의 정수를 입력받는다.
import Foundation
if let input = readLine() {
    let twoInts = input.components(separatedBy: " ")
        .flatMap{ Int($0)}.prefix(2)
    // ...
}

참고로 입력된 값을 예측하기 어렵고, init?(_:String)이기 때문에 옵셔널 값이 된다. 변경이 실패한 값을 제외하려면 flatMap을 이용하면 nil 값을 걸러낼 수 있다.4

사실 습관적으로 if let input = readLine() … 을 쓰기는 했지만, 사실 옵셔널은 “매우 우아한” 물건이다.

let twoInts = readLine()?.components(sepaartedBy:" ")?.flatMap{ Int($0) }?.prefix(2)
// [Int]? 타입이시다.
// for...in 을 쓸 수 없지만, forEach는 쓸 수 있지 않던가?
twoInts?.forEach{ a in 
  ....
}

문자열 뒤집기

이전에는 characters 를 사용하여 Character의 연속열을 얻어 이를 뒤집었으나, 지금은 String 자체가 Sequence이기 때문에 뒤집은 뒤에 String() 생성자를 사용하여 뒤집은 문자열을 사용할 수 있다.

let reversedString = String(string.reversed())

문자열을 특정 길이만큼의 단어로 분해하기

일정한 간격으로 띄워세는 stride()를 이용해서 인덱스를 구성해, 이를 맵핑하여 부분열을 자른다.

let string = " ...충분히 긴 문자열... "
let strides = stride(from:0, to:string.characters.count, by: 5)
let words = strides.map{ (n: Int) -> String in
    let a = string.index(startIndex, offsetBy: n)
    let b = string.index(a, offsetBy: 5)
    return s[a..<b]
}
print(words)

URL 쿼리로 만들기

URL 쿼리 문자열로 만들기 위해서는 영어대소문자 및 숫자와 일부 특수문자를 제외한 나머지 글자들을 퍼센트인코딩을 적용해서 변형한다. 퍼센트 인코딩은 NSString의 addmingPercentEncoding(withAllowedCharacters:)를 사용하여 만들 수 있다. 아래 예제는 각각 문자열로 된 키와 값의 딕셔너리를 URL 쿼리 문자열로 변형하는 코드이다. URL에서 허용 가능한 문자 집합은 편리하게도 이미 CharacterSet.urlQueryAllowed에 이미 정의되어 있다.

let fields:[String:String] = [ ... ]
let url = "http://some.where.com/document/"
let queryString = fields.map{ (k, v) in "\(k)=\(v)" }
    .joined(separator:"&")
    .addingPercentEncoding(withAllowedCharacters:
        CharacterSet.urlQueryAllowed)!
let query = "\(url)/?query=\(queryString)"