diff --git a/lib/models/assignment_generator.dart b/lib/models/assignment_generator.dart index 5a8a62b..d0d3b75 100644 --- a/lib/models/assignment_generator.dart +++ b/lib/models/assignment_generator.dart @@ -1,9 +1,41 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'dart:math'; import 'roster.dart'; import 'doctor.dart'; import 'shift.dart'; +class ScoredRoster { + Roster roster; + double totalScore; + double hoursStdevScore; + double weekendCallsStdevScore; + double weekdayCallsStdevScore; + double weekdayCaesarCoverStdevScore; + double weekdaySecondOnCallStdevScore; + double meanCallSpreadScore; + double meanCallSpreadStdevScore; + double callSpreadStdevScore; + + ScoredRoster( + this.roster, + this.totalScore, [ + this.hoursStdevScore = 0, + this.weekendCallsStdevScore = 0, + this.weekdayCallsStdevScore = 0, + this.weekdayCaesarCoverStdevScore = 0, + this.weekdaySecondOnCallStdevScore = 0, + this.meanCallSpreadScore = 0, + this.meanCallSpreadStdevScore = 0, + this.callSpreadStdevScore = 0, + ]); + + @override + String toString() { + return 'ScoredRoster(roster: $roster, totalScore: $totalScore, hoursStdevScore: $hoursStdevScore, weekendCallsStdevScore: $weekendCallsStdevScore, weekdayCallsStdevScore: $weekdayCallsStdevScore, weekdayCaesarCoverStdevScore: $weekdayCaesarCoverStdevScore, weekdaySecondOnCallStdevScore: $weekdaySecondOnCallStdevScore, meanCallSpreadScore: $meanCallSpreadScore, meanCallSpreadStdevScore: $meanCallSpreadStdevScore, callSpreadStdevScore: $callSpreadStdevScore)'; + } +} + class AssignmentGenerator { final Map hoursPerShiftType; final double maxOvertimeHours; @@ -25,43 +57,104 @@ class AssignmentGenerator { assert(maxOvertimeHours > 0); } - Future retryAssignments(Roster roster, int retries, - ValueNotifier progressNotifier) async { - roster.clearAssignments(); - Roster bestRoster = roster; - double bestScore = double.infinity; + /// Given a set of rosters and a number of retries, this function will attempt to + /// fill the rosters with valid assignments. If no valid assignments are found + /// after the given number of retries, the function will return false. + /// Otherwise, the function will return true and update the rosters, ordered by + /// the best score found. + /// The progressNotifier is used to update the progress of the function. + /// The function will return false if less than 10% of the roster permutations + /// were valid. + /// The function will return true if at least one valid roster permutation was found. + /// + /// Parameters: + /// - doctors: The list of doctors to assign to the rosters + /// - shifts: The list of shifts to assign to the rosters + /// - retries: The number of candidate rosters to generate + /// - progressNotifier: A ValueNotifier to update the progress of the function + /// - outputs: The number of top rosters to return + /// + /// Returns: + /// - A Future> containing the top roster permutations found + /// + /// Example: + /// ```dart + /// List doctors = [ + /// Doctor(name: 'Dr. A', canPerformAnaesthetics: true, canPerformCaesars: true), + /// Doctor(name: 'Dr. B', canPerformAnaesthetics: true, canPerformCaesars: true), + /// Doctor(name: 'Dr. C', canPerformAnaesthetics: true, canPerformCaesars: true), + /// Doctor(name: 'Dr. D', canPerformAnaesthetics: true, canPerformCaesars: true), + /// Doctor(name: 'Dr. E', canPerformAnaesthetics: true, canPerformCaesars: true), + /// ]; + /// List shifts = [ + /// Shift(date: DateTime(2022, 1, 1), type: 'Weekday'), + /// Shift(date: DateTime(2022, 1, 2), type: 'Weekend') + /// ]; + /// AssignmentGenerator generator = AssignmentGenerator(); + /// ValueNotifier progressNotifier = ValueNotifier(0.0); + /// Future> topRosters = generator.retryAssignments(doctors, shifts, 100, progressNotifier); + /// ``` + /// + /// See also: + /// - [Roster] + /// - [Doctor] + /// - [Shift] + /// - [ValueNotifier] + /// - [Future] + /// - [bool] + Future> retryAssignments(List doctors, + List shifts, int retries, ValueNotifier progressNotifier, + [int outputs = 10]) async { + List topScoredRosters = List.generate( + outputs, + (_) => ScoredRoster( + Roster(doctors: doctors, shifts: shifts), double.infinity)); int validRostersFound = 0; + Roster candidateRoster = Roster(doctors: doctors, shifts: shifts); for (int i = 0; i < retries; i++) { progressNotifier.value = (i + 1) / retries; await Future.delayed(const Duration(microseconds: 1)); - roster.clearAssignments(); - Roster tempRoster = roster; - - bool filled = assignShifts(tempRoster); + candidateRoster.clearAssignments(); + bool filled = assignShifts(candidateRoster); if (filled) { validRostersFound++; - double score = _calculateScore(tempRoster); - if (score < bestScore) { - bestScore = score; - bestRoster = tempRoster; + double score = _calculateScoreDetailed(candidateRoster).totalScore; + if (score < topScoredRosters[outputs - 1].totalScore) { + topScoredRosters[outputs - 1] = + ScoredRoster(candidateRoster.copy(), score); + topScoredRosters.sort((a, b) => a.totalScore.compareTo(b.totalScore)); } } } if (validRostersFound == 0) { - print('No valid roster permutations found in $retries tries'); - return false; + return []; } else { if (validRostersFound / retries < 0.1) { print('< 10% of roster permutations were valid'); } - roster.doctors = bestRoster.doctors; - roster.shifts = bestRoster.shifts; - return true; + List detailedScores = topScoredRosters + .map((scoredRoster) => _calculateScoreDetailed(scoredRoster.roster)) + .toList(); + print(detailedScores); + return topScoredRosters + .map((scoredRoster) => scoredRoster.roster) + .toList(); + } + } + + bool assignShiftsMultipleRosters(List rosters) { + for (Roster roster in rosters) { + roster.clearAssignments(); + bool filled = assignShifts(roster); + if (!filled) { + return false; + } } + return true; } bool assignShifts(Roster roster) { @@ -235,24 +328,20 @@ class AssignmentGenerator { } } - double _calculateScore(Roster roster, - [double hoursVarianceWeight = 1.0, - double weekendCallsVarianceWeight = 1.0, - double weekdayCallsVarianceWeight = 1.0, - double weekdayCaesarCoverVarianceWeight = 1.0, - double weekdaySecondOnCallVarianceWeight = 1.0, - double callSpreadWeight = 1.0]) { - // Calculate the variance of the weekend / PH calls among doctors + num _calculateWeekendCallsStdev(Roster roster) { + // Calculate the standard deviation of the weekend / PH calls among doctors double meanWeekendCalls = roster.doctors.fold(0.0, (sum, doctor) => sum + doctor.weekendCalls) / roster.doctors.length; - double weekendCallsVariance = roster.doctors.fold( - 0.0, - (sum, doctor) => - sum + pow(doctor.weekendCalls - meanWeekendCalls, 2)) / - roster.doctors.length; - - // Normalize the variance by the number of weekend days + num weekendCallsStdev = pow( + roster.doctors.fold( + 0.0, + (sum, doctor) => + sum + pow(doctor.weekendCalls - meanWeekendCalls, 2)) / + roster.doctors.length, + 0.5); + + // Normalize the standard deviation by the number of weekend days int weekendDays = 0; for (Shift shift in roster.shifts) { if (shift.type == 'Weekend' || shift.type == 'Holiday') { @@ -261,20 +350,32 @@ class AssignmentGenerator { } if (weekendDays != 0) { - weekendCallsVariance /= weekendDays; + weekendCallsStdev /= weekendDays; } - // Calculate the variance of overtime hours among doctors + return weekendCallsStdev; + } + + num _calculateHoursStdev(Roster roster) { + // Calculate the standard deviation of overtime hours among doctors double meanHours = roster.doctors.fold(0.0, (sum, doctor) => sum + doctor.overtimeHours) / roster.doctors.length; - double hoursVariance = roster.doctors.fold(0.0, - (sum, doctor) => sum + pow(doctor.overtimeHours - meanHours, 2)) / - roster.doctors.length; + num hoursStdev = pow( + roster.doctors.fold( + 0.0, + (sum, doctor) => + sum + pow(doctor.overtimeHours - meanHours, 2)) / + roster.doctors.length, + 0.5); - hoursVariance /= maxOvertimeHours; + hoursStdev /= maxOvertimeHours; + + return hoursStdev; + } - // Calculate the variance of weekday calls among doctors + // Calculate the standard deviation of weekday calls among doctors + num _calculateWeekdayCallsStdev(Roster roster, int weekendDays) { double meanWeekdayCalls = roster.doctors.fold( 0.0, (sum, doctor) => @@ -284,21 +385,26 @@ class AssignmentGenerator { doctor.secondOnCallWeekdayCalls) / roster.doctors.length; - double weekdayCallsVariance = roster.doctors.fold( - 0.0, - (sum, doctor) => - sum + - pow( - doctor.overnightWeekdayCalls + - doctor.caesarCoverWeekdayCalls + - doctor.secondOnCallWeekdayCalls - - meanWeekdayCalls, - 2)) / - roster.doctors.length; - - weekdayCallsVariance /= 30 - weekendDays; + num weekdayCallsStdev = pow( + roster.doctors.fold( + 0.0, + (sum, doctor) => + sum + + pow( + doctor.overnightWeekdayCalls + + doctor.caesarCoverWeekdayCalls + + doctor.secondOnCallWeekdayCalls - + meanWeekdayCalls, + 2)) / + roster.doctors.length, + 0.5); + + weekdayCallsStdev /= 30 - weekendDays; + return weekdayCallsStdev; + } - // Calculate the variance of weekday Caesar Cover calls among doctors +// Calculate the standard deviation of weekday Caesar Cover calls among doctors + num _calculateCaesarCoverStdev(Roster roster, int weekendDays) { double meanCaesarCoverCalls = roster.doctors.fold( 0.0, (sum, doctor) => @@ -307,37 +413,46 @@ class AssignmentGenerator { doctor.caesarCoverWeekendCalls) / roster.doctors.length; - double weekdayCaesarCoverVariance = roster.doctors.fold( - 0.0, - (sum, doctor) => - sum + - pow( - doctor.caesarCoverWeekdayCalls + - doctor.caesarCoverWeekendCalls - - meanCaesarCoverCalls, - 2)) / - roster.doctors.length; - - weekdayCaesarCoverVariance /= 30 - weekendDays; + num weekdayCaesarCoverStdev = pow( + roster.doctors.fold( + 0.0, + (sum, doctor) => + sum + + pow( + doctor.caesarCoverWeekdayCalls + + doctor.caesarCoverWeekendCalls - + meanCaesarCoverCalls, + 2)) / + roster.doctors.length, + 0.5); + + weekdayCaesarCoverStdev /= 30 - weekendDays; + return weekdayCaesarCoverStdev; + } - // Calculate the variance of weekday Second On Call calls among doctors +// Calculate the standard deviation of weekday Second On Call calls among doctors + num _calculateSecondOnCallStdev(Roster roster, int weekendDays) { double meanSecondOnCallCalls = roster.doctors .fold(0.0, (sum, doctor) => sum + doctor.secondOnCallWeekdayCalls) / roster.doctors.length; - double weekdaySecondOnCallVariance = roster.doctors.fold( - 0.0, - (sum, doctor) => - sum + - pow(doctor.secondOnCallWeekdayCalls - meanSecondOnCallCalls, - 2)) / - roster.doctors.length; - - weekdaySecondOnCallVariance /= 30 - weekendDays; + num weekdaySecondOnCallStdev = pow( + roster.doctors.fold( + 0.0, + (sum, doctor) => + sum + + pow(doctor.secondOnCallWeekdayCalls - meanSecondOnCallCalls, + 2)) / + roster.doctors.length, + 0.5); + + weekdaySecondOnCallStdev /= 30 - weekendDays; + return weekdaySecondOnCallStdev; + } - // Calculate the spread of calls over the month for each doctor (ideally want them as evenly-spaced as possible) - // This is a matter of maximising the space between each call for each doctor - larger is better - double callSpread = 0.0; +// Calculate the spread of calls over the month for each doctor + List _calculateDoctorsCallSpreadStdevs(Roster roster) { + List doctorsCallSpreadStdevs = []; for (Doctor doctor in roster.doctors) { List callDates = []; for (Shift shift in roster.shifts) { @@ -348,23 +463,188 @@ class AssignmentGenerator { callDates.add(shift.date); } } + callDates.sort(); + List doctorCallSpreads = []; + for (int i = 1; i < callDates.length; i++) { + doctorCallSpreads.add(callDates[i].difference(callDates[i - 1]).inDays); + } + double doctorMeanCallSpread = + doctorCallSpreads.fold(0, (sum, spread) => sum + spread) / + doctorCallSpreads.length; + doctorsCallSpreadStdevs.add(pow( + doctorCallSpreads.fold( + 0.0, + (sum, spread) => + sum + pow(spread - doctorMeanCallSpread, 2)) / + doctorCallSpreads.length, + 0.5)); + } + return doctorsCallSpreadStdevs; + } +// Calculate the mean call spread for each doctor + List _calculateDoctorsMeanCallSpreads(Roster roster) { + List doctorsMeanCallSpreads = []; + for (Doctor doctor in roster.doctors) { + List callDates = []; + for (Shift shift in roster.shifts) { + if (shift.mainDoctor == doctor || + shift.caesarCoverDoctor == doctor || + shift.secondOnCallDoctor == doctor || + shift.weekendDayDoctor == doctor) { + callDates.add(shift.date); + } + } callDates.sort(); + List doctorCallSpreads = []; for (int i = 1; i < callDates.length; i++) { - callSpread += callDates[i].difference(callDates[i - 1]).inDays; + doctorCallSpreads.add(callDates[i].difference(callDates[i - 1]).inDays); } + double doctorMeanCallSpread = + doctorCallSpreads.fold(0, (sum, spread) => sum + spread) / + doctorCallSpreads.length; + doctorsMeanCallSpreads.add(doctorMeanCallSpread); } + return doctorsMeanCallSpreads; + } + +// Calculate the maximum possible spread of calls over the roster + double _calculateMaxSpread(Roster roster) { + int weekendDays = _calculateWeekendDays(roster); + int rolesPerShiftWeekend = 4; + int rolesPerShiftWeekday = 3; + int totalRoles = rolesPerShiftWeekend * weekendDays + + rolesPerShiftWeekday * (30 - weekendDays); + double rolesPerShift = totalRoles / roster.doctors.length; + double maxSpread = totalRoles / (roster.doctors.length * rolesPerShift); + return maxSpread; + } - // Normalize the call spread, and to make it a score, subtract it from 1 - double maxSpread = 30 * roster.shifts.length / roster.doctors.length; - callSpread = 1 - (callSpread / maxSpread); +// Calculate the spread of calls over the month for each doctor + double _calculateMeanDoctorsCallSpreadStdev( + List doctorsCallSpreadStdevs) { + double meanDoctorsCallSpreadStdev = + doctorsCallSpreadStdevs.fold(0.0, (sum, stdev) => sum + stdev) / + doctorsCallSpreadStdevs.length; + return meanDoctorsCallSpreadStdev; + } + +// Calculate the mean call spread + double _calculateMeanCallSpread( + List doctorsMeanCallSpreads, double maxSpread) { + double meanDoctorsMeanCallSpread = + doctorsMeanCallSpreads.fold(0.0, (sum, spread) => sum + spread) / + doctorsMeanCallSpreads.length; + double meanCallSpread = (maxSpread - meanDoctorsMeanCallSpread) / maxSpread; + return meanCallSpread; + } + +// Calculate the standard deviation of the mean call spreads + num _calculateMeanCallSpreadStdev( + List doctorsMeanCallSpreads, double meanDoctorsMeanCallSpread) { + num meanCallSpreadStdev = pow( + doctorsMeanCallSpreads.fold( + 0.0, + (sum, meanSpread) => + sum + pow(meanSpread - meanDoctorsMeanCallSpread, 2)) / + doctorsMeanCallSpreads.length, + 0.5); + return meanCallSpreadStdev; + } + +// Calculate the standard deviation of call spread + num _calculateCallSpreadStdev(List doctorsCallSpreadStdevs, + double meanDoctorsCallSpreadStdev, double maxSpread) { + num callSpreadStdev = pow( + doctorsCallSpreadStdevs.fold( + 0.0, + (sum, stdev) => + sum + pow(stdev - meanDoctorsCallSpreadStdev, 2)) / + doctorsCallSpreadStdevs.length, + 0.5); + callSpreadStdev = callSpreadStdev / maxSpread; + return callSpreadStdev; + } + + int _calculateWeekendDays(Roster roster) { + return roster.shifts + .where((shift) => shift.type == 'Weekend' || shift.type == 'Holiday') + .length; + } - return hoursVarianceWeight * hoursVariance + - weekendCallsVarianceWeight * weekendCallsVariance + - weekdayCallsVarianceWeight * weekdayCallsVariance + - weekdayCaesarCoverVarianceWeight * weekdayCaesarCoverVariance + - weekdaySecondOnCallVarianceWeight * weekdaySecondOnCallVariance + - callSpreadWeight * callSpread; + ScoredRoster _calculateScoreDetailed(Roster roster, + [double hoursStdevWeight = 50.0, + double weekendCallsStdevWeight = 1.0, + double weekdayCallsStdevWeight = 1.0, + double weekdayCaesarCoverStdevWeight = 1.0, + double weekdaySecondOnCallStdevWeight = 1.0, + double meanCallSpreadWeight = 0.0, + double meanCallSpreadStdevWeight = 1.0, + double callSpreadStdevWeight = 1.0]) { + num weekendCallsStdev = _calculateWeekendCallsStdev(roster); + num hoursStdev = _calculateHoursStdev(roster); + + int weekendDays = _calculateWeekendDays(roster); + + num weekdayCallsStdev = _calculateWeekdayCallsStdev(roster, weekendDays); + num weekdayCaesarCoverStdev = + _calculateCaesarCoverStdev(roster, weekendDays); + num weekdaySecondOnCallStdev = + _calculateSecondOnCallStdev(roster, weekendDays); + + List doctorsCallSpreadStdevs = + _calculateDoctorsCallSpreadStdevs(roster); + List doctorsMeanCallSpreads = + _calculateDoctorsMeanCallSpreads(roster); + + double maxSpread = _calculateMaxSpread(roster); + + double meanDoctorsCallSpreadStdev = + _calculateMeanDoctorsCallSpreadStdev(doctorsCallSpreadStdevs); + double meanCallSpread = + _calculateMeanCallSpread(doctorsMeanCallSpreads, maxSpread); + + double meanDoctorsMeanCallSpread = + doctorsMeanCallSpreads.fold(0.0, (sum, spread) => sum + spread) / + doctorsMeanCallSpreads.length; + num meanCallSpreadStdev = _calculateMeanCallSpreadStdev( + doctorsMeanCallSpreads, meanDoctorsMeanCallSpread); + + num callSpreadStdev = _calculateCallSpreadStdev( + doctorsCallSpreadStdevs, meanDoctorsCallSpreadStdev, maxSpread); + + double hoursStdevScore = hoursStdevWeight * hoursStdev; + double weekendCallsStdevScore = weekendCallsStdevWeight * weekendCallsStdev; + double weekdayCallsStdevScore = weekdayCallsStdevWeight * weekdayCallsStdev; + double weekdayCaesarCoverStdevScore = + weekdayCaesarCoverStdevWeight * weekdayCaesarCoverStdev; + double weekdaySecondOnCallStdevScore = + weekdaySecondOnCallStdevWeight * weekdaySecondOnCallStdev; + double meanCallSpreadScore = meanCallSpreadWeight * meanCallSpread; + double meanCallSpreadStdevScore = + meanCallSpreadStdevWeight * meanCallSpreadStdev; + double callSpreadStdevScore = callSpreadStdevWeight * callSpreadStdev; + + double totalScore = hoursStdevScore + + weekendCallsStdevScore + + weekdayCallsStdevScore + + weekdayCaesarCoverStdevScore + + weekdaySecondOnCallStdevScore + + meanCallSpreadScore + + meanCallSpreadStdevScore + + callSpreadStdevScore; + + return ScoredRoster( + roster, + totalScore, + hoursStdevScore, + weekendCallsStdevScore, + weekdayCallsStdevScore, + weekdayCaesarCoverStdevScore, + weekdaySecondOnCallStdevScore, + meanCallSpreadScore, + meanCallSpreadStdevScore, + callSpreadStdevScore); } AssignmentGenerator copy() { diff --git a/lib/models/roster.dart b/lib/models/roster.dart index 077000e..5fb7d7b 100644 --- a/lib/models/roster.dart +++ b/lib/models/roster.dart @@ -27,7 +27,12 @@ class Roster { Future retryAssignments( int retries, ValueNotifier progressNotifier) async { - return await assigner.retryAssignments(this, retries, progressNotifier); + return await assigner + .retryAssignments(doctors, shifts, retries, progressNotifier, 1) + .then((rosters) { + filled = rosters.first.filled; + return filled; + }); } List? getAvailableDoctors(String role, DateTime date, diff --git a/lib/screens/roster_home_page.dart b/lib/screens/roster_home_page.dart index 49bff19..359c5c2 100644 --- a/lib/screens/roster_home_page.dart +++ b/lib/screens/roster_home_page.dart @@ -24,7 +24,8 @@ class RosterHomePageState extends State { List doctors = []; List shifts = []; late AssignmentGenerator assigner; - late Roster roster; + late List candidateRosters; + int candidateRosterIndex = 0; Map hoursPerShiftType = { 'Overnight Weekday': 16, 'Second On Call Weekday': 6, @@ -173,12 +174,8 @@ class RosterHomePageState extends State { maxOvertimeHours: maxOvertimeHours, postCallBeforeLeave: _postCallBeforeLeaveValueNotifier.value, ); - roster = Roster( - doctors: doctors, - shifts: shifts, - assigner: assigner, - ); - assigner.assignShifts(roster); + candidateRosters = []; + assigner.assignShiftsMultipleRosters(candidateRosters); } bool _isPublicHoliday(DateTime date) { @@ -244,11 +241,16 @@ class RosterHomePageState extends State { _progressNotifier.value = 0.0; _overlayEntry = _createOverlayEntry(); Overlay.of(context).insert(_overlayEntry!); - await roster.retryAssignments(retries, _progressNotifier); + candidateRosters = await assigner.retryAssignments( + doctors, + shifts, + retries, + _progressNotifier, + ); setState(() { - doctors = roster.doctors; - shifts = roster.shifts; + doctors = candidateRosters[0].doctors; + shifts = candidateRosters[0].shifts; }); _overlayEntry?.remove(); @@ -322,6 +324,38 @@ class RosterHomePageState extends State { ); } + Widget _buildCandidateRosterSelector() { + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + setState(() { + if (candidateRosterIndex > 0) { + candidateRosterIndex--; + doctors = candidateRosters[candidateRosterIndex].doctors; + shifts = candidateRosters[candidateRosterIndex].shifts; + } + }); + }, + ), + Text('Candidate ${candidateRosterIndex + 1}'), + IconButton( + icon: const Icon(Icons.arrow_forward), + onPressed: () { + setState(() { + if (candidateRosterIndex < candidateRosters.length - 1) { + candidateRosterIndex++; + doctors = candidateRosters[candidateRosterIndex].doctors; + shifts = candidateRosters[candidateRosterIndex].shifts; + } + }); + }, + ), + ], + ); + } + Widget _buildContent() { return AnimatedBuilder( animation: _sidebarController, @@ -391,6 +425,7 @@ class RosterHomePageState extends State { fontSize: 20.0, ), ), + _buildCandidateRosterSelector(), const SizedBox(height: 8.0), Expanded( child: RosterDisplay( @@ -476,7 +511,8 @@ class RosterHomePageState extends State { ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.download), - onPressed: () => roster.downloadAsCsv(context), + onPressed: () => + candidateRosters[candidateRosterIndex].downloadAsCsv(context), tooltip: 'Download as Spreadsheet (CSV)', ), ); diff --git a/test/models/roster_test.dart b/test/models/roster_test.dart index 04f9209..9fa81c6 100644 --- a/test/models/roster_test.dart +++ b/test/models/roster_test.dart @@ -9,8 +9,9 @@ import 'package:flutter/material.dart'; import 'package:file_selector/file_selector.dart'; import 'roster_test.mocks.dart'; -// class MockBuildContext extends Mock implements BuildContext {} +// class MockAssignmentGenerator extends Mock implements AssignmentGenerator {} +// dart run build_runner build --delete-conflicting-outputs @GenerateNiceMocks([ MockSpec(), MockSpec(), @@ -100,14 +101,20 @@ void main() { test('retryAssignments() calls assigner.retryAssignments()', () async { ValueNotifier progressNotifier = ValueNotifier(0); AssignmentGenerator assigner = MockAssignmentGenerator(); + when(assigner.retryAssignments(doctors, shifts, 3, progressNotifier, 1)) + .thenAnswer((_) async => [Roster(doctors: doctors, shifts: shifts)]); Roster roster = Roster( doctors: doctors, shifts: shifts, assigner: assigner, ); - await roster.retryAssignments(3, progressNotifier); - - verify(assigner.retryAssignments(roster, 3, progressNotifier)); + try { + roster.retryAssignments(3, progressNotifier); + } finally { + verify(assigner.retryAssignments( + doctors, shifts, 3, progressNotifier, 1)) + .called(1); + } }); test('Roster can be deeply copied', () { diff --git a/test/models/roster_test.mocks.dart b/test/models/roster_test.mocks.dart index dca7f27..3e9066a 100644 --- a/test/models/roster_test.mocks.dart +++ b/test/models/roster_test.mocks.dart @@ -6,16 +6,18 @@ import 'dart:async' as _i9; import 'package:file_selector_platform_interface/src/types/file_save_location.dart' - as _i5; + as _i6; import 'package:file_selector_platform_interface/src/types/x_type_group.dart' - as _i7; + as _i8; import 'package:flutter/foundation.dart' as _i3; import 'package:flutter/material.dart' as _i2; -import 'package:flutter/src/widgets/notification_listener.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i6; -import 'package:rostrix/models/assignment_generator.dart' as _i8; +import 'package:mockito/src/dummies.dart' as _i7; +import 'package:rostrix/models/assignment_generator.dart' as _i4; +import 'package:rostrix/models/doctor.dart' as _i11; import 'package:rostrix/models/roster.dart' as _i10; +import 'package:rostrix/models/shift.dart' as _i12; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -77,6 +79,17 @@ class _FakeDiagnosticsNode_2 extends _i1.SmartFake super.toString(); } +class _FakeAssignmentGenerator_3 extends _i1.SmartFake + implements _i4.AssignmentGenerator { + _FakeAssignmentGenerator_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [BuildContext]. /// /// See the documentation for Mockito's code generation for more information. @@ -157,7 +170,7 @@ class MockBuildContext extends _i1.Mock implements _i2.BuildContext { ); @override - void dispatchNotification(_i4.Notification? notification) => + void dispatchNotification(_i5.Notification? notification) => super.noSuchMethod( Invocation.method( #dispatchNotification, @@ -265,15 +278,15 @@ class MockBuildContext extends _i1.Mock implements _i2.BuildContext { /// /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable -class MockFileSaveLocation extends _i1.Mock implements _i5.FileSaveLocation { +class MockFileSaveLocation extends _i1.Mock implements _i6.FileSaveLocation { @override String get path => (super.noSuchMethod( Invocation.getter(#path), - returnValue: _i6.dummyValue( + returnValue: _i7.dummyValue( this, Invocation.getter(#path), ), - returnValueForMissingStub: _i6.dummyValue( + returnValueForMissingStub: _i7.dummyValue( this, Invocation.getter(#path), ), @@ -284,7 +297,7 @@ class MockFileSaveLocation extends _i1.Mock implements _i5.FileSaveLocation { /// /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable -class MockXTypeGroup extends _i1.Mock implements _i7.XTypeGroup { +class MockXTypeGroup extends _i1.Mock implements _i8.XTypeGroup { @override bool get allowsAny => (super.noSuchMethod( Invocation.getter(#allowsAny), @@ -307,7 +320,7 @@ class MockXTypeGroup extends _i1.Mock implements _i7.XTypeGroup { /// /// See the documentation for Mockito's code generation for more information. class MockAssignmentGenerator extends _i1.Mock - implements _i8.AssignmentGenerator { + implements _i4.AssignmentGenerator { @override Map get hoursPerShiftType => (super.noSuchMethod( Invocation.getter(#hoursPerShiftType), @@ -330,23 +343,39 @@ class MockAssignmentGenerator extends _i1.Mock ) as bool); @override - _i9.Future retryAssignments( - _i10.Roster? roster, + _i9.Future> retryAssignments( + List<_i11.Doctor>? doctors, + List<_i12.Shift>? shifts, int? retries, - _i3.ValueNotifier? progressNotifier, - ) => + _i3.ValueNotifier? progressNotifier, [ + int? outputs = 10, + ]) => (super.noSuchMethod( Invocation.method( #retryAssignments, [ - roster, + doctors, + shifts, retries, progressNotifier, + outputs, ], ), - returnValue: _i9.Future.value(false), - returnValueForMissingStub: _i9.Future.value(false), - ) as _i9.Future); + returnValue: _i9.Future>.value(<_i10.Roster>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i10.Roster>[]), + ) as _i9.Future>); + + @override + bool assignShiftsMultipleRosters(List<_i10.Roster>? rosters) => + (super.noSuchMethod( + Invocation.method( + #assignShiftsMultipleRosters, + [rosters], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override bool assignShifts(_i10.Roster? roster) => (super.noSuchMethod( @@ -357,4 +386,43 @@ class MockAssignmentGenerator extends _i1.Mock returnValue: false, returnValueForMissingStub: false, ) as bool); + + @override + bool isSameDate( + DateTime? date1, + DateTime? date2, + ) => + (super.noSuchMethod( + Invocation.method( + #isSameDate, + [ + date1, + date2, + ], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i4.AssignmentGenerator copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeAssignmentGenerator_3( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeAssignmentGenerator_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.AssignmentGenerator); }