nokken/lib/tools/bloodwork_data_generator.dart
2025-04-20 11:17:03 -04:00

383 lines
12 KiB
Dart

// SPDX-FileCopyrightText: © 2025 Nøkken.io <nokken.io@proton.me>
// SPDX-License-Identifier: AGPL-3.0
//
// bloodwork_data_generator.dart
//
import 'dart:math' show Random;
import 'package:flutter/material.dart';
import 'package:nokken/src/core/services/database/database_service.dart';
import 'package:nokken/src/features/bloodwork_tracker/models/bloodwork.dart';
/// A tool class to generate historical bloodwork data
class BloodworkDataGenerator {
final DatabaseService databaseService;
BloodworkDataGenerator(this.databaseService);
/// Generate bloodwork data, including:
/// - Bloodwork labs every 70-110 days
/// - 2-4 doctor appointments throughout the year
/// - 1 surgery appointment
Future<void> generateBloodworkData() async {
final random = Random();
// Start date: 364 days ago from today
final endDate = DateTime.now();
final startDate = endDate.subtract(const Duration(days: 364));
debugPrint(
'Generating bloodwork data from ${startDate.toIso8601String()} to ${endDate.toIso8601String()}');
// Generate bloodwork labs
await _generateBloodworkLabs(startDate, endDate, random);
// Generate doctor appointments
await _generateDoctorAppointments(startDate, endDate, random);
// Generate a surgery appointment
await _generateSurgeryAppointment(startDate, endDate, random);
debugPrint('Successfully generated bloodwork data.');
}
/// Generate bloodwork labs every 70-110 days
Future<void> _generateBloodworkLabs(
DateTime startDate, DateTime endDate, Random random) async {
// Start a bit before the actual start date to ensure we have consistent coverage
DateTime currentDate = startDate.subtract(const Duration(days: 30));
int labCount = 0;
while (currentDate.isBefore(endDate)) {
// Add a random interval between 70-110 days
final interval = random.nextInt(41) + 70; // 70-110 days
currentDate = currentDate.add(Duration(days: interval));
// Only add if it's within our target range
if (currentDate.isAfter(startDate) && currentDate.isBefore(endDate)) {
// Generate a bloodwork entry
final bloodwork = _generateBloodworkEntry(currentDate, random);
await databaseService.insertBloodwork(bloodwork);
labCount++;
}
}
debugPrint('Generated $labCount bloodwork lab records');
}
/// Generate 2-4 doctor appointments throughout the year
Future<void> _generateDoctorAppointments(
DateTime startDate, DateTime endDate, Random random) async {
// Decide how many appointments (2-4)
final appointmentCount = random.nextInt(3) + 2; // 2-4
debugPrint('Generating $appointmentCount doctor appointments');
// Generate appointment dates (evenly spaced with some randomness)
final yearInDays = 364;
final appointmentDates = <DateTime>[];
for (int i = 0; i < appointmentCount; i++) {
// Calculate roughly evenly spaced dates with some randomness
final targetDay = ((i + 1) * yearInDays / (appointmentCount + 1)).round();
final randomOffset = random.nextInt(21) - 10; // +/- 10 days
final dayOffset = targetDay + randomOffset;
final appointmentDate = startDate.add(Duration(days: dayOffset));
appointmentDates.add(appointmentDate);
}
// Sort dates and ensure no duplicates
appointmentDates.sort((a, b) => a.compareTo(b));
final uniqueDates = <DateTime>[];
DateTime? lastDate;
for (final date in appointmentDates) {
if (lastDate == null || date.difference(lastDate).inDays > 30) {
// Ensure at least 30 days between appointments
uniqueDates.add(date);
lastDate = date;
}
}
// Create and insert appointment records
for (final date in uniqueDates) {
final appointment = _generateAppointmentEntry(date, random);
await databaseService.insertBloodwork(appointment);
}
debugPrint('Generated ${uniqueDates.length} doctor appointment records');
}
/// Generate a surgery appointment within the year
Future<void> _generateSurgeryAppointment(
DateTime startDate, DateTime endDate, Random random) async {
// Place surgery in the middle third of the year (not too recent, not too far back)
final yearInDays = 364;
final targetDayStart = yearInDays ~/ 3;
final targetDayEnd = (yearInDays * 2) ~/ 3;
final dayOffset =
random.nextInt(targetDayEnd - targetDayStart) + targetDayStart;
final surgeryDate = startDate.add(Duration(days: dayOffset));
final surgery = _generateSurgeryEntry(surgeryDate, random);
await databaseService.insertBloodwork(surgery);
debugPrint('Generated surgery record for ${surgeryDate.toIso8601String()}');
}
/// Generate a bloodwork lab entry
Bloodwork _generateBloodworkEntry(DateTime date, Random random) {
// Always include the four required hormones
final hormoneReadings = <HormoneReading>[
_generateHormoneReading('Estradiol', random),
_generateHormoneReading('Testosterone', random),
_generateHormoneReading('Progesterone', random),
_generateHormoneReading('Prolactin', random),
];
// Add 0-6 additional random hormones
final additionalCount = random.nextInt(7); // 0-6
final allHormoneTypes = HormoneTypes.getHormoneTypes();
final requiredTypes = {
'Estradiol',
'Testosterone',
'Progesterone',
'Prolactin'
};
final availableTypes =
allHormoneTypes.where((type) => !requiredTypes.contains(type)).toList();
// Shuffle and take random selection
availableTypes.shuffle(random);
for (int i = 0; i < additionalCount && i < availableTypes.length; i++) {
hormoneReadings.add(_generateHormoneReading(availableTypes[i], random));
}
// Random time
final timeHour = random.nextInt(8) + 8; // Between a8am and 4pm
final timeMinute = random.nextInt(4) * 15; // 0, 15, 30, or 45 minutes
final dateTime = DateTime(
date.year,
date.month,
date.day,
timeHour,
timeMinute,
);
// Random location
final locations = [
'Main Hospital Lab',
'Downtown Clinic',
'Westside Medical Center',
'University Health Services',
'Eastside Lab',
];
final location = locations[random.nextInt(locations.length)];
// Random doctor
final doctors = [
'Dr. Martinez',
'Dr. Johnson',
'Dr. Williams',
'Dr. Chen',
'Dr. Taylor',
];
final doctor = doctors[random.nextInt(doctors.length)];
// Random notes
final notesOptions = [
'Routine bloodwork',
'Follow-up lab work',
'Quarterly hormone check',
'Regular monitoring',
'Medication adjustment labs',
null, // Sometimes no notes
];
final notes = notesOptions[random.nextInt(notesOptions.length)];
return Bloodwork(
date: dateTime,
appointmentType: AppointmentType.bloodwork,
hormoneReadings: hormoneReadings,
location: location,
doctor: doctor,
notes: notes,
);
}
/// Generate a doctor appointment entry
Bloodwork _generateAppointmentEntry(DateTime date, Random random) {
// Random time
final timeHour = random.nextInt(8) + 9; // Between 9am and 5pm
final timeMinute = random.nextInt(4) * 15; // 0, 15, 30, or 45 minutes
final dateTime = DateTime(
date.year,
date.month,
date.day,
timeHour,
timeMinute,
);
// Random location
final locations = [
'Primary Care Clinic',
'Endocrinology Department',
'Specialty Clinic',
'Medical Center',
'Healthcare Associates',
];
final location = locations[random.nextInt(locations.length)];
// Random doctor
final doctors = [
'Dr. Martinez',
'Dr. Johnson',
'Dr. Williams',
'Dr. Chen',
'Dr. Taylor',
];
final doctor = doctors[random.nextInt(doctors.length)];
// Random notes
final notesOptions = [
'Regular checkup',
'Follow-up appointment',
'Medication review',
'Consultation',
'Care planning',
'Treatment discussion',
null, // Sometimes no notes
];
final notes = notesOptions[random.nextInt(notesOptions.length)];
return Bloodwork(
date: dateTime,
appointmentType: AppointmentType.appointment,
hormoneReadings: [], // No hormone readings for appointments
location: location,
doctor: doctor,
notes: notes,
);
}
/// Generate a surgery entry
Bloodwork _generateSurgeryEntry(DateTime date, Random random) {
// Random time (surgeries often early)
final timeHour = random.nextInt(5) + 7; // Between 7am and 12pm
final timeMinute = random.nextInt(4) * 15; // 0, 15, 30, or 45 minutes
final dateTime = DateTime(
date.year,
date.month,
date.day,
timeHour,
timeMinute,
);
// Random location
final locations = [
'University Hospital',
'Memorial Surgery Center',
'Medical Center',
'Surgical Institute',
'Regional Hospital',
];
final location = locations[random.nextInt(locations.length)];
// Random doctor
final doctors = [
'Dr. Garcia',
'Dr. Smith',
'Dr. Patel',
'Dr. Wilson',
'Dr. Lee',
];
final doctor = doctors[random.nextInt(doctors.length)];
// Random notes about the surgery
final notesOptions = [
'Scheduled procedure',
'Outpatient surgery',
'Routine operation',
'Elective procedure',
'Standard intervention',
];
final notes = notesOptions[random.nextInt(notesOptions.length)];
return Bloodwork(
date: dateTime,
appointmentType: AppointmentType.surgery,
hormoneReadings: [], // No hormone readings for surgery
location: location,
doctor: doctor,
notes: notes,
);
}
/// Generate a random hormone reading
HormoneReading _generateHormoneReading(String hormoneName, Random random) {
// Get the default unit for this hormone
final unit = HormoneTypes.getDefaultUnit(hormoneName);
// Generate a reasonable value based on the hormone type
double value;
switch (hormoneName) {
case 'Estradiol':
// Typical range: 30-400 pg/mL
value = 30.0 + random.nextDouble() * 370.0;
break;
case 'Testosterone':
// Typical range: 15-70 ng/dL for women, 300-1000 ng/dL for men
value = 15.0 + random.nextDouble() * 500.0;
break;
case 'Progesterone':
// Typical range: 0.1-25 ng/mL
value = 0.1 + random.nextDouble() * 24.9;
break;
case 'Prolactin':
// Typical range: 3-30 ng/mL
value = 3.0 + random.nextDouble() * 27.0;
break;
case 'FSH':
// Typical range: 4-25 mIU/mL
value = 4.0 + random.nextDouble() * 21.0;
break;
case 'LH':
// Typical range: 2-20 mIU/mL
value = 2.0 + random.nextDouble() * 18.0;
break;
case 'TSH':
// Typical range: 0.4-4.0 μIU/mL
value = 0.4 + random.nextDouble() * 3.6;
break;
case 'Free T3':
// Typical range: 2.3-4.2 pg/mL
value = 2.3 + random.nextDouble() * 1.9;
break;
case 'Free T4':
// Typical range: 0.8-1.8 ng/dL
value = 0.8 + random.nextDouble() * 1.0;
break;
case 'SHBG':
// Typical range: 20-130 nmol/L
value = 20.0 + random.nextDouble() * 110.0;
break;
case 'DHT':
// Typical range: 24-65 ng/dL
value = 24.0 + random.nextDouble() * 41.0;
break;
default:
// Default range for other hormones
value = 10.0 + random.nextDouble() * 90.0;
}
// Round to 1 decimal place
value = (value * 10).round() / 10;
return HormoneReading(
name: hormoneName,
value: value,
unit: unit,
);
}
}