Commit 1246536
Changed files (5)
app
lib
components
measurement_list
screens
subsettings
export_import
health_data_store
lib
src
types
test
src
types
app/lib/components/measurement_list/intake_list_entry.dart
@@ -1,57 +0,0 @@
-import 'dart:async';
-
-import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:flutter/material.dart';
-import 'package:health_data_store/health_data_store.dart';
-import 'package:intl/intl.dart';
-
-/// Medicine intake to display in a list.
-class IntakeListEntry extends StatelessWidget {
- /// Display a medicine intake on a list tile.
- const IntakeListEntry({super.key,
- required this.settings,
- required this.intake,
- this.delete,
- });
-
- /// Settings to customize basic behavior.
- final Settings settings;
-
- /// Intake that provides the data to display.
- final MedicineIntake intake;
-
- /// Function to delete this intake.
- ///
- /// Gets called after the delete button has been pressed and the deletion got
- /// confirmed.
- /// When null no deletion button is displayed.
- final FutureOr<void> Function()? delete;
-
- @override
- Widget build(BuildContext context) => ListTile(
- title: Text(intake.medicine.designation),
- subtitle: Text(DateFormat(settings.dateFormatString).format(intake.time)),
- trailing: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(intake.dosis.toString()),
- if (delete != null)
- IconButton(
- onPressed: () async {
- bool confirmedDeletion = true;
- if (settings.confirmDeletion) {
- confirmedDeletion = await showConfirmDeletionDialoge(context);
- }
- if (confirmedDeletion) delete!();
- },
- icon: const Icon(Icons.delete),
- ),
- ],
- ),
- leading: const Icon(Icons.medication),
- iconColor: intake.medicine.color == null ? null : Color(intake.medicine.color!),
- );
-
-
-}
app/lib/components/measurement_list/measurement_list.dart
@@ -1,6 +1,5 @@
import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
@@ -32,23 +31,7 @@ class MeasurementList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
- final List<FullEntry> entries = [];
- for (final r in records) {
- final n = notes.where((n) => n.time == r.time).firstOrNull ?? Note(time: r.time);
- final i = intakes.where((n) => n.time == r.time).toList();
- entries.add((r, n, i));
- }
- Set<DateTime> times = entries.map((e) => e.time).toSet();
- final remainingNotes = notes.where((n) => !times.contains(n.time));
- for (final n in remainingNotes) {
- final i = intakes.where((n) => n.time == n.time).toList();
- entries.add((BloodPressureRecord(time: n.time), n, i));
- }
- times = entries.map((e) => e.time).toSet();
- final remainingIntakes = intakes.where((i) => !times.contains(i.time));
- for (final i in groupBy(remainingIntakes, (i) => i.time).values) {
- entries.add((BloodPressureRecord(time: i.first.time), Note(time: i.first.time), i));
- }
+ final entries = FullEntryList.merged(records, notes, intakes);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
health_data_store/lib/src/types/full_entry.dart
@@ -38,7 +38,7 @@ extension FastFullEntryGetters on FullEntry {
}
/// Utility methods to work on full entries.
-extension FullEntryListUtils on List<FullEntry> {
+extension FullEntryList on List<FullEntry> {
/// Create a list that only contains the records field from the entries.
List<BloodPressureRecord> get records => map((e) => e.$1).toList();
@@ -53,4 +53,68 @@ extension FullEntryListUtils on List<FullEntry> {
}));
return meds.toList();
}
+
+ /// Merges values at the same time from passed lists to FullEntries and
+ /// creates list of them.
+ ///
+ /// In the resulting list every passed value is contained exactly once. This
+ /// requires that passed [records] and [notes] contain only one entry per
+ /// timestamp.
+ static List<FullEntry> merged(
+ List<BloodPressureRecord> records,
+ List<Note> notes,
+ List<MedicineIntake> intakes,
+ ) {
+ assert(!records
+ .any((rOuter) => records
+ .where((rInner) => rOuter.time == rInner.time).length != 1,
+ ),
+ 'records should only contain one entry per timestamp',
+ );
+ assert(!notes
+ .any((nOuter) => notes
+ .where((nInner) => nOuter.time == nInner.time).length != 1,
+ ),
+ 'notes should only contain one entry per timestamp',
+ );
+
+ // Algorithm:
+ // 1. Create entry for every record and add notes and intakes at the same
+ // time.
+ // 2. Determine notes that are not already in the list.
+ // 3. Create entry for these notes and add intakes at the same time.
+ // 4. Determine intakes that are not already in the list.
+ // 5. Group intakes by time.
+ // 6. Create entries for intakes at those times.
+
+ final List<FullEntry> entries = [];
+ for (final r in records) {
+ final n = notes.where((n) => n.time == r.time).firstOrNull ?? Note(time: r.time);
+ final i = intakes.where((n) => n.time == r.time).toList();
+ entries.add((r, n, i));
+ }
+
+ Set<DateTime> times = entries.map((e) => e.time).toSet();
+ final remainingNotes = notes.where((n) => !times.contains(n.time));
+
+ for (final n in remainingNotes) {
+ final i = intakes.where((i) => i.time == n.time).toList();
+ entries.add((BloodPressureRecord(time: n.time), n, i));
+ }
+
+ times = entries.map((e) => e.time).toSet();
+ final remainingIntakes = intakes.where((i) => !times.contains(i.time));
+
+ final groupedIntakes = <DateTime, List<MedicineIntake>>{};
+ for (final intake in remainingIntakes) {
+ final list = (groupedIntakes[intake.time] ??= []);
+ list.add(intake);
+ }
+
+ for (final i in groupedIntakes.values) {
+ entries.add((BloodPressureRecord(time: i.first.time), Note(time: i.first.time), i));
+ }
+
+ return entries;
+ }
}
health_data_store/test/src/types/full_entry_test.dart
@@ -1,4 +1,5 @@
import 'package:health_data_store/src/types/full_entry.dart';
+import 'package:health_data_store/src/types/medicine_intake.dart';
import 'package:test/test.dart';
import 'blood_pressure_record_test.dart';
@@ -104,4 +105,98 @@ void main() {
expect(entry.recordObj, record);
expect(entry.noteObj, note);
});
+ test('merges lists', () {
+ final records = [
+ mockRecord(time: 10000, sys: 123, dia: 456),
+ mockRecord(time: 30000, dia: 456),
+ mockRecord(time: 40000, sys: 123, dia: 456),
+ mockRecord(time: 80000, sys: 123, dia: 456, pul: 567),
+ ];
+ final notes = [
+ mockNote(time: 10000, note: 'testnote', color: 123),
+ mockNote(time: 20000, note: 'testnote', color: 123),
+ mockNote(time: 70000, color: 123),
+ ];
+ final intakes = [
+ mockIntake(mockMedicine(), time: 10000,),
+ mockIntake(mockMedicine(), time: 20000,),
+ mockIntake(mockMedicine(), time: 30000,),
+ mockIntake(mockMedicine(), time: 50000,),
+ mockIntake(mockMedicine(), time: 50000,),
+ mockIntake(mockMedicine(), time: 60000, dosis: 12343.0),
+ mockIntake(mockMedicine(), time: 70000),
+ mockIntake(mockMedicine(), time: 70000),
+ mockIntake(mockMedicine(), time: 70000),
+ ];
+ final list = FullEntryList.merged(records, notes, intakes);
+ expect(list, hasLength(8));
+ expect(list, containsAll([
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 10000)
+ .having((e) => e.sys?.mmHg, 'sys', 123)
+ .having((e) => e.dia?.mmHg, 'dia', 456)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', 'testnote')
+ .having((e) => e.color, 'color', 123)
+ .having((e) => e.intakes, 'intakes', hasLength(1)),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 20000)
+ .having((e) => e.sys?.mmHg, 'sys', null)
+ .having((e) => e.dia?.mmHg, 'dia', null)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', 'testnote')
+ .having((e) => e.color, 'color', 123)
+ .having((e) => e.intakes, 'intakes', hasLength(1)),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 30000)
+ .having((e) => e.sys?.mmHg, 'sys', null)
+ .having((e) => e.dia?.mmHg, 'dia', 456)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', null)
+ .having((e) => e.color, 'color', null)
+ .having((e) => e.intakes, 'intakes', hasLength(1)),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 40000)
+ .having((e) => e.sys?.mmHg, 'sys', 123)
+ .having((e) => e.dia?.mmHg, 'dia', 456)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', null)
+ .having((e) => e.color, 'color', null)
+ .having((e) => e.intakes, 'intakes', isEmpty),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 50000)
+ .having((e) => e.sys?.mmHg, 'sys', null)
+ .having((e) => e.dia?.mmHg, 'dia', null)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', null)
+ .having((e) => e.color, 'color', null)
+ .having((e) => e.intakes, 'intakes', hasLength(2)),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 60000)
+ .having((e) => e.sys?.mmHg, 'sys', null)
+ .having((e) => e.dia?.mmHg, 'dia', null)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', null)
+ .having((e) => e.color, 'color', null)
+ .having((e) => e.intakes, 'intakes', hasLength(1))
+ .having((e) => e.intakes, 'intakes', contains(isA<MedicineIntake>()
+ .having((i) => i.dosis.mg, 'dosis', 12343.0))),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 70000)
+ .having((e) => e.sys?.mmHg, 'sys', null)
+ .having((e) => e.dia?.mmHg, 'dia', null)
+ .having((e) => e.pul, 'pul', null)
+ .having((e) => e.note, 'note', null)
+ .having((e) => e.color, 'color', 123)
+ .having((e) => e.intakes, 'intakes', hasLength(3)),
+ isA<FullEntry>()
+ .having((e) => e.time.millisecondsSinceEpoch, 'time', 80000)
+ .having((e) => e.sys?.mmHg, 'sys', 123)
+ .having((e) => e.dia?.mmHg, 'dia', 456)
+ .having((e) => e.pul, 'pul', 567)
+ .having((e) => e.note, 'note', null)
+ .having((e) => e.color, 'color', null)
+ .having((e) => e.intakes, 'intakes', isEmpty),
+ ]));
+ });
}