Search code examples
swiftrealmdistinctdistinct-valuesrealm-mobile-platform

Distinct date count of records in RealmSwift?


I have records with date field:

class SomeRealmObject: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var date: Date
    ... //some other properties
}

The goal is to calculate a count of records distinct by date ("distinct" should ignore time inside date).

If I already had a list of records I could work with them like usual elements of array:

let allObjects = getAll() //SomeRealmObject array
let dates = allObjects.map { $0.startOfDay() }.reduce([]) { partialResult, date in
                partialResult.contains(date) ?
                partialResult : partialResult + [date]
}
let result = dates.count

But is it possible to do the same with RealmSwift methods because I need only count of SomeRealmObject and don't need to get its inner data? There is for example distinct(by:) but it compares fields by their value and doesn't allow to specify additional conditions like startOfDay()


Solution

  • There are a few issues to overcome - first is that Realm functions don't allow us to 'dig' into dates - dates are the whole date, including the time.

    Because of that, ignoring the time is not possible using Realm syntax. e.g. you can't do this

    let distinctObjects = realm.objects(SomeObject.self).distinct {by: "someDate.yyyymmdd"}
    

    bummer.

    Another non-working thought is to use a predicates (Realm supports some predicates) with a block to isolate the date components

    let myPredicate = NSPredicate(block: { (evaluatedDate, _) -> Bool in
        //isoloate the yyyy mm dd components for evaluation
    })
    

    But alas, Realm "Only supports compound, comparison, and constant predicates"

    One solution (of many) is to consider what's being stored - a Date() object is actually a very specific timestamp, which includes the time. But that's not what your distinct needs - it needs the year, month and day

    So, store the date components you want to compare as a separate property; possibly string or a date built with no time component

    class SomeRealmObject: Object {
        @Persisted(primaryKey: true) var _id: ObjectId
        @Persisted var date: Date
        @Persisted var dateString = "" //stored as yyyymmdd
        //or
        @Persisted var dateNoTime: Date //stored as a date with 0's as time
    }
    

    The dateNoTime property could be built by extracting the date components you want to distict on, create a new date object and store that; for example

    let components = DateComponents(year: 2023, month: 04, day: 01)
    let theDate = Calendar.current.date(from: components)
    

    There are a number of paths from there, one is to use a function or computed property that's backed by Realm properties to handle the date, so when setting the date on an object it writes out the actual timestamp version as well as the one used for distinct.

    class SomeRealmObject: Object {
        @Persisted(primaryKey: true) var _id: ObjectId
        @Persisted var date: Date
        @Persisted var dateString = "" //stored as yyyymmdd
    
        func writeDate(withDate: Date) {
           self.date = withDate
           self.dateString = //break down withDate to yyyymmdd
        }
    }
    

    then you can use Realms distinct to get the results

    let distictObjects = realm.objects(SomeObject.self).distict(by: ["dateString"]
    

    *note this could also be done by loading the objects into an array and then use a Swift predicate to evaluate but that could cause memory issues and potentially be slower.