RxSwift ordering by date after convertiing from string - date

I have and array I'm subscribing, objects in this array has a property date, in form of a String. I need to order the array I receive from subscription by such date, but cannot make it if it is a string, I think need to convert that string in a date then use it to order them. I know how to convert from string to date, but no idea where to do
object is like to
struct Task: codable {
var date = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
}
self.viewModel.tasks
.map({ items in
return items.sorted(by: { $0.date.compare($1.date) == .orderedAscending })
})
.bind(to: self.tasksTableView.rx.items(dataSource: self.dataSource))
.disposed(by: self.rx.disposeBag)

If you don't want to change the type of date then do it all inside your map's closure.
tasks.map { tasks in
tasks.sorted(by: { lhs, rhs in
dateFormatter.date(from: lhs.date)!.compare(dateFormatter.date(from: rhs.date)!) == .orderedAscending
})
}
However, I would change the Task type to hold a Date instead of a String...
struct Task: Codable {
let date: Date
}
Then this would work:
tasks.map { tasks in
tasks.sorted(by: { $0.date.compare($1.date) == .orderedAscending })
}

Related

How to create a Swift Regex that outputs a custom type?

In the WWDC videos, it was shown that you can do something like this with Captures/TryCaptures in the Regex Builder:
let regex = Regex {
// ...
TryCapture {
OneOrMore(.digit)
} transform: {
Int($0)
}
// ...
}
And the output of the Regex will be type safe. The Regex will output an Int for that group, instead of a Substring like it normally does.
However, what I would like to do is to change the entire output type of the whole Regex, like applying a transform: at the end of the Regex closure. For example, to parse a line containing the name, age and date of birth of a person:
John (30) 1992-09-22
I would like to do something like:
// this doesn't work and is just for illustration - there is no such Regex.init
let regex = Regex {
Capture(/\w+/)
" ("
TryCapture(/\d+/) { Int($0) }
") "
Capture(.iso8601Date(timeZone: .gmt))
} transform: { (_, name, age, dob) in
Person(name: String(name), age: age, dob: dob)
}
And I would expect regex be of type Regex<Person>, and not Regex<(Substring, Substring, Int, Date)>. That is, someString.wholeMatch(of: regex).output would be a string, not a tuple.
I'm basically just trying to reduce the occurrence of tuples, because I find it very inconvenient to work with them, especially unnamed ones. Since RegexComponent is parameterised by the unconstrained RegexOutput type, and there are built-in types where RegexOutput is Date and Decimal, surely doing this for arbitrary types using regex is not impossible, right?
My attempt was:
struct Person {
let name: String
let age: Int
let dob: Date
}
let line = "John (30) 1992-09-22"
let regex = Regex {
Capture {
Capture(/\w+/)
" ("
TryCapture(/\d+/) { Int($0) }
") "
Capture(.iso8601Date(timeZone: .gmt))
} transform: { (_, name, age, dob) in
Person(name: String(name), age: age, dob: dob)
}
}
line.wholeMatch(of: regex)
but this crashed at runtime, giving the message:
Could not cast value of type 'Swift.Substring' (0x7ff865e3ead8) to '(Swift.Substring, Swift.Substring, Swift.Int, Foundation.Date)' (0x7ff863f2e660).
Another attempt of mine using CustomConsumingRegexComponent is shown here in this answer, but that has quite a large caveat, namely that it doesn't backtrack properly.
How can I create a Regex that outputs my own type?
From what I have read/seen in samples (e.g. swift-regex), it might be a good idea to create a regex component similar to .word, .digit, but nesting captures does not seem to work easily.
Here is an example run in the playground to create a Person struct instance:
public static func regexBuilderMatching(string: String = "John (30) 1992-09-22") {
struct Person: CustomStringConvertible {
let name: String
let age: Int
let dob: Date
public func dobToFormatterString() -> String {
let dateFormatter = DateFormatter()
// 1992-09-22 04:00:00 +0000
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter.string(from: self.dob)
}
var description: String {
return "\(name), age: \(age), has dob: \(dobToFormatterString())"
}
}
func dateFromString(dateString: String) -> Date? {
let formatter = DateFormatter()
formatter.timeStyle = .none // removes time from date
formatter.dateStyle = .full
formatter.dateFormat = "y-MM-d" // 1992-09-22
return formatter.date(from: dateString)
}
let regexWithBasicCapture = Regex {
/* 1. */ Capture { OneOrMore(.word) }
/* 2. */ " ("
/* 3. */ TryCapture { OneOrMore(.digit) }
transform: { match in
Int(match)
}
/* 4. */ ") "
/* 5. */ TryCapture { OneOrMore(.iso8601Date(timeZone: .gmt)) }
transform: { match in
dateFromString(dateString: String(match))
}
}
let matches = string.matches(of: regexWithBasicCapture)
for match in matches {
// shorthand syntax using match output
// https://developer.apple.com/documentation/swift/regex/match
let (_, name, age, date) = match.output
let person = Person(name: String(name), age: age, dob: date)
print(person)
}
}
The above code will output:
John, age: 30, has dob: 1992-09-22

