Commit a8055c5

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2023-10-03 04:01:16
add class for storing the display intervall
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 320d7c3
Changed files (3)
lib/model/storage/intervall_store.dart
@@ -0,0 +1,250 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+/// Class for storing the current interval, as it is needed in start page, statistics and export.
+class IntervallStorage extends ChangeNotifier {
+  IntervallStorage({TimeStep? stepSize, DateTimeRange? range}) {
+    _stepSize = stepSize ?? TimeStep.last7Days;
+    _currentRange = range ?? _getMostRecentDisplayIntervall();
+  }
+  
+  late TimeStep _stepSize;
+  late DateTimeRange _currentRange;
+
+  factory IntervallStorage.fromMap(Map<String, dynamic> map) => IntervallStorage(
+    stepSize: TimeStep.deserialize(map['stepSize']),
+    range: _parseRange(map['start'], map['end']),
+  );
+
+  factory IntervallStorage.fromJson(String json) {
+    try {
+      return IntervallStorage.fromMap(jsonDecode(json));
+    } catch (exception) {
+      return IntervallStorage();
+    }
+  }
+
+  Map<String, dynamic> toMap() => <String, dynamic>{
+    'stepSize': stepSize.serialize(),
+    'start': currentRange.start.millisecondsSinceEpoch,
+    'end': currentRange.end.millisecondsSinceEpoch,
+  };
+
+  String toJson() => jsonEncode(toMap());
+
+  /// The stepSize gets set through the changeStepSize method.
+  TimeStep get stepSize => _stepSize;
+
+  /// sets the stepSize to the new value and resets the currentRange to the most recent one. 
+  void changeStepSize(TimeStep value) {
+    _stepSize = value;
+    setToMostRecentIntervall();
+    notifyListeners();
+  }
+
+  DateTimeRange get currentRange {
+    return _currentRange;
+  }
+
+  set currentRange(DateTimeRange value) {
+    _currentRange = value;
+    notifyListeners();
+  }
+
+  /// Sets internal _currentRange to the most recent intervall and notifies listeners.
+  void setToMostRecentIntervall() {
+    _currentRange = _getMostRecentDisplayIntervall();
+    notifyListeners();
+  }
+
+  void moveDataRangeByStep(int directionalStep) {
+    final oldStart = currentRange.start;
+    final oldEnd = currentRange.end;
+    switch (stepSize) {
+      case TimeStep.day:
+        currentRange = DateTimeRange(
+          start: oldStart.copyWith(day: oldStart.day + directionalStep),
+          end: oldEnd.copyWith(day: oldEnd.day + directionalStep)
+        );
+        break;
+      case TimeStep.week:
+      case TimeStep.last7Days:
+        currentRange = DateTimeRange(
+          start: oldStart.copyWith(day: oldStart.day + directionalStep * 7),
+          end: oldEnd.copyWith(day: oldEnd.day + directionalStep * 7)
+        );
+        break;
+      case TimeStep.month:
+        currentRange = DateTimeRange(
+          start: oldStart.copyWith(month: oldStart.month + directionalStep),
+          end: oldEnd.copyWith(month: oldEnd.month + directionalStep)
+        );
+        break;
+      case TimeStep.year:
+        currentRange = DateTimeRange(
+          start: oldStart.copyWith(year: oldStart.year + directionalStep),
+          end: oldEnd.copyWith(year: oldEnd.year + directionalStep)
+        );
+        break;
+      case TimeStep.lifetime:
+        currentRange = DateTimeRange(
+            start: DateTime.fromMillisecondsSinceEpoch(1),
+            end: DateTime.now().copyWith(hour: 23, minute: 59, second: 59)
+        );
+        break;
+      case TimeStep.last30Days:
+        currentRange = DateTimeRange(
+            start: oldStart.copyWith(day: oldStart.day + directionalStep * 30),
+            end: oldEnd.copyWith(day: oldEnd.day + directionalStep * 30)
+        );
+        break;
+      case TimeStep.custom:
+        final step = oldEnd.difference(oldStart) * directionalStep;
+        currentRange = DateTimeRange(
+            start: oldStart.add(step),
+            end: oldEnd.add(step)
+        );
+        break;
+    }
+  }
+
+  DateTimeRange _getMostRecentDisplayIntervall() {
+    final now = DateTime.now();
+    switch (stepSize) {
+      case TimeStep.day:
+        final start = DateTime(now.year, now.month, now.day);
+        return DateTimeRange(start: start, end: start.copyWith(day: now.day + 1));
+      case TimeStep.week:
+        final start = DateTime(now.year, now.month, now.day - (now.weekday - 1)); // monday
+        return DateTimeRange(start: start, end: start.copyWith(day: start.day + DateTime.sunday)); // end of sunday
+      case TimeStep.month:
+        final start = DateTime(now.year, now.month);
+        return DateTimeRange(start: start, end: start.copyWith(month: now.month + 1));
+      case TimeStep.year:
+        final start = DateTime(now.year);
+        return DateTimeRange(start: start, end: start.copyWith(year: now.year + 1));
+      case TimeStep.lifetime:
+        final start = DateTime.fromMillisecondsSinceEpoch(1);
+        final endOfToday = now.copyWith(hour: 23, minute: 59, second: 59);
+        return DateTimeRange(start: start, end: endOfToday);
+      case TimeStep.last7Days:
+        final start = now.copyWith(day: now.day - 7);
+        final endOfToday = now.copyWith(hour: 23, minute: 59, second: 59);
+        return DateTimeRange(start: start, end: endOfToday);
+      case TimeStep.last30Days:
+        final start = now.copyWith(day: now.day - 30);
+        final endOfToday = now.copyWith(hour: 23, minute: 59, second: 59);
+        return DateTimeRange(start: start, end: endOfToday);
+      case TimeStep.custom:
+        // fallback, TimeStep will be reset by getter
+        // TODO: evaluate above comment for the new class
+        return DateTimeRange(
+          start: DateTime.fromMillisecondsSinceEpoch(-1), 
+          end: DateTime.fromMillisecondsSinceEpoch(-1));
+    }
+  }
+}
+
+enum TimeStep {
+  day,
+  month,
+  year,
+  lifetime,
+  week,
+  last7Days,
+  last30Days,
+  custom;
+
+  static const options = [TimeStep.day, TimeStep.week, TimeStep.month, TimeStep.year, TimeStep.lifetime, TimeStep.last7Days, TimeStep.last30Days, TimeStep.custom];
+
+  static String getName(TimeStep opt, BuildContext context) {
+    switch (opt) {
+      case TimeStep.day:
+        return AppLocalizations.of(context)!.day;
+      case TimeStep.month:
+        return AppLocalizations.of(context)!.month;
+      case TimeStep.year:
+        return AppLocalizations.of(context)!.year;
+      case TimeStep.lifetime:
+        return AppLocalizations.of(context)!.lifetime;
+      case TimeStep.week:
+        return AppLocalizations.of(context)!.week;
+      case TimeStep.last7Days:
+        return AppLocalizations.of(context)!.last7Days;
+      case TimeStep.last30Days:
+        return AppLocalizations.of(context)!.last30Days;
+      case TimeStep.custom:
+        return AppLocalizations.of(context)!.custom;
+    }
+  }
+
+  int serialize() {
+    switch (this) {
+      case TimeStep.day:
+        return 0;
+      case TimeStep.month:
+        return 1;
+      case TimeStep.year:
+        return 2;
+      case TimeStep.lifetime:
+        return 3;
+      case TimeStep.week:
+        return 4;
+      case TimeStep.last7Days:
+        return 5;
+      case TimeStep.last30Days:
+        return 6;
+      case TimeStep.custom:
+        return 7;
+    }
+  }
+
+  factory TimeStep.deserialize(dynamic value) {
+    int? intValue = _parseInt(value);
+    if (value == null || intValue == null) return TimeStep.last7Days;
+
+    switch (intValue) {
+      case 0:
+        return TimeStep.day;
+      case 1:
+        return TimeStep.month;
+      case 2:
+        return TimeStep.year;
+      case 3:
+        return TimeStep.lifetime;
+      case 4:
+        return TimeStep.week;
+      case 5:
+        return TimeStep.last7Days;
+      case 6:
+        return TimeStep.last30Days;
+      case 7:
+        return TimeStep.custom;
+      default:
+        assert(false);
+        return TimeStep.last7Days;
+    }
+  }
+}
+
+DateTimeRange? _parseRange(dynamic start, dynamic end) {
+  final startTimestamp = _parseInt(start);
+  final endTimestamp = _parseInt(end);
+  if (startTimestamp == null || endTimestamp == null) return null;
+  return DateTimeRange(
+      start: DateTime.fromMillisecondsSinceEpoch(startTimestamp),
+      end: DateTime.fromMillisecondsSinceEpoch(endTimestamp)
+  );
+}
+
+int? _parseInt(dynamic value) {
+  if (value is int || value is int?) {
+    return value;
+  }
+  if (value is String) {
+    return int.tryParse(value);
+  }
+  return null;
+}
\ No newline at end of file
lib/model/storage/settings_store.dart
@@ -87,8 +87,7 @@ class Settings extends ChangeNotifier {
       
   factory Settings.fromJson(String json) => Settings.fromMap(jsonDecode(json));
 
-  Map<String, dynamic> toMap() {
-    return <String, dynamic>{
+  Map<String, dynamic> toMap() => <String, dynamic>{
       'accentColor': accentColor.value,
       'sysColor': sysColor.value,
       'diaColor': diaColor.value,
@@ -109,7 +108,6 @@ class Settings extends ChangeNotifier {
       'useLegacyList': useLegacyList,
       'language': _serializeLocale(language),
     };
-  }
 
   String toJson() => jsonEncode(toMap());
 
test/model/intervall_store_test.dart
@@ -0,0 +1,157 @@
+
+import 'package:blood_pressure_app/model/storage/intervall_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('IntervallStorage', () {
+    test('base constructor should initialize with values', () {
+      final storageObject = IntervallStorage(stepSize: TimeStep.month, range: DateTimeRange(
+          start: DateTime.fromMillisecondsSinceEpoch(1234),
+          end: DateTime.fromMillisecondsSinceEpoch(5678)
+      ));
+
+      expect(storageObject.stepSize, TimeStep.month);
+      expect(storageObject.currentRange.start.millisecondsSinceEpoch, 1234);
+      expect(storageObject.currentRange.end.millisecondsSinceEpoch, 5678);
+    });
+
+    test('base constructor should initialize to default without values', () {
+      final storageObject = IntervallStorage();
+      expect(storageObject.stepSize, TimeStep.last7Days);
+      expect(storageObject.currentRange.start.millisecondsSinceEpoch, lessThanOrEqualTo(DateTime
+          .now()
+          .millisecondsSinceEpoch));
+    });
+
+    test('base constructor should initialize with only incomplete parameters', () {
+      // only tests for no crashes
+      IntervallStorage(stepSize: TimeStep.last30Days);
+      IntervallStorage(range: DateTimeRange(
+          start: DateTime.fromMillisecondsSinceEpoch(1234),
+          end: DateTime.fromMillisecondsSinceEpoch(5678)
+      ));
+    });
+
+    test('should create json without error', () {
+      final intervall = IntervallStorage(stepSize: TimeStep.year);
+      final json = intervall.toJson();
+      expect(json.length, greaterThan(0));
+    });
+
+    test('should load same data from json', () {
+      final initialData = IntervallStorage();
+      final json = initialData.toJson();
+      final recreatedData = IntervallStorage.fromJson(json);
+
+      expect(initialData.stepSize, recreatedData.stepSize);
+      expect(initialData.currentRange.start.millisecondsSinceEpoch,
+          recreatedData.currentRange.start.millisecondsSinceEpoch);
+      expect(initialData.currentRange.end.millisecondsSinceEpoch,
+          recreatedData.currentRange.end.millisecondsSinceEpoch);
+    });
+
+    test('should load same data from json in edge cases', () {
+      final initialData = IntervallStorage(stepSize: TimeStep.month, range: DateTimeRange(
+          start: DateTime.fromMillisecondsSinceEpoch(1234),
+          end: DateTime.fromMillisecondsSinceEpoch(5678)
+      ));
+      final json = initialData.toJson();
+      final recreatedData = IntervallStorage.fromJson(json);
+
+      expect(initialData.stepSize, TimeStep.month);
+      expect(recreatedData.currentRange.start.millisecondsSinceEpoch, 1234);
+      expect(recreatedData.currentRange.end.millisecondsSinceEpoch, 5678);
+    });
+
+    test('should not crash when parsing incorrect json', () {
+      IntervallStorage.fromJson('banana');
+      IntervallStorage.fromJson('{"stepSize" = 1}');
+      IntervallStorage.fromJson('{"stepSize": 1');
+      IntervallStorage.fromJson('{stepSize: 1}');
+      IntervallStorage.fromJson('green{stepSize: 1}');
+    });
+
+    test('should not crash when parsing invalid values and ignore them', () {
+      final v1 = IntervallStorage.fromJson('{"stepSize": true}');
+      final v2 = IntervallStorage.fromJson('{"stepSize": "month"}');
+      final v3 = IntervallStorage.fromJson('{"start": "month", "end": 10.5}');
+      final v4 = IntervallStorage.fromJson('{"start": 18.6, "end": 90.65}');
+
+      expect(v1.stepSize, TimeStep.last7Days);
+      expect(v2.stepSize, TimeStep.last7Days);
+      expect(v3.stepSize, TimeStep.last7Days);
+
+      // in minutes to avoid failing through performance
+      expect(v2.currentRange.duration.inMinutes, v1.currentRange.duration.inMinutes);
+      expect(v3.currentRange.duration.inMinutes, v1.currentRange.duration.inMinutes);
+      expect(v4.currentRange.duration.inMinutes, v1.currentRange.duration.inMinutes);
+    });
+
+
+    test('intervall lengths should match step size', () {
+      final dayIntervall = IntervallStorage(stepSize: TimeStep.day);
+      final weekIntervall = IntervallStorage(stepSize: TimeStep.week);
+      final monthIntervall = IntervallStorage(stepSize: TimeStep.month);
+      final yearIntervall = IntervallStorage(stepSize: TimeStep.year);
+      final last7DaysIntervall = IntervallStorage(stepSize: TimeStep.last7Days);
+      final last30DaysIntervall = IntervallStorage(stepSize: TimeStep.last30Days);
+      
+      expect(dayIntervall.currentRange.duration.inHours, 24);
+      expect(weekIntervall.currentRange.duration.inDays, 7);
+      expect(monthIntervall.currentRange.duration.inDays, inInclusiveRange(28, 31));
+      expect(yearIntervall.currentRange.duration.inDays, inInclusiveRange(365, 366));
+      expect(last7DaysIntervall.currentRange.duration.inDays, 7);
+      expect(last30DaysIntervall.currentRange.duration.inDays, 30);
+    });
+
+    test('intervall lengths should still be correct after moving', () {
+      final dayIntervall = IntervallStorage(stepSize: TimeStep.day);
+      final weekIntervall = IntervallStorage(stepSize: TimeStep.week);
+      final monthIntervall = IntervallStorage(stepSize: TimeStep.month);
+      final yearIntervall = IntervallStorage(stepSize: TimeStep.year);
+      final last7DaysIntervall = IntervallStorage(stepSize: TimeStep.last7Days);
+      final last30DaysIntervall = IntervallStorage(stepSize: TimeStep.last30Days);
+      final customIntervall = IntervallStorage(stepSize: TimeStep.custom, range: DateTimeRange(
+          start: DateTime.fromMillisecondsSinceEpoch(1234),
+          end: DateTime.fromMillisecondsSinceEpoch(1234 + 24 * 60 * 60 * 1000) // one day
+      ));
+
+      expect(customIntervall.currentRange.duration.inMilliseconds, 24 * 60 * 60 * 1000);
+
+      dayIntervall.moveDataRangeByStep(1);
+      weekIntervall.moveDataRangeByStep(1);
+      monthIntervall.moveDataRangeByStep(1);
+      yearIntervall.moveDataRangeByStep(1);
+      last7DaysIntervall.moveDataRangeByStep(1);
+      last30DaysIntervall.moveDataRangeByStep(1);
+      customIntervall.moveDataRangeByStep(1);
+
+      expect(dayIntervall.currentRange.duration.inHours, 24);
+      expect(weekIntervall.currentRange.duration.inDays, 7);
+      expect(monthIntervall.currentRange.duration.inDays, inInclusiveRange(28, 31));
+      expect(yearIntervall.currentRange.duration.inDays, inInclusiveRange(365, 366));
+      expect(last7DaysIntervall.currentRange.duration.inDays, 7);
+      expect(last30DaysIntervall.currentRange.duration.inDays, 30);
+      expect(customIntervall.currentRange.duration.inMilliseconds, 24 * 60 * 60 * 1000);
+
+      dayIntervall.moveDataRangeByStep(-2);
+      weekIntervall.moveDataRangeByStep(-2);
+      monthIntervall.moveDataRangeByStep(-2);
+      yearIntervall.moveDataRangeByStep(-2);
+      last7DaysIntervall.moveDataRangeByStep(-2);
+      last30DaysIntervall.moveDataRangeByStep(-2);
+      customIntervall.moveDataRangeByStep(-2);
+
+      expect(dayIntervall.currentRange.duration.inHours, 24);
+      expect(weekIntervall.currentRange.duration.inDays, 7);
+      expect(monthIntervall.currentRange.duration.inDays, inInclusiveRange(28, 31));
+      expect(yearIntervall.currentRange.duration.inDays, inInclusiveRange(365, 366));
+      expect(last7DaysIntervall.currentRange.duration.inDays, 7);
+      expect(last30DaysIntervall.currentRange.duration.inDays, 30);
+      expect(customIntervall.currentRange.duration.inMilliseconds, 24 * 60 * 60 * 1000);
+    });
+    // TODO: test if it's the most recent intervall
+  });
+
+}
\ No newline at end of file