1️⃣ 문제

String인 new_id를 아래와 같은 단계를 거친 추천 아이디 return하는 문자열 처리 문제

1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.
2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.
3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.
4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.
5단계 new_id가 빈 문자열이라면, new_id에 "a"를 대입합니다.
6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다.
     만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.
7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.

2️⃣ 풀이 방법

위 단계 따르기만 하면 됨.

근데 swift가 문자열 처리하기가 까다로워서 생각보다 쉽지 않았다. 이 문제를 코틀린으로 푼 적이 있었는데 그때는 생각보다 쉽게 풀었었는데… ( ꈨຶ ˙̫̮ ꈨຶ ) (그때보다 더 가독성 신경쓰면서 짜기도 해서 그렇지만)

한번 날잡고 String 관련해서 정리해야겠당 👩‍🏫

3️⃣ 코드

Extension

solution내에서 코드가 지저분해지기 싫어서 extension 지향 코드를 짜봄! 물론 그냥 함수를 만들어도 코드는 깔끔해지겠지만, map()같은 고차함수 내에서 또는 id에 바로 사용할 수 있게끔 Character 또는 String에 직접 접근하는 extension 기능을 추가하고 싶었다.

1️⃣ Character extension - 유효한 문자인지 검증하는 Bool (2단계)

  • 처음에는 아스키 표 검색해서 이렇게 세세하게 작성했었다.

    코틀린에서는 a in [a..z] 이런식이면 끝났어서, swift 나한테 이럴꺼야? 하며 입 툴툴대며 짰었음

            let asciiVal = self.asciiValue!
            if asciiVal >= 97 && asciiVal <= 122
            || asciiVal >= 48 && asciiVal <= 57
    

    근데 세상에 마상에 .isNumber(), .isLetter()라는 Bool 변수를 지원하다니 쏘엔젤…. 욕한거 미안하다 스윞아 내맘알지? (˵ ͡o ͜ʖ ͡o˵)

extension Character {
    var isValid: Bool {
        let specialChar = "-_."
        if self.isNumber
        || self.isLetter
        || specialChar.contains(self) { return true }
        return false
    }
}

2️⃣ String extension - 중복점 제거 함수(3단계), 가장자리 점 제거 함수(4단계), 길이조절 함수(6,7단계)

세 함수 모두 개선된 newId인 String을 반환한다. 변수(var)에 접근할 때는 mutating func으로 함수를 선언하며, 4단계만 mutating을 한 이유는 adjustLength()에서 var 변수인 newId에 대해서 4단계 과정을 한번 더 거쳐주어야 하기 때문이다.

  • removeDotsContinuous()

    ”..”이라는 문자가 없을때가지 이를 찾아 “.”으로 치환해준다.

    (나는 아무리 찾아도 swift에는 코틀린의 str.replace()같은 함수가 없길래 또 욕했는데 다른 사람 풀이에서 replacingOccurrences(of:)를 발견함. 쏘리 스윞ㅌ ( ͡o ͜ʖ ͡o) )

  • removeDotsAtEdges()

    앞서 말했듯이 6,7단계를 거친 후 또 써줘야해서 mutating

    str.removeFirst(), str.removeLast()는 callable이 아니기 때문에 변수에 할당해줄 필요 X (like append())

  • adjustLength()

    str.prefix(_ maxLength: Int)는 리턴타입이 subString이기 때문에 String 변환 필수

    문자열을 자르거나 늘리면 끝 자리에 마침표가 올 수도 있으므로 4단계 한번 더 거쳐주어야 한다.

extension String {
    func removeDotsContinuous() -> String {
        var newId = self
        while newId.contains("..") {
            newId = newId.replacingOccurrences(of: "..", with: ".")
        }
        return newId
    }
    mutating func removeDotsAtEdges() -> String {
        var newId = self
        if newId.first == "." { newId.removeFirst() }
        if newId.last == "." { newId.removeLast() }
        return newId
    }
    func adjustLength() -> String {
        var newId = self
        if newId.count >= 15 {
            newId = String(newId.prefix(15))
        }
        while newId.count < 3 {
            newId.append(newId.last!)
        }
        return newId.removeDotsAtEdges()
    }
}

Solution

짜잔✨ 솔루션이 아주 그냥 깔끔하쥬?

1단계랑 5단계는 한줄로 끝낼 수 있어서 따로 extension에 기능을 추가하지 않았다.

func solution(_ new_id:String) -> String {
    // 1,2단계
    let idStep12 = new_id.lowercased().filter { $0.isValid }
    // 3단계
    var idStep3 = idStep12.removeDotsContinuous()
    // 4단계
    let idStep4 = idStep3.removeDotsAtEdges()
    // 5단계
    let idStep5 = idStep4.isEmpty ? "a": idStep4
    // 6,7단계
    let idStep67 = idStep5.adjustLength()
    
    return idStep67
}

4️⃣ 이 문제를 풀면서 되새긴 점

문자열처리 더 익숙해지려면 문제 더 많이 풀어보자 ( ͠° ͟ʖ ͠°)