Retrieve key from Value Dictionary Array - arrays

I would to know how to get key if I have the values. Which class get higher marks?
let higherMarks = [
"ClassA": [10,20,30,40,50,60],
"ClassB": [15,25,35,45,55,65],
"ClassC": [18,28,38,48,58,68],
]
var largest = 0
var className = ""
for (classTypes, marks) in higherMarks {
for mark in marks {
if mark > largest {
largest = mark
}
}
}
print(largest)

What I'm saying in my comment is that you need to get the classTypes when you get the mark. Because when you get the higher mark, you want to also get the corresponding key value.
Keeping your code's logic I would do something like this:
let higherMarks = [
"ClassA": [10,20,30,40,50,60],
"ClassB": [15,25,35,45,55,65],
"ClassC": [18,28,38,48,58,68],
]
func findBestClass(in results: [String: [Int]]) -> (name: String, score: Int) {
var largest = 0
var type = ""
for (classType, marks) in results {
if let max = marks.max(), max > largest {
largest = max
type = classType
}
}
return (type, largest)
}
let best = findBestClass(in: higherMarks)
print("The best class is \(best.name) with a score of \(best.score).")
I just replaced your inner loop with .max() and changed the name of the key variable because it should not be plural. My method also returns a tuple because I find it relevant in this situation. But I didn't change your logic, so you can see what I meant by "also get the classTypes".

Related

Count string elements in an array in a multidimensional array

I have an array that I populate from firestore that uses a struct. Is there a way to count the number of times there is a matching string for the productName var.
This is my struct...
struct PlayerStock: Codable, Identifiable {
#DocumentID var id: String?
var productName: String
var qty: Int
var saleUID: String
var totalPrice: Int
var uid: String
var unitPrice: Int
}
This is what's in my VC, I populate this from firestore and then want to count matching strings in productName
var playerStock: [PlayerStock] = []
Is there a way to do this without using a for loop?
Strings I'd like to count in productName include "smartphone" or "laptop" I want to store the matching total count as an int like this:
var smartphoneTotal =
var laptopTotal =
etc etc..
I've tried using filters and compact map but can't find anything that works, I think its because the array is multidimensional or because its using a dictionary?
Pretty noob here so any help appreciated!
First group the array by productName
let groupedProducts = Dictionary.init(grouping: playerStock, by: \.productName)
you'll get
["smartphone":[PlayerStock(..), PlayerStock(..), PlayerStock(..)],
"laptop":[PlayerStock(..), PlayerStock(..)]
then map the values to their amount of items
.mapValues(\.count)
The result is
["smartphone":3, "laptop":2]
If you want to use filter, something like this should work with your struct:
var laptopTotal = playerStock.filter { $0.productName == "laptop" }.count
This may help
let wordsToFind = ["smartphone", "laptop"]
var foundCounts: [String: Int] = [:]
for p in playerStock {
for word in wordsToFind {
if p.name.contains(word) {
foundCounts[word] = foundCounts[word, default: 0] + 1
}
}
}
foundCounts
If you really want a functional "no for-loops" version, and if you mean you want to find things that contain your search terms, then:
let wordsToFind = ["smartphone", "laptop"]
let founds = wordsToFind.map { word -> (String, Int) in
playerStock.reduce(("", 0)) { partialResult, player in
(word, partialResult.1 + (player.name.contains(word) ? 1 : 0))
}
}
You could use the higher order functions filter() or reduce(). #ShawnFrank already gave an answer using filter(). (voted.)
For a small number of items, there isn't a big difference between filter() and reduce(). For large datasets, though, filter creates a second array containing all the items that match the filter criteria. Arrays are value types, so they hold copies of the entries they contain. This would increase the memory footprint needed to do the counting. (You'd have the original array and a copy containing all the matching elements in memory).
The higher order function reduce() works differently. it takes a starting value (a total starting at 0 in our case) for the result, and a closure. The closure takes the current result, and an element from the array you are parsing. At runtime, the reduce() function calls your closure over and over, passing in each element from the array you are reducing. In the first call to the closure, it sends the closure the initial value for result (a zero total, in our case.) In each subsequent call to the closure, it passes the result of the previous call. (The running total, for our implementation.) The reduce() function returns the result returned by the last call to your closure.
You can use reduce to count the number of items that match a given test without having to build a temporary array. Below is a sample implementation using reduce(). Note that I tweaked your PlayerStock type to add default values for all the properties other than productName since I don't care about those.
// Define the PlayerStock type, but use default values for everything but `productName`
struct PlayerStock: Codable, Identifiable {
var id: String? = nil
var productName: String
var qty: Int = Int.random(in: 1...10)
var saleUID: String = ""
var totalPrice: Int = Int.random(in: 10...200)
var uid: String = ""
var unitPrice: Int = Int.random(in: 10...200)
}
// Create an array of test data
let players = [
PlayerStock(productName: "smartphone"),
PlayerStock(productName: "CD Player"),
PlayerStock(productName: "laptop"),
PlayerStock(productName: "CD Player"),
PlayerStock(productName: "smartphone"),
PlayerStock(productName: "laptop"),
PlayerStock(productName: "smartphone"),
PlayerStock(productName: "boom box"),
PlayerStock(productName: "laptop"),
PlayerStock(productName: "smartphone"),
]
/// This is a function that counts and returns the number of PlayerStock items who's productName property matches a the string nameToFind.
/// If you pass in printResult = true, it logs its result for debugging.
/// - Parameter nameToFind: The `productName` to search for
/// - Parameter inArray: The array of `PlayerStock` items to search
/// - Parameter printResult: a debugging flag. If true, the function prints the count if items to the console. Defaults to `false`
/// - Returns: The number of `PlayerStock` items that have a `productName` == `nameToFind`
#discardableResult func countPlayers(nameToFind: String, inArray array: [PlayerStock], printResult: Bool = false) -> Int {
let count = array.reduce(0, { count, item in
item.productName == nameToFind ? count+1 : count
})
if printResult {
print("Found \(count) players with productName == \(nameToFind)")
}
return count
}
let smartphoneCount = countPlayers(nameToFind: "smartphone", inArray: players, printResult: true)
let laptopCount = countPlayers(nameToFind: "laptop", inArray: players, printResult: true)
let cdPlayerCount = countPlayers(nameToFind: "CD Player", inArray: players, printResult: true)
This sample code produces the following output:
Found 4 players with productName == smartphone
Found 3 players with productName == laptop
Found 2 players with productName == CD Player

