How to calculate streak of days in core data?
Asked Answered
M

3

16

I need to count last streak of days, but can't figure out how to do this. For example I got core data like this:

|id| isPresent(Bool)| date(NSDate)|
|==|================|=============|
| 1|               0| 2016-02-11  |
| 2|               1| 2016-02-11  |
| 3|               1| 2016-02-12  |
| 4|               0| 2016-02-14  |
| 5|               1| 2016-02-15  |
| 6|               1| 2016-02-16  |
| 7|               1| 2016-02-16  |

I try to check last not presented(isPresent = 0) date till today and get 2016-02-14 - so I can count days, it is easy.

But if I mark 2016-02-14 as isPresented = 1(like in table lower) I will get last not presented 2016-02-11 - but it is not correct there are no data for 2016-02-13, so isPresented for this date should be 0 and streak should count from this date

|id| isPresent(Bool)| date(NSDate)|
|==|================|=============|
| 1|               0| 2016-02-11  |
| 2|               1| 2016-02-11  |
| 3|               1| 2016-02-12  |
| 4|               1| 2016-02-14  |
| 5|               1| 2016-02-15  |
| 6|               1| 2016-02-16  |
| 7|               1| 2016-02-16  |

I searched for different algoritm for streaks or sql reuests for missing dates(sql server displaying missing dates) but cant figure out how to use it in core data.

I thinking about another data that will keep streak and updates every time when user opened app, but got same problem what if user didn't open app.

Output: I need to found just counts of days in streak or date when it break, so for

For first table: streak = 2 or breakDate = 2016-02-14 - I try this, but my solution wrong because second table

For second table: streak = 3 or breakDate = 2016-02-13 - can't figure out how to get missing date

Important Update: There will be cloud sync data, so I don't see any solution inside app, really need to find missing date or isPresented = 0 in coredata

p.s. I'm using swift if you can help me via swift it would be great, but I also understand Obj-C. And sorry for my bad English

Mora answered 16/2, 2016 at 7:43 Comment(8)
You can fetch data with some selector.This may be help but not for your question.Conducive
Please show us your expected outputTarbox
Your main issue is to fill the missing dates (days?) with isPresent to 0. But this poses also this question: Do you mean every day? Or maybe only from Monday to Friday, etc.? Maybe at launch of the app, its check the current date, and fill the missing days from last start? Then, you can use the fetchLimit and the condition to isPresent == 0 and an order descending to find your date.Dowser
@Conducive true I use predicate, sorting for my wrong solution, fut can't understand how to use complex sql requests(like in link in question) to core dataMora
@Dowser main issue to get date that missing in coredata ot date where isPresent is 0 I think about setting missing dates, but think that it is wrong direction, user can forgot about app for 100+ or more days and setting 0 for all days isn't good idea. BTW I don't know what to do if user just install app, check shared variable if it is null set it to today, if not null set all dates till today isPresent=0, but if user check isPresent via notification? in notification action do same?Mora
@Dowser by the way, I need every day, so how to find missing dates from last start, check all dates from last start in core data and if nil set 0?Mora
Just an idea: for each date, calculate the number of days since a fixed date, put the number of days in a NSIndexSet and get the last range.Oven
Why is this tagged SQL? What SQL interface to Core Data are you suing?Umbilical
S
4

From your question, I guess you have a item entity with a NSDate object. Here is some code you can use to do it.

let userDefaults = NSUserDefaults.standardUserDefaults()
var moc: NSManagedObjectContext!

var lastStreakEndDate: NSDate!
var streakTotal: Int!



override func viewDidLoad() {
    super.viewDidLoad()


    // checks for object if nil creates one (used for first run)
    if userDefaults.objectForKey("lastStreakEndDate") == nil {
        userDefaults.setObject(NSDate(), forKey: "lastStreakEndDate")
    }

    lastStreakEndDate = userDefaults.objectForKey("lastStreakEndDate") as! NSDate

    streakTotal = calculateStreak(lastStreakEndDate)
}


// fetches dates since last streak
func fetchLatestDates(moc: NSManagedObjectContext, lastDate: NSDate) -> [NSDate] {
    var dates = [NSDate]()

    let fetchRequest = NSFetchRequest(entityName: "YourEntity")
    let datePredicate = NSPredicate(format: "date < %@", lastDate)

    fetchRequest.predicate = datePredicate

    do {
        let result = try moc.executeFetchRequest(fetchRequest)
        let allDates = result as! [NSDate]
        if allDates.count > 0 {
            for date in allDates {
                dates.append(date)
            }
        }
    } catch {
        fatalError()
    }
    return dates
}


