Swift에서 배열 요소별로 그룹화하는 방법


89

이 코드가 있다고 가정 해 봅시다.

class Stat {
   var statEvents : [StatEvents] = []
}

struct StatEvents {
   var name: String
   var date: String
   var hours: Int
}


var currentStat = Stat()

currentStat.statEvents = [
   StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
   StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
]

var filteredArray1 : [StatEvents] = []
var filteredArray2 : [StatEvents] = []

2 개의 배열을 "동일한 이름"으로 그룹화하기 위해 다음 함수를 수동으로 여러 번 호출 할 수 있습니다.

filteredArray1 = currentStat.statEvents.filter({$0.name == "dinner"})
filteredArray2 = currentStat.statEvents.filter({$0.name == "lunch"})

문제는 변수 값 (이 경우 "저녁 식사"및 "점심")을 알 수 없기 때문에이 statEvents 배열을 이름별로 자동으로 그룹화하고 싶습니다. 따라서 이름이 달라지는만큼 많은 배열을 얻습니다.

어떻게 할 수 있습니까?


새로운 이니셜 라이저 를 사용 하는 Swift 4에 대한 내 대답을 참조하십시오 Dictionary init(grouping:by:).
Imanou Petit

답변:


191

스위프트 4 :

Swift 4 이후이 기능은 표준 라이브러리추가 되었습니다 . 다음과 같이 사용할 수 있습니다.

Dictionary(grouping: statEvents, by: { $0.name })
[
  "dinner": [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ],
  "lunch": [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
]

스위프트 3 :

public extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        var categories: [U: [Iterator.Element]] = [:]
        for element in self {
            let key = key(element)
            if case nil = categories[key]?.append(element) {
                categories[key] = [element]
            }
        }
        return categories
    }
}

불행히도 append위 의 함수는 기본 배열을 제자리에서 변경하는 대신 복사하므로 바람직합니다. 이로 인해 상당히 큰 속도 저하가 발생합니다 . 참조 유형 래퍼를 사용하여 문제를 해결할 수 있습니다.

class Box<A> {
  var value: A
  init(_ val: A) {
    self.value = val
  }
}

public extension Sequence {
  func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
    var categories: [U: Box<[Iterator.Element]>] = [:]
    for element in self {
      let key = key(element)
      if case nil = categories[key]?.value.append(element) {
        categories[key] = Box([element])
      }
    }
    var result: [U: [Iterator.Element]] = Dictionary(minimumCapacity: categories.count)
    for (key,val) in categories {
      result[key] = val.value
    }
    return result
  }
}

최종 사전을 두 번 탐색하더라도이 버전은 대부분의 경우 원본보다 여전히 빠릅니다.

스위프트 2 :

public extension SequenceType {

  /// Categorises elements of self into a dictionary, with the keys given by keyFunc

  func categorise<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
  }
}

귀하의 경우 "키"를 keyFunc이름으로 반환 할 수 있습니다 .

currentStat.statEvents.categorise { $0.name }
[  
  dinner: [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ], lunch: [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
  ]
]

따라서 모든 키가 이름이고 모든 값이 해당 이름을 가진 StatEvents의 배열 인 사전을 얻을 수 있습니다.

스위프트 1

func categorise<S : SequenceType, U : Hashable>(seq: S, @noescape keyFunc: S.Generator.Element -> U) -> [U:[S.Generator.Element]] {
  var dict: [U:[S.Generator.Element]] = [:]
  for el in seq {
    let key = keyFunc(el)
    dict[key] = (dict[key] ?? []) + [el]
  }
  return dict
}

categorise(currentStat.statEvents) { $0.name }

출력을 제공합니다.

extension StatEvents : Printable {
  var description: String {
    return "\(self.name): \(self.date)"
  }
}
print(categorise(currentStat.statEvents) { $0.name })
[
  dinner: [
    dinner: 01-01-2015,
    dinner: 01-01-2015,
    dinner: 01-01-2015
  ], lunch: [
    lunch: 01-01-2015,
    lunch: 01-01-2015
  ]
]