Set class property inside an array in swift

I was wondering if there is a possibility to set a class property via an array:
class Test {
var x: Int = 0
var y: Int = 0
var z: Int = 0
}
let newTest = Test()
let myArray = [newTest.x, newTest.y, newTest.z]
So I created an Array with the properties of a class. Now I want to access the property object itself and set it to a value, kind of like this (I know this sets the array at index 0 to 1, but I hope the general idea is clear):
myArray[0] = 1 // I want to set newTest.x = 1
The following one of course works but I need the property objects themself in the array:
let myArray = [newTest, newTest, newTest]
myArray[0].x = 1
UPDATE
What I have now is an array with integers, but I need the reference to the class property, so I can set its value.
The Use Case:
I have an TextField as a user input. The user input should be splitted by a ','. And I don't know in advance, how many items I will get.
And therefore I thought I run a for loop, so I can set class properties based on the number of items.
newItem = Item()
var myClassPorperties = [newItem.cat1, newItem.cat2, newItem.cat3]
if let categoriesArray = categoriesTextField.text?.components(separatedBy: ",") {
for i in 0...categoriesArray.count - 1 {
myClassProperties[i] = categoriesArray[i]
}
}
It seems a bit strange to take a list of comma separated values and use them to update an object but given that here is one way to do it.
Create a function to map between an index (in the array) and a property in your class
func update(at index: Int, with value: Int) {
switch index {
case 0:
x = value
case 1:
y = value
case 2:
z = value
default:
print("Value \(value) at \(index) igonred")
}
}
and use it like this
categoriesArray.compactMap(Int.init).enumerated().forEach(newTest.update)
Another solution is to work with KeyPath
func keyPath(for index: Int) -> WritableKeyPath<Test, Int>? {
switch index {
case 0:
return \.x
case 1:
return \.y
case 2:
return \.z
default:
return nil
}
}
and then use the following code to update the object using your array
categoriesArray.compactMap(Int.init).enumerated().forEach {
if let keyPath = keyPath(for: $0.offset) {
newTest[keyPath: keyPath] = $0.element
}
}
I still think it would be better to have one input field in the UI for every property in your class so you can have a direct one-to-one connection between them.

2D array lookup in swift - similar to vlookup in excel

