Commit 37affaa

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-03-30 13:05:44
encapsulate stored units
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent de17081
.github/workflows/app-CI.yml
@@ -22,6 +22,17 @@ jobs:
         sparse-checkout: |
           app
           health_data_store
+    - name: Setup dart
+      uses: dart-lang/setup-dart@v1
+      with:
+        sdk: 'beta'
+    - name: Generate code
+      run: dart pub get
+      working-directory: ./health_data_store
+    - name: Generate code
+      run: dart run build_runner build
+      working-directory: ./health_data_store
+
     - name: Setup Flutter
       uses: subosito/flutter-action@v2
       with:
@@ -45,6 +56,17 @@ jobs:
       with:
         distribution: 'zulu'
         java-version: '11'
+    - name: Setup dart
+      uses: dart-lang/setup-dart@v1
+      with:
+        sdk: 'beta'
+    - name: Generate code
+      run: dart pub get
+      working-directory: ./health_data_store
+    - name: Generate code
+      run: dart run build_runner build
+      working-directory: ./health_data_store
+
     - name: Setup Flutter
       uses: subosito/flutter-action@v2
       with:
.github/workflows/pkg-CI.yml
@@ -28,6 +28,9 @@ jobs:
     - name: Get dependencies
       run: dart pub get
       working-directory: ./health_data_store
+    - name: Generate code
+      run: dart run build_runner build
+      working-directory: ./health_data_store
     - name: Analyze code
       run: dart analyze
       working-directory: ./health_data_store
health_data_store/lib/src/repositories/blood_pressure_repository_impl.dart
@@ -4,6 +4,7 @@ import 'package:health_data_store/src/extensions/datetime_seconds.dart';
 import 'package:health_data_store/src/repositories/blood_pressure_repository.dart';
 import 'package:health_data_store/src/types/blood_pressure_record.dart';
 import 'package:health_data_store/src/types/date_range.dart';
+import 'package:health_data_store/src/types/units/pressure.dart';
 import 'package:sqflite_common/sqflite.dart';
 
 /// Implementation of repository for [BloodPressureRecord]s.
@@ -32,19 +33,19 @@ class BloodPressureRepositoryImpl extends BloodPressureRepository {
       if (record.sys != null) {
         await txn.insert('Systolic', {
           'entryID': entryID,
-          'sys': record.sys,
+          'sys': record.sys!.kPa,
         });
       }
       if (record.dia != null) {
         await txn.insert('Diastolic', {
           'entryID': entryID,
-          'dia': record.dia,
+          'dia': record.dia!.kPa,
         });
       }
       if (record.pul != null) {
         await txn.insert('Pulse', {
           'entryID': entryID,
-          'pul': record.pul,
+          'pul': record.pul!.kPa,
         });
       }
     });