Sum value of an array of structs in Swift

I'm looking for some help on how to sum a value within an array of structs.
If I have a struct defined like this:
struct Item {
let value : Float
let name : String
let planDate : String
}
And then an array of these structs like this:
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")
]
How can I sum the value while grouping by the name and planDate as well as sorting by name and planDate?
Here's what I'd like to return:
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")
]
The easiest way (well, easy is in the eye of the beholder) is to make a dictionary that groups by a composite of your criteria, name and planDate. Now for each entry in the dictionary you've got an array of all the Items that go together! So just sum their values. Now make the dictionary back into an array and sort it.
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)
I would take a different approach as well. First make your Item conform to Equatable and Comparable. Then you can reduce your sorted items, check if each item is equal to the last item of the result. If true increase the value otherwise append a new item to the result:
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)
}
}
I'll offer another variant using 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)}
For completeness, here's a solution that explicitly makes use of the fact that you want to use name&planDate as both group identifier, and sort key.
You can make use of the Identifiable protocol, and build a struct with name&planDate (structs are almost free in 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)
}
}
Then you can destructure the Item struct by its identifier, accumulate the values, and restructure it back:
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:))
We can take it even further and refine the operations we need by extensing 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] }
}
}
The two new additions, sort by a key path, and accumulate key paths by using another key, are independent enough to deserve function for they own, as they are generic enough to be reusable in other contexts.
The actual business logic becomes simple as
let result = dataArray
.accumulated(values: \.value, by: \.id)
.map(Item.init(id:value:))
.sorted(by: \.id)
Even if this solution is more verbose than the other one, it has the following advantages:
clear separation of concerns
breaking the code into smaller units, which can be independently unit tested
code reusability
simple caller code, easy to understand and review

Custom comparator for Swift

This is my code (simplified code):
struct SomeStruct {
let id: Int
let age: Int
}
extension SomeStruct: Hashable {
var hashValue: Int {
return id.hashValue * age.hashValue
}
static func ==(lhs: SomeStruct, rhs: SomeStruct) -> Bool {
return lhs.id == rhs.id && lhs.age == rhs.age
}
}
struct Calculator {
let struct1: [SomeStruct]
let struct2: [SomeStruct]
func uniqueById() {
let struct3 = Set(struct2).union(Set(struct1))
// I want to union it by property 'id' only.
// If the property 'id' is equal for both objects,
// the object in struct2 should be used (since that can have a different age property)
}
}
SomeStruct is a generated struct which I do not want to edit. I want to create a Set for SomeStruct that is based on 1 property: id. For that, I think I need a custom Comparator, just as Java has. Is there any Swifty way? This is the only thing I can come up with, but I am wondering if there is a better way:
struct SomeStructComparatorById: Hashable {
let someStruct: SomeStruct
var hashValue: Int {
return someStruct.id.hashValue
}
static func ==(lhs: SomeStructComparatorById, rhs: SomeStructComparatorById) -> Bool {
return lhs.someStruct.id == rhs.someStruct.id
}
}
First, I don't think this would work in Java. addAll() doesn't take a Comparator (nor does contains, etc.) Comparators are for sorting, not equality. Conceptually this is breaking how Set works in any language. Two items are not "equal" unless they can be swapped in all cases.
That tells us that we don't want a Set here. What you want here is uniqueness based on some key. That's a Dictionary (as Daniel discusses).
You could either just have a "id -> age" dictionary or "id -> struct-of-other-properties" dictionary as your primary data type (rather than using Array). Or you can turn your Array into a temporary Dictionary like this:
extension Dictionary {
init<S>(_ values: S, uniquelyKeyedBy keyPath: KeyPath<S.Element, Key>)
where S : Sequence, S.Element == Value {
let keys = values.map { $0[keyPath: keyPath] }
self.init(uniqueKeysWithValues: zip(keys, values))
}
}
And merge them like this:
let dict1 = Dictionary(struct1, uniquelyKeyedBy: \.id)
let dict2 = Dictionary(struct2, uniquelyKeyedBy: \.id)
let merged = dict1.merging(dict2, uniquingKeysWith: { old, new in old }).values
This leaves merged as [SomeStruct].
Note that this Dictionary(uniquelyKeyedBy:) has the same preconditions as Dictionary(uniqueKeysWithValues:). If there are duplicate keys, it's a programming error and will raise precondition failure.
You could do something like this:
var setOfIds: Set<Int> = []
var struct3 = struct2.filter { setOfIds.insert($0.id).inserted }
struct3 += struct1.filter { setOfIds.insert($0.id).inserted }
The result would be an array of SomeStruct, with all elements with unique ids.
You could define this as a custom operator :
infix operator *>
func *> (lhs: [SomeStruct], rhs: [SomeStruct]) -> [SomeStruct] {
var setOfIds: Set<Int> = []
var union = lhs.filter { setOfIds.insert($0.id).inserted }
union += rhs.filter { setOfIds.insert($0.id).inserted }
return union
}
Your code would then look like this:
func uniqueById() {
let struct3 = struct2 *> struct1
//use struct3
}
The short answer is no. Swift sets do not have any way to accept a custom comparator and if you absolutely must have a Set, then your wrapper idea is the only way to do it. I question the requirement for a set though.
Instead of using Set in your calculator, I recommend using dictionary.
You can use a Dictionary to produce an array where each item has a unique ID...
let struct3 = Dictionary(grouping: struct1 + struct2, by: { $0.id })
.compactMap { $0.value.max(by: { $0.age < $1.age })}
Or you can keep the elements in a [Int: SomeStruct] dictionary:
let keysAndValues = (struct1 + struct2).map { ($0.id, $0) }
let dictionary = Dictionary(keysAndValues, uniquingKeysWith: { lhs, rhs in
lhs.age > rhs.age ? lhs : rhs
})

