diff --git a/src/NurseScheduling.jl b/src/NurseScheduling.jl index e59716a..82ae537 100644 --- a/src/NurseScheduling.jl +++ b/src/NurseScheduling.jl @@ -16,7 +16,8 @@ export Schedule, n_split_nbhd, perform_random_jumps!, get_shifts_distance, - Shifts + Shifts, + cmp_workers_worktime using JSON using SuperEnum @@ -27,9 +28,11 @@ include("schedule.jl") include("validation.jl") include("scoring.jl") include("neighborhood.jl") +include("comparator.jl") using .ScheduleValidation using .ScheduleScoring +using .ScheduleComparing using .NeighborhoodGen end # NurseSchedules diff --git a/src/comparator.jl b/src/comparator.jl new file mode 100644 index 0000000..0c89bae --- /dev/null +++ b/src/comparator.jl @@ -0,0 +1,117 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +module ScheduleComparing + +export cmp_workers_worktime + +using ..NurseSchedules: + Schedule, + get_shift_options, + get_month_info, + get_shift_length, + get_workers_info, + WEEK_DAYS_NO, + NUM_WORKING_DAYS, + MAX_OVERTIME, + MAX_UNDERTIME, + WORKTIME_DAILY, + W_ID, + SUNDAY_NO, + ErrorCode, + ScheduleShifts, + ScoringResult, + is_working, + get_hours_reduction + +""" + Evaluates current workers worktime restrictions based on a schedules from the beginning of the month and at the end + Args: + old_schedule::ScheduleShifts - array containing shcedule from the beginning of the month + new_schedule::ScheduleShifts - array containing shcedule from the end of the month + schedule::Schedule - Schedule metadata for a given month + Returns: + ScoringResult - Evaluated score with errors description + +""" +function cmp_workers_worktime( + old_schedule::ScheduleShifts, + new_schedule::ScheduleShifts, + schedule::Schedule +)::ScoringResult + shift_info = get_shift_options(schedule) + month_info = get_month_info(schedule) + workers_info = get_workers_info(schedule) + workers, old_shifts = old_schedule + _, new_shifts = new_schedule + + penalty = 0 + errors = Vector{Dict{String, Any}}() + + num_weeks = ceil(Int, size(old_shifts, 2) / WEEK_DAYS_NO) + num_days = num_weeks * NUM_WORKING_DAYS + + max_overtime = num_weeks * MAX_OVERTIME + max_undertime = num_weeks * MAX_UNDERTIME + + # Current assumption + # Reduce norm only when working shift is replaced by non-working + + holidays_no = length(filter( + day_no -> day_no % WEEK_DAYS_NO != SUNDAY_NO, + get(month_info, "holidays", Int[]) + )) + + for worker_no in axes(old_shifts, 1) + # Catch all hours reduction + negative_hours = sum( + map( + pos -> get_hours_reduction(shift_info[new_shifts[pos]]), + filter( + pos -> + is_working(shift_info[old_shifts[worker_no, pos]]) && + !is_working(shift_info[new_shifts[worker_no, pos]]) && + new_shifts[worker_no, pos] != W_ID, + keys(old_shifts[worker_no, :]) + ))) + + hours_per_day::Float32 = workers_info["time"][workers[worker_no]] * WORKTIME_DAILY + req_worktime = ((num_days - holidays_no) * hours_per_day) - negative_hours + act_worktime = sum(map(s -> get_shift_length(shift_info[s]), new_shifts[worker_no, :])) + + worktime = act_worktime - req_worktime + + if worktime > max_overtime + pen_diff = worktime - max_overtime + @debug "The worker '$(worker_no)' has too much overtime: '$(pen_diff)'" + push!( + errors, + Dict( + "code" => string(ErrorCode.WORKER_OVERTIME_HOURS), + "hours" => pen_diff, + "worker" => worker_no, + )) + penalty += pen_diff + elseif worktime < -max_undertime + pen_diff = -(worktime+max_undertime) + @debug "The worker '$(worker_no)' has too much undertime: '$(pen_diff)'" + push!( + errors, + Dict( + "code" => string(ErrorCode.WORKER_UNDERTIME_HOURS), + "hours" => pen_diff, + "worker" => worker_no, + )) + penalty += pen_diff + end + end + + if penalty > 0 + @debug "Total penalty from undertime and overtime: $(penalty)" + @debug "Max overtime hours: '$(max_overtime)'" + @debug "Max undertime hours: '$(max_undertime)" + end + ScoringResult((penalty, errors)) +end + +end # Module \ No newline at end of file diff --git a/src/constants.jl b/src/constants.jl index 5955e2a..e63e192 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -6,6 +6,7 @@ # Scoring ScoringResult = @NamedTuple{penalty::Int, errors::Vector{Dict{String,Any}}} ScoringResultOrPenalty = Union{ScoringResult,Int} + # Schedule related Workers = Vector{String} Shifts = Matrix{UInt8} @@ -52,6 +53,7 @@ const PERIOD_BEGIN = 7 # weekly worktime const WORKTIME_BASE = 40 +const DEFAULT_NORM_SUBSTRACTION = 8 const DAY_HOURS_NO = 24 const WEEK_DAYS_NO = 7 diff --git a/src/shifts.jl b/src/shifts.jl index d20207f..3db63cd 100644 --- a/src/shifts.jl +++ b/src/shifts.jl @@ -6,6 +6,12 @@ ShiftType = Dict{String, Any} +""" + Access operators +""" +@inline is_working(shift::ShiftType)::Bool = shift["is_working_shift"] +@inline get_hours_reduction(shift::ShiftType)::Int = get(shift, "norm_substraction", DEFAULT_NORM_SUBSTRACTION) + """ Computes wheter an hour is inside the shift Args: diff --git a/test/comparator.jl b/test/comparator.jl new file mode 100644 index 0000000..812c980 --- /dev/null +++ b/test/comparator.jl @@ -0,0 +1,41 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +include("../src/scoring.jl") + +using .ScheduleScoring: + ck_workers_worktime + +using .NurseSchedules: + cmp_workers_worktime + +function check(schedule, old, new; info=false) + l = cmp_workers_worktime(old, new, schedule) + r = ck_workers_worktime(new, schedule) + + if l.penalty != r.penalty || keys(l.errors) != keys(r.errors) + if info + @info "Different penalties: " l.penalty, r.penalty + @info "CMP \\ CK " setdiff(keys(l.errors), keys(r.errors)) + @info "CK \\ CMP " setdiff(keys(r.errors), keys(l.errors)) + end + false + else + if info + @info "Received score: " l.penalty + end + true + end +end + +@testset "Basic cmp_workers_worktime case" begin + base = Schedule("schedules/schedule_2016_august_medium.json") + working_type = base.shift_map["PN"] + + base_s = get_shifts(base) + working_s = (base_s[1], fill(working_type, size(base_s[2]))) + + @test check(base, base_s, base_s ) == false + @test check(base, working_s, base_s, info=true) == true +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 050836b..218b3e5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,3 +23,4 @@ using .NurseSchedules: include("engine_tests.jl") include("shifts.jl") include("schedule.jl") +include("comparator.jl")