(vim) jump 관련 명령 정리

jump와 관련된 명령들을 한 번은 정리하고 가자.

마크

m을 이용해서 현재 위치를 특정한 마크로 지정할 수 있고, 백팃 ` 이나 '작은 따옴표를 이용해서 그 위치로 되돌아 갈 수 있다.

마크는 a-z, A-Z, 0-9 와 몇 가지 특수문자가 적용되는데, 특수문자들은 특별한 의미를 가지는 것들이다.

  • a-z 영어 소문자는 일반적인 마크. 파일마다 따로 관리된다.

  • A-Z는 여러 파일간에 유지된다.

  • 0-9는 역시 여러 파일간에 유지되는데 … .viminfo 파일 내에 저장된다고 한다. 따라서 vim을 종료/재시작한 후에도 위치를 기억할 수 있다. (실제로 이는 vim을 빠져나갔을 때 위치를 기억한다고 한다.)

  • < > 는 이전 선택 영역의 처음과 끝을 가리킨다.

  • " 는 최종적으로 수정한 위치를 가리킨다.
  • ' 는 점프하기 이전 위치로 돌아간다.
  • ^는 삽입모드가 최종적으로 종료된 위치로 돌아간다.
  • .은 최종 변경이 시작된 위치이다.
  • [ ] 는 최종적으로 수정한 영역의 처음과 끝을 가리킨다.

marks를 사용하면 현재 이동할 수 있는 마크들을 보여준다.

점프

점프는 마커 이동을 비롯하여 ', ", G, /, ?, n, N, %, (, ), [, ], {, } :s, :tag, L, M, H 등의 명령으로 불연속적으로 커서가 이동한 것을 말한다. 점프가 발생하면 항상 이전 위치가 기록된다. 이는 점프 리스트 사이를 오가는 명령으로 이동할 수 있고, 점프 발생시에는 ', " 마커도 업데이트되므로 돌아가는 방법은 다 있는셈이다.

H, M, L 은 현재 윈도의 위/중간/아래로 가는 점프명령이다.

<C-O>는 점프리스트의 이전위치로, <C-I>는 점프리스트의 이후 위치로 이동한다.

변경 위치

  • g;, g,는 최근 변경 위치를 전/후로 옮겨다닌다.
  • :changes는 최근 변경 위치들을 보여준다.

그외 모션

  • %: 괄호 내에서 괄호의 시작과 끝으로 반복 이동한다.
  • [( [{ ]) ]} : 짝이 맞지 않는 괄호를 찾아 이동한다.;

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

목차

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

응용 변경

기본적인 문자열 조작 방법을 살펴보았으니 그외의 문자열과 관련된 변경을 찾아보자. 참고로 String 타입의 경우 NSString에서 많은 편의 메소드들을 제공하고 있기 때문에 그리 많은 메소드를 지원하지는 않는다. 따라서 문자열을 입맛에 맞게 조작하려면 Foundation을 임포트하는 것을 잊지 말자.

대문자/소문자로 변경

주어진 영단어를 전체 대문자, 소문자로 변경된 사본을 얻는 것은 기본적으로 지원된다. 이는 영어 기본 알파벳뿐만 아니라 추가 글리프가 붙은 합자들까지도 지원된다. Café &#x1f375; -> CAFÉ &#x1f375; 대소문자 변경과 관련된 해당 메소드는 lowercased(), uppercased() 를 사용하며1, 이름에서 알 수 있듯이 변경된 사본을 만든다.

그냥 무식한 방법으로 단순히 알파벳에서 대/소문자를 변경하는 것은 영어 대문자의 아스키코드값 범위가 65 ~ 90 이고 소문자 범위가 97~122 임을 이용해서 다음과 같이 처리할 수 있다. (물론, 대소문자 변경을 지원하는 메소드가 있는데 이렇게 할 필요는 없고, 타언어와 비슷하게 문자열을 개별문자값의 배열처럼 다루는 예임을 보인다.)

import Foundation

func lower(_ s: String) -> String {
    let convert: (UInt8) -> UInt8 = { x in 
        if case (65..<91) = x {
            return x + 97 - 65
        }
        return x
    }
    let t = s.utf8.map(convert)
    return String(bytes:t, encoding:.utf8)!
}

func upper(_ s: String) -> String {
    let convert: (UInt8) -> UInt8 = { x in 
        if case (97..<123) = x {
            return x - 97 + 65
        }
        return x
    }
    let t = s.utf8.map(convert)
    return String(bytes:t, encoding:.utf8)!
}

대문자/소문자 확인

대소문자 확인은 반대로 한 글자라도 반대 케이스에 있는 글자가 있는지를 보면 된다. 역시 (65~90), (97~122) 범위를 체크하면 된다. 혹은 개별 UnicodeScalar 값들이 CharacterSet의 미리 정의된 집합내에 속하는지를 보는 것으로도 확인할 수 있다.

extension String {
  var isLowercased: Bool {
     for c in utf8 where (65...90) ~= c { return false }
     return true
  }
  var isUppercased: Bool {
      for c in utf8 where (97...122) ~= c { return false }
      return true
  }
}

숫자인지 확인

문자열이 숫자로만 이루어져 있는지 보려면 대/소문자 확인과 비슷하게 숫자들의 코드를 보면 된다. 0~9의 숫자는 (48~57)의 범위를 갖는다. 이 범위 밖의 문자코드가 있으면 거짓으로 판정한다. (음수가 들어올 수 있다거나 하는 경우라면 또 말이 다르지만)2

extension String {
    var isNumeric: Bool {
        for c in utf8 where !((48...57) ~= c) { return }
    }
}

혹은 CharacterSetdecimalDigits 를 이용해도 된다. 참고로 CharacterSetcontainsCharacter가 아니라 UnicodeScalar 값을 받는다.

func isNumeric(_ s: String) -> Bool {
    let set = CharacterSet.decimalDigits
    for us in s.unicodeScalars where !set.contains(us) { return false }
    return true
}

alphanumeric 검사

문자열의 모든 글자가 영문자 혹은 숫자로만 되어 있는지 검사하려는 경우가 가끔 있는데 역시 CharacterSet을 이용한다. (귀찮으니 코드는 생략한다.)

트리밍

주로 키보드로 입력됐거나, 폼 전송 받은 내용에서 앞/뒤 공백을 제거하고 싶은 경우에 NSString이 제공한 trimmingCharacters(in:)을 이용한다. 공백문자의 경우에는 스페이스 외에 탭같은 것들이 있으니 커스텀 셋을 쓰지 않고 Characterset.whitespaces, CharacterSet.whitesapcesAndNewlines 를 사용하면 앞뒤로 눈에 보이지 않는 글자들이 붙어 있는 것들을 떼낼 수 있다.

대소문자 변환 및 대문자화 (Capitalize)

Foundation 내에는 .capitalized 확장이 정의되어 있다.

import Foundation
let s = "hello   world"
print(s.capitalized)

또한 단순히 대/소문자 한쪽으로 전체 변환하는 것은 .uppercased(), .lowercased(), uppercased(with:), lowercased(with:) 등이 존재한다. 또한 NSString도 아주 오래전부터 다국어 지원을 하고 있기 때문에 비영어권 알파벳을 고려한 변환이 가능하다.

NSString의 도움 없이 다음과 같이 직접 만드는 방법도 있다. 소문자 a~z의 범위는 97~122이고 대문자 A의 코드는 65이므로..

let upper: (UInt8) -> UInt8 { x in
    switch x {
     case (97...122): return x + 65 - 97
     default: return x
    }
}
let s = "hello world"
let t = s.utf8.map(upper).map{ Characters(UnicodeScalar($0)) }
print(String(t)) /// using init(_:[Characters])

위 예제에서는 변경된 문자값을 다시 Character로 만들어서 문자열로 복구했지만, [UInt8] 타입데이터는 실질적으로 cString에 대응하므로 다음과 같이 써도 된다.

let t = s.utf8.map(upper)
print(String(cString:t))

대소문자 변경의 경우에는 다음과 같은 로직이 필요하다.

  1. 플래그는 true로 시작한다.
  2. 한글자씩 읽어들이면서 해당 문자가 공백(혹은 개행)에 해당되면 플래그값을 true로 업데이트한다.
  3. 알파벳을 만나면 플래그값에 따라 대/소문자로 변환한다. 그리고 플래그는 다시 false가 된다.

개행문자는 CharacterSet.whiteㄴpacesAndNewㅣines 에 있는지를 포함한다. 여기서 중요한 지점은 CharacterSet은 실제로 Character가 아닌 UnicodeScalar의 집합이라는 점이다. 따라서 contains , insert 등의 메소드는 모두 UnicodeScalar 타입을 받는다.

/// Capitalize
import Foundation

let s = "hello world   2type   here"

func capitalize(str: String) -> String {
    let upper: (UInt8) -> UInt8 = { (97...122) ~= $0 ? $0 + 65 - 97 : $0 }
    let lower: (UInt8) -> UInt8 = { (65...90) ~= $0 ? $0 - 65 + 97 : $0 }
    var flag = true
        var result = [UInt8]()
        for u in str.utf8 {
            if CharacterSet.whitespacesAndNewlines.contains(UnicodeScalar(u)) {
                flag = true
                result.append(u)
            }
            else if flag == true {
                flag = false
                result.append(upper(u))
            } else {
                result.append(lower(u))
            }
    }
    return String(cString: result)
}

print(capitalize(str: s))

특정 문자 기준으로 자르기

특정 문자 기준으로 자르는 경우, Character 기준으로 자르는게 가능하다. 이는 Arraysplit을 기준으로 하면 됨. 단 이 때 실제 잘리는 결과는 [[Character]] 타입이 될 것이므로 각각의 조각에 대해 문자열로 변환해주는 맵핑 처리를 해야한다.

let s = "hello world split strings"
let splits = s.characters.split(separator: " ").map{ String($0) }

자르는 구분자가 단일 문자가 아닌 문자열인 경우, Foundation.components(separatedBy:)를 쓴다. 이 메소드는 String 혹은 CharacterSet을 인자로 받을 수 있다.

import Foundation
let s = "hello, , world"
let p = s.components(separatedBy: ", ") /// ", "를 기준으로 자른다.
/// ["hello", "", "world"]

코드값 변환

Swift의 문자열 타입은 내부에 utf8, utf16 등의 뷰 프로퍼티를 가지고 있다. 따라서 주어진 문자열을 ASCII 코드로 변환하고 싶다면, 다음과 같이 한다. (UTF8기준으로 각각의 코드는 UInt8로 대체된다.)

let s = 'hello world split strings'
let codes = s.utf8.map{ UInt8($0) }
/// [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 116, 121, 112, 101, 32, 104, 101, 114, 101]

이렇게 만들어진 CChar 타입의 배열은 String.init(cString) 이나 init?(cString:encoding:) 을 이용해서 바로 문자열값으로 변환할 수 있다.

회문 검사

보통 C에서 하는 거랑 비슷하게 검사해보자.

func isPalindrome(_ str: String) -> Bool {
  let l = str.characters.count
  for i in 0...(l/2) {
    let iLeft = str.index(str.startIndex, offsetBy: i)
    let iRight = str.index(str.endIndex, offsetBy: -1-i)
    if str[iLeft] != str[iRight] { return false }
  }
  return true
}

characters 속성으로 각 문자 뷰의 원소들을 비교하는 것도 유의미 할 수 있을 것 같다. 문자열이 기본적인 아스키코드내 범위로만 구성되어 있다면 각 글자는 1바이트의 값만 사용하므로 utf8을 이용해서 좀 더 쉽게3 체크할 수 있을 것이다.

데이터로 변환

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

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

이 메소드는 Foundation.NSString에 정의된 것으로 Foundation을 반드시 임포트해야 한다.

Data의 이니셜라이저 중에는 이런 게 있다. (https://developer.apple.com/reference/foundation/data/1780062-init) init(bytes:ArraySlice<UInt8>) 따라서 UTF8View를 그대로 사용해서 Data 인스턴스를 만드는 것도 가능하다.

UTF8 데이터를 그냥 만들고 싶다면 UnsafeMutablePoitner<UInt8>을 뷰 크기만큼 생성해주면 된다. 이 경우에 NULL 종료문자를 위한 공간을 만들어주는 것이 좋다. (어떤 경우에 문자열 끝을 인식하지 못하는 경우가 생긴다.)

let s = "hello world"
let ptr = UnsafeMutablePointer<UInt8>.allocate(capacity: 0)
var q = ptr
for u in s.utf8 {
  q.pointee = u
  q += 1
}
q.pointee = 0
// ptr -> C문자열의 시작 포인터가 됐다. 

이를 UnsafeRawPointer로 만들어서 Data로 사용하든, 그냥 CAPI에 던져넣든 방법은 여러가지이다.

예제에서 사용하는 방법들

키보드로부터 입력받기

키보드(표준입력)으로부터 전달받은 데이터는 문자열로 변환되어 프로그램 내로 들어오게 되는데, 이 때 입력은 항상 성공한다는 보장이 없기 때문에 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

문자열 뒤집기

개별 글자들은 CharacterView 를 이용해서 액세스한다. 이는 sequence이므로 reversed()를 가지기 때문에 뒤집은 사본을 다시 문자열로 만들어주면 된다.

let reversedString = String(string.characters.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)

조금더 고급 예제

파일에 쓰기

NSString에 구현된 기능 가져와서 파일이나 URL에 문자열을 텍스트 파일 형태로 기록할 수 있다.

func write(to url: URL, atomically useAuxiliaryFile: Bool, encoding enc: String.Encoding) throws

write 함수는 다음과 같은 variation이 있다.

  • write(to:) : 출력 스트림에 쓰기
  • write(to:atomically:encoding:) : 파일 URL에 쓰기
  • write(toFile:atomically:encoding:) 주어진 파일 경로에 쓰기

참고로 write(_:)가 있는데, 이는 다른 문자열의 내용을 자신의 뒤에 덧붙이는 동작으로 TextOutputStream에서 정의되어 있다. 이 프로토콜은 write(to:)의 대상이 되는 타입이 갖춰야 하는 조건이다.

URL 쿼리로 만들기

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)" 


  1. Swift2.x 버전에서는 lowercasedString, uppercasedString의 프로퍼티였다가 메소드로 변경되었다. 
  2. 소수점, 진법, 콤마 등 복잡한 요소가 들어가는 경우는 정규식을 이용하거나 NSNumberDetector 클래스를 사용하는 것이 좋다. 
  3. 배열로 변환해버리면 정수 인덱스로 참조할 수 있으니 코드가 더 간단해진다. 
  4. 이는 모노이드의 성질 중 하나인데, Array<Array<T>> 타입의 경우 flatten 처리를 하게 되면 중간에 빈 배열이 있은 것은 무시되고 Array<T> 타입이 되는 것 처럼, nil 값인 옵셔널이 무시되고 Array<Optional<T>>Array<T> 가 된다. 옵셔널 집합에서 non-nil 인 값만 필터링할 때 아주 유용하니 참고하자.