Files
habo/lib/statistics/statistics.dart
dazhuang aa69f2a91e feat: initial commit - Habo habit tracking app
- 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)
2026-04-13 15:02:30 +00:00

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;
}
}