@@ -66,9 +67,9 @@ class BloodPressureRepositoryImpl extends BloodPressureRepository {
       final timeS = r['timestampUnixS'] as int;
       final newRec = BloodPressureRecord(
         time: DateTimeS.fromSecondsSinceEpoch(timeS),
-        sys: r['sys'] as int?,
-        dia: r['dia'] as int?,
-        pul: r['pul'] as int?,
+        sys: _decode(r['sys']),
+        dia: _decode(r['dia']),
+        pul: _decode(r['pul']),
       );
       if (newRec.sys !=null || newRec.dia != null || newRec.pul != null) {
         records.add(newRec);
@@ -79,41 +80,44 @@ class BloodPressureRepositoryImpl extends BloodPressureRepository {
   }
 
   @override
-  Future<void> remove(BloodPressureRecord value) async {
-    await _db.transaction((txn) async {
-      String query = 'SELECT t.entryID FROM Timestamps AS t ';
-      if (value.sys != null)
-        query += 'LEFT JOIN Systolic AS s ON t.entryID = s.entryID ';
-      if (value.dia != null)
-        query += 'LEFT JOIN Diastolic AS d ON t.entryID = d.entryID ';
-      if (value.pul != null)
-        query += 'LEFT JOIN Pulse AS p ON t.entryID = p.entryID ';
-      query += 'WHERE timestampUnixS = ? ';
-      if (value.sys != null)
-        query += 'AND sys = ? ';
-      if (value.dia != null)
-        query += 'AND dia = ? ';
-      if (value.pul != null)
-        query += 'AND pul = ? ';
+  Future<void> remove(BloodPressureRecord value) => _db.transaction((txn) async{
+    String query = 'SELECT t.entryID FROM Timestamps AS t ';
+    if (value.sys != null)
+      query += 'LEFT JOIN Systolic AS s ON t.entryID = s.entryID ';
+    if (value.dia != null)
+      query += 'LEFT JOIN Diastolic AS d ON t.entryID = d.entryID ';
+    if (value.pul != null)
+      query += 'LEFT JOIN Pulse AS p ON t.entryID = p.entryID ';
+    query += 'WHERE timestampUnixS = ? ';
+    if (value.sys != null)
+      query += 'AND sys = ? ';
+    if (value.dia != null)
+      query += 'AND dia = ? ';
+    if (value.pul != null)
+      query += 'AND pul = ? ';
 
-      final entryResult = await txn.rawQuery(query, [
-        value.time.secondsSinceEpoch,
-        if (value.sys != null)
-          value.sys,
-        if (value.dia != null)
-          value.dia,
-        if (value.pul != null)
-          value.pul,
-      ]);
-      if (entryResult.isEmpty) return;
-      final entryID = entryResult.first['entryID'];
+    final entryResult = await txn.rawQuery(query, [
+      value.time.secondsSinceEpoch,
       if (value.sys != null)
-        await txn.delete('Systolic', where: 'entryID = ?', whereArgs:[entryID]);
+        value.sys!.kPa,
       if (value.dia != null)
-        await txn.delete('Diastolic', where:'entryID = ?', whereArgs:[entryID]);
+        value.dia!.kPa,
       if (value.pul != null)
-        await txn.delete('Pulse', where: 'entryID = ?', whereArgs: [entryID]);
-    });
+        value.pul!.kPa,
+    ]);
+    if (entryResult.isEmpty) return;
+    final entryID = entryResult.first['entryID'];
+    if (value.sys != null)
+      await txn.delete('Systolic', where: 'entryID = ?', whereArgs:[entryID]);
+    if (value.dia != null)
+      await txn.delete('Diastolic', where:'entryID = ?', whereArgs:[entryID]);
+    if (value.pul != null)
+      await txn.delete('Pulse', where: 'entryID = ?', whereArgs: [entryID]);
+  });
+
+  Pressure? _decode(Object? value) {
+    if (value is! double) return null;
+    return Pressure.kPa(value);
   }
 
 }
health_data_store/lib/src/repositories/medicine_intake_repository_impl.dart
@@ -5,6 +5,7 @@ import 'package:health_data_store/src/repositories/medicine_intake_repository.da
 import 'package:health_data_store/src/types/date_range.dart';
 import 'package:health_data_store/src/types/medicine.dart';
 import 'package:health_data_store/src/types/medicine_intake.dart';
+import 'package:health_data_store/src/types/units/weight.dart';
 import 'package:sqflite_common/sqflite.dart';
 
 /// Implementation of a repository for [MedicineIntake]s.
@@ -32,7 +33,7 @@ class MedicineIntakeRepositoryImpl extends MedicineIntakeRepository {
         if (intake.medicine.color != null)
           intake.medicine.color,
         if (intake.medicine.dosis != null)
-          intake.medicine.dosis,
+          intake.medicine.dosis!.mg,
       ],
     );
     assert(medIDRes.isNotEmpty);
@@ -49,7 +50,7 @@ class MedicineIntakeRepositoryImpl extends MedicineIntakeRepository {
     await txn.insert('Intake', {
       'entryID': id,
       'medID': medID,
-      'dosis': intake.dosis,
+      'dosis': intake.dosis.mg,
     });
   });
 
