Суммарное значение массива структур в Swift

#swift #struct #sum

#swift #структура #сумма

Вопрос:

Я ищу некоторую помощь в том, как суммировать значение в массиве структур.

Если у меня есть структура, определенная следующим образом:

 struct Item {
    let value : Float
    let name  : String
    let planDate : String
}
  

И затем массив этих структур, подобных этому:

 let dataArray = [Item(value:100, name:"apple", planDate:"2020-10-01"),
                 Item(value:200, name:"lemon", planDate:"2020-10-04"),
                 Item(value:300, name:"apple", planDate:"2020-10-04"),
                 Item(value:400, name:"apple", planDate:"2020-10-01")
]
  

Как я могу суммировать value при группировке по name и planDate , а также при сортировке по name и planDate ?

Вот что я хотел бы вернуть:

 let resultArray = [Item(value:500, name:"apple", planDate:"2020-10-01"),
                   Item(value:300, name:"apple", planDate:"2020-10-04"),
                   Item(value:200, name:"lemon", planDate:"2020-10-04")
]
  

Ответ №1:

Самый простой способ (ну, легкий в глазах наблюдателя) — создать словарь, который группируется по совокупности ваших критериев, name и planDate . Теперь для каждой записи в словаре у вас есть массив всех элементов, которые идут вместе! Так что просто суммируйте их значения. Теперь преобразуйте словарь обратно в массив и отсортируйте его.

 let dataArray = [Item(value:100, name:"apple", planDate:"2020-10-01"),
                 Item(value:200, name:"lemon", planDate:"2020-10-04"),
                 Item(value:300, name:"apple", planDate:"2020-10-04"),
                 Item(value:400, name:"apple", planDate:"2020-10-01")
]
let dict = Dictionary(grouping: dataArray) { $0.name   $0.planDate }
let dict2 = dict.mapValues { (arr:[Item]) -> Item in
    let sum = arr.reduce(0) {
        $0   $1.value
    }
    return Item(value:sum, name:arr[0].name, planDate:arr[0].planDate)
}
let dataArray2 = dict2.values.sorted { ($0.name, $0.planDate) < ($1.name, $1.planDate) }
print(dataArray2)
  

Комментарии:

1. Спасибо за это. Я пошел с этим подходом, поскольку его было немного легче читать и проникать в мою «новичковую» голову разработчика Swift.

Ответ №2:

Я бы также выбрал другой подход. Сначала убедитесь, что вы Item соответствуете Equatable и Comparable . Затем вы можете уменьшить количество отсортированных элементов, проверить, равен ли каждый элемент последнему элементу результата. Если значение true, увеличьте значение, в противном случае добавьте новый элемент к результату:

 extension Item: Equatable, Comparable {
    static func ==(lhs: Self, rhs: Self) -> Bool {
        (lhs.name, lhs.planDate) == (rhs.name, rhs.planDate)
    }
    static func < (lhs: Self, rhs: Self) -> Bool {
        (lhs.name, lhs.planDate) < (rhs.name, rhs.planDate)
    }
}
  

 let result: [Item] = items.sorted().reduce(into: []) { partial, item in
    if item == partial.last {
        partial[partial.endIndex-1].value  = item.value
    } else {
        partial.append(item)
    }
}
  

Ответ №3:

Я предложу другой вариант, используя Dictionary(_:uniquingKeysWith:) :

 let dict = Dictionary(dataArray.map { ($0.name   $0.planDate, $0) },
      uniquingKeysWith: {           
         Item(value: $0.value   $1.value, name: $0.name, planDate: $0.planDate)
      })

let result = dict.values.sorted {($0.name, $0.planDate) < ($1.name, $1.planDate)}
  

Ответ №4:

Для полноты картины приведем решение, которое явно использует тот факт, что вы хотите использовать nameamp;planDate как идентификатор группы, так и ключ сортировки.

Вы можете использовать Identifiable протокол и создать структуру с именем amp; planDate (структуры почти бесплатны в Swift):

 extension Item: Identifiable {
    struct ID: Hashable, Comparable {
        let name: String
        let planDate: String
        
        static func < (lhs: Item.ID, rhs: Item.ID) -> Bool {
            (lhs.name, lhs.planDate) < (rhs.name, rhs.planDate)
        }
    }
    
    var id: ID { ID(name: name, planDate: planDate) }
    
    // this will come in handly later
    init(id: ID, value: Float) {
        self.init(value: value, name: id.name, planDate: id.planDate)
    }
}
  

Затем вы можете разрушить Item структуру по ее идентификатору, накопить значения и реструктурировать ее обратно:

 let valueGroups = dataArray
    .reduce(into: [:]) { groups, item in
        // here we destructure the Item into id and value, and accumulate the value
        groups[item.id, default: 0]  = item.value
    }
    .sorted { $0.key < $1.key } // the key of the dictionary is the id, we sort by that

// and were we restructure it back
let result = valueGroups.map(Item.init(id:value:))
  

Мы можем пойти еще дальше и усовершенствовать необходимые нам операции, расширив Sequence :

 extension Sequence {
    
    /// returns an array made of the sorted elements by the given key path
    func sorted<T>(by keyPath: KeyPath<Element, T>) -> [Element] where T: Comparable {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }
    
    /// Accumulates the values by the specified key
    func accumulated<K, T>(values: KeyPath<Element, T>,
                           by key: KeyPath<Element, K>) -> [K:T]
    where K: Hashable, T: AdditiveArithmetic {
        reduce(into: [:]) { $0[$1[keyPath: key], default: T.zero]  = $1[keyPath: values] }
    }
}
  

Два новых дополнения, сортировка по ключевому пути и накопление ключевых путей с использованием другого ключа, достаточно независимы, чтобы заслужить функцию для них, поскольку они достаточно универсальны, чтобы их можно было использовать повторно в других контекстах.

Фактическая бизнес-логика становится простой, поскольку

 let result = dataArray
    .accumulated(values: .value, by: .id)
    .map(Item.init(id:value:))
    .sorted(by: .id)
  

Даже если это решение более подробное, чем другое, оно имеет следующие преимущества:

  • четкое разделение проблем
  • разбиение кода на более мелкие блоки, которые могут быть независимо протестированы
  • возможность повторного использования кода
  • простой код вызывающего абонента, легко понять и просмотреть