(swiftstub은 여기에 있습니다 )


@oisdk 정말 감사합니다! 생성 된 사전 값의 인덱스에 액세스 할 수있는 방법이 있는지 알고 있습니까? 내 말은, 키와 값을 얻는 방법을 알고 있지만 그 사전의 인덱스 "0", "1", "2"를 얻고 싶습니다
Ruben

따라서 원하는 경우 사전에있는 세 개의 "저녁 식사"값을 말하면 dict[key], (제 첫 번째 예에서는 ans["dinner"])로 이동합니다. 세 가지 자체의 색인을 원하면 enumerate(ans["dinner"]), 또는 색인을 통해 액세스 하려는 경우 다음 과 같이 할 수 있습니다. ans["dinner"]?[0], 아래에 저장된 배열의 첫 번째 요소를 반환합니다 dinner.
oisdk

업 항상 전무 저를 반환
루벤

아, 알겠습니다.하지만 문제는이 예제에서 "저녁 식사"라는 값을 알아야한다는 것입니다.하지만 실제 코드에서는이 값이나 사전에 얼마나 많은 항목이 있는지 알 수 없습니다.
Ruben

1
이것은 해결책을 향한 좋은 시작이지만 몇 가지 부족한 점이 있습니다. 여기서 패턴 매칭 ( if case)을 사용하는 것은 불필요하지만 더 중요한 것은 딕셔너리 내에 저장된에 추가 dict[key]?.append)하면 매번 복사가 발생합니다. 참조 rosslebeau.com/2016/...
알렉산더 - 분석 재개 모니카

65

스위프트 5, Dictionary라는 초기화 방법이있다 init(grouping:by:).init(grouping:by:)다음과 같은 선언이 있습니다.

init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence

키가 지정된 클로저에 의해 반환 된 그룹이고 값이 각 특정 키를 반환 한 요소의 배열 인 새 사전을 만듭니다.


다음 플레이 그라운드 코드는 init(grouping:by:)문제를 해결하기 위해 사용하는 방법을 보여줍니다 .

struct StatEvents: CustomStringConvertible {
    
    let name: String
    let date: String
    let hours: Int
    
    var description: String {
        return "Event: \(name) - \(date) - \(hours)"
    }
    
}

let statEvents = [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
]

let dictionary = Dictionary(grouping: statEvents, by: { (element: StatEvents) in
    return element.name
})
//let dictionary = Dictionary(grouping: statEvents) { $0.name } // also works  
//let dictionary = Dictionary(grouping: statEvents, by: \.name) // also works

print(dictionary)
/*
prints:
[
    "dinner": [Event: dinner - 01-01-2015 - 1, Event: dinner - 01-01-2015 - 1],
    "lunch": [Event: lunch - 01-01-2015 - 1, Event: lunch - 01-01-2015 - 1]
]
*/

4
좋아요, 다음과 같이 쓸 수도 있다는 것도 포함 해 주 let dictionary = Dictionary(grouping: statEvents) { $0.name }
시겠습니까

1
이것은 빠른 4부터 시작하는 대답이어야합니다. 애플이 완벽하게 지원하고, 성능이 좋기를 바랍니다.
Herbal7ea

또한 술어에서 반환 된 비 옵티 널 키에주의하십시오. 그렇지 않으면 "표현식 유형이 더 많은 컨텍스트없이 모호합니다 ..."라는 오류가 표시됩니다.
Asike

1
@ user1046037 Swift 5.2Dictionary(grouping: statEvents, by: \.name)
Leo

31

Swift 4 : 애플 개발자 사이트 에서 init (grouping : by :) 를 사용할 수 있습니다 .

:

let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let studentsByLetter = Dictionary(grouping: students, by: { $0.first! })
// ["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]

그래서 당신의 경우

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })

1
이것은이 덕분에 존재 몰랐다, 지금까지 가장 좋은 대답이다)
RichAppz

이것은 또한 keypath와 함께 작동합니다 : let dictionary = Dictionary (grouping : currentStat.statEvents, by : \ .name)
Jim Haungs

26

Swift 3 :

