Commit a8055c5
Changed files (3)
lib
model
test
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