Commit a6c5680

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-03-08 07:21:07
rewrite csv converter to more granular methods
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent db1ea77
lib/model/export_import/csv_converter.dart
@@ -47,48 +47,75 @@ class CsvConverter {
   /// A needle pin takes precedent over a color.
   RecordParsingResult parse(String csvString) {
     // Turn csv into lines.
-    final lines = (){
-        final converter = CsvToListConverter(
-          fieldDelimiter: settings.fieldDelimiter,
-          textDelimiter: settings.textDelimiter,
-          shouldParseNumbers: false,
-        );
-        final csvLines = converter.convert(csvString, eol: '\r\n');
-        if (csvLines.length < 2) return converter.convert(csvString, eol: '\n');
-        return csvLines;
-    }();
+    final lines = getCsvLines(csvString);
     if (lines.length < 2) return RecordParsingResult.err(RecordParsingErrorEmptyFile());
 
     // Get and validate columns from csv title.
-    final List<ExportColumn> columns = [];
-    for (final titleText in lines.removeAt(0)) {
-      assert(titleText is String);
-      final formattedTitleText = (titleText as String).trim();
-      final column = availableColumns.firstWhere(
-              (c) => c.csvTitle == formattedTitleText
-                  && c.restoreAbleType != null,);
-      if (column == null) return RecordParsingResult.err(RecordParsingErrorUnknownColumn(titleText));
-      columns.add(column);
-    }
-    if (columns.where((e) => e.restoreAbleType == RowDataFieldType.timestamp).isEmpty) {
+    final List<String> titles = lines.removeAt(0).cast();
+    final Map<String, ExportColumn> columns = getColumns(titles);
+    // TODO: consider returning `RecordParsingResult.err(RecordParsingErrorUnknownColumn(columnTitle))` for unknownColumns
+    
+    if (columns.values.none((e) => e.restoreAbleType == RowDataFieldType.timestamp)) {
       return RecordParsingResult.err(RecordParsingErrorTimeNotRestoreable());
     }
 
     // Convert data to records.
+    return parseRecords(lines.map((e) => e.cast<String>()).toList(), titles, columns);
+  }
+
+  /// Returns
+  List<List<dynamic>> getCsvLines(String csvString) {
+    final converter = CsvToListConverter(
+      fieldDelimiter: settings.fieldDelimiter,
+      textDelimiter: settings.textDelimiter,
+      shouldParseNumbers: false,
+    );
+    final csvLines = converter.convert(csvString, eol: '\r\n');
+    if (csvLines.length < 2) return converter.convert(csvString, eol: '\n');
+    return csvLines;
+  }
+
+  /// Map column names in the first csv-line to matching [ExportColumn].
+  Map<String, ExportColumn> getColumns(List<String> firstLine) {
+    final Map<String, ExportColumn> columns = {};
+    for (final titleText in firstLine) {
+      final formattedTitleText = titleText.trim();
+      final column = availableColumns.firstWhere(
+            (c) => c.csvTitle == formattedTitleText
+            && c.restoreAbleType != null,);
+      if (column != null) columns[titleText] = column;
+    }
+    return columns;
+  }
+
+  /// Parse csv data in [dataLines] using [parsers] according to [orderedColumns].
+  ///
+  /// [dataLines] contains all lines of the csv file without the headline and
+  /// [orderedColumns] must have the same length as every line in [dataLines]
+  /// for parsing to succeed.
+  ///
+  /// [assumeHeadline] controls whether the line number should be offset by one
+  /// in case of error.
+  RecordParsingResult parseRecords(
+      List<List<String>> dataLines,
+      List<String> orderedColumns,
+      Map<String,ExportColumn> parsers, [
+        bool assumeHeadline = true,
+      ]) {
     final List<BloodPressureRecord> records = [];
-    int currentLineNumber = 1;
-    for (final currentLine in lines) {
-      if (currentLine.length < columns.length) {
+    int currentLineNumber = assumeHeadline ? 1 : 0;
+    for (final currentLine in dataLines) {
+      if (currentLine.length < orderedColumns.length) {
         return RecordParsingResult.err(RecordParsingErrorExpectedMoreFields(currentLineNumber));
       }
-      
+
       final List<(RowDataFieldType, dynamic)> recordPieces = [];
-      for (int fieldIndex = 0; fieldIndex < columns.length; fieldIndex++) {
-        assert(currentLine[fieldIndex] is String);
-        final piece = columns[fieldIndex].decode(currentLine[fieldIndex]);
+      for (int fieldIndex = 0; fieldIndex < orderedColumns.length; fieldIndex++) {
+        final parser = parsers[orderedColumns[fieldIndex]];
+        final piece = parser?.decode(currentLine[fieldIndex]);
         // Validate that the column parsed the expected type.
         // Null can be the result of empty fields.
-        if (piece?.$1 != columns[fieldIndex].restoreAbleType
+        if (piece?.$1 != parser?.restoreAbleType
             && piece != null) { // TODO: consider making some RowDataFieldType values nullable and handling this in the parser.
           return RecordParsingResult.err(RecordParsingErrorUnparsableField(currentLineNumber, currentLine[fieldIndex]));
         }
@@ -96,27 +123,27 @@ class CsvConverter {
       }
 
       final DateTime? timestamp = recordPieces.firstWhereOrNull(
-              (piece) => piece.$1 == RowDataFieldType.timestamp,)?.$2;
+            (piece) => piece.$1 == RowDataFieldType.timestamp,)?.$2;
       if (timestamp == null) {
         return RecordParsingResult.err(RecordParsingErrorTimeNotRestoreable());
       }
 
       final int? sys = recordPieces.firstWhereOrNull(
-              (piece) => piece.$1 == RowDataFieldType.sys,)?.$2;
+            (piece) => piece.$1 == RowDataFieldType.sys,)?.$2;
       final int? dia = recordPieces.firstWhereOrNull(
-              (piece) => piece.$1 == RowDataFieldType.dia,)?.$2;
+            (piece) => piece.$1 == RowDataFieldType.dia,)?.$2;
       final int? pul = recordPieces.firstWhereOrNull(
-              (piece) => piece.$1 == RowDataFieldType.pul,)?.$2;
+            (piece) => piece.$1 == RowDataFieldType.pul,)?.$2;
       final String note = recordPieces.firstWhereOrNull(
-              (piece) => piece.$1 == RowDataFieldType.notes,)?.$2 ?? '';
+            (piece) => piece.$1 == RowDataFieldType.notes,)?.$2 ?? '';
       final MeasurementNeedlePin? needlePin = recordPieces.firstWhereOrNull(
-              (piece) => piece.$1 == RowDataFieldType.needlePin,)?.$2;
+            (piece) => piece.$1 == RowDataFieldType.needlePin,)?.$2;
 
       records.add(BloodPressureRecord(timestamp, sys, dia, pul, note, needlePin: needlePin));
       currentLineNumber++;
     }
-    
-    assert(records.length == lines.length, 'every line should have been parse'); // first line got removed
+
+    assert(records.length == dataLines.length, 'every line should have been parse');
     return RecordParsingResult.ok(records);
   }
 }
