3

I have array thay contains data like this: [value]#[mode] (mode is optional)

I want to split this array to arrays of [value] by [mode] but keeping the same position for data so it can be many array for same mode :

ex:

let array = ["23.88", "24", "30",  "24.16#C", "25#C", "12#C", "24.44#O", "50#O" , "31", "40" , "44#C", "55#C"]  

// Result 

No mode  ---> [23.88,24,30] 
mode = C ---> [24.16,25,12]
mode = O ---> [24.44,50]
No mode  ---> [31,40] 
mode = C ---> [44,55]

I tried this extension but is not what i want

extension SequenceType {
    func groupBy<U : Hashable>(@noescape keyFunc: Generator.Element -> U) -> [U:[Generator.Element]] {
        var dict: [U:[Generator.Element]] = [:]
        for el in self {
            let key = keyFunc(el)
            if case nil = dict[key]?.append(el) { dict[key] = [el] }
        }
        return dict
    }
}

it give me result like this :

No mode  ---> [23.88,24,30,31,40] 
mode = C ---> [24.16,25,12,44,55]
mode = O ---> [24.44,50] 
7
  • I tried extension GroupBy but is not what i want. I updated my post Commented Aug 12, 2016 at 15:49
  • Swift dictionaries require keys to be unique. If you want to generate your results using a dictionary the way you have them broken up you would need to instead have a key map to an array of arrays. Commented Aug 12, 2016 at 15:50
  • why 24.44 not in same array as 23.88? in your expected result arrays Commented Aug 12, 2016 at 15:52
  • We need your keyFunc code and the actual invocation of your extension method. Commented Aug 12, 2016 at 15:53
  • Also your claimed desired result doesn't match your input. "31" and "40" are the "O" mode, not "No mode" in the input. Commented Aug 12, 2016 at 15:55

4 Answers 4

2

I offer you to use array with tuples (to hold keys). Compose this array and then you easily can remap it to format you need:

let array = ["23.88", "24", "30",  "24.16#C", "25#C", "12#C", "24.44#O", "50#O" , "31", "40" , "44#C", "55#C"]

let noModeTag = "#NoMode"

func group(array: [String]) -> [(String, [Double])]
{
    var result = [(String, [Double])]()

    func addNextElement(number: Double?, _ mode: String?) {
        guard let number = number, mode = mode else {fatalError("input format error")}
        if result.last?.0 == mode {
            var array = result.last!.1
            array.append(number)
            result[result.count - 1] = (mode, array)
        } else {
            result.append((mode, [number]))
        }
    }

    for element in array {
        if element.containsString("#") {
            let separated = element.componentsSeparatedByString("#")
            addNextElement(Double(separated.first ?? "_"), separated.last)
        } else {
            addNextElement(Double(element), noModeTag)
        }
    }
    return result
}

print(group(array))
//[("#NoMode", [23.88, 24.0, 30.0]), ("C", [24.16, 25.0, 12.0]), ("O", [24.44, 50.0]), ("#NoMode", [31.0, 40.0]), ("C", [44.0, 55.0])]
Sign up to request clarification or add additional context in comments.

Comments

1

You'll need to modify the groupBy extension to group into an array of 2-tuples rather than an dictionary, where the first tuple element corresponds to a non-unique "key", and the 2nd tuple element is an array of subsequent elements in the self array that can be categorized to the given key.

Modified SequenceType extension

extension SequenceType {
    func groupBy<U : Comparable>(@noescape keyFunc: Generator.Element -> U) -> [(U,[Generator.Element])] {
        var tupArr: [(U,[Generator.Element])] = []
        for el in self {
            let key = keyFunc(el)
            if tupArr.last?.0 == key {
                tupArr[tupArr.endIndex-1].1.append(el)
            }
            else {
                tupArr.append((key,[el]))
            }
        }
        return tupArr
    }
}

Note also that is now suffices that the generic U in the extension conforms to Comparable, as we only use the U elements as "fake" keys in a tuple.

Call to extension

With this modification, we can call the groupBy(..) method as

let array = ["23.88", "24", "30",  "24.16#C", "25#C", "12", "24.44", "50" , "31#O", "40#O" , "44#C", "55#C"]

/* assuming we know the last character always describe the mode,
   given one is included (#) */
let groupedArray: [(String,[String])] = array.groupBy {
    guard $0.characters.contains("#") else { return "No mode" }
    return "mode = " + String($0.characters.last!)
}

print(groupedArray)
/* [("No mode", ["23.88", "24", "30"]), 
    ("mode = C", ["24.16#C", "25#C"]), 
    ("No mode", ["12", "24.44", "50"]), 
    ("mode = O", ["31#O", "40#O"]), 
    ("mode = C", ["44#C", "55#C"])] */

