วิธีจัดกลุ่มตามองค์ประกอบของอาร์เรย์ใน Swift


90

สมมติว่าฉันมีรหัสนี้:

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

คำตอบ:


196

Swift 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)
]

Swift 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ฟังก์ชั่นด้านบนคัดลอกอาร์เรย์ที่อยู่ข้างใต้แทนที่จะเปลี่ยนรูปแบบแทนซึ่งจะดีกว่า ซึ่งทำให้การชะลอตัวค่อนข้างมากค่อนข้างมาก คุณสามารถแก้ไขปัญหาได้โดยใช้ Wrapper ประเภทการอ้างอิง:

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

แม้ว่าคุณจะสำรวจพจนานุกรมสุดท้ายสองครั้ง แต่เวอร์ชันนี้ก็ยังเร็วกว่าต้นฉบับในกรณีส่วนใหญ่

Swift 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

โอ้ใช่ฉันเข้าใจแล้ว แต่ปัญหาคือในตัวอย่างนี้ฉันควรจะรู้ค่า "อาหารเย็น" แต่ในรหัสจริงฉันไม่รู้ค่านี้หรือว่าจะมีพจนานุกรมกี่รายการ
Ruben

1
นี่เป็นการเริ่มต้นที่ดีในการแก้ปัญหา แต่มีบางส่วนสั้น ๆ การใช้การจับคู่รูปแบบที่นี่ ( if case) เป็นสิ่งที่ไม่จำเป็น แต่ที่สำคัญกว่านั้นคือการต่อท้ายที่จัดเก็บไว้ในพจนานุกรมโดยdict[key]?.append)จะทำให้เกิดสำเนาทุกครั้ง ดูrosslebeau.com/2016/…
Alexander

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

สร้างพจนานุกรมใหม่โดยที่คีย์คือการจัดกลุ่มที่ส่งคืนโดยการปิดที่กำหนดและค่าคืออาร์เรย์ขององค์ประกอบที่ส่งคืนแต่ละคีย์เฉพาะ


รหัส Playground ต่อไปนี้แสดงวิธีใช้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 }- Syntax Sugar coating
user1046037

1
นี่ควรเป็นคำตอบที่เริ่มต้นอย่างรวดเร็ว 4 - ได้รับการสนับสนุนอย่างเต็มที่จาก apple และหวังว่าจะมีประสิทธิภาพสูง
Herbal7ea

ให้ความสนใจกับคีย์ที่ไม่เหมาะสมที่ส่งคืนในเพรดิเคตมิฉะนั้นคุณจะเห็นข้อผิดพลาด: "ประเภทของนิพจน์ไม่ชัดเจนโดยไม่มีบริบทเพิ่มเติม ... "
Asike

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

31

Swift 4: คุณสามารถใช้init (การจัดกลุ่ม: โดย :)จาก ไซต์นักพัฒนาแอปเปิ้ล

ตัวอย่าง :

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

นอกจากนี้ยังใช้งานได้กับแป้นพิมพ์: ให้พจนานุกรม = พจนานุกรม (การจัดกลุ่ม: currentStat.statEvents โดย: \
.name

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

นี่คือตัวอย่างการใช้งาน: 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 ควรนับเป็นแบบเดียวกัน ...
Moin Shirazi

1
@MoinShirazi assets.group(by: { $0.coin.uppercased() })แต่มัน 'ดีกว่าที่จะจับคู่แล้วจัดกลุ่ม
duan

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หรือระดับ :

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 (การจัดกลุ่ม: 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" ตัวอย่างเช่น การขยายโซลูชันเพื่อจัดกลุ่มอ็อบเจ็กต์ตามชื่อคลาสลิงก์ Demo & Source Codeการเชื่อมโยงการสาธิตและรหัสที่มา

ข้อมูลโค้ดสำหรับการจัดกลุ่มตามชื่อคลาส:

 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.