#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)
Даже если это решение более подробное, чем другое, оно имеет следующие преимущества:
- четкое разделение проблем
- разбиение кода на более мелкие блоки, которые могут быть независимо протестированы
- возможность повторного использования кода
- простой код вызывающего абонента, легко понять и просмотреть