// set date time to the end of the day so the user has 24hrs to add to the streak
func changeDateTime(userDate: NSDate) -> NSDate {
    let dateComponents = NSDateComponents()
    let currentCalendar = NSCalendar.currentCalendar()
    let year = Int(currentCalendar.component(NSCalendarUnit.Year, fromDate: userDate))
    let month = Int(currentCalendar.component(NSCalendarUnit.Month, fromDate: userDate))
    let day = Int(currentCalendar.component(NSCalendarUnit.Day, fromDate: userDate))

    dateComponents.year = year
    dateComponents.month = month
    dateComponents.day = day
    dateComponents.hour = 23
    dateComponents.minute = 59
    dateComponents.second = 59

    guard let returnDate = currentCalendar.dateFromComponents(dateComponents) else {
        return userDate
    }
    return returnDate
}


// adds a day to the date
func addDay(today: NSDate) -> NSDate {
    let tomorrow = NSCalendar.currentCalendar().dateByAddingUnit(.Day, value: 1, toDate: today, options: NSCalendarOptions(rawValue: 0))

    return tomorrow!
}

// this method returns the total of the streak and sets the ending date of the last streak
func calculateStreak(lastDate: NSDate) -> Int {
    let dateList = fetchLatestDates(moc, lastDate: lastDate)
    let compareDate = changeDateTime(lastDate)
    var streakDateList = [NSDate]()
    var tomorrow = addDay(compareDate)

    for date in dateList {
        changeDateTime(date)
        if date == tomorrow {
           streakDateList.append(date)
        }
        tomorrow = addDay(tomorrow)
    }

    userDefaults.setObject(streakDateList.last, forKey: "lastStreakEndDate")
    return streakDateList.count
}

I put the call in the viewDidLoad, but you can add it to a button if you like.

Suchlike answered 16/2, 2016 at 13:58 Comment(4)
Thanks, D. Greg, will check it as soon as possible. Not fully understand, but understand main idea.Mora
what for loop in calculateStreak do?Mora
especially if there are multiple dates as in exampleMora
It sets each date to have the same hours, minutes, and seconds. Then compares that date to the previous date in the array. Without the loop, it would judge the dates in exactly 24hr intervals. That would be a problem. For example, the streak is to count exercising each day. If the user exercises at 10pm the first day, then 8am the next day, it would count as being in the same 24hr period. Not ideal. So, the loop makes just count the by days (well, 23hr,59min,59sec days).Suchlike
C
1

ALL SQL Solution

Please note that this assumes that "Today" counts as a day in the streak for the calculation. The SQL returns both the streak in terms of days, and the date just prior to when the streak started.

with max_zero_dt (zero_dt) as
(
  select max(dt)
    from ( 
         select max(isPresent) as isPresent, dt from check_tab group by dt
         union select 0, min(dt) from check_tab
         )
   where isPresent = 0
),
days_to_check (isPresent, dt) as
(
  select isPresent, dt
    from check_tab ct
    join max_zero_dt on ( ct.dt >= zero_dt )
),
missing_days (isPresent, dt) as
(
  select 0, date(dt, '-1 day') from days_to_check
  UNION
  select 0, date('now')
),
all_days_dups (isPresent, dt) as
(
  select isPresent, dt from days_to_check
  union
  select isPresent, dt from missing_days
),
all_days (isPresent, dt) as
(
  select max(isPresent) as isPresent, dt
  from all_days_dups
  group by dt
)
select cast(min(julianday('now') - julianday(dt)) as int) as day_streak
     , max(dt) as dt
  from all_days
 where isPresent = 0

Here is a sqlfiddle for the first scenario: http://sqlfiddle.com/#!7/0f781/2

Here is a sqlfiddle for the second scenario: http://sqlfiddle.com/#!7/155bb/2

NOTE ABOUT THE FIDDLES: They change the dates to be relative to "Today" so that it tests the streak accurately.

