I'm having a heck of a time in Swift 3 sorting an array of dictionaries.
In Swift 2, I would do it this way, which worked fine:
var dicArray = [Dictionary<String, String>()]
let dic1 = ["last": "Smith", "first": "Robert"]
dicArray.append(dic1)
let dic2 = ["last": "Adams", "first": "Bill"]
dicArray.append(dic2)
let sortedArray = dicArray.sort { ($0["last"] as? String) < ($1["last"] as? String) }
Converting the same code to Swift 3 has not gone well. The system guided me to this (through a circuitous route):
let sortedArray = dicArray.sorted { ($0["last"]! as String) < ($1["last"]! as String) }
But the app always crashes, with the error that it found nil while unwrapping an Optional value.
After banging my head against the table for too long, putting ?s and !s in every imaginable combination, I resorted to an old approach to get the job done:
let sortedArray = (dicArray as NSArray).sortedArray(using: [NSSortDescriptor(key: "last", ascending: true)]) as! [[String:AnyObject]]
That works, and I'm moving along, but it's not very Swifty, is it?
Where did it all go wrong? How can I make the pure Swift sort function work in a case like this?
Where did it all go wrong?
It went wrong on your first line:
var dicArray = [Dictionary<String, String>()]
That never did what you want, even in Swift 2, because you are actually inserting an extra, empty dictionary into the array. That's where the crash comes from; the empty dictionary has no "last" key, because it is, uh, empty.
You want this:
var dicArray = [Dictionary<String, String>]()
See the difference? After that change, everything falls into place:
var dicArray = [Dictionary<String, String>]()
let dic1 = ["last": "Smith", "first": "Robert"]
dicArray.append(dic1)
let dic2 = ["last": "Adams", "first": "Bill"]
dicArray.append(dic2)
let sortedArray = dicArray.sorted {$0["last"]! < $1["last"]!}
// [["first": "Bill", "last": "Adams"], ["first": "Robert", "last": "Smith"]]
Rather than using dictionaries with a fixed set of keys, it's generally advisable to create your own custom types:
struct Person {
let lastName: String
let firstName: String
}
This way, you never have to worry about whether you got the key for a particular value in a dictionary right, because the compiler will enforce checks for the names of the property. It makes it easier to write robust, error-free code.
And, coincidentally, it makes sorting cleaner, too. To make this custom type sortable, you make it conform to the Comparable protocol:
extension Person: Comparable {
public static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.lastName == rhs.lastName && lhs.firstName == rhs.firstName
}
public static func < (lhs: Person, rhs: Person) -> Bool {
// if lastnames are the same, compare first names,
// otherwise we're comparing last names
if lhs.lastName == rhs.lastName {
return lhs.firstName < rhs.firstName
} else {
return lhs.lastName < rhs.lastName
}
}
}
Now, you can just sort them, keeping the comparison logic nicely encapsulated within the Person type:
let people = [Person(lastName: "Smith", firstName: "Robert"), Person(lastName: "Adams", firstName: "Bill")]
let sortedPeople = people.sorted()
Now, admittedly, the above dodges your implicit question of how to compare optionals. So, below is an example where firstName and lastName are optionals. But, rather than worrying about where to put the ? or !, I'd use nil-coalescing operator, ??, or a switch statement, e.g.:
struct Person {
let lastName: String?
let firstName: String?
}
extension Person: Comparable {
public static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.lastName == rhs.lastName && lhs.firstName == rhs.firstName
}
public static func < (lhs: Person, rhs: Person) -> Bool {
// if lastnames are the same, compare first names,
// otherwise we're comparing last names
var lhsString: String?
var rhsString: String?
if lhs.lastName == rhs.lastName {
lhsString = lhs.firstName
rhsString = rhs.firstName
} else {
lhsString = lhs.lastName
rhsString = rhs.lastName
}
// now compare two optional strings
return (lhsString ?? "") < (rhsString ?? "")
// or you could do
//
// switch (lhsString, rhsString) {
// case (nil, nil): return false
// case (nil, _): return true
// case (_, nil): return false
// default: return lhsString! < rhsString!
// }
}
}
The switch statement is more explicit regarding the handling of nil values (e.g. nil sorted before or after non-optional values) and will distinguish between a nil value and an empty string, should you need that. The nil coalescing operator is simpler (and IMHO, more intuitive for the end-user), but you can use switch approach if you want.
let descriptor: NSSortDescriptor = NSSortDescriptor.init(key: "YOUR KEY", ascending: true)
let sortedResults: NSArray = tempArray.sortedArray(using: [descriptor]) as NSArray
Related
i want to filter a list of student in java. I have a student class in kotlin like this.
class Student(
var id: String? = null,
var firstName: String? = null,
var lastName: String? = null
) {
constructor(entity: StudentCourse?): this() {
if (entity != null) {
this.id = entity.id.id
this.name = entity.name
}
}
}
class StudentCourse (#EmbeddedId open var id: StudentCourseId) {
constructor() : this(StudentCourseId())
open var name: Boolean? = null
}
#Embeddable
open class StudentCourseId: Serializable {
open var id: String? = null
open var deptName: String? = null
}
this is the list i want to filter :
var students: List<Student> = listOf(
Student("14adbv45", "dan", "GEG"),
Student("96adbv42","Bob", "Bowyer"),
Student("30adbv45","Emily", "Eden")
)
I do this
List<students> studentListContainsFirstNameBob = students.stream()
.map(StudentCourse)
.filter(e -> e.getFirstName.equals("Bob"))
.flatMap(List::stream);
but it doesn't work.
How can i do it please
There are multiple issues in your code.
For example in this part:
constructor(entity: StudentCourse?): this() {
if (entity != null) {
this.id = entity.id.id
this.name = entity.name
}
}
The entity.name refers to StudentCourse#name, but this property is actually of Boolean type, so comparing it to String doe snot make much sense. You have also doubled .id which is incorrect.
Next thing, I am not sure what this snipped should do, was the intention to link a student with given course? If so, you would probably like to add a list of students to a course, or a list of courses to a student (which sounds more correct).
Finally, when it comes to filtering a list, you can get students with first name Bob in this way:
var studentListContainsFirstNameBob: List<Student> = students
.filter { it.firstName.equals("Bob") }
.toList()
Mind the it variable refers to the element from the list, it could also be written:
var studentListContainsFirstNameBob: List<Student> = students
.filter { student -> student.firstName.equals("Bob") }
.toList()
I have an array:
var month: List<String> = arrayListOf("January", "February", "March")
I have to filter the list so I am left with only "January".
You can use this code to filter out January from array, by using this code
var month: List<String> = arrayListOf("January", "February", "March")
// to get the result as list
var monthList: List<String> = month.filter { s -> s == "January" }
// to get a string
var selectedMonth: String = month.filter { s -> s == "January" }.single()
There are a number of functions for filtering collections, if you want to keep only values matching "January", you can use the simple filter():
val months = listOf("January", "February", "March")
months.filter { month -> month == "January" } // with explicit parameter name
months.filter { it == "January" } // with implicit parameter name "it"
These will give you a list containing only "January".
If you want all months that are not "January", you can either reverse the condition using !=, or use filterNot():
months.filter { it != "January" }
months.filterNot { it == "January" }
These will give you a list containing "February" and "March".
Note that unlike Java, using the == and != operators in Kotlin is actually the same as calling the equals function on the objects. For more, see the docs about equality.
For the complete list of collection functions in the standard library, see the API reference.
You want to filter this list of Strings containing months.
var month : List<String> = arrayListOf("January", "February", "March")
You can use filterNot() method of list. It returns a list containing all elements except the given predicate.
var filteredMonthList : List<String> = month.filterNot { s -> s == "January" }
// results: ["February", "March"]
You can use filter() method of list. It returns a list containing all elements matching the given predicate.
var filteredMonthList : List<String> = month.filter { s -> s == "January" }
// results: ["January"]
After filter() if we use single() method then it will return a single value and throw an exception if more than one value is in the list.
var filteredMonth : String = month.filter { s -> s == "January" }.single()
// result: "January"
I am just sharing that if you have custom list and check whether it is null or blank you can check in Kotlin in single line
Just do it like that
fun filterList(listCutom: List<Custom>?) {
var fiterList = listCutom!!.filter { it.label != "" }
//Here you can get the list which is not having any kind of lable blank
}
You can check multiple conditions also
fun filterList(listCutom: List<Custom>?) {
var fiterList = listCutom!!.filter { it.label != "" && it.value != ""}
//Here you can get the list which is not having any kind of lable or value blank
}
Note : I am assuming that label & value are the variables of Custom Model class.
You can also use find or findLast. This is specifically meant to return only one value instead of a list of String returned in case of filter.
var month = arrayListOf("January", "February", "March")
var result = month.find { s -> s == "January" }
Filtering by predicate
val numbers = listOf("one", "two", "three", "four")
var items: List<String> = numbers.filter { s -> s == "one" }
var item = numbers.singleOrNull { it == "one" }
if (item != null) {
print("FOUND:$item")
} else {
print("Not FOUND!")
}
Target: The following function shall iterate over an array of objects and check a specific property of all objects. This property is a string and shall be matched with a user input via regex. If there's a match the object shall be added to an array which will further be passed to another function.
Problem: I don't know how to set up regex in Swift 3. I'm rather new in Swift at all, so an easily understandable solution would be very helpful :)
How it currently looks like:
func searchItems() -> [Item] {
var matches: [Item] = []
if let input = readLine() {
for item in Storage.storage.items { //items is a list of objects
if let query = //regex with query and item.name goes here {
matches.append(item)
}
}
return matches
} else {
print("Please type in what you're looking for.")
return searchItems()
}
}
This is what Item looks like (snippet):
class Item: CustomStringConvertible {
var name: String = ""
var amount: Int = 0
var price: Float = 0.00
var tags: [String] = []
var description: String {
if self.amount > 0 {
return "\(self.name) (\(self.amount) pcs. in storage) - \(price) €"
} else {
return "\(self.name) (SOLD OUT!!!) - \(price) €"
}
}
init(name: String, price: Float, amount: Int = 0) {
self.name = name
self.price = price
self.amount = amount
}
}
extension Item: Equatable {
static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.name == rhs.name
}
}
Solved. I just edited this post to get a badge :D
For the purpose of letting the answer to be generic and clear, I will assume that the Item model is:
struct Item {
var email = ""
}
Consider that the output should be a filtered array of items that contains items with only valid email.
For such a functionality, you should use NSRegularExpression:
The NSRegularExpression class is used to represent and apply regular
expressions to Unicode strings. An instance of this class is an
immutable representation of a compiled regular expression pattern and
various option flags.
According to the following function:
func isMatches(_ regex: String, _ string: String) -> Bool {
do {
let regex = try NSRegularExpression(pattern: regex)
let matches = regex.matches(in: string, range: NSRange(location: 0, length: string.characters.count))
return matches.count != 0
} catch {
print("Something went wrong! Error: \(error.localizedDescription)")
}
return false
}
You can decide if the given string does matches the given regex.
Back to the example, consider that you have the following array of Item Model:
let items = [Item(email: "invalid email"),
Item(email: "email#email.com"),
Item(email: "Hello!"),
Item(email: "example#example.net")]
You can get the filtered array by using filter(_:) method:
Returns an array containing, in order, the elements of the sequence
that satisfy the given predicate.
as follows:
let emailRegex = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
let emailItems = items.filter {
isMatches(emailRegex, $0.email)
}
print(emailItems) // [Item(email: "email#email.com"), Item(email: "example#example.net")]
Hope this helped.
You can do the same with filter function
let matches = Storage.storage.items.filter({ $0.yourStringPropertyHere == input })
I am updating my code to swift3.0 but getting ambiguous refrence to member? What wrong i might be doing. Here is the method I am getting error in.
open class func parseJsonTenantList(_ list: [NSDictionary]?, strElementName: String, attrName1: String, attrNameValue2: String) -> [TenantRegister]
{
var renantList: [TenantRegister] = []
var key: String?
if let dict : [NSDictionary] = list {
var value: String?
for i in 0..<dict.count {
/// if attribute name doesn't match then it returns nil
if let s1: AnyObject = dict[i].value(forKey: attrName1)
{
key = s1 as? String
}
if let s2: AnyObject = dict[i].value(forKey: attrNameValue2)
{
value = s2 as? String
}
if (!(String.stringIsNilOrEmpty(value) && String.stringIsNilOrEmpty(key)))
{
let t: TenantRegister = TenantRegister()
t.name = key
t.tenantId = Guid(value!)
renantList.append(t)
}
}
}
return renantList
}
The issue is you are using NSDictionary, to solved your problem simply cast the list to Swift's native type [[String:Any]] and then use subscript with it instead of value(forKey:)
if let dict = list as? [[String:Any]] {
var value: String?
for i in 0..<dict.count {
/// if attribute name doesn't match then it returns nil
if let s1 = dict[i][attrName1] as? String
{
key = s1
}
if let s2 = dict[i][attrNameValue2] as? String
{
value = s2
}
if (!(String.stringIsNilOrEmpty(value) && String.stringIsNilOrEmpty(key)))
{
let t: TenantRegister = TenantRegister()
t.name = key
t.tenantId = Guid(value!)
renantList.append(t)
}
}
}
In Swift use native type Dictionary [:] and Array [] instead of NSDictionary and NSArray to overcome this type of issues.
I have those data classes:
data class RouteType(
#SerializedName("type")
val type: String,
#SerializedName("items")
val items: List<RouteItem>)
data class RouteItem(
#SerializedName("id")
val id: String,
#SerializedName("route")
private val route: List<DoubleArray>)
I want to filter list of RouteType by type and filter list of RouteItem in it by id.
My code now:
// val filter: HashMap<String, List<String>>
val result = routeTypes // List<RouteType>
.filter { it.type in filter.keys }
.map {
routeType -> routeType.items.filter { it.id in filter[routeType.type]!! }
}
How to make .map return list with filtered list in it? Or maybe there's another way?
EDIT
Thanks, but flatmap not exactly what I need, I think. flatmap returns nested list(List<RouteItem>), but I want List<RouteType>.
I got it by this code:
val result = routeTypes
.filter { it.type in filter.keys }
.map {
routeType -> RouteType(
routeType.type,
routeType.items.filter { it.id in filter[routeType.type]!! })
}
Is there another way to get it?
Since your data is immutable (that's a good thing) you need to copy it while filtering. Use copy to make it more extensible:
val result = routeTypes
.filter { it.type in filter.keys }
.map { it.copy(items = it.items.filter { it.id in filter[routeType.type]!! }) }
You can use flatMap for this, it works as map, but merges all your mapped collections to one:
val result = routeTypes // List<RouteType>
.filter { it.type in filter.keys }
.flatMap {
routeType -> routeType.items.filter { it.id in filter[routeType.type]!! }
}