- Complete MVP with Repository Pattern, SQLite storage - Provider + ChangeNotifier state management - Navigation 2.0 with deep link support - Habit CRUD with twoDayRule, notifications, categories - Backup/Restore via JSON - Statistics with streak tracking - Material You theme support - Biometric lock support - Desktop widget support - 27 languages i18n structure - Comprehensive test suite (87/89 passing)
123 lines
3.6 KiB
Dart
123 lines
3.6 KiB
Dart
import 'dart:collection';
|
|
import 'package:habo/constants.dart';
|
|
import 'package:habo/habits/habit.dart';
|
|
import 'package:habo/model/habit_data.dart';
|
|
|
|
class StatisticsData {
|
|
String title;
|
|
int topStreak = 0;
|
|
int actualStreak = 0;
|
|
int checks = 0;
|
|
int fails = 0;
|
|
int skips = 0;
|
|
int progress = 0;
|
|
SplayTreeMap<int, Map<DayType, List<int>>> monthlyTracking;
|
|
|
|
StatisticsData({
|
|
this.title = '',
|
|
SplayTreeMap<int, Map<DayType, List<int>>>? monthlyTracking,
|
|
}) : monthlyTracking = monthlyTracking ?? SplayTreeMap<int, Map<DayType, List<int>>>();
|
|
}
|
|
|
|
class OverallStatisticsData {
|
|
int totalChecks = 0;
|
|
int totalFails = 0;
|
|
int totalSkips = 0;
|
|
int totalProgress = 0;
|
|
}
|
|
|
|
class AllStatistics {
|
|
List<StatisticsData> allStatistics = [];
|
|
OverallStatisticsData overallStatistics = OverallStatisticsData();
|
|
}
|
|
|
|
class Statistics {
|
|
static AllStatistics calculateStatistics(List<Habit> habits) {
|
|
final result = AllStatistics();
|
|
|
|
for (final habit in habits) {
|
|
final data = StatisticsData(title: habit.habitData.title);
|
|
final events = habit.habitData.events;
|
|
bool usingTwoDayRule = false;
|
|
|
|
final dates = events.keys.toList();
|
|
for (int i = 0; i < dates.length; i++) {
|
|
final date = dates[i];
|
|
final event = events[date]!;
|
|
final dayType = event[0] is DayType ? event[0] as DayType : DayType.values[event[0] as int];
|
|
|
|
// Check date gap
|
|
if (i > 0) {
|
|
final diff = date.difference(dates[i - 1]).inDays;
|
|
if (diff > 1) {
|
|
data.actualStreak = 0;
|
|
usingTwoDayRule = false;
|
|
}
|
|
}
|
|
|
|
final yearMonth = date.year * 100 + date.month;
|
|
data.monthlyTracking[yearMonth] ??= {};
|
|
data.monthlyTracking[yearMonth]![dayType] ??= [];
|
|
data.monthlyTracking[yearMonth]![dayType]!.add(date.day);
|
|
|
|
switch (dayType) {
|
|
case DayType.check:
|
|
data.checks++;
|
|
data.actualStreak++;
|
|
if (data.actualStreak > data.topStreak) {
|
|
data.topStreak = data.actualStreak;
|
|
}
|
|
usingTwoDayRule = false;
|
|
break;
|
|
case DayType.progress:
|
|
data.progress++;
|
|
if (habit.habitData.habitType == HabitType.numeric && event.length >= 4) {
|
|
final progressValue = event[2] as double? ?? 0.0;
|
|
final target = event[3] as double? ?? habit.habitData.targetValue;
|
|
if (progressValue >= target) {
|
|
data.actualStreak++;
|
|
if (data.actualStreak > data.topStreak) {
|
|
data.topStreak = data.actualStreak;
|
|
}
|
|
}
|
|
}
|
|
usingTwoDayRule = false;
|
|
break;
|
|
case DayType.fail:
|
|
data.fails++;
|
|
if (habit.habitData.twoDayRule) {
|
|
if (usingTwoDayRule) {
|
|
data.actualStreak = 0;
|
|
} else {
|
|
usingTwoDayRule = true;
|
|
}
|
|
} else {
|
|
data.actualStreak = 0;
|
|
}
|
|
break;
|
|
case DayType.skip:
|
|
data.skips++;
|
|
if (usingTwoDayRule) {
|
|
data.actualStreak = 0;
|
|
}
|
|
break;
|
|
case DayType.clear:
|
|
break;
|
|
}
|
|
}
|
|
|
|
result.allStatistics.add(data);
|
|
}
|
|
|
|
// Calculate overall
|
|
for (final stat in result.allStatistics) {
|
|
result.overallStatistics.totalChecks += stat.checks;
|
|
result.overallStatistics.totalFails += stat.fails;
|
|
result.overallStatistics.totalSkips += stat.skips;
|
|
result.overallStatistics.totalProgress += stat.progress;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|