main
1import 'dart:math';
2
3import 'package:collection/collection.dart';
4import 'package:health_data_store/health_data_store.dart';
5
6// TODO: test calculations work and return null in case of error
7
8/// Analysis utils for a list of blood pressure records.
9class BloodPressureAnalyser {
10 /// Create a analyzer for a list of records.
11 BloodPressureAnalyser(this._records);
12
13 final List<BloodPressureRecord> _records;
14
15 /// The amount of records saved.
16 int get count => _records.length;
17
18 /// The average diastolic values of all records.
19 Pressure? get avgDia => _records.map((r) => r.dia?.kPa).tryAverage?.asKPa;
20
21 /// The average pulse values of all records.
22 int? get avgPul => _records.map((r) => r.pul).tryAverage?.toInt();
23
24 /// The average systolic values of all records.
25 Pressure? get avgSys => _records.map((r) => r.sys?.kPa).tryAverage?.asKPa;
26
27 /// The maximum diastolic values of all records.
28 Pressure? get maxDia => _records.map((r) => r.dia?.kPa).tryMax?.asKPa;
29
30 /// The maximum pulse values of all records.
31 int? get maxPul => _records.map((r) => r.pul).tryMax?.toInt();
32
33 /// The maximum systolic values of all records.
34 Pressure? get maxSys => _records.map((r) => r.sys?.kPa).tryMax?.asKPa;
35
36 /// The minimal diastolic values of all records.
37 Pressure? get minDia => _records.map((r) => r.dia?.kPa).tryMin?.asKPa;
38
39 /// The minimal pulse values of all records.
40 int? get minPul => _records.map((r) => r.pul).tryMin?.toInt();
41
42 /// The minimal systolic values of all records.
43 Pressure? get minSys => _records.map((r) => r.sys?.kPa).tryMin?.asKPa;
44
45 /// The earliest timestamp of all records.
46 DateTime? get firstDay {
47 if (_records.isEmpty) return null;
48 _records.sort((a, b) => a.time.compareTo(b.time));
49 return _records.first.time;
50 }
51
52 /// The latest timestamp of all records.
53 DateTime? get lastDay {
54 if (_records.isEmpty) return null;
55 _records.sort((a, b) => a.time.compareTo(b.time));
56 return _records.last.time;
57 }
58
59 /// Average amount of measurements entered per day.
60 int? get measurementsPerDay {
61 final c = count;
62 if (c <= 1) return null;
63
64 final firstDay = this.firstDay;
65 final lastDay = this.lastDay;
66 if (firstDay == null || lastDay == null) return null;
67
68 assert(firstDay.millisecondsSinceEpoch != -1
69 && lastDay.millisecondsSinceEpoch != -1);
70 if (lastDay.difference(firstDay).inDays <= 0) {
71 return c;
72 }
73
74 return c ~/ lastDay.difference(firstDay).inDays;
75 }
76
77 /// Creates analyzers for each hour of the day (0-23).
78 ///
79 /// This function groups records by the hour of the day (e.g 23:30-00:29.59)
80 /// and creates an [BloodPressureAnalyser] for each. The analyzers are
81 /// returned ordered by the hour of the day and the index can be used as the
82 /// hour.
83 List<BloodPressureAnalyser> groupAnalysers() {
84 // Group records around the full hour so that there are 24 sublists from 0
85 // to 23. ([0] -> 23:30-00:29.59; [1] -> ...).
86 final Map<int, List<BloodPressureRecord>> grouped = _records.groupListsBy((BloodPressureRecord record) {
87 int hour = record.time.hour;
88 if(record.time.minute >= 30) hour += 1;
89 hour %= 24; // midnight jumps
90 return hour;
91 });
92 for (int i = 0; i <= 23; i++) {
93 grouped[i] ??= [];
94 }
95 final groupedAnalyzers = grouped.map((hour, subList) => MapEntry(
96 hour,
97 BloodPressureAnalyser(subList),
98 ));
99 final sortedAnalyzersList = groupedAnalyzers.entries
100 .sorted((a,b) => a.key.compareTo(b.key));
101 return sortedAnalyzersList
102 .map((e) => e.value)
103 .toList();
104 }
105}
106
107extension _NullableMath on Iterable<num?> {
108 /// Gets the average value or null if the iterable is empty.
109 num? get tryAverage {
110 final nonNull = whereNotNull();
111 if(nonNull.isEmpty) return null;
112 final double result = nonNull.fold(0.0, (last, next) => last + next / length);
113 return result;
114 }
115
116 /// Gets the minimum value or null if the iterable is empty.
117 num? get tryMin {
118 final nonNull = whereNotNull();
119 if(nonNull.isEmpty) return null;
120 return nonNull.reduce(min);
121 }
122
123 /// Gets the maximum value or null if the iterable is empty.
124 num? get tryMax {
125 final nonNull = whereNotNull();
126 if(nonNull.isEmpty) return null;
127 return nonNull.reduce(max);
128 }
129}
130
131extension _Pressure on num {
132 /// Return [Pressure.kPa] of this values
133 Pressure get asKPa => Pressure.kPa(toDouble());
134}