public extension Sequence {
    func categorise<U : Hashable>(_ key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        var dict: [U:[Iterator.Element]] = [:]
        for el in self {
            let key = key(el)
            if case nil = dict[key]?.append(el) { dict[key] = [el] }
        }
        return dict
    }
}

용법:

currentStat.statEvents.categorise { $0.name }
[  
  dinner: [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ], lunch: [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
  ]
]

9
사용 예는 매우 감사하겠습니다 :) 감사합니다!
Centurion 2016

다음은 사용 예입니다. yourArray.categorise (currentStat.statEvents) {$ 0.name}. 이 함수는 Dictionary <String, Array <StatEvents >>
Centurion

6

Swift 4에서이 확장은 최고의 성능을 제공하며 운영자를 연결하는 데 도움이됩니다.

extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        return Dictionary.init(grouping: self, by: key)
    }
}

예:

struct Asset {
    let coin: String
    let amount: Int
}

let assets = [
    Asset(coin: "BTC", amount: 12),
    Asset(coin: "ETH", amount: 15),
    Asset(coin: "BTC", amount: 30),
]
let grouped = assets.group(by: { $0.coin })

생성 :

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]

사용법의 예를 쓸 수 있습니까?
Utku Dalmaz

@duan이 가능하고 이러한 BTC BTC 같은 경우를 무시하는 것은 동일한 ...로 카운트한다
모인 Shirazi

1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }),하지만 '더 나은 매핑 한 후 그룹
두안

3

다음 KeyPath과 같이 별로 그룹화 할 수도 있습니다 .

public extension Sequence {
    func group<Key>(by keyPath: KeyPath<Element, Key>) -> [Key: [Element]] where Key: Hashable {
        return Dictionary(grouping: self, by: {
            $0[keyPath: keyPath]
        })
    }
}

@duan의 암호화 예제 사용 :

struct Asset {
    let coin: String
    let amount: Int
}

let assets = [
    Asset(coin: "BTC", amount: 12),
    Asset(coin: "ETH", amount: 15),
    Asset(coin: "BTC", amount: 30),
]

그러면 사용법은 다음과 같습니다.

let grouped = assets.group(by: \.coin)

동일한 결과를 산출합니다.

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]

키 경로 대신 술어를 전달할 func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }수 있습니다. 이렇게하면 전화를 걸 수 있습니다. assets.grouped(by: \.coin)또는assets.grouped { $0.coin }
Leo Dabus

2

스위프트 4

struct Foo {
  let fizz: String
  let buzz: Int
}

let foos: [Foo] = [Foo(fizz: "a", buzz: 1), 
                   Foo(fizz: "b", buzz: 2), 
                   Foo(fizz: "a", buzz: 3),
                  ]
