Commit 2795ad7

derdilla <derdilla06@gmail.com>
2023-08-03 12:21:36
use export columns for import
1 parent 7ca44a2
lib/model/blood_pressure.dart
@@ -106,7 +106,7 @@ class BloodPressureRecord {
   final int? systolic;
   final int? diastolic;
   final int? pulse;
-  final String? notes;
+  final String notes;
 
   const BloodPressureRecord(
       this.creationTime, this.systolic, this.diastolic, this.pulse, this.notes);
lib/model/export_import.dart
@@ -27,8 +27,8 @@ extension PdfCompatability on Color {
   }
 }
 
-// TODO: respect new export columns
-// TODO: delete entries in list | button
+// TODO: update import warning
+// TODO: more testing
 class ExportFileCreator {
   final Settings settings;
   final AppLocalizations localizations;
@@ -105,56 +105,59 @@ class ExportFileCreator {
       converter = CsvToListConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter, eol: '\n');
       csvLines = converter.convert(fileContents);
     }
+
     final attributes = csvLines.removeAt(0);
-    var creationTimePos = -1;
-    var isoTimePos = -1;
-    var sysPos = -1;
-    var diaPos = -1;
-    var pulPos = -1;
-    var notePos = -1;
-    for (var i = 0; i<attributes.length; i++) {
-      switch (attributes[i].toString().trim()) {
-        case 'timestampUnixMs':
-          creationTimePos = i;
-          break;
-        case 'isoUTCTime':
-          isoTimePos = i;
-          break;
-        case 'systolic':
-          sysPos = i;
-          break;
-        case 'diastolic':
-          diaPos = i;
-          break;
-        case 'pulse':
-          pulPos = i;
-          break;
-        case 'notes':
-          notePos = i;
-          break;
+    final availableFormatsMap = exportColumnsConfig.availableFormatsMap;
+
+    for (var lineIndex = 0; lineIndex < csvLines.length; lineIndex++) {
+      // get values from columns
+      int? timestamp, sys, dia, pul;
+      String? notes;
+      for (var attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
+        if (timestamp != null && sys != null && dia !=null && pul != null) continue; // optimization
+
+        // get colum from internal name
+        final columnInternalTitle = attributes[attributeIndex].toString().trim();
+        final columnFormat = availableFormatsMap[columnInternalTitle];
+        if (columnFormat == null) {
+          throw ArgumentError('Unknown column: $columnInternalTitle');
+        }
+        if(!columnFormat.isReversible) continue;
+
+        final parsedRecord = columnFormat.parseRecord(csvLines[lineIndex][attributeIndex].toString());
+        for (final parsedRecordDataType in parsedRecord) {
+          switch (parsedRecordDataType.$1) {
+            case RowDataFieldType.notes:
+              assert(parsedRecordDataType.$2 is String?);
+              notes ??= parsedRecordDataType.$2;
+              break;
+            case RowDataFieldType.sys:
+              assert(parsedRecordDataType.$2 is double?);
+              sys ??= (parsedRecordDataType.$2 as double?)?.toInt();
+              break;
+            case RowDataFieldType.dia:
+              assert(parsedRecordDataType.$2 is double?);
+              dia ??= (parsedRecordDataType.$2 as double?)?.toInt();
+              break;
+            case RowDataFieldType.pul:
+              assert(parsedRecordDataType.$2 is double?);
+              pul ??= (parsedRecordDataType.$2 as double?)?.toInt();
+              break;
+            case RowDataFieldType.timestamp:
+              assert(parsedRecordDataType.$2 is int?);
+              timestamp ??= parsedRecordDataType.$2 as int?;
+              break;
+          }
+        }
       }
-    }
-    if(creationTimePos < 0 && isoTimePos < 0) {
-      throw ArgumentError('File didn\'t save timestamps');
-    }
 
-    int? convert(dynamic e) {
-      if (e is int?) {
-        return e;
+      // create record
+      if (timestamp == null) {
+        throw ArgumentError('File didn\'t save timestamps');
       }
-      return null;
-    }
-    for (final line in csvLines) {
-      records.add(
-          BloodPressureRecord(
-              (creationTimePos >= 0 ) ? DateTime.fromMillisecondsSinceEpoch(line[creationTimePos]) : DateTime.parse(line[isoTimePos]),
-              (sysPos >= 0) ? convert(line[sysPos]) : null,
-              (diaPos >= 0) ? convert(line[diaPos]) : null,
-              (pulPos >= 0) ? convert(line[pulPos]) : null,
-              (notePos >= 0) ? line[notePos] : null
-          )
-      );
+      records.add(BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(timestamp), sys, dia, pul, notes ?? ''));
     }
+
     return records;
   }
 
@@ -242,7 +245,7 @@ class ExportFileCreator {
                 (data[row].systolic ?? '-').toString(),
                 (data[row].diastolic ?? '-').toString(),
                 (data[row].pulse ?? '-').toString(),
-                data[row].notes ?? '-'
+                (data[row].notes.isNotEmpty) ? data[row].notes : '-'
               ],
         )
     );