lib/model/export_import/csv_record_parsing_actor.dart
@@ -0,0 +1,27 @@
+
+import 'package:blood_pressure_app/model/export_import/column.dart';
+
+/// A intermediate class that allows to manage record parsing.
+class CsvRecordParsingActor {
+  /// Create an intermediate object to manage a record parsing process.
+  CsvRecordParsingActor(this.csvString) {
+
+  }
+
+  /// The initial csv String provided to the parser
+  final String csvString;
+
+  /// All columns defined in the csv headline.
+  List<String> columnNames;
+
+  /// The current interpretation of columns in the csv data.
+  ///
+  /// There is no guarantee that every column in [columnNames] has a parser.
+  Map<String, ExportColumn> columnParsers;
+
+  final List<void Function(CsvRecordParsingActor)> _listeners = [];
+
+  /// Register a listener that gets called once a value gets changed.
+  void addListener(void Function(CsvRecordParsingActor state) listener)
+    => _listeners.add(listener);
+}
lib/model/export_import/record_parsing_result.dart
@@ -41,6 +41,7 @@ sealed class RecordParsingError {}
 class RecordParsingErrorEmptyFile implements RecordParsingError {}
 
 /// There is no column that allows restoring a timestamp.
+// TODO: return line.
 class RecordParsingErrorTimeNotRestoreable implements RecordParsingError {}
 
 /// There is no column with this csv title that can be reversed.