Here is how it works:

  • SUMMARY: We are going to "fill in" the missing days with zeros, and then do the simple check to see when the max 0 date is. But, we must take into account if there are no rows for today, and if there are no rows with zeros.
  • max_zero_dt: contains a single row that contains the latest explicit 0 date, or the earliest date minus 1 day. This is to reduce the number of rows for later queries.
  • days_to_check: This is the reduced # of rows to check based on when the latest 0 is.
  • missing_days: We need to fill-in the missing days, so we get a list of all the days minus 1 day. We also add today in case there are no rows for it.
  • all_days_dups: We simply combine the days_to_check and the missing_days.
  • all_days: Here we get the 'max' isPresent for each day, so that now we have the true list of days to search through, knowing that there will not be any gaps with a 0 for isPresent.
  • Final Query: This is the simple calculation providing the streak and start date.

ASSUMPTIONS:

  • TABLE NAME IS: check_tab
  • The Current Date must be in the table with a 1. Otherwise, the streak is 0. The query can be modified if this is not the case.
  • If a single day has both a 0 and a 1 for isPresent, the 1 takes precedence, and the streak can continue to prior days.
  • That Core Data uses SQLite, and the above SQL which works in SQLite will also work with Core Data's database.
Conveyancing answered 19/2, 2016 at 23:26 Comment(0)
E
0
import SwiftUI

extension Date {
    
    // for tomorow's Date
    static var tomorrow:  Date { return Date().dayAfter }
    static var today: Date {return Date()}
    var dayAfter: Date {
       return Calendar.current.date(byAdding: .day, value: 1, to: Date())!
        // just add .minute after byAdding: , to create a streak minute counter and check the logic.
    }

    static func getTodayDate() -> String {

           let dateFormatter = DateFormatter()

           dateFormatter.dateFormat = "E d MMM yyyy"
        //to continue with the minute streak builder just add "E d MMM yyyy h:mm a" above, it will allow date formatting with minutes and follow the changes in dayAfter

        return dateFormatter.string(from: Date.today)

       }
    
    static func getTomDate() -> String {
        let dateFormatter = DateFormatter()
        
        dateFormatter.dateFormat = "E d MMM yyyy"
        
        return dateFormatter.string(from: Date.tomorrow)
    }
}

struct StreakApp: View {
    
    @AppStorage("counter") var counter = 0
    @AppStorage("tapDate") var TapDate: String?
    @AppStorage("Tappable") var ButtonTapped = false
    var body: some View {
        NavigationView {
            VStack{
                VStack {
                    Text("\(counter)").foregroundColor(.gray)
                    Text("Restore your streak on ")
                    Text(TapDate ?? "No Date")
                    Image(systemName: "flame")
                        .resizable()
                        .frame(width: 40, height: 50)
                        .padding()
                        .scaledToFit()
                        .background(ButtonTapped ? Color.red : Color.gray)
                        .foregroundColor(ButtonTapped ? Color.orange : Color.black)
                        .cornerRadius(12)
                }
                Button {
                    if  TapDate == nil {
                        //Check if user has already tapped
                        self.ButtonTapped = true
                        counter += 1
                        self.TapDate = ("\(Date.getTomDate())")
                    }
                    else if ("\(Date.getTodayDate())") == TapDate {
                        //Check for the consecutive Day of Streak
                       
                        self.TapDate = ("\(Date.getTomDate())")
                        counter += 1
                        //Let's light the flame back again.
                        self.ButtonTapped = true
                    }
                    
                } label: {
                    RoundedRectangle(cornerRadius: 12, style: .continuous)
                        .foregroundColor(.black)
                        .frame(width: 120, height: 40)
                        .overlay {
                            Text("Add Streak")
                                .foregroundColor(.white)
                        }
                }
                .padding()
                //This button is only for testing purpose.
                Button {
                    self.TapDate = nil
                    self.ButtonTapped = false
                    self.counter = 0
                } label: {
                    RoundedRectangle(cornerRadius: 12, style: .continuous)
                        .foregroundColor(.black)
                        .frame(width: 160, height: 40)
                        .overlay {
                            Text("Reset Streak")
                                .foregroundColor(.white)
                        }
                }
          
            }
            //Ensuer the flame dies out if we run into any other day except today or tommorow.
            .onAppear {
                if ("\(Date.getTodayDate())") == TapDate ||
                    ("\(Date.getTomDate())") == TapDate {
                    self.ButtonTapped = true
                }
                //Breaking the Streak
                else {
                    self.TapDate = nil
                    self.ButtonTapped = false
                    self.counter = 0
                }
          
            }
        }
    }
}

struct StreakApp_Previews: PreviewProvider {
    static var previews: some View {
        StreakApp()
    }
}
Earthman answered 11/11, 2022 at 13:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.