Re:
Finding a value in an array of arrays (similar to VLOOKUP function in Excel) in Swift
The above shows a method for determining the next lowest value in a 2D array given a search value. Reproduced here for convenience:
let testArray: [[Double]] = [
[0,0],
[1000,20.5],
[3000,21],
[3500,22.5],
[3300,21],
]
let income: Double = 3500
var closest = testArray[0][0]
var closestDif = closest - income
for innerArray in testArray {
let value = innerArray[0]
let thisDif = value - income
guard thisDif <= 0 else {
continue
}
if closestDif < thisDif {
closestDif = thisDif
closest = value
guard closestDif != 0 else {
break
}
}
}
print(closest)
The value returned for closest is 3500. Can someone please describe how we then retrieve the corresponding second number in the array [3500, 22.5] i.e. 22.5?
(edit)
Is enumerated(){....} a cleaner way to do this?
Thanks!
You can easily modify Martin R's answer from the linked Q&A to keep the whole inner array in compactMap and then find the maximum based on the first element of each inner array.
let result = testArray.compactMap { $0[0] <= income ? $0 : nil }.max(by: {$0.first! < $1.first!})! // [3500, 22.5]
#David Pasztor thanks your solution works nicely. I’m working swift 3 so I had to substitute “flatMap” for "compactMap" but otherwise it works great and just one line of code! I have used the same technique to also obtain the nearest higher values in the data to the search value (income) and then interpolate to get a value in the second column proportional to the search value income. The interpolation requires guarding against divide by zero when the search value income equals one of the values in the first column in which case the corresponding result0[0],[1] and result1[0],[1] are identical.
let testarray:[[Double]] = [
[0,0],
[1000,20.5],
[3000,21],
[3500,22.5],
[3300,21],
]
let income:Double = 3400
let result0 = testarray.flatMap { $0[0] <= income ? $0 : nil }.max(by: {$0.first! < $1.first!})!
let result1 = testarray.flatMap { $0[0] >= income ? $0 : nil }.min(by: {$0.first! < $1.first!})!
if income - result0[0] < 0.001 {
let interp = result0[1]
print(interp)
}
else {
let interp = result0[1] + (result1[1] - result0[1])*(income - result0[0])/(result1[0] - result0[0])
print(interp) // 21.75
}

Swift: Merge custom object elements of an Array if they have the same key

I have a Realm Object class called Performance which looks like this
class Performance: Object {
#objc dynamic var move = ""
#objc dynamic var score = 0
}
and when I print the results of the Realm object I get something like this
[Performance {
move = Run;
score = 3;
}, Performance {
move = Walk;
score = 3;
}, Performance {
move = Run;
score = 2;
}]
Then I try to convert the realm results into an Array so that I can merge the performances with the same move and add their score so that I can show the user which of their move has the highest and lowest scores.
I would want to know if there is any chance where I can merge the Performance with the same move and add their score so that the array would look like this.
[Performance {
move = Run;
score = 5;
}, Performance {
move = Walk;
score = 3;
}]
I have tried few solutions like One, two and three.
But they are not really helpful for my problem.
You could iterate through the array and create a dictionary with your move type being the key.
For example:
var totals: [MoveType: Int] = [:]
for item in performances {
totals[item.move] = item.score + (totals[item.move] ?? 0)
}
For each item, this will add the score to the dictionary for the relevant move type, creating a new dictionary entry if it's the first item with that move type.

Swift 4 array issue in changing text colour

I have a string and I want to change colors of two words in that string. So, I created a function
func setup()
{
let main_string = "By continuing you agree to our Term of use and Privacy Policy "
var string_to_color = ["By continuing you agree to our","and"]
for i in 0..<2
{
let range = (main_string as NSString).range(of: string_to_color[i])
let attributedString = NSMutableAttributedString.init(string:main_string)
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.black , range: range)
privacyL.attributedText = attributedString
}
But, it only changes the colour for the second word and not for the first one.
Can anyone help?
let main_string = "By continuing you agree to our Term of use and Privacy Policy "
var string_to_color = ["By continuing you agree to our","and"]
for i in 0..<2
{
let range = (main_string as NSString).range(of: string_to_color[i])
let attributedString = NSMutableAttributedString.init(string:main_string)
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.black , range: range)
privacyL.attributedText = attributedString
}
You are overriding each time privacyL.attributedText, so you'll get only the "result" of last iteration.
Instead, do:
let attributedString = NSMutableAttributedString.init(string:main_string)
for i in 0..<2
{
let range = (main_string as NSString).range(of: string_to_color[i])
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.black , range: range)
}
privacyL.attributedText = attributedString
Now, next issues:
Don't do a loop for i in 0..<2, instead use at least the count of string_to_color (or a for each loop). If tomorrow you add a string in it or remove one, you'll encounter an issue.
Also range(of:) will return the first occurence found, so if you have:
let main_string = "and and"
var string_to_color = ["something", "and"]
only the first "and" will be colored.
You have then to iterate or use a NSRegularExpression.
Here is a related question: Color all occurrences of string in swift
Just want to add to Larme's answer. You can create extension of String, which will be responsible for string coloring
extension String {
func colored(_ strings: [String], with color: UIColor) -> NSAttributedString {
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
for string in strings {
let range = (self as NSString).range(of: string)
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: color , range: range)
}
return attributedString
}
}
And now you can use it anywhere in your code, like this:
let main_string = "By continuing you agree to our Term of use and Privacy Policy "
let string_to_color = ["By continuing you agree to our","and"]
privacyL.attributedText = main_string.colored(string_to_color, with: UIColor.red)

Resources