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}