lib/model/export_options.dart
@@ -106,7 +106,7 @@ class ExportColumn {
   late final String internalName;
   /// Display title of the column. Possibly localized
   late final String columnTitle;
-  /// Pattern to create the field contents from: TODO documentation
+  /// Pattern to create the field contents from:
   /// It supports inserting values for $TIMESTAMP, $SYS $DIA $PUL and $NOTE. Where $TIMESTAMP is the time since unix epoch in milliseconds.
   /// To format a timestamp in the same format as the $TIMESTAMP variable, $FORMAT(<timestamp>, <formatString>).
   /// It is supported to use basic mathematics inside of double brackets ("{{}}"). In case one of them is not present in the record, -1 is provided.
@@ -174,8 +174,59 @@ class ExportColumn {
     return fieldContents;
   }
 
+  List<(RowDataFieldType, dynamic)> parseRecord(String formattedRecord) {
+    if (!isReversible || formattedRecord == 'null') return [];
+
+    if (formatPattern == r'$NOTE') return [(RowDataFieldType.notes, formattedRecord)];
+
+    // records are parse by replacing the values with capture groups
+    final types = RegExp(r'\$(TIMESTAMP|SYS|DIA|PUL)').allMatches(formatPattern).map((e) => e.group(0)).toList();
+    final numRegex = formatPattern.replaceAll(RegExp(r'\$(TIMESTAMP|SYS|DIA|PUL)'), '([0-9]+.?[0-9]*)'); // ints and doubles
+    final numMatches = RegExp(numRegex).allMatches(formattedRecord);
+    final numbers = [];
+    if (numMatches.isNotEmpty) {
+      for (var i = 1; i <= numMatches.first.groupCount; i++) {
+        numbers.add(numMatches.first[i]);
+      }
+    }
+
+    List<(RowDataFieldType, dynamic)> records = [];
+    for (var i = 0; i < types.length; i++) {
+      switch (types[i]) {
+        case r'$TIMESTAMP':
+          records.add((RowDataFieldType.timestamp, int.tryParse(numbers[i] ?? '')));
+          break;
+        case r'$SYS':
+          records.add((RowDataFieldType.sys, double.tryParse(numbers[i] ?? '')));
+          break;
+        case r'$DIA':
+          records.add((RowDataFieldType.dia, double.tryParse(numbers[i] ?? '')));
+          break;
+        case r'$PUL':
+          records.add((RowDataFieldType.pul, double.tryParse(numbers[i] ?? '')));
+          break;
+      }
+    }
+    return records;
+  }
+
+  /// Checks if the pattern can be used to parse records. This is the case when the pattern contains variables without
+  /// containing curly brackets or commas.
+  bool get isReversible {
+    return formatPattern == r'$TIMESTAMP' ||
+        formatPattern.contains(RegExp(r'\$(TIMESTAMP|SYS|DIA|PUL|NOTE)')) && !formatPattern.contains(RegExp(r'[{},]'));
+  }
+
   @override
   String toString() {
     return 'ExportColumn{internalColumnName: $internalName, columnTitle: $columnTitle, formatPattern: $formatPattern}';
   }
+}
+
+enum RowDataFieldType {
+  timestamp,
+  sys,
+  dia,
+  pul,
+  notes
 }
\ No newline at end of file
lib/screens/add_measurement.dart
@@ -170,7 +170,7 @@ class _AddMeasurementPageState extends State<AddMeasurementPage> {
                                     widget.initSys,
                                     widget.initDia,
                                     widget.initPul,
-                                    widget.initNote));
+                                    widget.initNote ?? ''));
                               }
                               Navigator.of(context).pop();
                             },
@@ -189,7 +189,7 @@ class _AddMeasurementPageState extends State<AddMeasurementPage> {
                                 final model = Provider.of<BloodPressureModel>(context, listen: false);
                                 final navigator = Navigator.of(context);
 
-                                await model.add(BloodPressureRecord(_time, _systolic, _diastolic, _pulse, _note));
+                                await model.add(BloodPressureRecord(_time, _systolic, _diastolic, _pulse, _note ?? ''));
                                 if (settings.exportAfterEveryEntry && context.mounted) {
                                   final exporter = Exporter(settings, model, ScaffoldMessenger.of(context),
                                       AppLocalizations.of(context)!, Theme.of(context),
pubspec.lock
@@ -122,7 +122,7 @@ packages:
     source: hosted
     version: "4.5.0"
   collection:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: collection
       sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
@@ -683,7 +683,7 @@ packages:
     source: hosted
     version: "2.5.0"
   sqflite_common_ffi:
-    dependency: "direct dev"
+    dependency: "direct main"
     description:
       name: sqflite_common_ffi
       sha256: "8e3b8fc8bc53e1eac87a80a255a1fb88549359aafcfb58107402c69bf0b88828"
pubspec.yaml
@@ -33,11 +33,12 @@ dependencies:
   function_tree: ^0.8.13
   badges: ^3.1.1
   flutter_markdown: ^0.6.17
+  collection: ^1.17.1
+  sqflite_common_ffi: ^2.3.0
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  sqflite_common_ffi:
   file: any
   flutter_lints: ^2.0.0
   mockito: ^5.4.1