Removing original mode markings (#X) from grouped array

If you'd like to remove original mode markings (#X) in the resulting array, you can apply an additional map operation following the call to groupBy.

Removing markings with resulting values as String:

let groupedArrayClean = groupedArray.map { ($0.0, $0.1.map {
    String($0.characters.prefixUpTo($0.characters.indexOf("#") ?? $0.characters.endIndex))
    })
}

print(groupedArrayClean)
/* [("No mode", ["23.88", "24", "30"]), 
    ("mode = C", ["24.16", "25"]), 
    ("No mode", ["12", "24.44", "50"]), 
    ("mode = O", ["31", "40"]), 
    ("mode = C", ["44", "55"])] */

Or, with resulting values as Double:

let groupedArrayClean = groupedArray.map { ($0.0, $0.1.flatMap {
    Double(
        String(($0.characters.prefixUpTo($0.characters.indexOf("#") ?? $0.characters.endIndex))))
    })
}

print(groupedArrayClean)
/* [("No mode", [23.879999999999999, 24.0, 30.0]), 
    ("mode = C", [24.16, 25.0]), 
    ("No mode", [12.0, 24.440000000000001, 50.0]), 
    ("mode = O", [31.0, 40.0]), 
    ("mode = C", [44.0, 55.0])] */

Alternatively: group and clean up mode markings in single chained call

Or, both groupBy followed by map at once, without an intermediate assignment:

let groupedArrayClean: [(String,[String])] = array.groupBy {
    guard $0.characters.contains("#") else { return "No mode" }
    return "mode = " + String($0.characters.last!)
    }
    .map { ($0.0, $0.1.map {
        String($0.characters
            .prefixUpTo($0.characters.indexOf("#") ?? $0.characters.endIndex))
        })
}

(Analogously for the resulting Double values case.)

8 Comments

So far this is the best answer IMO. Adds on to OP's current function without tons of code and does exactly what it should do.
@HaydenHolligan I should agree. Thought, when I started to write my answer, there was no any function provided by OP.
@ShadowOf fair point. Yours does the same thing and works but there's definitely more code than necessary, function or not
@OuSS Happy to help!
@TimVermeulen to the point of downvoting? If that criteria were sufficient to downvote answers, there's alot of downvoting cut out for those up for that... I'm only at a mobile device but you're welcome to update the answer with parameter names you deem more appropriate.
|
1

Look at this code:

func newArray(incomeArray:[String], index: Int) -> [String: [String]] {
    let modeArray = incomeArray[index].componentsSeparatedByString("#")
    let currentMode = modeArray.count > 1 ? modeArray.last : "No mode"
    var arr: [String] = []
    for i in index...incomeArray.count-1 {
        let modeArray = incomeArray[i].componentsSeparatedByString("#")
        let mode = modeArray.count > 1 ? modeArray.last : "No mode"
        if mode == currentMode || (mode == "" && currentMode == "") {
            arr.append(modeArray.first!)
        } else {
            break
        }
    }
    return ["Mode = " + currentMode!: arr];
}


let array: [String] = ["23.88", "24", "30",  "24.16#C", "25#C", "12", "24.44", "50" , "31#O", "40#O" , "44#C", "55#C"]

var index = 0
while index < array.count {
    let dict = newArray(array, index: index)
    print(dict)
    index += dict[Array(dict.keys).first!]!.count
}

result:

["Mode = No mode": ["23.88", "24", "30"]]
["Mode = C": ["24.16", "25"]]
["Mode = No mode": ["12", "24.44", "50"]]
["Mode = O": ["31", "40"]]
["Mode = C": ["44", "55"]]

4 Comments

This will give me array with all #C mode, but i want to keep the same order of values. if #C are separated then i need 2 arrays of #C
So, you need new array every time when #mode changed?
Yes that what i want
This is incomplete. You would have to call this multiple times on the array and doesn't do what OuSS is asking
0

Six years later, there's still no simple way to get this kind of segmentation. At least I'm not aware of any. I came across this issue today and here's how I solved it (in 2022, using Swift 5+).

Implementation is a an extension to Collection:

extension Collection {
    func segments<Eq: Equatable>(_ key: @escaping (Element) -> Eq) -> AnySequence<SubSequence> {
        AnySequence { () -> AnyIterator<SubSequence> in
            var idx = startIndex
            var prevKey: Eq?

            return AnyIterator {
                guard idx < endIndex else { return nil }
            
                let subSequence = self[idx ..< endIndex]
                    .prefix { element in
                        let elementKey = key(element)
                        defer {prevKey = elementKey}
                    
                        if let prevKey = prevKey {
                            return prevKey == elementKey
                        } else {
                            return true
                        }
                    }
                idx = index(idx, offsetBy: subSequence.count, limitedBy: endIndex) ?? endIndex
                return subSequence
            }
        }
    }
}

Using it is a simple one-liner, like in this test case:

    struct ElementOptionalKey: Equatable {
        let key: String?
        let value: Double
    }

    func testArraySplitterOptionalKey() throws {
        let array: [ElementOptionalKey] = [
            ElementOptionalKey(key: "1", value: 1.0),
            ElementOptionalKey(key: "1", value: 1.1),
            ElementOptionalKey(key: "2", value: 2.0),
            ElementOptionalKey(key: "2", value: 2.1),
            ElementOptionalKey(key: nil, value: 0.0),
            ElementOptionalKey(key: nil, value: 0.1),
            ElementOptionalKey(key: "1", value: 1.2),
            ElementOptionalKey(key: "1", value: 1.3),
            ElementOptionalKey(key: "1", value: 1.4),
            ElementOptionalKey(key: nil, value: 0.2),
            ElementOptionalKey(key: "3", value: 3.0)
        ]
    
        // HERE is the line :-)
        let result = array.segments { $0.key }
    
        XCTAssertEqual(Array(result), [
            [ElementOptionalKey(key: "1", value: 1.0), ElementOptionalKey(key: "1", value: 1.1)],
            [ElementOptionalKey(key: "2", value: 2.0), ElementOptionalKey(key: "2", value: 2.1)],
            [ElementOptionalKey(key: nil, value: 0.0), ElementOptionalKey(key: nil, value: 0.1)],
            [ElementOptionalKey(key: "1", value: 1.2), ElementOptionalKey(key: "1", value: 1.3), ElementOptionalKey(key: "1", value: 1.4)],
            [ElementOptionalKey(key: nil, value: 0.2)],
            [ElementOptionalKey(key: "3", value: 3.0)]
        ])
    }

I tested it with empty collections and even with optional keys, like you have in the original question. Works well.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.