Swift Date literal / Date initialisation from string

I need to initialize a date to a known, fixed, point in time. Swift lacking date literals, I tried :
extension DateFormatter
{
convenience init(_ format: String)
{
self.init()
self.dateFormat = format
}
}
extension Date
{
init?(_ yyyyMMdd: String)
{
let formatter = DateFormatter("yyyyMMdd")
self = formatter.date(from: yyyyMMdd)
}
}
Unfortunately, I can't write self = ... or return formatter.date... as part of the initializer.
However, I would very much like to write :
let date = Date("20120721")
How can I achieve this ?
You can initialize a struct by assigning a value to self.
The problem in your case is that formatter.date(from:)
returns an optional, so you have to unwrap that first:
extension Date {
init?(yyMMdd: String) {
let formatter = DateFormatter("yyyyMMdd")
guard let date = formatter.date(from: yyMMdd) else {
return nil
}
self = date
}
}
You can even initialize a date from a string literal by
adopting the ExpressibleByStringLiteral protocol:
extension Date: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
let formatter = DateFormatter("yyyyMMdd")
self = formatter.date(from: value)!
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}
public init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}
}
let date: Date = "20120721"
Note however that this would crash at runtime for invalid dates.
Two more remarks:
You should set the formatters locale to Locale(identifier: "en_US_POSIX") to allow parsing the date string independent of
the user's locale settings.
The result depends on the current time zone, unless you also
set the formatters time zone to a fixed value (such as TimeZone(secondsFromGMT: 0).

Converting a rawValue into a string in Swift

Apple has made changes from Swift 3 to 4. When I run the following code:
let metadata = [ PDFDocumentAttribute.titleAttribute,
PDFDocumentAttribute.authorAttribute,
PDFDocumentAttribute.subjectAttribute,
PDFDocumentAttribute.creatorAttribute,
PDFDocumentAttribute.producerAttribute,
PDFDocumentAttribute.creationDateAttribute,
PDFDocumentAttribute.modificationDateAttribute,
PDFDocumentAttribute.keywordsAttribute ]
if var attributes = pdfDoc.documentAttributes {
for (index, value) in metadata.enumerated() {
if attributes[value] != nil {
print("\(metadata[index])): \(String(describing: attributes[value]!))")
} else {
print("\(metadata[index]): nil")
}
}
I now get: PDFDocumentAttribute(_rawValue: Title) instead of "Title", which I got before as the value of metadata[index].
How do I get rid of the rawValue stuff?
The PDFDocumentAttribute type has a property called rawValue that contains the old string value. So you can say
print("\(metadata[index].rawValue): \(String(describing: attributes[value]!))")
As an aside, instead of force-unwrapping the attribute you can use an if let, as in
if let attr = attributes[value] {
print("\(metadata[index].rawValue): \(attr)")
} else {
print("\(metadata[index].rawValue): nil")
}
If you add this extension:
extension PDFDocumentAttribute: CustomStringConvertible {
public var description: String {
return self.rawValue
}
}
Now you can just do:
// Forcing the downcast has little risk here
// but you may want to use `as?` and test for the optional instead
let attributes = pdfDoc.documentAttributes as! [PDFDocumentAttribute:Any]
for meta in metadata {
print("\(meta): \(attributes[meta] ?? "nil")")
}
Note that you can also do:
for attribute in attributes {
print("\(attribute.key): \(attribute.value)")
}
Which will just print out the attributes that exist on the document.