Search code examples
swiftnsstringnsrange

What's the best way to cut Swift string into 2-letter-strings?


I need to split a string into 2-letter pieces. Like “friend" -> "fr" "ie" "nd". (Okay, its a step for me to change HEX string to Uint8 Array)

My code is

    for i=0; i<chars.count/2; i++ {
        let str = input[input.startIndex.advancedBy(i*2)..<input.startIndex.advancedBy(i*2+1)]
        bytes.append(UInt8(str,radix: 16)!)
    }

But I don't know why I cannot use Range to do this split. And I have no idea what will happen when i*2+1 is bigger than string's length. So what's the best way to cut Swift string into 2-letter-strings?


Solution

  • Your range wasn't working because you need to use ... instead of ..<.

    let input = "ff103"
    var bytes = [UInt8]()
    
    let strlen = input.characters.count
    for i in 0 ..< (strlen + 1)/2 {
        let str = input[input.startIndex.advancedBy(i*2)...input.startIndex.advancedBy(min(strlen - 1, i*2+1))]
        bytes.append(UInt8(str,radix: 16) ?? 0)
    }
    
    print(bytes)  // [255, 16, 3]
    

    Here is another take on splitting the string into 2-letter strings. advancedBy() is an expensive O(n) operation, so this version keeps track of start and just marches it ahead by 2 each loop, and end is based on start:

    let input = "friends"
    var strings = [String]()
    
    let strlen = input.characters.count
    var start = input.startIndex
    let lastIndex = strlen > 0 ? input.endIndex.predecessor() : input.startIndex
    
    for i in 0 ..< (strlen + 1)/2 {
        start = i > 0 ? start.advancedBy(2) : start
        let end = start < lastIndex ? start.successor() : start
        let str = input[start...end]
        strings.append(str)
    }
    
    print(strings) // ["fr", "ie", "nd", "s"]
    

    Alternate Answer:

    Using ranges is probably overkill. It is easy just to add the characters to an array and make Strings from those:

    let input = "friends"
    var strings = [String]()
    var newchars = [Character]()
    
    for c in input.characters {
        newchars.append(c)
        if newchars.count == 2 {
            strings.append(String(newchars))
            newchars = []
        }
    }
    
    if newchars.count > 0 {
        strings.append(String(newchars))
    }
    
    print(strings) // ["fr", "ie", "nd", "s"]
    

    And here is the new version for making [UInt8]:

    let input = "ff103"
    var bytes = [UInt8]()
    var newchars = [Character]()
    
    for c in input.characters {
        newchars.append(c)
        if newchars.count == 2 {
            bytes.append(UInt8(String(newchars), radix: 16) ?? 0)
            newchars = []
        }
    }
    
    if newchars.count > 0 {
        bytes.append(UInt8(String(newchars), radix: 16) ?? 0)
    }
    
    print(bytes) // [255, 16, 3]
    

    Based on @LeoDabus' answer, we can make an extension with a method that will return substrings of any length, and a computed property that returns [UInt8]:

    extension String {
        func substringsOfLength(length: Int) -> [String] {
            if length < 1 { return [] }
    
            var result:[String] = []
            let chars = Array(characters)
            for index in 0.stride(to: chars.count, by: length) {
                result.append(String(chars[index ..< min(index+length, chars.count)]))
            }
            return result
        }
    
        var toUInt8: [UInt8] {
            var result:[UInt8] = []
            let chars = Array(characters)
            for index in 0.stride(to: chars.count, by: 2) {
                let str = String(chars[index ..< min(index+2, chars.count)])
                result.append(UInt8(str, radix: 16) ?? 0)
            }
            return result
        }
    }
    
    let input = "friends"
    let str2 = input.substringsOfLength(2)  // ["fr", "ie", "nd", "s"]
    let str0 = input.substringsOfLength(0)  // []
    let str3 = input.substringsOfLength(3)  // ["fri", "end", "s"]
    
    let bytes = "ff107".toUInt8  // [255, 16, 7]