import '../about/external/data/RoutineChain.dart'; import '../about/internal/application/ProgressPeriod.dart'; import '../about/internal/application/ProgressTargets.dart'; import '../about/internal/application/RoutineRunRecord.dart'; /// Progress over time, derived from the device's run log. Pure functions: /// the screen asks, this answers, nothing is stored. class ProgressEngine { static DateTime dayOf(DateTime value) => DateTime(value.year, value.month, value.day); /// Weeks start on Monday, matching routine `days` (1 = Monday). static DateTime weekStart(DateTime value) => dayOf(value).subtract(Duration(days: value.weekday - 1)); static DateTime monthStart(DateTime value) => DateTime(value.year, value.month); /// The start of the period containing [now], and of the one before it. static DateTime periodStart(ProgressPeriod period, DateTime now) { switch (period) { case ProgressPeriod.Day: return dayOf(now); case ProgressPeriod.Week: return weekStart(now); case ProgressPeriod.Month: return monthStart(now); } } static DateTime periodEnd(ProgressPeriod period, DateTime start) { switch (period) { case ProgressPeriod.Day: return start.add(const Duration(days: 1)); case ProgressPeriod.Week: return start.add(const Duration(days: 7)); case ProgressPeriod.Month: return DateTime(start.year, start.month + 1); } } static DateTime previousStart(ProgressPeriod period, DateTime start) { switch (period) { case ProgressPeriod.Day: return start.subtract(const Duration(days: 1)); case ProgressPeriod.Week: return start.subtract(const Duration(days: 7)); case ProgressPeriod.Month: return DateTime(start.year, start.month - 1); } } /// Routine slots the schedule asks for each week. static int scheduledPerWeek(List routines) { int count = 0; for (RoutineChain routine in routines) { count = count + routine.days.length; } return count; } static List between( List records, DateTime start, DateTime end) { return records .where((record) => !record.startedAt.isBefore(start) && record.startedAt.isBefore(end)) .toList(); } static int held(List records) => records.where((record) => record.held).length; static int minutes(List records) { int seconds = 0; for (RoutineRunRecord record in records) { seconds = seconds + record.seconds; } return (seconds / 60).round(); } /// The weekly targets scaled to the period: a day is a seventh of a week, /// a month is its days over seven. static int routineTarget( ProgressTargets targets, ProgressPeriod period, DateTime start) { return _scale(targets.routinesPerWeek, period, start); } static int minuteTarget( ProgressTargets targets, ProgressPeriod period, DateTime start) { return _scale(targets.minutesPerWeek, period, start); } static int _scale(int weekly, ProgressPeriod period, DateTime start) { switch (period) { case ProgressPeriod.Day: return (weekly / 7).ceil(); case ProgressPeriod.Week: return weekly; case ProgressPeriod.Month: final int days = periodEnd(period, start).difference(start).inDays; return (weekly * days / 7).round(); } } /// Held routines per bucket: days of the week, or weeks of the month. /// A day has no buckets; it lists its runs instead. static List buckets( List records, ProgressPeriod period, DateTime start) { switch (period) { case ProgressPeriod.Day: return []; case ProgressPeriod.Week: return List.generate(7, (index) { final DateTime day = start.add(Duration(days: index)); return held(between(records, day, day.add(const Duration(days: 1)))); }); case ProgressPeriod.Month: final DateTime end = periodEnd(period, start); final List result = []; DateTime week = weekStart(start); while (week.isBefore(end)) { final DateTime from = week.isBefore(start) ? start : week; final DateTime next = week.add(const Duration(days: 7)); result.add(held(between(records, from, next.isAfter(end) ? end : next))); week = next; } return result; } } /// Which bucket [now] falls in, so the chart can highlight it. static int currentBucket(ProgressPeriod period, DateTime start, DateTime now) { switch (period) { case ProgressPeriod.Day: return 0; case ProgressPeriod.Week: return now.weekday - 1; case ProgressPeriod.Month: return dayOf(now).difference(weekStart(start)).inDays ~/ 7; } } /// Consecutive days with at least one held routine, ending today — or /// yesterday, so a streak is not shown as broken before the day is out. static int streak(List records, DateTime now) { final Set days = records .where((record) => record.held) .map((record) => dayOf(record.startedAt)) .toSet(); DateTime cursor = dayOf(now); if (!days.contains(cursor)) { cursor = cursor.subtract(const Duration(days: 1)); } int count = 0; while (days.contains(cursor)) { count = count + 1; cursor = cursor.subtract(const Duration(days: 1)); } return count; } }