// use foos.lazy.map instead of foos.map to avoid allocating an
// intermediate Array. We assume the Dictionary simply needs the
// mapped values and not an actual Array
let foosByFizz: [String: Foo] = 
    Dictionary(foos.lazy.map({ ($0.fizz, $0)}, 
               uniquingKeysWith: { (lhs: Foo, rhs: Foo) in
                   // Arbitrary business logic to pick a Foo from
                   // two that have duplicate fizz-es
                   return lhs.buzz > rhs.buzz ? lhs : rhs
               })
// We don't need a uniquing closure for buzz because we know our buzzes are unique
let foosByBuzz: [String: Foo] = 
    Dictionary(uniqueKeysWithValues: foos.lazy.map({ ($0.buzz, $0)})

0

정렬 된 그룹화 를 허용 하도록 수락 된 답변 확장 :

extension Sequence {
    func group<GroupingType: Hashable>(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] {
        var groups: [GroupingType: [Iterator.Element]] = [:]
        var groupsOrder: [GroupingType] = []
        forEach { element in
            let key = key(element)
            if case nil = groups[key]?.append(element) {
                groups[key] = [element]
                groupsOrder.append(key)
            }
        }
        return groupsOrder.map { groups[$0]! }
    }
}

그런 다음 모든 튜플에서 작동합니다 .

let a = [(grouping: 10, content: "a"),
         (grouping: 20, content: "b"),
         (grouping: 10, content: "c")]
print(a.group { $0.grouping })

모든 구조체 또는 클래스 :

struct GroupInt {
    var grouping: Int
    var content: String
}
let b = [GroupInt(grouping: 10, content: "a"),
         GroupInt(grouping: 20, content: "b"),
         GroupInt(grouping: 10, content: "c")]
print(b.group { $0.grouping })

0

다음은 Swift 4 KeyPath 를 그룹 비교기로 사용하면서 순서를 유지하기위한 튜플 기반 접근 방식입니다 .

extension Sequence{

    func group<T:Comparable>(by:KeyPath<Element,T>) -> [(key:T,values:[Element])]{

        return self.reduce([]){(accumulator, element) in

            var accumulator = accumulator
            var result :(key:T,values:[Element]) = accumulator.first(where:{ $0.key == element[keyPath:by]}) ?? (key: element[keyPath:by], values:[])
            result.values.append(element)
            if let index = accumulator.index(where: { $0.key == element[keyPath: by]}){
                accumulator.remove(at: index)
            }
            accumulator.append(result)

            return accumulator
        }
    }
}

사용 방법의 예 :

struct Company{
    let name : String
    let type : String
}

struct Employee{
    let name : String
    let surname : String
    let company: Company
}

let employees : [Employee] = [...]
let companies : [Company] = [...]

employees.group(by: \Employee.company.type) // or
employees.group(by: \Employee.surname) // or
companies.group(by: \Company.type)

0

해시 사전 대신 요소를 그룹화하는 동안 순서를 유지해야한다면 튜플을 사용하고 그룹화하는 동안 목록의 순서를 유지했습니다.

extension Sequence
{
   func zmGroup<U : Hashable>(by: (Element) -> U) -> [(U,[Element])]
   {
       var groupCategorized: [(U,[Element])] = []
       for item in self {
           let groupKey = by(item)
           guard let index = groupCategorized.index(where: { $0.0 == groupKey }) else { groupCategorized.append((groupKey, [item])); continue }
           groupCategorized[index].1.append(item)
       }
       return groupCategorized
   }
}

0

Thr Dictionary (grouping : arr)는 너무 쉽습니다!

 func groupArr(arr: [PendingCamera]) {

    let groupDic = Dictionary(grouping: arr) { (pendingCamera) -> DateComponents in
        print("group arr: \(String(describing: pendingCamera.date))")

        let date = Calendar.current.dateComponents([.day, .year, .month], from: (pendingCamera.date)!)

        return date
    }

    var cams = [[PendingCamera]]()

    groupDic.keys.forEach { (key) in
        print(key)
        let values = groupDic[key]
        print(values ?? "")

        cams.append(values ?? [])
    }
    print(" cams are \(cams)")

    self.groupdArr = cams
}

-2

"oisdk"예제 에서 리프 가져 오기 . 클래스 이름 데모 및 소스 코드 링크를 기반으로 개체를 그룹화하도록 솔루션 확장 .

클래스 이름에 따라 그룹화하기위한 코드 조각 :

 func categorise<S : SequenceType>(seq: S) -> [String:[S.Generator.Element]] {
    var dict: [String:[S.Generator.Element]] = [:]
    for el in seq {
        //Assigning Class Name as Key
        let key = String(el).componentsSeparatedByString(".").last!
        //Generating a dictionary based on key-- Class Names
        dict[key] = (dict[key] ?? []) + [el]
    }
    return dict
}
//Grouping the Objects in Array using categorise
let categorised = categorise(currentStat)
print("Grouped Array :: \(categorised)")

//Key from the Array i.e, 0 here is Statt class type
let key_Statt:String = String(currentStat.objectAtIndex(0)).componentsSeparatedByString(".").last!
print("Search Key :: \(key_Statt)")

//Accessing Grouped Object using above class type key
let arr_Statt = categorised[key_Statt]
print("Array Retrieved:: ",arr_Statt)
print("Full Dump of Array::")
dump(arr_Statt)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.