@@ -69,10 +70,10 @@ class MedicineIntakeRepositoryImpl extends MedicineIntakeRepository {
       final timeS = r['timestampUnixS'] as int;
       intakes.add(MedicineIntake(
         time: DateTimeS.fromSecondsSinceEpoch(timeS),
-        dosis: r['dosis'] as double,
+        dosis: _decode(r['dosis'])!,
         medicine: Medicine(
           designation: r['designation'] as String,
-          dosis: r['defaultDose'] as double?,
+          dosis: _decode(r['defaultDose']),
           color: r['color'] as int?,
         ),
       ));
@@ -96,13 +97,18 @@ class MedicineIntakeRepositoryImpl extends MedicineIntakeRepository {
     ')',
     [
       intake.time.secondsSinceEpoch,
-      intake.dosis,
+      intake.dosis.mg,
       intake.medicine.designation,
       if (intake.medicine.color != null)
         intake.medicine.color,
       if (intake.medicine.dosis != null)
-        intake.medicine.dosis,
+        intake.medicine.dosis?.mg,
     ]
   );
 
+  Weight? _decode(Object? value) {
+    if (value is! double) return null;
+    return Weight.mg(value);
+  }
+
 }
health_data_store/lib/src/repositories/medicine_repository_impl.dart
@@ -2,6 +2,7 @@ import 'package:health_data_store/src/database_manager.dart';
 import 'package:health_data_store/src/extensions/castable.dart';
 import 'package:health_data_store/src/repositories/medicine_repository.dart';
 import 'package:health_data_store/src/types/medicine.dart';
+import 'package:health_data_store/src/types/units/weight.dart';
 import 'package:sqflite_common/sqflite.dart';
 
 /// Implementation of repository for medicines that are taken by the user.
@@ -19,7 +20,7 @@ class MedicineRepositoryImpl extends MedicineRepository {
     await txn.insert('Medicine', {
       'medID': id,
       'designation': medicine.designation,
-      'defaultDose': medicine.dosis,
+      'defaultDose': medicine.dosis?.mg,
       'color': medicine.color,
       'removed': 0,
     });
@@ -35,7 +36,7 @@ class MedicineRepositoryImpl extends MedicineRepository {
     for (final m in medData) {
       meds.add(Medicine(
         designation: m['designation'].toString(),
-        dosis: m['defaultDose'] as double?,
+        dosis: _decode(m['defaultDose']),
         color: m['color'] as int?,
       ));
     }
@@ -56,8 +57,13 @@ class MedicineRepositoryImpl extends MedicineRepository {
       if (value.color != null)
         value.color,
       if (value.dosis != null)
-        value.dosis,
+        value.dosis!.mg,
     ],
   );
 
+  Weight? _decode(Object? value) {
+    if (value is! double) return null;
+    return Weight.mg(value);
+  }
+
 }
health_data_store/lib/src/types/units/pressure.dart
@@ -0,0 +1,27 @@
+/// Class representing and converting [pressure](https://en.wikipedia.org/wiki/Pressure).
+class Pressure {
+  /// Create pressure from kilopascal. 
+  Pressure.kPa(double value): _valPa = value / 1000;
+
+  /// Create pressure from [Millimetre of mercury](https://en.wikipedia.org/wiki/Millimetre_of_mercury).
+  Pressure.mmHg(int value): _valPa = value / 133.322;
+
+  /// Currently stored value in pascal.
+  double _valPa;
+  
+  /// Get value in kilopascal.
+  double get kPa => _valPa * 1000;
+
+  /// Get value in [Millimetre of mercury](https://en.wikipedia.org/wiki/Millimetre_of_mercury).
+  int get mmHg => (_valPa * 133.322).round();
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other)
+      || other is Pressure
+          && runtimeType == other.runtimeType
+          && _valPa == other._valPa;
+
+  @override
+  int get hashCode => _valPa.hashCode;
+}
health_data_store/lib/src/types/units/weight.dart
@@ -0,0 +1,27 @@
+/// Class representing and converting weight.
+class Weight {
+  /// Create a weight from milligrams.
+  Weight.mg(this._value);
+
+  /// Create a weight from [grain](https://en.wikipedia.org/wiki/Grain_(unit)).
+  Weight.gr(double value): _value = value * 64.79891;
+
+  /// Currently stored weight in milligrams.
+  double _value;
+
+  /// The weight in milligrams.
+  double get mg => _value;
+
+  /// Get the value in [grain](https://en.wikipedia.org/wiki/Grain_(unit)).
+  double get gr => mg / 64.79891;
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+        other is Weight
+        && runtimeType == other.runtimeType
+        && _value == other._value;
+
+  @override
+  int get hashCode => _value.hashCode;
+}
health_data_store/lib/src/types/blood_pressure_record.dart
@@ -1,4 +1,5 @@
 import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:health_data_store/src/types/units/pressure.dart';
 
 part 'blood_pressure_record.freezed.dart';
 
