Commit 2795ad7
Changed files (6)
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