main
1import 'dart:convert';
2
3import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
4import 'package:blood_pressure_app/model/export_import/import_field_type.dart';
5import 'package:blood_pressure_app/model/export_import/record_formatter.dart';
6import 'package:blood_pressure_app/l10n/app_localizations.dart';
7import 'package:health_data_store/health_data_store.dart';
8
9// TODO: respect preferred Pressure unit
10
11/// Converters for [BloodPressureRecord] attributes.
12class NativeColumn extends ExportColumn {
13 NativeColumn._create(this._csvTitle, this._restoreableType, this._encode, this._decode);
14
15 /// All native columns that exist.
16 ///
17 /// They are all part of [ExportImportPreset.bloodPressureApp].
18 static final List<NativeColumn> allColumns = [
19 timestampUnixMs,
20 systolic,
21 diastolic,
22 pulse,
23 notes,
24 color,
25 needlePin,
26 intakes,
27 bodyweight,
28 ];
29
30 static final NativeColumn timestampUnixMs = NativeColumn._create(
31 'timestampUnixMs',
32 RowDataFieldType.timestamp,
33 (record, _, __, ___) => record.time.millisecondsSinceEpoch.toString(),
34 (pattern) {
35 final value = int.tryParse(pattern);
36 return (value == null) ? null : DateTime.fromMillisecondsSinceEpoch(value);
37 }
38 );
39 static final NativeColumn systolic = NativeColumn._create(
40 'systolic',
41 RowDataFieldType.sys,
42 (record, _, __, ___) => (record.sys?.mmHg).toString(),
43 int.tryParse,
44 );
45 static final NativeColumn diastolic = NativeColumn._create(
46 'diastolic',
47 RowDataFieldType.dia,
48 (record, _, __, ___) => (record.dia?.mmHg).toString(),
49 int.tryParse,
50 );
51 static final NativeColumn pulse = NativeColumn._create(
52 'pulse',
53 RowDataFieldType.pul,
54 (record, _, __, ___) => record.pul.toString(),
55 int.tryParse,
56 );
57 static final NativeColumn notes = NativeColumn._create(
58 'notes',
59 RowDataFieldType.notes,
60 (_, note, __, ___) => note.note ?? '',
61 (pattern) => pattern,
62 );
63 static final NativeColumn color = NativeColumn._create(
64 'color',
65 RowDataFieldType.color,
66 (_, note, __, ___) => note.color?.toString() ?? '',
67 (pattern) {
68 final value = int.tryParse(pattern);
69 return value;
70 }
71 );
72 static final NativeColumn needlePin = NativeColumn._create(
73 'needlePin',
74 RowDataFieldType.color,
75 (_, note, __, ___) => '{"color":${note.color}}',
76 (pattern) {
77 try {
78 final json = jsonDecode(pattern);
79 if (json is! Map<String, dynamic>) return null;
80 if (json.containsKey('color')) {
81 final value = json['color'];
82 return (value is int)
83 ? value
84 : null;
85 }
86 } on FormatException {
87 // ignore
88 }
89 return null;
90 }
91 );
92 static final NativeColumn intakes = NativeColumn._create(
93 'intakes',
94 RowDataFieldType.intakes,
95 (_, __, intakes, ___) => intakes
96 .map((i) => '${i.medicine.designation}(${i.dosis.mg})')
97 .join('|'),
98 (String pattern) {
99 final intakes = [];
100 for (final e in pattern.split('|')) {
101 final es = e.split('(');
102 if (es.length < 2) return null;
103 final [med, dosisStr, ...] = es;
104 final dosis = double.tryParse(dosisStr.replaceAll(')', ''));
105 if (dosis == null) return null;
106 intakes.add((med, dosis));
107 }
108 return intakes;
109 }
110 );
111 static final NativeColumn bodyweight = NativeColumn._create(
112 'bodyweight',
113 RowDataFieldType.weightKg,
114 (_, __, ___, weight) => weight?.kg.toString() ?? '',
115 double.tryParse,
116 );
117
118 final String _csvTitle;
119 final RowDataFieldType _restoreableType;
120 final String Function(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) _encode;
121 /// Function to attempt decoding.
122 ///
123 /// Must either return null or the type indicated by [_restoreableType].
124 final Object? Function(String pattern) _decode;
125
126 @override
127 String get csvTitle => _csvTitle;
128
129 @override
130 (RowDataFieldType, Object)? decode(String pattern) {
131 final value = _decode(pattern);
132 if (value == null) return null;
133 return (_restoreableType, value);
134 }
135
136 @override
137 String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) =>
138 _encode(record, note, intakes, bodyweight);
139
140 @override
141 String? get formatPattern => null;
142
143 @override
144 String get internalIdentifier => 'native.$csvTitle';
145
146 @override
147 RowDataFieldType? get restoreAbleType => _restoreableType;
148
149 @override
150 String userTitle(AppLocalizations localizations) => _restoreableType.localize(localizations);
151
152
153}
154
155/// Useful columns that are present by default and recreatable through a formatPattern.
156class BuildInColumn extends ExportColumn {
157 /// Creates a build in column and adds it to allColumns.
158 BuildInColumn._create(this.internalIdentifier, this.csvTitle, String formatString, this._userTitle)
159 : _formatter = ScriptedFormatter(formatString);
160
161 static final List<ExportColumn> allColumns = [
162 pulsePressure,
163 formattedTime,
164 mhDate,
165 mhSys,
166 mhDia,
167 mhPul,
168 mhDesc,
169 mhTags,
170 mhWeight,
171 mhOxygen,
172 ];
173
174 static final pulsePressure = BuildInColumn._create(
175 'buildin.pulsePressure',
176 'pulsePressure',
177 r'{{$SYS-$DIA}}',
178 (localizations) => localizations.pulsePressure,
179 );
180 static final formattedTime = TimeColumn.explicit(
181 'buildin.formattedTime',
182 'Time',
183 'dd MMM yyyy, HH:mm',
184 );
185
186 // my heart columns
187 static final mhDate = TimeColumn.explicit(
188 'buildin.mhDate',
189 'DATUM',
190 r'yyyy-MM-dd HH:mm:ss',
191 '"My Heart" export time',
192 );
193 static final mhSys = BuildInColumn._create(
194 'buildin.mhSys',
195 'SYSTOLE',
196 r'$SYS',
197 (_) => '"My Heart" export sys',
198 );
199 static final mhDia = BuildInColumn._create(
200 'buildin.mhDia',
201 'DIASTOLE',
202 r'$DIA',
203 (_) => '"My Heart" export dia',
204 );
205 static final mhPul = BuildInColumn._create(
206 'buildin.mhPul',
207 'PULSE',
208 r'$PUL',
209 (_) => '"My Heart" export pul',
210 );
211 static final mhDesc = BuildInColumn._create(
212 'buildin.mhDesc',
213 'Beschreibung',
214 r'null',
215 (_) => '"My Heart" export description',
216 );
217 static final mhTags = BuildInColumn._create(
218 'buildin.mhTags',
219 'Tags',
220 r'',
221 (_) => '"My Heart" export tags',
222 );
223 static final mhWeight = BuildInColumn._create(
224 'buildin.mhWeight',
225 'Gewicht',
226 r'0.0',
227 (_) => '"My Heart" export weight',
228 );
229 static final mhOxygen = BuildInColumn._create(
230 'buildin.mhOxygen',
231 'Sauerstoffsättigung',
232 r'0',
233 (_) => '"My Heart" export oxygen',
234 );
235
236 @override
237 final String internalIdentifier;
238
239 @override
240 final String csvTitle;
241
242 final String Function(AppLocalizations localizations) _userTitle;
243
244 @override
245 String userTitle(AppLocalizations localizations) => _userTitle(localizations);
246
247 final Formatter _formatter;
248
249 @override
250 (RowDataFieldType, dynamic)? decode(String pattern) => _formatter.decode(pattern);
251
252 @override
253 String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) =>
254 _formatter.encode(record, note, intakes, bodyweight);
255
256 @override
257 String? get formatPattern => _formatter.formatPattern;
258
259 @override
260 RowDataFieldType? get restoreAbleType => _formatter.restoreAbleType;
261}
262
263/// Class for storing data of user added columns.
264class UserColumn extends ExportColumn {
265 /// Create a object that handles export behavior for data in a column.
266 ///
267 /// [formatter] will be created according to [formatPattern].
268 ///
269 /// [internalIdentifier] is automatically prefixed with 'userColumn.' during
270 /// object creation.
271 UserColumn(String internalIdentifier, this.csvTitle, String formatPattern):
272 formatter = ScriptedFormatter(formatPattern),
273 internalIdentifier = 'userColumn.$internalIdentifier';
274
275 /// UserColumn constructor that keeps the internalIdentifier.
276 UserColumn.explicit(this.internalIdentifier, this.csvTitle, String formatPattern):
277 formatter = ScriptedFormatter(formatPattern);
278
279 /// Unique identifier of userColumn.
280 ///
281 /// Is automatically prefixed with `userColumn.` to avoid name collisions with
282 /// build-ins.
283 @override
284 final String internalIdentifier;
285
286 @override
287 final String csvTitle;
288
289 @override
290 String userTitle(AppLocalizations localizations) => csvTitle;
291
292 /// Converter associated with this column.
293 final Formatter formatter;
294
295 @override
296 (RowDataFieldType, dynamic)? decode(String pattern) => formatter.decode(pattern);
297
298 @override
299 String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) =>
300 formatter.encode(record, note, intakes, bodyweight);
301
302 @override
303 String? get formatPattern => formatter.formatPattern;
304
305 @override
306 RowDataFieldType? get restoreAbleType => formatter.restoreAbleType;
307}
308
309/// A measurement formatters that converts the timestamp to a string using ICU
310/// patterns.
311class TimeColumn extends ExportColumn {
312 /// Create a formatter that converts between [String]s and [DateTime]s
313 /// through a format pattern.
314 ///
315 /// [internalIdentifier] is automatically prefixed with 'userColumn.' during
316 /// object creation.
317 TimeColumn(this.csvTitle, this.formatPattern):
318 _localization = null,
319 internalIdentifier = 'timeFormatter.$csvTitle';
320
321 /// UserColumn constructor that does not change the [internalIdentifier].
322 TimeColumn.explicit(this.internalIdentifier, this.csvTitle, this.formatPattern, [this._localization]);
323
324 ScriptedTimeFormatter? _formatter;
325
326 @override
327 final String csvTitle;
328
329 final String? _localization;
330
331 @override
332 (RowDataFieldType, dynamic)? decode(String pattern) {
333 _formatter ??= ScriptedTimeFormatter(formatPattern);
334 return _formatter!.decode(pattern);
335 }
336
337 @override
338 String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes, Weight? bodyweight) {
339 _formatter ??= ScriptedTimeFormatter(formatPattern);
340 return _formatter!.encode(record, note, intakes, bodyweight);
341 }
342
343 @override
344 final String formatPattern;
345
346 /// Unique identifier of userColumn.
347 ///
348 /// Is automatically prefixed with `timeFormatter.` to avoid name collisions
349 /// with build-ins.
350 @override
351 final String internalIdentifier;
352
353 @override
354 RowDataFieldType? get restoreAbleType => RowDataFieldType.timestamp;
355
356 @override
357 String userTitle(AppLocalizations localizations) => _localization ?? csvTitle;
358
359}
360
361/// Interface for converters that allow formatting and provide metadata.
362sealed class ExportColumn implements Formatter {
363 /// Unique internal identifier that is used to identify a column in the app.
364 ///
365 /// A identifier can be any string, but is usually structured with a prefix
366 /// and a name. For example `buildin.sys`, `user.fancyvalue` or
367 /// `convert.myheartsys`. These examples are not guaranteed to be the prefixes
368 /// used in the rest of the app.
369 ///
370 /// It should not be used instead of [csvTitle].
371 String get internalIdentifier; // TODO: why is this needed
372
373 /// Column title in a csv file.
374 ///
375 /// May not contain characters intended for CSV column separation (e.g. `,`).
376 String get csvTitle;
377
378 /// Column title in user facing places that don't require strict rules.
379 ///
380 /// It will be displayed on the exported PDF file or in the column selection.
381 String userTitle(AppLocalizations localizations);
382}