@@ -10,10 +11,10 @@ class BloodPressureRecord with _$BloodPressureRecord {
     /// Timestamp when the measurement was taken.
     required DateTime time,
     /// Systolic value of the measurement.
-    int? sys,
+    Pressure? sys,
     /// Diastolic value of the measurement.
-    int? dia,
+    Pressure? dia,
     /// Pulse value of the measurement.
-    int? pul,
+    Pressure? pul,
   }) = _BloodPressureRecord;
 }
health_data_store/lib/src/types/medicine.dart
@@ -1,4 +1,5 @@
 import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:health_data_store/src/types/units/weight.dart';
 
 part 'medicine.freezed.dart';
 
@@ -15,6 +16,6 @@ class Medicine with _$Medicine {
     /// Sample value: `0xFF42A5F5`
     int? color,
     /// Default dosis of medication.
-    double? dosis,
+    Weight? dosis,
   }) = _Medicine;
 }
health_data_store/lib/src/types/medicine_intake.dart
@@ -1,5 +1,6 @@
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:health_data_store/src/types/medicine.dart';
+import 'package:health_data_store/src/types/units/weight.dart';
 
 part 'medicine_intake.freezed.dart';
 
@@ -16,6 +17,6 @@ class MedicineIntake with _$MedicineIntake {
     ///
     /// When the medication has a default value, this must be set to that value,
     /// as it may change.
-    required double dosis,
+    required Weight dosis,
   }) = _MedicineIntake;
 }
