Search code examples
swiftstringfoundation

How to separate characters in String by whitespace with multiple strides?


I have a working function that separates every n character with whitespace, which works fine.

Here is the code (Swift 5):

extension String {
    /// Creates a new string, separating characters specified by stride lenght.
    /// - Parameters:
    ///   - stride: Desired stride lenght.
    ///   - separator: Character to be placed in between separations
    func separate(every stride: Int, with separator: Character) -> String {
        return String(self.enumerated().map { $0 > 0 && $0 % stride == 0 ? [separator, $1] : [$1] }.joined())
    }
}

This prints an example string of 1234123412341234 like this

1234 1234 1234 1234

Now, how can i separate this string 1234123412341234 with multiple strides, for example white space to be set after 4th, then after 6th and then after 5th character, like this:

1234 123412 34123 4

Solution

  • Here's how I would do this:

    // Prints sequences of bools using 1/0s for easy reading
    func p<S: Sequence>(_ bools: S) where S.Element == Bool {
        print(bools.map { $0 ? "1" : "0"}.joined())
    }
    
    // E.g. makeWindow(span: 3) returns 0001
    func makeWindow(span: Int) -> UnfoldSequence<Bool, Int> {
        return sequence(state: span) { state in
            state -= 1
            switch state {
                case -1: return nil
                case 0: return true
                case _: return false
            }
        }
    }
    
    // E.g. calculateSpacePositions(spans: [4, 6, 5]) returns 000100000100001
    func calculateSpacePositions<S: Sequence>(spans: S)
        -> LazySequence<FlattenSequence<LazyMapSequence<S, UnfoldSequence<Bool, Int>>>>
        where S.Element == Int {
        return spans.lazy.flatMap(makeWindow(span:))
    }
    
    extension String {
        func insertingSpaces(at spans: [Int]) -> String {
            let spacePositions = calculateSpacePositions(spans: spans + [Int.max])
    //      p(spacePositions.prefix(self.count))
            let characters = zip(inputString, spacePositions)
                .flatMap { character, shouldHaveSpace -> [Character] in 
                return shouldHaveSpace ? [character, "_"] : [character]
            }
    
            return String(characters)
        }
    }
    
    
    let inputString = "1234123412341234"
    let result = inputString.insertingSpaces(at: [4, 6, 5])
    print(result)
    

    The main idea is that I want to zip(self, spacePositions), so that I obtain a sequence of the characters of self, along with a boolean that tells me if I should append a space after the current character.

    To calculate spacePositions, I first started by making a function that when given an Int input span, would return span falses followed by a true. E.g. makeWindow(span: 3) returns a sequence that yields false, false, false, true.

    From there, it's just a matter of making one of these windows per element of the input, and joining them all together using flatMap. I do this all lazily, so that we don't actually need to store all of these repeated booleans.

    I hit one snag though. If you give the input [4, 6, 5], the output I would get used to be 4 characters, space, 6 characters, space, 5 characters, end. The rest of the string was lost, because zip yields a sequence whose length is equal to the length of the shorter of the two inputs.

    To remedy this, I append Int.max on the spans input. That way, the space positions are 000010000001000001 ...now followed by Int.max falses.