I want to get a sum of each object which will be classified by its id.
So, my model is:
struct MyObject {
let id: String
var amount: Double
}
And my data are:
var myObjectArray = [
MyObject(id: "A", amount: 1.0),
MyObject(id: "B", amount: 0.2),
MyObject(id: "A", amount: 0.4),
MyObject(id: "B", amount: 0.8),
MyObject(id: "C", amount: 2.1)
]
The results should be something like this:
myObjectArrayResults = [
MyObject(id: "A", amount: 1.4),
MyObject(id: "B", amount: 1.0),
MyObject(id: "C", amount: 2.1)
]
I tried something to do like this, but it didn't work.
for (index, object2) in newObjectArray.enumerated() {
for object in myObjectArray {
if object2.id == object.id {
newObjectArray[index].amount = newObjectArray[index].amount + object.amount
} else {
newObjectArray.append(object)
}
}
}
What might be wrong?
Thank you in advance for your contribution.
You can use reduce(into:) to calculate the sums using an interim dictionary object and then map the result back to MyModel
let result = myObjectArray
.reduce(into: [:]) { $0[$1.id, default: 0] += $1.amount }
.map(MyObject.init)
You wrote:
for (index, object2) in newObjectArray.enumerated() {
for object in myObjectArray {
if object2.id == object.id {
newObjectArray[index].amount = newObjectArray[index].amount + object.amount
} else {
newObjectArray.append(object)
}
}
}
But at start newObjectArray is empty, no? So it won't work. Then, the logic rest has be re-checked again.
You can do it like that with a for loop:
var newObjectArray = [MyObject]()
for object in myObjectArray {
if let existingIndex = newObjectArray.firstIndex(where: { $0.id == object.id }) {
newObjectArray[existingIndex].amount += object.amount
} else {
newObjectArray.append(object)
}
}
The idea, is to iterate over all the objects in myObjectArray, then find if it already exists, in which case we sum, or else we just append.
With reduced(into:_:), keeping the same kind of logic:
let reduced = myObjectArray.reduce(into: [MyObject]()) { partialResult, current in
if let existingIndex = partialResult.firstIndex(where: { $0.id == current.id }) {
partialResult[existingIndex].amount += current.amount
} else {
partialResult.append(current)
}
}
Related
In the code below, how can I change Jack's drink from "Lemonade" to "Soda" inside the groupingDict.
struct User {
var name: String?
var drink: String?
}
let u1 = User(name: "Jack", drink: "Lemonade")
let u2 = User(name: "Jill", drink: "Iced Tea")
let list = [u1, u2]
var groupingDict = Dictionary(grouping: list, by: { $0.name })
print("groupingDict-original: ", groupingDict)
for (index, dict) in groupingDict.enumerated() {
if dict.key == "Jack" {
}
}
print("groupingDict-changed: ", groupingDict)
Access Jack's group, go through each object using reduce(into:) and create a copy of the object and set the drink to "Soda" if it is "Lemonade"
if let jacksGroup = groupingDict["Jack"] {
let modified = jacksGroup.reduce(into: []) {
$0.append($1.drink == "Lemonade" ? User(name: $1.name, drink: "Soda") : $1)
}
groupingDict["Jack"] = modified
}
I found another way to do it. You can use firstIndex(where: { } to get the index of the item, change the property, create a copy tf the item then assign to the dictionary value:
for (_, dict) in groupingDict.enumerated() {
if dict.key == "Jack" {
if var arr = groupingDict["Jack"] {
if let indexOfItem = arr.firstIndex(where: { $0.name == "Jack" }) {
arr[indexOfItem].drink = "Soda"
let copyOfItem = arr[indexOfItem]
groupingDict["Jack"] = [copyOfItem]
}
}
}
}
Model:
enum TaskType: Int, Codable {
case upcoming = 0
case inProgress
case testing
case completed
var title: String {
switch self {
case .upcoming:
return "Upcoming"
case .inProgress:
return "In Progress"
case .testing:
return "Testing"
case .completed:
return "Completed"
}
}
}
struct TasksModel: Encodable, Decodable {
var upcomingArray: [TaskInfo]
var inProgressArray: [TaskInfo]
var testingArray: [TaskInfo]
var completedArray: [TaskInfo]
}
struct TaskInfo: Codable, Equatable, Identifiable {
var id: String
var title: String
var description: String
var taskStatus: TaskType
var taskDate = Date()
}
VM:
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel
self.tasksArray = TasksModel.init(upcomingArray: [], inProgressArray: [], testingArray: [], completedArray: [])
}
So now that I could locate the record with received taskID and change the taskStatus, I need also to move the record from upcomingArray to inProgressArray. This is what I’m trying:
func inProgressSetTask(taskID: String) {
#StateObject var viewModel = HomeVM()
if let index = viewModel.tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
// Update task status
viewModel.tasksArray.upcomingArray[index].taskStatus = .inProgress
// Need to remove from upcomingArray and append into inProgressArray
viewModel.tasksArray.upcomingArray.remove(at: index)
var lastIndex = viewModel.tasksArray.inProgressArray.last
viewModel.tasksArray.inProgressArray[lastIndex].append()
viewModel.save()
// End
} else {
…
Updating taskStatus above working fine but remove from one array into another is not.
This code above will repeat for each array after else. Appreciate any help.
you could try the following example code to achieve what you want:
(note, you should have #StateObject var viewModel = HomeVM() outside of the func inProgressSetTask(taskID: String) {...}
or pass it in as a parameter)
EDIT-1: moving the function with all arrays into HomeVM and assuming id are unique.
func inProgressSetTask(taskID: String) {
print("InProgress Set ID# \(taskID)")
// with index, using `firstIndex`
if let index = viewModel.tasksArray.inProgressArray.firstIndex(where: {$0.id == taskID}) {
// do something with the index
viewModel.tasksArray.inProgressArray[index].title = "xxx"
}
// with TaskInfo, using `first`
if var taskInfo = viewModel.tasksArray.inProgressArray.first(where: {$0.id == taskID}) {
// do something with the taskInfo
taskInfo.title = "xxx"
}
}
With all arrays of TaskInfo, use the function setTaskFromAll(...) in HomeVM. For example: viewModel.setTaskFromAll(taskID: "1")
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel = TasksModel.init(upcomingArray: [], inProgressArray: [], testingArray: [], completedArray: [])
func setTaskFromAll(taskID: String) {
if let index = tasksArray.inProgressArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.inProgressArray[index].title = "inProgress"
} else {
if let index = tasksArray.completedArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.completedArray[index].title = "completed"
} else {
if let index = tasksArray.testingArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.testingArray[index].title = "testing"
} else {
if let index = tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.upcomingArray[index].title = "upcoming"
}
}
}
}
}
}
EDIT-2:
However, since you already have the "TaskType" of each array in the TaskInfo struct, why not remove TasksModel
and use a single array of TaskInfo in your HomeVM. Like this:
class HomeVM: ObservableObject {
#Published var tasksArray: [TaskInfo] = [
TaskInfo(id: "1", title: "title1", description: "description1", taskStatus: .upcoming),
TaskInfo(id: "2", title: "title2", description: "description2", taskStatus: .inProgress)
// ....
]
func setTask(taskID: String, to taskType: TaskType) {
if let index = tasksArray.firstIndex(where: {$0.id == taskID}) {
tasksArray[index].taskStatus = taskType
}
}
func getAllTaskInfo(_ oftype: TaskType) -> [TaskInfo] {
return tasksArray.filter{$0.taskStatus == oftype}
}
}
and use it like this: viewModel.setTask(taskID: "1", to: .testing) and viewModel.getAllTaskInfo(.inProgress)
EDIT-3: to remove from one array and append to another, using your TasksModel scheme, use this:
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel = TasksModel(upcomingArray: [
TaskInfo(id: "1", title: "title1", description: "description1", taskStatus: .upcoming),
TaskInfo(id: "2", title: "title2", description: "description2", taskStatus: .upcoming)
], inProgressArray: [
TaskInfo(id: "3", title: "title3", description: "description3", taskStatus: .inProgress),
TaskInfo(id: "4", title: "title4", description: "description4", taskStatus: .inProgress)
], testingArray: [], completedArray: [])
func inProgressSetTask(taskID: String) {
if let index = tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
// update task status
tasksArray.upcomingArray[index].taskStatus = .inProgress
// get the upcomingArray taskInfo
let taskInfo = tasksArray.upcomingArray[index]
// remove from upcomingArray
tasksArray.upcomingArray.remove(at: index)
// append to inProgressArray
tasksArray.inProgressArray.append(taskInfo)
} else {
// ...
}
}
}
Use it like this: viewModel.inProgressSetTask(taskID: "1")
As you can plainly see, you are much better-off with the EDIT-2, you are repeating/duplicating things in EDIT-3 for no reason. There is no need for separate arrays for the different TaskType, you already have this info in the TaskInfo var taskStatus: TaskType. With EDIT-2, use viewModel.getAllTaskInfo(.inProgress) to get all TaskInfo of a particular type, just like it would be if you used a separate array.
You are attempting to compare a String to a TaskInfo, because the elements of an inProgressArray are of type TaskInfo. What you need to do is drill into the array and get to the .id. That is simple to do. In the .first(where:), you simply pass a closure of $0.id == taskID.
if let index = viewModel.tasksArray.inProgressArray.first(where: { $0.id == taskID } ) {
I called a function to count, and I would like to return the data as an array and use it then, how can I do the same
. where this returned arr is been stored, so I can use t in main code
var newArr =[]
countData(jsonData).then(function (res) {
console.log(arr)
console.log('end')
})
function countData(jsonData){
var five=0, two =0
for(var i in jsonData){
console.log(jsonData)
if(jsonData.num == '5'){
five++;
}
else{
two++;
}
}//for
var arr =[]
arr[0]=five
arr[1]=two
return arr
}//function
The return value of countData is an array.
It isn't a Promise. The doesn't have a then method. Just use the return value directly.
const data = {
"num": "2",
"num": "5",
"num": "5",
"num": "2",
"num": "2",
"num": "2"
}
const result = countData(data);
console.log(result);
function countData(jsonData) {
var five = 0,
two = 0
for (var i in jsonData) {
console.log(jsonData)
if (jsonData.num == '5') {
five++;
} else {
two++;
}
} //for
var arr = []
arr[0] = five
arr[1] = two
return arr
} //function
I have one array "users" with all user data and and second array "userIds" having user's id. I have to fetch User from "users" array using "userIds" array
struct User {
let name: String
let id: Int
}
let users: [User] = [User(name: "a", id: 1),
User(name: "b", id: 2),
User(name: "c", id: 3),
User(name: "d", id: 4),
User(name: "d", id: 5)]
let userIds = [2,3,2,5]
result array that I want is :
[User(name: "b", id: 2),
User(name: "c", id: 3),
User(name: "b", id: 2),
User(name: "d", id: 5)]
so it can have duplicate data according to the data in "userIds".
Now I tried using Higher order function filter:
let result = users.filter { (user) -> Bool in
return userIds.contains(user.id)
}
but this removes the duplicate data and the output is :
[User(name: "b", id: 2),
User(name: "c", id: 3),
User(name: "d", id: 5)]
One approach that I tried is using for loop :
var result = [User]()
for i in userIds {
result.append(users.filter({ $0.id == i }).first!)
}
which gives the desired output but if there is a better approach please suggest.
You can solve this using first(where:) to search through users:
let result = userIds.compactMap { desiredDataValue in
users.first(where: { $0.id == desiredDataValue })
}
But if you're doing this a lot, it would probably speed things up if you built a datastructure that allows for fast lookup by the "id" value. You should compare the performance for yourself, and see if you do this enough/frequently enough for it to be worthwhile:
let dictsByData = Dictionary(uniqueKeysWithValues:
users
.lazy
.map { dict in
(key: dict.id, value: dict)
}
)
let result = userIds.compactMap { desiredDataValue in dictsByData[desiredDataValue]! }
result.forEach { print($0) }
Well after digging few more and with the help of this blog:
https://medium.com/#abhimuralidharan/higher-order-functions-in-swift-filter-map-reduce-flatmap-1837646a63e8
I tried doing like this:
let results = userIds.compactMap { (int) -> User? in
var matchedUser: User?
if users.contains(where: { (user) -> Bool in
if user.id == int {
matchedUser = user
}
return user.id == int
}) {
return matchedUser
}
return nil
}
and in playground I checked the count the code was executed :
and it seems like the count is less comparing to "for" loop.
I have an object with an array that looks like this:
some_object = {
some_array: [
{ id: "foo0" },
{ id: "foo1" },
{ id: "foo2" },
{ id: "foo3" },
]
}
And I have an input of another array that I want to rearrange the array in
target_order = [
{ id: "foo0", new_position: 3 },
{ id: "foo3", new_position: 0 },
{ id: "foo1", new_position: 2 },
{ id: "foo2", new_position: 1 }
]
How do I go about using the second target_order array to modify the order of the first some_object[:some_array]?
I recommend you use sort_by with a custom block that finds the position of the item in the new array.
new_array = some_object[:some_array].sort_by do |item|
order = target_order.detect { |order| order[:id] == item[:id] }
next unless order
order[:new_position]
end
This returns the following value.
=> [{:id=>"foo2"}, {:id=>"foo1"}, {:id=>"foo0"}, {:id=>"foo3"}]
Further considerations
Perhaps you wanted to give each item a position in a list instead of just sorting them. For instance
target_order = [
{ id: "foo0", new_position: 0 },
{ id: "foo1", new_position: 2 }
]
would give
=> [{ id: "foo0" }, nil, { id: "foo1" }]
To do this, you should use each_with_object instead of sort_by.
new_array = target_order.each_with_object([]) do |order, memo|
item = some_object[:some_array].detect { |item| item[:id] == order[:id] }
next unless item
memo[order[:new_position]] = item
end
Just to be simplistic, this is what I would do ...
temp_arr = []
target_order.each do |o|
x = some_json_object[:some_array].find { |i| o[:id] == i[:id] }
temp_arr[o[:new_position] - 1] = x
end
some_json_object = {
"some_array": temp_arr
}
If there is one to one correspondence between some_array and target_order elements, maybe you can do a direct assignment, something like:
some_object[:some_array] = target_order.sort_by{ |h| h[:new_position] }.map { |h| h.delete_if { |k, _| k == :new_position } }
So, you'll end up with
some_object #=> {:some_array=>[{:id=>"foo3"}, {:id=>"foo2"}, {:id=>"foo1"}, {:id=>"foo0"}]}
There is no need to sort, which has time-complexity of O(n*log(n)). Here is a O(n) solution.
{ some_array: target_order.each_with_object([]) { |h,a|
a[h[:new_position]] = h.slice(:id) } }
#=> {:some_array=>[{:id=>"foo3"}, {:id=>"foo2"}, {:id=>"foo1"}, {:id=>"foo0"}]}
Note that there is no reference to some_object.
If some_object is to be modified in place:
some_object[:some_array] = target_order.each_with_object([]) { |h,a|
a[h[:new_position]] = h.slice(:id) }
some_object
#=> {:some_array=>[{:id=>"foo3"}, {:id=>"foo2"}, {:id=>"foo1"}, {:id=>"foo0"}]}
Using Enumerable#sort_by, though less efficient, one could write:
{ some_array: target_order.sort_by { |h| h[:new_position] }.map { |h| h.slice(:id) } }
#=> {:some_array=>[{:id=>"foo3"}, {:id=>"foo2"}, {:id=>"foo1"}, {:id=>"foo0"}]}