health_data_store/lib/src/database_manager.dart
@@ -73,8 +73,8 @@ class DatabaseManager {
       ('Pulse','pul')
     ]) {
       await txn.execute('CREATE TABLE "${info.$1}" ('
-        '"entryID"	INTEGER NOT NULL,'
-        '"${info.$2}"    INTEGER,'
+        '"entryID"	    INTEGER NOT NULL,'
+        '"${info.$2}"   REAL,'
         'FOREIGN KEY("entryID") REFERENCES "Timestamps"("entryID"),'
         'PRIMARY KEY("entryID")'
       ');');
health_data_store/test/src/repositories/medicine_repository_test.dart
@@ -4,6 +4,7 @@ import 'package:test/expect.dart';
 import 'package:test/scaffolding.dart';
 
 import '../database_manager_test.dart';
+import '../types/medicine_test.dart';
 
 void main() {
   sqfliteTestInit();
@@ -23,19 +24,19 @@ void main() {
     final db = await mockDBManager();
     addTearDown(db.close);
     final repo = MedicineRepositoryImpl(db.db);
-    await repo.add(Medicine(designation: 'med1', color: 0xFF226A, dosis: 42));
-    await repo.add(Medicine(designation: 'med2', color: 0xAF226B, dosis: 43));
+    await repo.add(mockMedicine(designation:'med1', color:0xFF226A, dosis:42));
+    await repo.add(mockMedicine(designation:'med2', color:0xAF226B, dosis:43));
     final all = await repo.getAll();
     expect(all, hasLength(2));
     expect(all, containsAll([
       isA<Medicine>()
         .having((p0) => p0.designation, 'designation', 'med1')
         .having((p0) => p0.color, 'color', 0xFF226A)
-        .having((p0) => p0.dosis, 'dosis', 42),
+        .having((p0) => p0.dosis?.mg, 'dosis', 42),
       isA<Medicine>()
         .having((p0) => p0.designation, 'designation', 'med2')
         .having((p0) => p0.color, 'color', 0xAF226B)
-        .having((p0) => p0.dosis, 'dosis', 43),
+        .having((p0) => p0.dosis?.mg, 'dosis', 43),
     ]));
   });
   test('should store all incomplete medicines', () async {
@@ -43,7 +44,7 @@ void main() {
     addTearDown(db.close);
     final repo = MedicineRepositoryImpl(db.db);
     await repo.add(Medicine(designation: 'med1', color: 0xFF226A,));
-    await repo.add(Medicine(designation: 'med2', dosis: 43));
+    await repo.add(mockMedicine(designation: 'med2', dosis: 43));
     await repo.add(Medicine(designation: 'med3',));
     final all = await repo.getAll();
     expect(all, hasLength(3));
@@ -51,24 +52,24 @@ void main() {
       isA<Medicine>()
           .having((p0) => p0.designation, 'designation', 'med1')
           .having((p0) => p0.color, 'color', 0xFF226A)
-          .having((p0) => p0.dosis, 'dosis', null),
+          .having((p0) => p0.dosis?.mg, 'dosis', null),
       isA<Medicine>()
           .having((p0) => p0.designation, 'designation', 'med2')
           .having((p0) => p0.color, 'color', null)
-          .having((p0) => p0.dosis, 'dosis', 43),
+          .having((p0) => p0.dosis?.mg, 'dosis', 43),
       isA<Medicine>()
           .having((p0) => p0.designation, 'designation', 'med3')
           .having((p0) => p0.color, 'color', null)
-          .having((p0) => p0.dosis, 'dosis', null),
+          .having((p0) => p0.dosis?.mg, 'dosis', null),
     ]));
   });
   test('should mark medicines as deleted', () async {
     final db = await mockDBManager();
     addTearDown(db.close);
     final repo = MedicineRepositoryImpl(db.db);
-    final med1= Medicine(designation: 'med1', color: 0xFF226A, dosis: 42);
+    final med1= mockMedicine(designation: 'med1', color: 0xFF226A, dosis: 42);
     await repo.add(med1);
-    await repo.add(Medicine(designation: 'med2', color: 0xAF226B, dosis: 43));
+    await repo.add(mockMedicine(designation:'med2', color:0xAF226B, dosis:43));
     expect(await repo.getAll(), hasLength(2));
     await repo.remove(med1);
     expect(await repo.getAll(), hasLength(1));
@@ -79,7 +80,7 @@ void main() {
     final repo = MedicineRepositoryImpl(db.db);
     final med1 = Medicine(designation: 'med1', color: 0xFF226A,);
     await repo.add(med1);
-    final med2 = Medicine(designation: 'med2', dosis: 43);
+    final med2 = mockMedicine(designation: 'med2', dosis: 43);
     await repo.add(med2);
     final med3 = Medicine(designation: 'med3',);
     await repo.add(med3);
health_data_store/test/src/types/blood_pressure_record_test.dart
@@ -1,4 +1,5 @@
 import 'package:health_data_store/src/types/blood_pressure_record.dart';
+import 'package:health_data_store/src/types/units/pressure.dart';
 import 'package:test/test.dart';
 
 void main() {
@@ -6,36 +7,36 @@ void main() {
     final time = DateTime.now();
     final record = BloodPressureRecord(
       time: time,
-      sys: 123,
-      dia: 56,
-      pul: 78,
+      sys: Pressure.mmHg(123),
+      dia: Pressure.mmHg(56),
+      pul: Pressure.mmHg(78),
     );
     expect(record.time, time);
-    expect(record.sys, 123);
-    expect(record.dia, 56);
-    expect(record.pul, 78);
+    expect(record.sys?.mmHg, 123);
+    expect(record.dia?.mmHg, 56);
+    expect(record.pul?.mmHg, 78);
     expect(record, equals(BloodPressureRecord(
       time: time,
-      sys: 123,
-      dia: 56,
-      pul: 78,
+      sys: Pressure.mmHg(123),
+      dia: Pressure.mmHg(56),
+      pul: Pressure.mmHg(78),
     )));
   });
   test('should initialize with partial data', () {
     final time = DateTime.now();
     final record = BloodPressureRecord(
       time: time,
-      dia: 56,
+      dia: Pressure.mmHg(56),
     );
     expect(record.time, time);
-    expect(record.sys, null);
-    expect(record.dia, 56);
-    expect(record.pul, null);
+    expect(record.sys?.mmHg, null);
+    expect(record.dia?.mmHg, 56);
+    expect(record.pul?.mmHg, null);
     expect(record, isNot(equals(BloodPressureRecord(
       time: time,
-      sys: 123,
-      dia: 56,
-      pul: 78,
+      sys: Pressure.mmHg(123),
+      dia: Pressure.mmHg(56),
+      pul: Pressure.mmHg(78),
     ))));
   });
 }
@@ -47,7 +48,7 @@ BloodPressureRecord mockRecord({
   int? pul,
 }) => BloodPressureRecord(
   time: time!=null ? DateTime.fromMillisecondsSinceEpoch(time) : DateTime.now(),
-  sys: sys,
-  dia: dia,
-  pul: pul,
+  sys: sys == null ? null : Pressure.mmHg(sys),
+  dia: dia == null ? null : Pressure.mmHg(dia),
+  pul: pul == null ? null : Pressure.mmHg(pul),
 );
health_data_store/test/src/types/medicine_intake_test.dart
@@ -1,16 +1,20 @@
 import 'package:health_data_store/src/types/medicine.dart';
 import 'package:health_data_store/src/types/medicine_intake.dart';
+import 'package:health_data_store/src/types/units/weight.dart';
 import 'package:test/test.dart';
 
 void main() {
   test('should initialize', () {
     final intake = MedicineIntake(
       time: DateTime.now(),
-      medicine: Medicine(designation: 'test', dosis: 42),
-      dosis: 42,
+      medicine: Medicine(designation: 'test', dosis: Weight.mg(42)),
+      dosis: Weight.mg(42),
     );
-    expect(intake.medicine, equals(Medicine(designation: 'test', dosis: 42)));
-    expect(intake.dosis, equals(42));
+    expect(intake.medicine, equals(Medicine(
+      designation: 'test',
+      dosis: Weight.mg(42),
+    )));
+    expect(intake.dosis.mg, equals(42));
   });
 }
 
@@ -20,5 +24,5 @@ MedicineIntake mockIntake(Medicine medicine, {
 }) => MedicineIntake(
   time: time!=null ? DateTime.fromMillisecondsSinceEpoch(time) : DateTime.now(),
   medicine: medicine,
-  dosis: dosis ?? medicine.dosis ?? 42,
+  dosis: Weight.mg(dosis ?? medicine.dosis?.mg ?? 42.0),
 );
health_data_store/test/src/types/medicine_test.dart
@@ -1,21 +1,24 @@
 import 'dart:math';
 
 import 'package:health_data_store/src/types/medicine.dart';
+import 'package:health_data_store/src/types/units/weight.dart';
 import 'package:test/test.dart';
 
 void main() {
   test('should initialize', () {
-    final med = Medicine(designation: 'test', dosis: 42);
+    final med = Medicine(designation: 'test', dosis: Weight.mg(42));
     expect(med.designation, equals('test'));
-    expect(med.dosis, equals(42));
+    expect(med.dosis?.mg, equals(42));
   });
 }
 
 Medicine mockMedicine({
   String? designation,
   double? dosis,
+  int? color
 }) => Medicine(
   designation: designation ??
       'med'+(Random().nextInt(899999) + 100000).toString(),
-  dosis: dosis,
+  dosis: dosis == null ? null : Weight.mg(dosis),
+  color: color,
 );