weekdaze-0.0.0.1: src-lib/WeekDaze/ProblemConfiguration/ProblemParameters.hs
{-# LANGUAGE CPP, FlexibleContexts #-}
{-
Copyright (C) 2013-2016 Dr. Alistair Ward
This file is part of WeekDaze.
WeekDaze is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
WeekDaze is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with WeekDaze. If not, see <http://www.gnu.org/licenses/>.
-}
{- |
[@AUTHOR@] Dr. Alistair Ward
[@DESCRIPTION@] Encapsulates the data which defines the problem, rather than the solution-mechanism, or its presentation.
-}
module WeekDaze.ProblemConfiguration.ProblemParameters(
-- * Types
-- ** Data-types
ProblemParameters(..),
-- * Constants
-- tag,
timeslotIdBoundsTag,
-- groupTag,
-- locationTag,
-- studentBodyToProfileAssociationTag,
-- teacherTag,
-- minTag,
-- maxTag,
-- defaultGroupCatalogue,
-- * Functions
calculateNTimeslotsPerDay,
-- calculateStudentWorkloadBounds,
extractDistinctGroupMembership,
findExcessTotalWorkloadByStudentBody,
findHumanResourceIdsByGroupId,
-- findHumanResourcesOverloadedWithMeetings,
-- findSynchronousSeparateMandatoryMeetingsByTimeByHumanResourceId,
-- findSynchronousMeetingsByTimeByHumanResourceId,
findSynchronousMeetingsByTimeByStudentBodyMnemonic,
findSynchronousMeetingsByTimeByTeacherId,
-- findUnavailableDaysByGroupIdByHumanResourceId,
-- countNTimeslotsPerWeekForMeetings,
reduceStudentBodyRegister,
removeRedundantCourses,
removePointlessGroups,
removeUnsubscribedGroups,
mergeConstraintsOnSynchronisedCourses,
disableAnyValidationInappropriateForTemporaryStudentBodyMerger,
-- ** Predicates
hasAnyFreePeriodPreference,
hasVariousMinimumConsecutiveLessons
) where
import Control.Arrow((&&&), (***))
import Data.Map((!))
import Data.Set((\\))
import qualified Control.Arrow
import qualified Control.DeepSeq
import qualified Control.Monad.Writer
import qualified Data.Default
import qualified Data.Foldable
import qualified Data.List
import qualified Data.List.Extra
import qualified Data.Map
import qualified Data.Maybe
import qualified Data.Ord
import qualified Data.Set
import qualified Factory.Data.Interval
import qualified Text.XML.HXT.Arrow.Pickle as HXT
import qualified ToolShed.Data.Foldable
import qualified ToolShed.Data.List
import qualified ToolShed.Data.Pair
import qualified ToolShed.SelfValidate
import qualified WeekDaze.Aggregate.GroupCatalogue as Aggregate.GroupCatalogue
import qualified WeekDaze.Aggregate.LocationCatalogue as Aggregate.LocationCatalogue
import qualified WeekDaze.Aggregate.StudentBody as Aggregate.StudentBody
import qualified WeekDaze.Aggregate.StudentBodyRegister as Aggregate.StudentBodyRegister
import qualified WeekDaze.Aggregate.StudentClass as Aggregate.StudentClass
import qualified WeekDaze.Aggregate.TeacherRegister as Aggregate.TeacherRegister
import qualified WeekDaze.Configuration as Configuration
import qualified WeekDaze.Data.Course as Data.Course
import qualified WeekDaze.Data.Group as Data.Group
import qualified WeekDaze.Data.HumanResource as Data.HumanResource
import qualified WeekDaze.Data.Location as Data.Location
import qualified WeekDaze.Data.Requirements as Data.Requirements
import qualified WeekDaze.Data.Resource as Data.Resource
import qualified WeekDaze.Data.Student as Data.Student
import qualified WeekDaze.Data.Subject as Data.Subject
import qualified WeekDaze.Data.Teacher as Data.Teacher
import qualified WeekDaze.ProblemConfiguration.ProblemValidationSwitches as ProblemConfiguration.ProblemValidationSwitches
import qualified WeekDaze.ProblemConfiguration.TimetableValidationSwitches as ProblemConfiguration.TimetableValidationSwitches
import qualified WeekDaze.ProblemConfiguration.ValidationSwitch as ProblemConfiguration.ValidationSwitch
import qualified WeekDaze.Size as Size
import qualified WeekDaze.Temporal.Availability as Temporal.Availability
import qualified WeekDaze.Temporal.Day as Temporal.Day
import qualified WeekDaze.Temporal.FreePeriodPreference as Temporal.FreePeriodPreference
import qualified WeekDaze.Temporal.Time as Temporal.Time
import qualified WeekDaze.Temporal.TimeslotRequest as Temporal.TimeslotRequest
import qualified WeekDaze.Temporal.Workload as Temporal.Workload
import WeekDaze.Temporal.Workload((<+>))
#ifdef USE_HDBC
#ifdef QUERY_DB_CONCURRENTLY
import qualified Control.Concurrent
#endif
import qualified Control.Monad
import qualified Database.HDBC
import qualified Data.Convertible
import qualified Data.Typeable
import qualified WeekDaze.Database.Selector as Database.Selector
instance (
#ifdef QUERY_DB_CONCURRENTLY
Control.DeepSeq.NFData campus,
Control.DeepSeq.NFData level,
Control.DeepSeq.NFData locationId,
Control.DeepSeq.NFData stream,
Control.DeepSeq.NFData synchronisationId,
Control.DeepSeq.NFData teacherId,
Control.DeepSeq.NFData teachingRatio,
Control.DeepSeq.NFData timeslotId,
#endif
Data.Convertible.Convertible Database.HDBC.SqlValue campus, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue level, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue locationId, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue stream, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue synchronisationId, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue teacherId, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue teachingRatio, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue timeslotId, -- Flexible context.
Data.Default.Default campus,
Data.Default.Default stream,
Data.Typeable.Typeable teachingRatio,
Ord level,
Ord locationId,
Ord synchronisationId,
Ord teacherId,
Ord timeslotId,
RealFrac teachingRatio,
Show campus,
Show level,
Show locationId,
Show synchronisationId,
Show timeslotId
) => Database.Selector.Selector (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId) where
fromDatabase connection projectIdSql = let
timeslotIdBoundsTableName :: Database.Selector.TableName
timeslotIdBoundsTableName = Database.Selector.tablePrefix ++ timeslotIdBoundsTag
in do
(problemValidationSwitches, timetableValidationSwitches)
#ifdef QUERY_DB_CONCURRENTLY
<- Database.Selector.fromDatabaseConcurrently connection projectIdSql
#else
<- Database.Selector.fromDatabase connection projectIdSql
#endif
timeslotIdBoundsRows <- map (
map $ Data.Maybe.fromMaybe (
error $ "WeekDaze.ProblemConfiguration.ProblemParameters.fromDatabase:\tnull " ++ show minTag ++ " or " ++ show maxTag ++ "."
) . either (
error . showString "WeekDaze.ProblemConfiguration.ProblemParameters.fromDatabase:\tfailed to parse the value for a timeslotId read from the database; " . show
) id . Database.HDBC.safeFromSql
) `fmap` Database.Selector.select connection [minTag, maxTag] [timeslotIdBoundsTableName] [(Database.Selector.projectIdColumnName, projectIdSql)]
#ifdef QUERY_DB_CONCURRENTLY
l <- Control.Concurrent.newEmptyMVar
t <- Control.Concurrent.newEmptyMVar
s <- Control.Concurrent.newEmptyMVar
g <- Control.Concurrent.newEmptyMVar
Database.Selector.queryConcurrently Aggregate.LocationCatalogue.fromDatabase connection projectIdSql l
Database.Selector.queryConcurrently Aggregate.TeacherRegister.fromDatabase connection projectIdSql t
Database.Selector.queryConcurrently Aggregate.StudentBodyRegister.fromDatabase connection projectIdSql s
Database.Selector.queryConcurrently Aggregate.GroupCatalogue.fromDatabase connection projectIdSql g
locationCatalogue <- Control.Concurrent.takeMVar l
teacherRegister <- Control.Concurrent.takeMVar t
studentBodyRegister <- Control.Concurrent.takeMVar s
groupCatalogue <- Control.Concurrent.takeMVar g
#else
locationCatalogue <- Aggregate.LocationCatalogue.fromDatabase connection projectIdSql
teacherRegister <- Aggregate.TeacherRegister.fromDatabase connection projectIdSql
studentBodyRegister <- Aggregate.StudentBodyRegister.fromDatabase connection projectIdSql
groupCatalogue <- Aggregate.GroupCatalogue.fromDatabase connection projectIdSql
#endif
Control.Monad.when (null timeslotIdBoundsRows) . error . showString "WeekDaze.ProblemConfiguration.ProblemParameters.fromDatabase:\tzero rows were selected from table " . shows timeslotIdBoundsTableName . showString " where " . showString Database.Selector.projectIdColumnName . showChar '=' $ Database.HDBC.fromSql projectIdSql
let timeslotIdBoundsRow = head timeslotIdBoundsRows -- There can't be more than one if the database is correctly constrained.
case timeslotIdBoundsRow of
[minTimeslotId, maxTimeslotId] -> return {-to IO-Monad-} MkProblemParameters {
getProblemValidationSwitches = problemValidationSwitches,
getTimetableValidationSwitches = timetableValidationSwitches,
getTimeslotIdBounds = (minTimeslotId, maxTimeslotId),
getLocationCatalogue = locationCatalogue,
getTeacherRegister = teacherRegister,
getStudentBodyRegister = studentBodyRegister,
getGroupCatalogue = groupCatalogue
}
_ -> error . showString "WeekDaze.ProblemConfiguration.ProblemParameters.fromDatabase:\tunexpected number of columns=" . shows (length timeslotIdBoundsRow) . showString " in row of table " $ shows timeslotIdBoundsTableName "."
#endif /* USE_HDBC */
-- | Used to qualify XML.
tag :: String
tag = "problemParameters"
-- | Used to qualify XML.
timeslotIdBoundsTag :: String
timeslotIdBoundsTag = "timeslotIdBounds"
-- | Used to qualify XML.
groupTag :: String
groupTag = "group"
-- | Used to qualify XML.
locationTag :: String
locationTag = "location"
-- | Used to qualify XML.
studentBodyToProfileAssociationTag :: String
studentBodyToProfileAssociationTag = "studentBodyToProfileAssociation"
-- | Used to qualify XML.
teacherTag :: String
teacherTag = "teacher"
-- | Used to qualify SQL & XML.
minTag :: String
minTag = "min"
-- | Used to qualify SQL & XML.
maxTag :: String
maxTag = "max"
-- | Default value for a 'getGroupCatalogue'.
defaultGroupCatalogue :: Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId
defaultGroupCatalogue = Data.Map.empty
-- | Encapsulates the data which defines the problem.
data ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId = MkProblemParameters {
getProblemValidationSwitches :: ProblemConfiguration.ProblemValidationSwitches.ProblemValidationSwitches, -- ^ Defines toggle-switches governing validity-checks.
getTimetableValidationSwitches :: ProblemConfiguration.TimetableValidationSwitches.TimetableValidationSwitches, -- ^ Defines toggle-switches governing validity-checks.
getTimeslotIdBounds :: Factory.Data.Interval.Interval timeslotId, -- ^ The closed bounds, defining the /timeslot-id/s, in any /day/.
getLocationCatalogue :: Aggregate.LocationCatalogue.LocationCatalogue locationId campus, -- ^ Defines the profiles of available /location/s, indexed by /location-id/.
getTeacherRegister :: Aggregate.TeacherRegister.TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio, -- ^ Defines the profiles of /teacher/s, indexed by /teacher-id/.
getStudentBodyRegister :: Aggregate.StudentBodyRegister.StudentBodyRegister level stream teachingRatio, -- ^ Defines the common profile of /student/s, indexed by the mnemonic of the /student-body/ of which they're all members.
getGroupCatalogue :: Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId -- ^ Defines the /group/s of which any /student/ or /teacher/ may be a member.
} deriving (Eq, Show)
instance (
Data.Default.Default campus,
Data.Default.Default stream,
Eq campus,
Eq stream,
HXT.XmlPickler campus,
HXT.XmlPickler level,
HXT.XmlPickler locationId,
HXT.XmlPickler stream,
HXT.XmlPickler synchronisationId,
HXT.XmlPickler teacherId,
HXT.XmlPickler teachingRatio,
HXT.XmlPickler timeslotId,
Ord level,
Ord locationId,
Ord synchronisationId,
Ord teacherId,
Ord timeslotId,
Real teachingRatio,
Show campus,
Show level,
Show synchronisationId,
Show timeslotId
) => HXT.XmlPickler (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId) where
xpickle = HXT.xpElem tag . HXT.xpWrap (
\(a, b, c, d, e, f, g) -> MkProblemParameters a b c d e f g, -- Construct from a tuple.
\MkProblemParameters {
getProblemValidationSwitches = problemValidationSwitches,
getTimetableValidationSwitches = timetableValidationSwitches,
getTimeslotIdBounds = timeslotIdBounds,
getLocationCatalogue = locationCatalogue,
getTeacherRegister = teacherRegister,
getStudentBodyRegister = studentBodyRegister,
getGroupCatalogue = groupCatalogue
} -> (
problemValidationSwitches,
timetableValidationSwitches,
timeslotIdBounds,
locationCatalogue,
teacherRegister,
studentBodyRegister,
groupCatalogue
) -- Deconstruct to a tuple.
) $ HXT.xp7Tuple (
HXT.xpDefault Data.Default.def HXT.xpickle {-ProblemValidationSwitches-}
) (
HXT.xpDefault Data.Default.def HXT.xpickle {-TimetableValidationSwitches-}
) (
HXT.xpElem timeslotIdBoundsTag $ HXT.xpElem minTag HXT.xpickle `HXT.xpPair` HXT.xpElem maxTag HXT.xpickle
) (
HXT.xpElem Aggregate.LocationCatalogue.tag . HXT.xpWrap (
Data.Map.fromList, -- Construct from an association-list.
Data.Map.toList -- Deconstruct to an association-list.
) . HXT.xpList1 {-can't be null-} $ HXT.xpElem locationTag HXT.xpickle {-Pair-}
) (
HXT.xpElem Aggregate.TeacherRegister.tag . HXT.xpWrap (
Data.Map.fromList, -- Construct from an association-list.
Data.Map.toList -- Deconstruct to an association-list.
) . HXT.xpList1 {-can't be null-} $ HXT.xpElem teacherTag HXT.xpickle {-Pair-}
) (
HXT.xpElem Aggregate.StudentBodyRegister.tag . HXT.xpWrap (
Data.Map.fromList, -- Construct from an association-list.
Data.Map.toList -- Deconstruct to an association-list.
) . HXT.xpList1 {-can't be null-} $ HXT.xpElem studentBodyToProfileAssociationTag HXT.xpickle {-Pair-}
) (
HXT.xpDefault defaultGroupCatalogue . HXT.xpElem Aggregate.GroupCatalogue.tag $ HXT.xpMap groupTag Data.Group.groupIdTag HXT.xpText {-can't be null-} HXT.xpickle {-Profile-}
)
instance (
Control.DeepSeq.NFData campus,
Control.DeepSeq.NFData level,
Control.DeepSeq.NFData locationId,
Control.DeepSeq.NFData stream,
Control.DeepSeq.NFData synchronisationId,
Control.DeepSeq.NFData teacherId,
Control.DeepSeq.NFData teachingRatio,
Control.DeepSeq.NFData timeslotId
) => Control.DeepSeq.NFData (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId) where
rnf (MkProblemParameters x0 x1 x2 x3 x4 x5 x6) = Control.DeepSeq.rnf (x0, x1, x2, x3, x4, x5, x6)
{- |
* Get the length of the list defined by the specified bounds.
* CAVEAT: this implementation accounts for the potential fence-post error when called for a closed interval of 'Integral' timeslots,
but will result in an equivalent error if (bizarrely) called for an interval of 'Fractional' quantities.
Regrettably, it can't be restricted to 'Integral', because it may legitimately be required to work with 'Char'.
-}
calculateNTimeslotsPerDay :: Enum timeslotId => Factory.Data.Interval.Interval timeslotId -> Size.NTimeslots
calculateNTimeslotsPerDay = succ . uncurry (flip (-)) . (fromEnum *** fromEnum)
{- |
* Total the workload of the core & optional /subject-requirements/, for the specified /student-profile/.
* Since each /course/ may be offered by more than one /teacher/, the actual workload can only be constrained within bounds.
* CAVEAT: assumes that all requested /subject/s, exist amongst the /course/s offered by /teacher/s.
-}
calculateStudentWorkloadBounds :: (
Ord level,
Ord synchronisationId,
Ord timeslotId
)
=> Aggregate.TeacherRegister.TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio
-> Data.Student.Profile level stream teachingRatio
-> Data.Requirements.Requirements Temporal.Workload.Bounds
calculateStudentWorkloadBounds teacherRegister = ToolShed.Data.Pair.mirror (
Data.Foldable.foldr (
\subject -> ((Aggregate.TeacherRegister.calculateWorkloadBoundsBySubject teacherRegister ! subject) <+>)
) Temporal.Workload.unloaded
) . Data.Student.getKnowledgeRequirements
-- | Finds /student-bodies/, who've requested more /subject/s (both core & optional), than can be taught by the /course/s on offer, in the time allocated to tuition.
findExcessTotalWorkloadByStudentBody :: (
Enum timeslotId,
Ord level,
Ord synchronisationId,
Ord timeslotId,
RealFrac teachingRatio
) => ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Data.Map.Map Aggregate.StudentBody.StudentBody Size.NTimeslots
findExcessTotalWorkloadByStudentBody problemParameters = Data.Map.filter (> 0) $ Data.Map.map (
uncurry (-) . (
(
Temporal.Workload.getMinimum {-select the shortest courses offered-} . uncurry (<+>) -- Add the workloads associated with core & optional subject-requirements.
) . calculateStudentWorkloadBounds (
getTeacherRegister problemParameters
) &&& Data.HumanResource.getNTimeslotsPerWeekOfTeaching (
calculateNTimeslotsPerDay $ getTimeslotIdBounds problemParameters
)
)
) $ getStudentBodyRegister problemParameters
-- | Finds the set of /group/s, of which a /human-resource/ claims membership.
extractDistinctGroupMembership :: RealFrac teachingRatio => ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Data.Group.Membership
extractDistinctGroupMembership = uncurry Data.Set.union . (Data.HumanResource.extractDistinctGroupMembership . getStudentBodyRegister &&& Data.HumanResource.extractDistinctGroupMembership . getTeacherRegister)
{- |
* For each /human-resource/, identifies /time/s at which they're required to attend more than one meeting, in separate /location/s,
for any of the /group/s of which they're a member.
* Also returns the offending /group-id/s & /location-id/s.
-}
findSynchronousSeparateMandatoryMeetingsByTimeByHumanResourceId :: (
Data.HumanResource.HumanResource humanResource,
Ord locationId,
Ord timeslotId
)
=> Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId
-> Data.Resource.ResourceMap humanResourceId humanResource
-> Data.Map.Map humanResourceId (Data.Map.Map (Temporal.Time.Time timeslotId) [(Data.Group.Id, locationId)])
findSynchronousSeparateMandatoryMeetingsByTimeByHumanResourceId groupCatalogue = Data.Map.filter (
not . Data.Map.null -- Leave only those human-resources with a scheduling-conflict.
) . Data.Map.map (
Data.Map.filter (
(> 1) . length . Data.List.Extra.groupSortOn snd {-locationId-} -- More than one location specified for this human-resource & time.
) . Data.Map.map (
Data.Map.toList . Data.Map.map (
Data.Maybe.fromJust . fst {-locationId-} -- Reduce to 'Map groupId locationId'.
) . Data.Map.filter (
uncurry (&&) . Control.Arrow.first {-Maybe locationId-} Data.Maybe.isJust -- Ensure there's both a defined location & that attendance is mandated.
) . Data.Map.map (
Data.Group.getMaybeLocationId &&& Data.Group.getMandatesAttendance -- Construct 'Map groupId (maybe locationId, mandatesAttendance)'.
) . Data.Map.fromList {-Construct 'Map groupId groupProfile'-}
) . Data.Foldable.foldr (
\groupId m -> let
groupProfile = groupCatalogue ! groupId
in Data.Foldable.foldr (
Data.Map.insertWith (++) `flip` [(groupId, groupProfile)]
) m $ Data.Group.getMeetingTimes groupProfile
) Data.Map.empty . Data.HumanResource.getGroupMembership
)
{- |
* For each /human-resource/, identifies /time/s at which they have attend more than one meeting.
for any of the /group/s of which they're a member.
* Returns the offending /group-id/s.
-}
findSynchronousMeetingsByTimeByHumanResourceId :: (
Data.HumanResource.HumanResource humanResource,
Ord timeslotId
)
=> Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId
-> Data.Resource.ResourceMap humanResourceId humanResource
-> Data.Map.Map humanResourceId (Data.Map.Map (Temporal.Time.Time timeslotId) [Data.Group.Id])
findSynchronousMeetingsByTimeByHumanResourceId groupCatalogue = Data.Map.filter (
not . Data.Map.null -- Leave only those human-resources with a scheduling-conflict.
) . Data.Map.map (
Data.Map.filter (
(> 1) . length -- Select human-resources with more than one meeting @ this time.
) . Data.Foldable.foldr (
\groupId m -> Data.Foldable.foldr (
Data.Map.insertWith (++) `flip` [groupId]
) m . Data.Group.getMeetingTimes $ groupCatalogue ! groupId
) Data.Map.empty . Data.HumanResource.getGroupMembership
)
{- |
* For each /student-body/, identifies /time/s at which they have attend more than one meeting.
for any of the /group/s of which they're a member.
* Returns the offending /group-id/s.
-}
findSynchronousMeetingsByTimeByStudentBodyMnemonic :: (
Ord timeslotId,
RealFrac teachingRatio
)
=> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Data.Map.Map Aggregate.StudentBody.StudentBody (Data.Map.Map (Temporal.Time.Time timeslotId) [Data.Group.Id])
findSynchronousMeetingsByTimeByStudentBodyMnemonic = uncurry findSynchronousMeetingsByTimeByHumanResourceId . (getGroupCatalogue &&& getStudentBodyRegister)
{- |
* For each /teacherId/, identifies /time/s at which they have attend more than one meeting.
for any of the /group/s of which they're a member.
* Returns the offending /group-id/s.
-}
findSynchronousMeetingsByTimeByTeacherId :: (
Ord timeslotId,
RealFrac teachingRatio
)
=> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Data.Map.Map teacherId (Data.Map.Map (Temporal.Time.Time timeslotId) [Data.Group.Id])
findSynchronousMeetingsByTimeByTeacherId = uncurry findSynchronousMeetingsByTimeByHumanResourceId . (getGroupCatalogue &&& getTeacherRegister)
-- | Identifies those /human-resource/s commited to more /meeting/s than their non-teaching time can accommodate.
findHumanResourcesOverloadedWithMeetings :: (
Data.Resource.Resource humanResource,
Data.HumanResource.HumanResource humanResource
)
=> Size.NTimeslots
-> Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId
-> Data.Resource.ResourceMap humanResourceId humanResource
-> Data.Map.Map humanResourceId (Size.NTimeslots, Size.NTimeslots)
findHumanResourcesOverloadedWithMeetings nTimeslotsPerDay groupCatalogue = Data.Map.filter (
uncurry (>)
) . Data.Map.map (
countNTimeslotsPerWeekForMeetings groupCatalogue &&& Data.HumanResource.getNTimeslotsPerWeekOfNonTeaching nTimeslotsPerDay
)
{- |
* Identifies for each /human-resource/, any /day/s on which they're unavailable for the meetings of the mandatory-attendance /group/s, of which they're members.
* CAVEAT: only /human-resource/s unable to attend a meeting, appear in the results.
-}
findUnavailableDaysByGroupIdByHumanResourceId :: (
#if !MIN_VERSION_containers(0,5,2)
Ord timeslotId, -- Not required after "Data.Set-7.8.1".
#endif
Data.Resource.Resource humanResource,
Data.HumanResource.HumanResource humanResource
)
=> Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId
-> Data.Resource.ResourceMap humanResourceId humanResource
-> Data.Map.Map humanResourceId (Data.Map.Map Data.Group.Id (Data.Set.Set Temporal.Day.Day))
findUnavailableDaysByGroupIdByHumanResourceId groupCatalogue = Data.Map.filter (
not . Data.Map.null -- Purge humanResourceIds, who can attend all meetings of their mandatory-attendance groups.
) . Data.Map.map (
\humanResource -> Data.Map.filter (
not . Data.Set.null -- Purge those groupIds, where the human-resource is fully available for all mandatory meetings.
) . Data.Map.map (
Data.Set.filter (
not . (`Data.Resource.isAvailableOn` humanResource)
) . Data.Set.map Temporal.Time.getDay . Data.Group.getMeetingTimes
) . Data.Map.filter Data.Group.getMandatesAttendance . Data.Map.fromList . map (
id &&& (groupCatalogue !) -- Find the group-association.
) . Data.Set.toList $ Data.HumanResource.getGroupMembership humanResource
)
-- | Finds the members (/student-bodies/, /teacher/s) of each /group/.
findHumanResourceIdsByGroupId :: (Ord teacherId, RealFrac teachingRatio)
=> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> (Aggregate.GroupCatalogue.ResourceIdsByGroupId Aggregate.StudentBody.StudentBody, Aggregate.GroupCatalogue.ResourceIdsByGroupId teacherId)
findHumanResourceIdsByGroupId = findHumanResourceIdsByGroupId' . getStudentBodyRegister &&& findHumanResourceIdsByGroupId' . getTeacherRegister where
findHumanResourceIdsByGroupId' :: (Data.HumanResource.HumanResource humanResource, Ord humanResourceId) => Data.Resource.ResourceMap humanResourceId humanResource -> Aggregate.GroupCatalogue.ResourceIdsByGroupId humanResourceId
findHumanResourceIdsByGroupId' = Data.Map.foldrWithKey (
\humanResourceId humanResource m -> Data.Foldable.foldr (
($ Data.Set.singleton humanResourceId) . Data.Map.insertWith Data.Set.union
) m $ Data.HumanResource.getGroupMembership humanResource
) Data.Map.empty
{- |
* The number of /time-slot/s per week, which this /human-resource/ regularly requires, for the meetings of all /groups/ of which they're a member.
* CAVEAT: days on which the /resource/ is unavailable, aren't counted on the assumption that this is because attendance for that group isn't mandatory.
-}
countNTimeslotsPerWeekForMeetings :: (
Data.Resource.Resource humanResource,
Data.HumanResource.HumanResource humanResource
)
=> Aggregate.GroupCatalogue.GroupCatalogue timeslotId locationId
-> humanResource
-> Size.NTimeslots
countNTimeslotsPerWeekForMeetings groupCatalogue humanResource = Data.Foldable.foldr (
(+) . Data.Set.size . Data.Set.filter (
(`Data.Resource.isAvailableOn` humanResource) . Temporal.Time.getDay
) . Data.Group.getMeetingTimes . (
groupCatalogue !
)
) 0 $ Data.HumanResource.getGroupMembership humanResource
instance (
Enum timeslotId,
Ord level,
Ord locationId,
Ord stream,
Ord synchronisationId,
Ord teacherId,
Ord timeslotId,
RealFrac teachingRatio,
Show level,
Show locationId,
Show stream,
Show synchronisationId,
Show teacherId,
Show teachingRatio,
Show timeslotId
) => ToolShed.SelfValidate.SelfValidator (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId) where
getErrors problemParameters@MkProblemParameters {
getProblemValidationSwitches = problemValidationSwitches,
getTimeslotIdBounds = timeslotIdBounds,
getLocationCatalogue = locationCatalogue,
getGroupCatalogue = groupCatalogue,
getTeacherRegister = teacherRegister,
getStudentBodyRegister = studentBodyRegister
} = ToolShed.SelfValidate.extractErrors [
-- Check 'getTimeslotIdBounds'.
(
ProblemConfiguration.ProblemValidationSwitches.getCheckTimeslotIdBounds problemValidationSwitches && nTimeslotsPerDay < 1,
show timeslotIdBoundsTag ++ " are too narrow; " ++ show timeslotIdBounds
),
-- Check that resource-maps have been defined.
(
ProblemConfiguration.ProblemValidationSwitches.getCheckNullLocationCatalogue problemValidationSwitches && Data.Map.null locationCatalogue,
show ProblemConfiguration.ProblemValidationSwitches.checkNullLocationCatalogueTag ++ ": zero locations have been defined"
), (
ProblemConfiguration.ProblemValidationSwitches.getCheckNullTeacherRegister problemValidationSwitches && Data.Map.null teacherRegister,
show ProblemConfiguration.ProblemValidationSwitches.checkNullTeacherRegisterTag ++ ": zero teachers have been defined"
), (
ProblemConfiguration.ProblemValidationSwitches.getCheckNullStudentBodyRegister problemValidationSwitches && Data.Map.null studentBodyRegister,
show ProblemConfiguration.ProblemValidationSwitches.checkNullStudentBodyRegisterTag ++ ": zero student-bodies have been defined"
), (
ProblemConfiguration.ProblemValidationSwitches.getCheckNullGroupId problemValidationSwitches && Data.Map.member "" groupCatalogue,
show ProblemConfiguration.ProblemValidationSwitches.checkNullGroupIdTag ++ ": null identifier in " ++ show Aggregate.GroupCatalogue.tag
),
-- Check course-definitions.
let
-- isValid :: timeslotId -> Bool
isValid = (`Factory.Data.Interval.elem'` timeslotIdBounds)
coursesWithIllDefinedTimeslotRequests = Data.Set.filter (
\course -> case Data.Course.getTimeslotRequest course of
Temporal.TimeslotRequest.Ideally idealTimeslotId -> not $ isValid idealTimeslotId
Temporal.TimeslotRequest.Specifically specifiedTimes -> Data.Foldable.any (not . isValid . Temporal.Time.getTimeslotId) specifiedTimes
) $ Aggregate.TeacherRegister.extractDistinctCourses teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckTimeslotRequests problemValidationSwitches && not (Data.Set.null coursesWithIllDefinedTimeslotRequests),
show ProblemConfiguration.ProblemValidationSwitches.checkTimeslotRequestsTag ++ ": the timeslot-request for a course, specifies times outside permissible bounds " ++ show timeslotIdBounds ++ "; " ++ show (Data.Set.toList coursesWithIllDefinedTimeslotRequests)
),
let
coursesWithExcessiveMinimumConsecutiveDays = Data.Set.filter (
(> nTimeslotsPerDay) . Data.Course.getMinimumConsecutiveLessons
) $ Aggregate.TeacherRegister.extractDistinctCourses teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckMinimumConsecutiveLessons problemValidationSwitches && not (Data.Set.null coursesWithExcessiveMinimumConsecutiveDays),
show ProblemConfiguration.ProblemValidationSwitches.checkMinimumConsecutiveLessonsTag ++ ": the minimum consecutive lessons for a course, can't exceed the number of time-slots per day " ++ show nTimeslotsPerDay ++ "; " ++ show (Data.Set.toList coursesWithExcessiveMinimumConsecutiveDays)
),
let
excessiveLessonsPerWeekByTeacherId = Data.Map.filter (
not . null
) $ Data.Map.map (
\teacherProfile -> filter (
> Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay teacherProfile
) . map Data.Course.getRequiredLessonsPerWeek . Data.Set.toList $ Data.Teacher.getService teacherProfile
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckRequiredLessonsPerWeek problemValidationSwitches && not (Data.Map.null excessiveLessonsPerWeekByTeacherId),
show ProblemConfiguration.ProblemValidationSwitches.checkRequiredLessonsPerWeekTag ++ ": some teachers offer courses which individually require more lessons per week than they teach; " ++ show (Data.Map.toList excessiveLessonsPerWeekByTeacherId) -- This could be checked in module "Data.Teacher", but the teacherId isn't available there.
),
let
teachersOfferingMultipleCoursesPerSynchronisationId = Data.Map.filter (not . null) $ Data.Map.map (
map head . filter (
(/= 1) . length -- Select those teachers referencing a synchronisationId from different courses.
) . ToolShed.Data.Foldable.gather . Data.Maybe.mapMaybe Data.Course.getMaybeSynchronisationId . Data.Set.toList . Data.Teacher.getService
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForMultipleCoursesPerTeacherPerSynchronisationId problemValidationSwitches && not (Data.Map.null teachersOfferingMultipleCoursesPerSynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForMultipleCoursesPerTeacherPerSynchronisationIdTag ++ ": some teachers offer multiple courses with the same synchronisationId; " ++ show (Data.Map.toList teachersOfferingMultipleCoursesPerSynchronisationId)
),
let
singletonTeachersBySynchronisationId = Data.Map.map head . Data.Map.filter ((== 1) . length) $ Data.Map.map Data.Map.keys coursesByTeacherIdBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSingletonSynchronisedCourses problemValidationSwitches && not (Data.Map.null singletonTeachersBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSingletonSynchronisedCoursesTag ++ ": some synchronisationIds, are only referenced by the relevant courses of a single teacher; " ++ show (Data.Map.toList singletonTeachersBySynchronisationId)
),
let
differentLessonsPerWeekByTeacherIdBySynchronisationId = Data.Map.filter (
(> 1) . Data.Set.size . Data.Set.fromList . Data.Map.elems -- Select those synchronised courses which required more than one distinct number of lessons per week.
) $ Data.Map.map (
Data.Map.map Data.Course.getRequiredLessonsPerWeek
) coursesByTeacherIdBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithDifferentLessonsPerWeek problemValidationSwitches && not (Data.Map.null differentLessonsPerWeekByTeacherIdBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithDifferentLessonsPerWeekTag ++ ": some synchronised courses, require different numbers of lessons per week; " ++ show (Data.Map.toList $ Data.Map.map Data.Map.toList differentLessonsPerWeekByTeacherIdBySynchronisationId)
),
let
differentIdealTimeslotIdsBySynchronisationId = Data.Map.filter (
(> 1) . Data.Set.size -- Select those synchronised courses which specify more than one distinct ideal timeslotId.
) $ Data.Map.map (
Data.Set.map Data.Maybe.fromJust . Data.Set.filter Data.Maybe.isJust . Data.Set.map (
Temporal.TimeslotRequest.getMaybeIdealTimeslotId . Data.Course.getTimeslotRequest
)
) distinctCoursesBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithDifferentIdealTimeslots problemValidationSwitches && not (Data.Map.null differentIdealTimeslotIdsBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithDifferentIdealTimeslotsTag ++ ": the members of some sets of synchronised courses, have different ideal timeslot-identifiers; " ++ show (Data.Map.toList $ Data.Map.map Data.Set.toList differentIdealTimeslotIdsBySynchronisationId)
),
let
excessSpecifiedTimesBySynchronisationId = Data.Map.filter (
uncurry (<) -- Select those synchronised courses which specify too many booking-times.
) $ Data.Map.map (
(
minimum {-requiredLessonsPerWeek-} *** Data.Set.size . Data.Set.unions {-number of distinct specified times-}
) . unzip . Data.Set.toList . Data.Set.map (
Data.Course.getRequiredLessonsPerWeek &&& Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest
) {-typically reduces the set to a singleton-}
) distinctCoursesBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithExcessSpecifiedTimes problemValidationSwitches && not (Data.Map.null excessSpecifiedTimesBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithExcessSpecifiedTimesTag ++ ": some synchronised courses require fewer lessons per week, than they have specified booking-times; " ++ show (Data.Map.toList excessSpecifiedTimesBySynchronisationId)
),
let
availableVsRequiredLessonsBySynchronisationId = Data.Map.filter (
uncurry (<)
) . Data.Map.map (
Control.Arrow.first $ (* nTimeslotsPerDay) . Temporal.Availability.countDaysPerWeekAvailable
) . Data.Map.unionWith (
\(availability, requiredLessons) -> Temporal.Availability.findIntersection availability *** max requiredLessons {-they should all be the same-}
) (
Data.Map.mapWithKey (
\synchronisationId -> (,) (
Data.Resource.getAvailability $ Data.Map.filter (
Data.Student.requiresAnySubjectBy (
`Data.Set.member` Data.Set.map Data.Course.getSubject (
distinctCoursesBySynchronisationId ! synchronisationId
)
)
) studentBodyRegister
) . Data.Set.findMax {-they should all be the same-} . Data.Set.map Data.Course.getRequiredLessonsPerWeek
) distinctCoursesBySynchronisationId
) $ Data.Map.map (
Data.Resource.getAvailability . map (
teacherRegister !
) . Data.Map.keys {-teacherIds-} &&& Data.Foldable.maximum {-they should all be the same-} . Data.Map.map Data.Course.getRequiredLessonsPerWeek
) coursesByTeacherIdBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithExcessLessonsPerWeek problemValidationSwitches && not (Data.Map.null availableVsRequiredLessonsBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithExcessLessonsPerWeekTag ++ ": the number of time-slots when the interested student-bodies & required teachers, are simultaneously available, is fewer than the required lessons of some synchronised courses; " ++ show (Data.Map.toList availableVsRequiredLessonsBySynchronisationId)
),
let
unavailableSpecifiedDaysByTeacherIdBySynchronisationId = Data.Map.filter (
not . Data.Map.null -- Select the synchronisationId of courses taught by one or more unavailable teachers.
) $ Data.Map.map (
Data.Map.filter (
not . Data.Set.null -- Select teachers who're unavailable on one or more days.
) . Data.Map.mapWithKey (
\teacherId -> Data.Set.filter (
not . (
`Data.Resource.isAvailableOn` (teacherRegister ! teacherId)
)
) . Data.Set.map Temporal.Time.getDay . Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest
)
) coursesByTeacherIdBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithUnavailableSpecifiedDays problemValidationSwitches && not (Data.Map.null unavailableSpecifiedDaysByTeacherIdBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithUnavailableSpecifiedDaysTag ++ ": some synchronised courses, specify booking-times on days when not all the required teachers are available; " ++ show (
Data.Map.toList $ Data.Map.map (
Data.Map.toList . Data.Map.map Data.Set.toList
) unavailableSpecifiedDaysByTeacherIdBySynchronisationId
)
),
let
unavailableStudentBodiesBySpecifiedDaysBySynchronisationId = Data.Map.filter (
not . Data.Map.null -- Select the synchronisationId of courses required on one or more unavailable days.
) $ Data.Map.mapWithKey (
\synchronisationId -> Data.Map.filter (
not . null -- Select the days on which one or more student-bodies are unavailable.
) . Data.Map.fromList . map (
\day -> (
day,
Data.Map.keys $ Data.Map.filter (
uncurry (&&) . (
not . Data.Resource.isAvailableOn day &&& Data.Student.requiresAnySubjectBy (
`Data.Set.member` Data.Set.map Data.Course.getSubject (
distinctCoursesBySynchronisationId ! synchronisationId
)
)
)
) studentBodyRegister
)
) . Data.Set.toList . Data.Set.unions . map (
Data.Set.map Temporal.Time.getDay . Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest
) . Data.Set.toList
) distinctCoursesBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithUnavailableSpecifiedDays problemValidationSwitches && not (Data.Map.null unavailableStudentBodiesBySpecifiedDaysBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithUnavailableSpecifiedDaysTag ++ ": some synchronised courses, are requested by student-bodies who're unavailable on days when booking-times have been specified; " ++ show (
Data.Map.toList $ Data.Map.map (
Data.Map.toList . Data.Map.map (map Aggregate.StudentBody.getMnemonic)
) unavailableStudentBodiesBySpecifiedDaysBySynchronisationId
)
),
let
excessTimeslotRequestsBySynchronisationId = Data.Map.filter (
uncurry (&&) . (not . null *** not . null) -- Select synchronisationIds whose courses define both ideal timeslots & specify times.
) $ Data.Map.map (
(
ToolShed.Data.List.nub' . Data.Maybe.catMaybes *** Data.Set.toList . Data.Set.unions
) . unzip . Data.Set.toList . Data.Set.map (
(
Temporal.TimeslotRequest.getMaybeIdealTimeslotId &&& Temporal.TimeslotRequest.getSpecifiedTimes
) . Data.Course.getTimeslotRequest
)
) distinctCoursesBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithExcessTimeslotRequests problemValidationSwitches && not (Data.Map.null excessTimeslotRequestsBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithExcessTimeslotRequestsTag ++ ": some synchronised courses, define both ideal timeslot-Ids & specify booking-times; " ++ show (Data.Map.toList excessTimeslotRequestsBySynchronisationId)
),
let
subjectsByStudentBodyBySynchronisationId = Data.Map.filter (
not . Data.Map.null -- Select synchronisationIds, where there's an issue with some student-bodies.
) $ Data.Map.mapWithKey (
\synchronisationId -> Data.Map.filter (
(> 1) . Data.Set.size -- Select student-bodies requiring more than one subject from this set of synchronised courses.
) . Data.Map.map (
Data.Set.filter (
\subject -> not $ Data.Foldable.any (
Data.Foldable.any (
uncurry (&&) . (
(/= Just synchronisationId) . Data.Course.getMaybeSynchronisationId &&& (== subject) . Data.Course.getSubject
)
) . Data.Teacher.getService
) teacherRegister -- Check for any teachers offering this subject outside of this set of synchronised courses.
)
) . Data.Map.unionsWith Data.Set.union . map (
\synchronisedCourse -> Data.Map.filter (
not . Data.Set.null -- Select student-bodies requiring this course's subject; there shouldn't be more than one.
) $ Data.Map.map (
Data.Set.filter (== Data.Course.getSubject synchronisedCourse) . Data.Student.deriveAmalgamatedKnowledgeRequirement
) studentBodyRegister
) . Data.Set.toList
) distinctCoursesBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForStudentsRequiringMultipleSynchronisedSubjects problemValidationSwitches && not (Data.Map.null subjectsByStudentBodyBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForStudentsRequiringMultipleSynchronisedSubjectsTag ++ ": some student-bodies require multiple subjects only offered within one set of synchronised courses; " ++ show (
Data.Map.toList $ Data.Map.map (
map (Aggregate.StudentBody.getMnemonic *** Data.Set.toList) . Data.Map.toList
) subjectsByStudentBodyBySynchronisationId
)
),
let
duplicateSubjectsByTeacherIdBySynchronisationId = Data.Map.filter (
not . Data.Set.null -- Select synchronisedCourseIds with one or more alternatives.
) $ Data.Map.map (
Data.Set.filter (
not . Data.Map.null -- Select those members of this synchronised course, for which there're one or more alternative teachers.
) . Data.Set.map (
\synchronisedCourse -> Data.Map.map Data.Set.findMin {-a teacher can't offer same subject more than once-} . Data.Map.filter (
not . Data.Set.null -- Select teachers offering courses in the same subject as the current synchronised one.
) $ Data.Map.map (
Data.Set.filter (
== Data.Course.getSubject synchronisedCourse
) . Data.Set.map Data.Course.getSubject . Data.Set.filter (
/= synchronisedCourse -- Except self. TODO should teachers be permitted to offer similar subjects with the same synchronisationId ?
) . Data.Teacher.getService
) teacherRegister
) -- Map over one set of synchronised courses.
) distinctCoursesBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForAlternativesToSynchronisedCourses problemValidationSwitches && not (Data.Map.null duplicateSubjectsByTeacherIdBySynchronisationId),
show ProblemConfiguration.ProblemValidationSwitches.checkForAlternativesToSynchronisedCoursesTag ++ ": courses are offered with the same subject as synchronised ones, potentially inhibiting migration of students; " ++ show (
Data.Map.toList $ Data.Map.map (Data.Foldable.concatMap Data.Map.toList) duplicateSubjectsByTeacherIdBySynchronisationId
)
),
let
synchronisationIdsWithoutSuitableLocations = Data.Map.keys $ Data.Map.filter (
null . ToolShed.Data.List.permutationsBy (
-- CAVEAT: we haven't confirmed that there's some common availability with the interested student-bodies & this specific permutation of teachers & suitable locations.
\l r -> let
lookupResources = (teacherRegister !) *** (locationCatalogue !)
in snd {-locationId-} l /= snd {-locationId-} r {-allocate different locations to each teacher-} && Data.Resource.isAvailable (
lookupResources l,
lookupResources r
) {-ensure common availability with each remaining list-}
) {-permute teacherIds into locationIds-} . map (
uncurry zip . Control.Arrow.first {-teacherId-} repeat -- Construct '[(teacherId, locationId)]'.
) . Data.Map.toList . Data.Map.mapWithKey (
\teacherId course -> Data.Map.keys $ Data.Map.filter (
uncurry (&&) . (
(
Data.Course.getRequiredFacilityNames course `Data.Set.isSubsetOf` -- Check that this location offers the required facilities.
) . Data.Location.getFacilityNames &&& Data.Resource.isAvailable . (,,) (
teacherRegister ! teacherId
) (
Data.Map.filter (
Data.Foldable.elem (Data.Course.getSubject course) . Data.Student.deriveAmalgamatedKnowledgeRequirement
) studentBodyRegister
) -- Select locations which have some common availability with this teacher, & all interested students.
)
) locationCatalogue
)
) coursesByTeacherIdBySynchronisationId
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForSynchronisedCoursesWithoutSuitableLocations problemValidationSwitches && not (null synchronisationIdsWithoutSuitableLocations),
show ProblemConfiguration.ProblemValidationSwitches.checkForSynchronisedCoursesWithoutSuitableLocationsTag ++ ": no permutation of those locations offering the required facilities, is simultaneously available to the teachers of a synchronised course & to all interested students; " ++ show synchronisationIdsWithoutSuitableLocations
),
let
duplicateStudentIds :: [(Data.Student.Id, [Aggregate.StudentBody.Mnemonic])]
duplicateStudentIds = map (
(
\studentId -> (
studentId,
map Aggregate.StudentBody.getMnemonic . Data.Set.toList . Data.Set.filter (
(studentId `Data.Set.member`) . Aggregate.StudentBody.getStudentIds
) $ Data.Map.keysSet studentBodyRegister
) -- Pair.
) . head
) . filter (
(/= 1) . length -- Search for duplicates.
) . ToolShed.Data.Foldable.gather . concatMap (
Data.Set.toList . Aggregate.StudentBody.getStudentIds
) $ Aggregate.StudentBodyRegister.getStudentBodies studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForDuplicateStudentIds problemValidationSwitches && not (null duplicateStudentIds),
show ProblemConfiguration.ProblemValidationSwitches.checkForDuplicateStudentIdsTag ++ ": some student-identifiers exist in more than one student-body; " ++ show duplicateStudentIds
),
-- Check the relative size of resources.
let
daysOnWhichStudentBodiesExceedTeachers :: Data.Map.Map Temporal.Day.Day String
daysOnWhichStudentBodiesExceedTeachers = Data.Map.map (
\(nStudentBodies, nTeachers) -> showString "number of student-bodies=" . shows nStudentBodies . showString " > number of teachers=" $ shows nTeachers ""
) . Data.Map.filter (
uncurry (>)
) . Data.Map.fromList $ map (
id &&& (
Data.Map.size . (
`Data.Resource.extractAvailableResources` studentBodyRegister
) &&& Data.Map.size . (
`Data.Resource.extractAvailableResources` teacherRegister
)
)
) Temporal.Day.range
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodiesExceedTeachers problemValidationSwitches && not (Data.Map.null daysOnWhichStudentBodiesExceedTeachers),
show ProblemConfiguration.ProblemValidationSwitches.checkIfStudentBodiesExceedTeachersTag ++ ": on some days, the number of student-bodies exceeds the number of available teachers; " ++ show (Data.Map.toList daysOnWhichStudentBodiesExceedTeachers)
),
let
availableLocationCapacitiesByStudentBody = Data.Map.filterWithKey (
\studentBody -> Data.Foldable.all (Aggregate.StudentBody.getSize studentBody >)
) $ Data.Map.map (
\studentProfile -> Data.Map.map Data.Location.getCapacity $ Data.Map.filter (
Data.Resource.isAvailable . (,) studentProfile
) locationCatalogue
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodySizeExceedsCapacityOfAllLocations problemValidationSwitches && not (Data.Map.null availableLocationCapacitiesByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkIfStudentBodySizeExceedsCapacityOfAllLocationsTag ++ ": some student-bodies can't be accommodated by the largest available location; " ++ show (
map (
(
Aggregate.StudentBody.getMnemonic &&& Aggregate.StudentBody.getSize
) *** take 1 {-there may be zero available locations-} . Data.List.sortBy (
flip $ Data.Ord.comparing snd {-capacity-} -- Largest first.
) . Data.Map.toList
) $ Data.Map.toList availableLocationCapacitiesByStudentBody
)
),
let
daysOnWhichStudentBodySizeExceedsCapacity :: Data.Map.Map Temporal.Day.Day String
daysOnWhichStudentBodySizeExceedsCapacity = Data.Map.map (
(
\(studentBodySize, capacity) -> showString "student-body of size=" . shows studentBodySize . showChar ' ' $ if capacity == 0
then "for which zero available locations exist"
else showString "> capacity=" $ shows capacity ""
) . head
) . Data.Map.filter (
not . null
) . Data.Map.fromList $ map (
\day -> (
day,
dropWhile (
uncurry (<=)
) $ zip (
sortByDecreasingSize . map Aggregate.StudentBody.getSize . Aggregate.StudentBodyRegister.getStudentBodies $ Data.Resource.extractAvailableResources day studentBodyRegister
) (
(++ repeat 0) . sortByDecreasingSize . Data.Map.elems . Data.Map.map Data.Location.getCapacity $ Data.Resource.extractAvailableResources day locationCatalogue
)
) -- Pair.
) Temporal.Day.range where
sortByDecreasingSize :: [Size.NStudents] -> [Size.NStudents]
sortByDecreasingSize = Data.List.sortBy $ flip compare
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodySizeExceedsLocationCapacity problemValidationSwitches && not (Data.Map.null daysOnWhichStudentBodySizeExceedsCapacity),
show ProblemConfiguration.ProblemValidationSwitches.checkIfStudentBodySizeExceedsLocationCapacityTag ++ ": on some days, a match between the sizes of student-bodies & the available locations, reveals inadequate capacity; " ++ show (Data.Map.toList daysOnWhichStudentBodySizeExceedsCapacity)
),
let
timeslotsCapacity, timeslotsRequired :: Size.NTimeslots
timeslotsCapacity = (
* nTimeslotsPerDay
) . Data.Foldable.sum $ Data.Map.map (
Temporal.Availability.countDaysPerWeekAvailable . Temporal.Availability.findIntersections . (
: [
Temporal.Availability.findUnions $ Data.Map.map Data.Resource.getAvailability studentBodyRegister,
Temporal.Availability.findUnions $ Data.Map.map Data.Resource.getAvailability teacherRegister
]
) . Data.Resource.getAvailability -- Select those days on which this location, @ least one student-body, & @ least one teacher, are simultaneously available.
) locationCatalogue
timeslotsRequired = Data.Foldable.sum $ Data.Map.map (
sum . map Data.Course.getRequiredLessonsPerWeek . Data.Set.toList . Data.Teacher.getService
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckLocationsAvailabilityToSupportCourses problemValidationSwitches && timeslotsRequired > timeslotsCapacity,
show ProblemConfiguration.ProblemValidationSwitches.checkLocationsAvailabilityToSupportCoursesTag ++ ": the " ++ show timeslotsRequired ++ " time-slots required to teach all the courses offered, exceeds the " ++ show timeslotsCapacity ++ " time-slots available in the configured locations"
),
let
subjectsByStudentBody = Data.Map.filter (
not . Data.Set.null -- Select those student-bodies, which require any subjects for which there are zero suitable locations.
) $ Data.Map.mapWithKey (
\studentBody studentProfile -> Data.Set.filter (
\subject -> not $ Data.Foldable.any (
\teacherProfile -> Data.Foldable.any (
uncurry (&&) . (
Data.Course.isSuitable (Aggregate.StudentBody.getSize studentBody) subject &&& (
\requiredFacilityNames -> Data.Foldable.any (
\locationProfile -> all ($ locationProfile) [
(>= Aggregate.StudentBody.getSize studentBody) . Data.Location.getCapacity, -- Check that the location has adequate capacity.
Data.Resource.isAvailable . (,,) studentProfile teacherProfile, -- Check that all resources are simultaneously available.
Data.Set.isSubsetOf requiredFacilityNames . Data.Location.getFacilityNames -- Check that this location has adequate facilities.
]
) locationCatalogue
) . Data.Course.getRequiredFacilityNames
)
) $ Data.Teacher.getService teacherProfile -- Select the relevant portion of this teacher's service, which should be either zero or one course.
) teacherRegister
) $ Data.Student.deriveAmalgamatedKnowledgeRequirement studentProfile
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckSuitableLocationsForKnowledgeRequirements problemValidationSwitches && not (Data.Map.null subjectsByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkSuitableLocationsForKnowledgeRequirementsTag ++ ": some student-bodies require subjects for which there's no suitable location; " ++ show (
map (
Aggregate.StudentBody.getMnemonic *** Data.Set.toList
) $ Data.Map.toList subjectsByStudentBody
)
),
-- Check cross-references; must validate the existence of all requested subjects, before calling 'calculateStudentWorkloadBounds'.
let
nonExistentSubjectsByStudentBody = Data.Map.filter (
not . Data.Set.null
) $ Data.Map.map (
Data.Set.filter (
`Data.Set.notMember` Aggregate.TeacherRegister.extractDistinctSubjects teacherRegister
) . Data.Student.deriveAmalgamatedKnowledgeRequirement
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckSubjectExistence problemValidationSwitches && not (Data.Map.null nonExistentSubjectsByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkSubjectExistenceTag ++ ": not all subjects required by student-bodies, are offered; " ++ show (
map (
Control.Arrow.first Aggregate.StudentBody.getMnemonic
) . Data.Map.toList $ Data.Map.map Data.Set.toList nonExistentSubjectsByStudentBody
)
),
let
nonExistentMeetingLocationIdsByGroupId = Data.Map.filter (`Data.Map.notMember` locationCatalogue) $ Data.Map.mapMaybe Data.Group.getMaybeLocationId groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForNonExistentMeetingLocationIds problemValidationSwitches && not (Data.Map.null nonExistentMeetingLocationIdsByGroupId),
show ProblemConfiguration.ProblemValidationSwitches.checkForNonExistentMeetingLocationIdsTag ++ ": not all locations required for meetings, exist; " ++ show (Data.Map.toList nonExistentMeetingLocationIdsByGroupId)
),
let
nonExistentOwnLocationIdsByTeacherId = Data.Map.filter (`Data.Map.notMember` locationCatalogue) $ Data.Map.mapMaybe Data.Teacher.getMaybeOwnLocationId teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForNonExistentOwnLocationIds problemValidationSwitches && not (Data.Map.null nonExistentOwnLocationIdsByTeacherId),
show ProblemConfiguration.ProblemValidationSwitches.checkForNonExistentOwnLocationIdsTag ++ ": not all personal locations claimed by teachers, exist; " ++ show (Data.Map.toList nonExistentOwnLocationIdsByTeacherId)
), -- CAVEAT: teachers may legitimately share a personal classroom, provided they either have mutually exclusive working-weeks, or interleave their classes.
let
unsupportedCoursesByTeacherId = Data.Map.filter (not . Data.Set.null) $ Data.Map.map (
\teacherProfile -> Data.Set.filter (
\course -> not . Data.Foldable.any (
(Data.Course.getRequiredFacilityNames course `Data.Set.isSubsetOf`) . Data.Location.getFacilityNames -- Check that @ least one location offers the required facilities.
) $ Data.Map.filter (
Data.Resource.isAvailable . (,) teacherProfile -- Check that this location & the teacher are simultaneously available.
) locationCatalogue
) $ Data.Teacher.getService teacherProfile
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForNonExistentFacilities problemValidationSwitches && not (Data.Map.null unsupportedCoursesByTeacherId),
show ProblemConfiguration.ProblemValidationSwitches.checkForNonExistentFacilitiesTag ++ ": there are zero available locations, which offer all the facilities required, for @ least one of the courses offered; " ++ show (Data.Map.toList $ Data.Map.map Data.Set.toList unsupportedCoursesByTeacherId)
),
let
nonExistentGroupIds :: Data.Group.Membership
nonExistentGroupIds = extractDistinctGroupMembership problemParameters \\ Data.Map.keysSet groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForNonExistentGroupIds problemValidationSwitches && not (Data.Set.null nonExistentGroupIds),
show ProblemConfiguration.ProblemValidationSwitches.checkForNonExistentGroupIdsTag ++ ": not all groups of which some student-bodies & teachers are members, exist; " ++ show (Data.Set.toList nonExistentGroupIds)
),
let
groupIdsByLocationId = Data.Map.filterWithKey (
\groupId locationId -> uncurry (+) (
(
Data.Maybe.maybe 0 Aggregate.StudentClass.getSize . Data.Map.lookup groupId *** Data.Maybe.maybe 0 Data.Set.size . Data.Map.lookup groupId
) $ findHumanResourceIdsByGroupId problemParameters
) > Data.Location.getCapacity (locationCatalogue ! locationId)
) $ Data.Map.mapMaybe Data.Group.getMaybeLocationId groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckCapacityOfLocationsForMeetings problemValidationSwitches && not (Data.Map.null groupIdsByLocationId),
show ProblemConfiguration.ProblemValidationSwitches.checkCapacityOfLocationsForMeetingsTag ++ ": some groups hold meetings in locations with inadequate capacity; " ++ show (Data.Map.toList groupIdsByLocationId)
),
let
groupIdsByLocationIdByTime = Data.Map.filter (
Data.Foldable.any $ (> 1) . length
) $ Data.Map.foldrWithKey (
\groupId groupProfile m -> Data.Maybe.maybe m (
\locationId -> Data.Foldable.foldr (
Data.Map.insertWith (
Data.Map.unionWith (++)
) `flip` Data.Map.singleton locationId [groupId]
) m $ Data.Group.getMeetingTimes groupProfile
) $ Data.Group.getMaybeLocationId groupProfile
) Data.Map.empty groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckDuplicateMeetingLocationIds problemValidationSwitches && not (Data.Map.null groupIdsByLocationIdByTime),
show ProblemConfiguration.ProblemValidationSwitches.checkDuplicateMeetingLocationIdsTag ++ ": several groups require the same location simultaneously; " ++ show (Data.Map.toList $ Data.Map.map Data.Map.toList groupIdsByLocationIdByTime)
),
let
invalidMeetingTimesByGroupId = Data.Map.filter (
Data.Foldable.any (
\time -> any ($ Temporal.Time.getTimeslotId time) [
(< Factory.Data.Interval.getMinBound timeslotIdBounds),
(> Factory.Data.Interval.getMaxBound timeslotIdBounds)
]
)
) $ Data.Map.map Data.Group.getMeetingTimes groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForInvalidMeetingTimes problemValidationSwitches && not (Data.Map.null invalidMeetingTimesByGroupId),
show ProblemConfiguration.ProblemValidationSwitches.checkForInvalidMeetingTimesTag ++ ": some groups specify meeting-times, using timeslot-identifiers which are outside permissible bounds " ++ show timeslotIdBounds ++ "; " ++ show (Data.Map.toList $ Data.Map.map Data.Set.toList invalidMeetingTimesByGroupId)
),
let
daysAndLocationIdsByGroupId = Data.Map.filter (
not . Data.Set.null . fst {-days-}
) . Data.Map.map (
\(meetingDays, locationId) -> (
Data.Set.filter (
not . (`Data.Resource.isAvailableOn` (locationCatalogue ! locationId))
) meetingDays,
locationId
) -- Pair.
) $ Data.Map.mapMaybe (
uncurry fmap . (
(,) . Data.Set.map Temporal.Time.getDay . Data.Group.getMeetingTimes &&& Data.Group.getMaybeLocationId
)
) groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckMeetingLocationsAvailability problemValidationSwitches && not (Data.Map.null daysAndLocationIdsByGroupId),
show ProblemConfiguration.ProblemValidationSwitches.checkMeetingLocationsAvailabilityTag ++ ": some groups require a location, which isn't available @ all of the specified meeting-times; " ++ show (
Data.Map.toList $ Data.Map.map (Control.Arrow.first Data.Set.toList) daysAndLocationIdsByGroupId
)
),
let
groupsWithIndependentlyAvailableMembers = filter (
Temporal.Availability.isUnavailable . Data.Resource.getAvailability . (
(`Data.HumanResource.extractGroupMembersOf` studentBodyRegister) &&& (`Data.HumanResource.extractGroupMembersOf` teacherRegister)
)
) . Data.Map.keys $ Data.Map.filter (
uncurry (&&) . (
(> 0) . Data.Group.countNTimeslotsPerWeek &&& Data.Group.getMandatesAttendance
) -- If no meetings are required or members needn't attend, then they needn't ever be simultaneously available.
) groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckSimultaneousAvailabilityOfGroupMembers problemValidationSwitches && not (null groupsWithIndependentlyAvailableMembers),
show ProblemConfiguration.ProblemValidationSwitches.checkSimultaneousAvailabilityOfGroupMembersTag ++ ": some groups which mandate attendance, can never meet because their members are never simultaneously available; " ++ show groupsWithIndependentlyAvailableMembers
),
let
unavailableDaysByGroupId :: Data.Map.Map Data.Group.Id (Data.Set.Set Temporal.Day.Day)
unavailableDaysByGroupId = Data.Map.filter (
not . Data.Set.null
) . Data.Map.mapWithKey (
\groupId -> let
memberStudents = Data.HumanResource.extractGroupMembersOf groupId studentBodyRegister
memberTeachers = Data.HumanResource.extractGroupMembersOf groupId teacherRegister
in Data.Set.filter (
\day -> (
not (Data.Map.null memberStudents) || not (Data.Map.null memberTeachers)
) && not (
any (
Temporal.Availability.isAvailableOn day
) $ map Data.Resource.getAvailability (Data.Map.elems memberStudents) ++ map Data.Resource.getAvailability (Data.Map.elems memberTeachers)
)
)
) $ Data.Map.map (
Data.Set.map Temporal.Time.getDay . Data.Group.getMeetingTimes
) groupCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckAvailabilityOfAnyGroupMember problemValidationSwitches && not (Data.Map.null unavailableDaysByGroupId),
show ProblemConfiguration.ProblemValidationSwitches.checkAvailabilityOfAnyGroupMemberTag ++ ": specific meetings of some groups can never be attended, because none of the members are available on that day; " ++ show (Data.Map.toList $ Data.Map.map Data.Set.toList unavailableDaysByGroupId)
),
let
unavailableDaysByStudentBody = findUnavailableDaysByGroupIdByHumanResourceId groupCatalogue studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsAvailabilityForMandatoryMeetings problemValidationSwitches && not (Data.Map.null unavailableDaysByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsAvailabilityForMandatoryMeetingsTag ++ ": some student-bodies are unavailable for some of the meetings, of those groups mandating attendance, of which they're members; " ++ show (
Data.Map.toList $ Data.Map.map (
Data.Map.toList . Data.Map.map Data.Set.toList
) unavailableDaysByStudentBody
)
),
let
unavailableDaysByTeacherId = findUnavailableDaysByGroupIdByHumanResourceId groupCatalogue teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckTeachersAvailabilityForMandatoryMeetings problemValidationSwitches && not (Data.Map.null unavailableDaysByTeacherId),
show ProblemConfiguration.ProblemValidationSwitches.checkTeachersAvailabilityForMandatoryMeetingsTag ++ ": some teachers are unavailable for some of the meetings, of those groups mandating attendance, of which they're members; " ++ show (
Data.Map.toList $ Data.Map.map (
Data.Map.toList . Data.Map.map Data.Set.toList
) unavailableDaysByTeacherId
)
),
let
synchronousTimesByStudentBody = Data.Map.filter (
not . Data.Set.null -- Leave only those student-bodies who're double-booked.
) $ Data.Map.mapWithKey (
\studentBody studentProfile -> uncurry Data.Set.intersection $ (
Data.Foldable.foldr Data.Set.union Data.Set.empty . Data.Set.map (
\subject -> Data.Foldable.foldr1 Data.Set.intersection . Data.Map.map (
Data.Foldable.foldr Data.Set.union Data.Set.empty . Data.Set.filter (
Data.Foldable.all (
(`Data.Resource.isAvailableOn` studentProfile) . Temporal.Time.getDay
) -- Remove any course which specifies any time when the student's unavailable.
) . Data.Set.map (
Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest
) -- Extract the specified times from the relevant portion of this teacher's service; which could be null.
) . Data.Map.filter (
not . Data.Set.null -- Remove those teachers who don't offer a course in the required subject.
) . Data.Map.map (
Data.Set.filter (
Data.Course.isSuitable (Aggregate.StudentBody.getSize studentBody) subject
) . Data.Teacher.getService -- Select the relevant portion of this teacher's service, which should be either zero or one course.
) $ Data.Map.filter (
Data.Resource.isAvailable . (,) studentProfile -- Confirm that this teacher & student-body are simultaneously available.
) teacherRegister
) . Data.Student.deriveAmalgamatedKnowledgeRequirement &&& Data.Foldable.foldr (
Data.Set.union . Data.Group.getMeetingTimes . (
groupCatalogue ! -- Acquire the group-profile.
)
) Data.Set.empty . {- Data.Set.filter Data.Group.getMandatesAttendance . -} Data.HumanResource.getGroupMembership
) studentProfile
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckIndependenceOfStudentTimeslotsRequestsAndMeetings problemValidationSwitches && not (Data.Map.null synchronousTimesByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkIndependenceOfStudentTimeslotsRequestsAndMeetingsTag ++ ": some student-bodies are members of groups, which meet @ times specified for all courses offering one of their knowledge-requirements; " ++ show (
map (
Control.Arrow.first Aggregate.StudentBody.getMnemonic
) . Data.Map.toList $ Data.Map.map Data.Set.toList synchronousTimesByStudentBody
)
),
let
synchronousTimesByTeacherId = Data.Map.filter (
not . Data.Set.null -- Leave only those teachers who're double-booked.
) $ Data.Map.map (
uncurry Data.Set.intersection . (
Data.Teacher.findSpecifiedTimes &&& Data.Foldable.foldr (
Data.Set.union . Data.Group.getMeetingTimes . (
groupCatalogue ! -- Acquire the group-profile.
)
) Data.Set.empty . {- Data.Set.filter Data.Group.getMandatesAttendance . -} Data.HumanResource.getGroupMembership
)
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckIndependenceOfTeacherTimeslotsRequestsAndMeetings problemValidationSwitches && not (Data.Map.null synchronousTimesByTeacherId),
show ProblemConfiguration.ProblemValidationSwitches.checkIndependenceOfTeacherTimeslotsRequestsAndMeetingsTag ++ ": some teachers are members of groups, which meet @ times specified for one of their courses; " ++ show (Data.Map.toList $ Data.Map.map Data.Set.toList synchronousTimesByTeacherId)
),
let
synchronousSeparateMeetingsByTimeByTeacherId = findSynchronousSeparateMandatoryMeetingsByTimeByHumanResourceId groupCatalogue teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckTeachersForSynchronousMeetings problemValidationSwitches && not (Data.Map.null synchronousSeparateMeetingsByTimeByTeacherId),
show ProblemConfiguration.ProblemValidationSwitches.checkTeachersForSynchronousMeetingsTag ++ ": some teachers are members of groups with synchronous meeting-times, in separate locations; " ++ show (Data.Map.toList $ Data.Map.map Data.Map.toList synchronousSeparateMeetingsByTimeByTeacherId)
),
let
synchronousSeparateMeetingsByTimeByStudentBody = findSynchronousSeparateMandatoryMeetingsByTimeByHumanResourceId groupCatalogue studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsForSynchronousMeetings problemValidationSwitches && not (Data.Map.null synchronousSeparateMeetingsByTimeByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsForSynchronousMeetingsTag ++ ": some student-bodies are members of groups with synchronous meeting-times, in separate locations; " ++ show (Data.Map.toList $ Data.Map.map Data.Map.toList synchronousSeparateMeetingsByTimeByStudentBody)
),
let
teacherIds = Data.Map.keys . Data.Map.filter (
not . Data.Resource.isAvailable -- Operate on Pair.
) $ Data.Map.mapMaybe (
\teacherProfile -> ((,) teacherProfile . (locationCatalogue !)) `fmap` Data.Teacher.getMaybeOwnLocationId teacherProfile
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckOwnLocationsAvailability problemValidationSwitches && not (null teacherIds),
show ProblemConfiguration.ProblemValidationSwitches.checkOwnLocationsAvailabilityTag ++ ": some teachers claim a personal location, which isn't available @ any time during their working-week; " ++ show teacherIds
),
let
duplicateClaimsByLocationId = Data.Map.map (
Aggregate.TeacherRegister.getTeacherIds . fst {-teacherRegister-}
) . Data.Map.filter (
uncurry (&&) . (
(> 1) . Data.Map.size . fst {-teacherRegister-} &&& Data.Resource.isAvailable {-days on which the location & all relevant teachers are available-}
)
) $ Data.Map.mapWithKey (
(,) . (`Data.Map.filter` teacherRegister) . Data.Teacher.inhabits
) locationCatalogue
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckDuplicateOwnLocationIds problemValidationSwitches && not (Data.Map.null duplicateClaimsByLocationId),
show ProblemConfiguration.ProblemValidationSwitches.checkDuplicateOwnLocationIdsTag ++ ": ownership of a single location, is claimed by teachers whose working-weeks aren't mutually exclusive; " ++ show (Data.Map.toList duplicateClaimsByLocationId)
),
let
teacherIdsByTime = Data.Map.filter (
(> Data.Map.size locationCatalogue) . length -- CAVEAT: a lax constraint, since some locations may be unavailable or unsuitable.
) . Data.Map.foldrWithKey (
\teacherId -> flip $ Data.Foldable.foldr (
Data.Map.insertWith (++) `flip` [teacherId]
) -- Invert the map.
) Data.Map.empty $ Data.Map.map Data.Teacher.findSpecifiedTimes teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckLocationsForSynchronousSpecifiedTimes problemValidationSwitches && not (Data.Map.null teacherIdsByTime),
show ProblemConfiguration.ProblemValidationSwitches.checkLocationsForSynchronousSpecifiedTimesTag ++ ": more courses specify the same booking-time, than there are locations in which to hold them; " ++ show (Data.Map.toList teacherIdsByTime)
),
let
unavailableSubjectsByStudentBody = Data.Map.filter (not . Data.Set.null) $ Data.Map.mapWithKey (
\studentBody studentProfile -> Data.Set.filter (
\subject -> not . Data.Foldable.any (
Data.Foldable.all (
(`Data.Resource.isAvailableOn` studentProfile) . Temporal.Time.getDay
) . Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest
) . Aggregate.TeacherRegister.findSuitableCourseByTeacherId (
Aggregate.StudentBody.getSize studentBody -- Ensure the student-body fits within 'Data.Course.getMaybeMaximumClassSize'.
) subject $ Data.Map.filter (
Data.Resource.isAvailable . (,) studentProfile -- Find teachers available simultaneously with this student.
) teacherRegister
) $ Data.Student.deriveAmalgamatedKnowledgeRequirement studentProfile
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsAvailabilityForSpecifiedTimes problemValidationSwitches && not (Data.Map.null unavailableSubjectsByStudentBody),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsAvailabilityForSpecifiedTimesTag ++ ": some student-bodies have requested subjects, for which either there're zero suitable courses, or all suitable courses specify booking-times when they're unavailable; " ++ show (Data.Map.toList $ Data.Map.map Data.Set.toList unavailableSubjectsByStudentBody)
),
let
studentBodiesWithSynchronousTimeslotRequests :: [Aggregate.StudentBody.StudentBody]
studentBodiesWithSynchronousTimeslotRequests = Data.Map.keys . Data.Map.filter null {-no acceptable permutation of courses-} $ Data.Map.mapWithKey (
\studentBody studentProfile -> filter {-[[[Time]]]-} (
not . Data.List.Extra.anySame . concat {-merge the specified times, for a permutation of courses providing all required subjects-}
) . ToolShed.Data.List.permutations {-where several teachers offer a matching course-} . map (
\subject -> filter (
all ((`Data.Resource.isAvailableOn` studentProfile) . Temporal.Time.getDay) -- The student-body must be available @ all the times specified in any course.
) . map (
Data.Set.toList . Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest -- Extract the potentially null list of times specified by each course.
) . Data.Map.elems . Aggregate.TeacherRegister.findSuitableCourseByTeacherId (Aggregate.StudentBody.getSize studentBody) subject $ Data.Map.filter (
Data.Resource.isAvailable . (,) studentProfile
) teacherRegister
) . Data.Set.toList $ Data.Student.deriveAmalgamatedKnowledgeRequirement studentProfile
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckCoursesForSynchronousSpecifiedTimes problemValidationSwitches && not (null studentBodiesWithSynchronousTimeslotRequests),
show ProblemConfiguration.ProblemValidationSwitches.checkCoursesForSynchronousSpecifiedTimesTag ++ ": some student-bodies require subjects, for which either there're zero suitable courses, or present a conflict between the specified booking-times of those courses which are suitable; " ++ show studentBodiesWithSynchronousTimeslotRequests
),
let
subjectsWithInsufficientTeachers = Data.Map.filter (
uncurry (>) -- Demand exceeds supply.
) . Data.Map.mapWithKey (
\subject nClassesRequired -> (
nClassesRequired,
sum {-aggregate supply-} [
nTimeslotPerWeekOfTeaching `div` Data.Course.getRequiredLessonsPerWeek course |
(nTimeslotPerWeekOfTeaching, Just course) <- map (
Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay &&& Data.Teacher.lookupCourseIn subject
) $ Data.Map.elems teacherRegister
] -- List-comprehension.
) -- Pair.
) . Data.Map.fromListWith (+) . map (
flip (,) 1 -- Count the number of separate classes required in each subject. CAVEAT: ignores the class-size, which may overflow all suitable locations.
) . concatMap (
Data.Set.toList . Data.Set.unions . map Data.Student.deriveAmalgamatedKnowledgeRequirement -- Derive total knowledge-requirement of each student-class.
) $ ToolShed.Data.Foldable.gatherBy Data.Student.getStream studentBodyRegister -- Gather student-bodies which can be temporarily merged. CAVEAT: assumes 'ExecutionConfiguration.ExecutionOptions.getPermitTemporaryStudentBodyMerger'.
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckTeachingCapacityBySubject problemValidationSwitches && not (Data.Map.null subjectsWithInsufficientTeachers),
show ProblemConfiguration.ProblemValidationSwitches.checkTeachingCapacityBySubjectTag ++ ": the total demand for separate courses in some subjects exceeds that offered by teachers; " ++ show (Data.Map.toList subjectsWithInsufficientTeachers)
),
-- Check workload-bounds.
let
idleStudentBodies = Data.Map.filter (
uncurry (&&) . (
Data.Set.null . Data.Student.deriveAmalgamatedKnowledgeRequirement &&& (> 0) . Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay
)
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForIdleStudents problemValidationSwitches && not (Data.Map.null idleStudentBodies),
show ProblemConfiguration.ProblemValidationSwitches.checkForIdleStudentsTag ++ ": some student-bodies require zero subjects, but have allocated one or more time-slots for teaching; " ++ show (Data.Map.toList idleStudentBodies)
),
let
overloadedStudentBodies = Data.Map.filter (
uncurry (&&) . (
not . Data.Set.null . Data.Student.deriveAmalgamatedKnowledgeRequirement &&& (== 0) . Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay
)
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForOverloadedStudents problemValidationSwitches && not (Data.Map.null overloadedStudentBodies),
show ProblemConfiguration.ProblemValidationSwitches.checkForOverloadedStudentsTag ++ ": some student-bodies have allocated zero time-slots for teaching, but have requested one or more subjects; " ++ show (Data.Map.toList overloadedStudentBodies)
),
let
idleTeachers = Data.Map.filter (
uncurry (&&) . (
not . Data.Teacher.offersService &&& (> 0) . Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay
)
) teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForIdleTeachers problemValidationSwitches && not (Data.Map.null idleTeachers),
show ProblemConfiguration.ProblemValidationSwitches.checkForIdleTeachersTag ++ ": some teachers offer zero courses required by student-bodies, but have allocated one or more time-slots for teaching; " ++ show (Data.Map.toList idleTeachers)
),
let
slackStudentsBodies :: Data.Map.Map Aggregate.StudentBody.StudentBody (Size.NTimeslots, Size.NTimeslots)
slackStudentsBodies = Data.Map.filter (uncurry (<)) $ Data.Map.map (
(
Temporal.Workload.getMaximum . uncurry (<+>) -- Add the workloads associated with core & optional subject-requirements.
) . calculateStudentWorkloadBounds teacherRegister &&& Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsLowerWorkloadBound problemValidationSwitches && not (Data.Map.null slackStudentsBodies),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsLowerWorkloadBoundTag ++ ": the workload associated with all subject-requirements, for some student-bodies, is insufficient to meet the tuition-time in their working-week; " ++ show (map (Control.Arrow.first Aggregate.StudentBody.getMnemonic) $ Data.Map.toList slackStudentsBodies)
),
let
overloadedStudentBodies :: Data.Map.Map Aggregate.StudentBody.StudentBody (Size.NTimeslots, Size.NTimeslots)
overloadedStudentBodies = Data.Map.filter (uncurry (>)) $ Data.Map.map (
Temporal.Workload.getMinimum . Data.Requirements.getCore . calculateStudentWorkloadBounds teacherRegister &&& Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsUpperWorkloadBound problemValidationSwitches && not (Data.Map.null overloadedStudentBodies),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsUpperWorkloadBoundTag ++ ": the workload associated with core subject-requirements, for some student-bodies, exceeds the tuition-time available in their working-week; " ++ show (map (Control.Arrow.first Aggregate.StudentBody.getMnemonic) $ Data.Map.toList overloadedStudentBodies)
),
let
overloadedStudentBodies = findHumanResourcesOverloadedWithMeetings nTimeslotsPerDay groupCatalogue studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsUpperWorkloadBound problemValidationSwitches && not (Data.Map.null overloadedStudentBodies),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsUpperWorkloadBoundTag ++ ": the time associated with meetings, for some student-bodies, exceeds the non-teaching time available in their working-week; " ++ show (Data.Map.toList overloadedStudentBodies)
),
let
overloadedTeachers = findHumanResourcesOverloadedWithMeetings nTimeslotsPerDay groupCatalogue teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckTeachersUpperWorkloadBound problemValidationSwitches && not (Data.Map.null overloadedTeachers),
show ProblemConfiguration.ProblemValidationSwitches.checkTeachersUpperWorkloadBoundTag ++ ": the time associated with meetings, for some teachers, exceeds the non-teaching time available in their working-week; " ++ show (Data.Map.toList overloadedTeachers)
),
-- Check miscellaneous.
let
rogueStudentBodies = Data.Map.filter (
Data.Foldable.any (not . Data.Set.null)
) $ Data.Map.map (
Data.Map.filter (
(> 1) . Data.Set.size
) . Data.Map.fromListWith Data.Set.union . map (
Data.Subject.getTopic &&& Data.Set.singleton . Data.Subject.getLevel
) . Data.Set.toList . Data.Student.deriveAmalgamatedKnowledgeRequirement
) studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckStudentsForMultipleLevelsOfSameTopic problemValidationSwitches && not (Data.Map.null rogueStudentBodies),
show ProblemConfiguration.ProblemValidationSwitches.checkStudentsForMultipleLevelsOfSameTopicTag ++ ": some student-body's knowledge-requirements include more than one level of the same topic; " ++ show (
Data.Map.toList $ Data.Map.map (Data.Map.toList . Data.Map.map Data.Set.toList) rogueStudentBodies
)
),
let
studentBodiesWithUnrealisableFreePeriodPreference = extractResourceIdsWithUnrealisableFreePeriodPreference studentBodyRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForStudentsWithUnrealisableFreePeriodPreference problemValidationSwitches && not (null studentBodiesWithUnrealisableFreePeriodPreference),
show ProblemConfiguration.ProblemValidationSwitches.checkForStudentsWithUnrealisableFreePeriodPreferenceTag ++ ": the preference of some student-bodies, for the position of free time-slots, within those days on which they're available, can never be realised because of the meeting-times of groups of which they're members; " ++ show (map Aggregate.StudentBody.getMnemonic studentBodiesWithUnrealisableFreePeriodPreference)
),
let
teacherIdsWithUnrealisableFreePeriodPreference = extractResourceIdsWithUnrealisableFreePeriodPreference teacherRegister
in (
ProblemConfiguration.ProblemValidationSwitches.getCheckForTeachersWithUnrealisableFreePeriodPreference problemValidationSwitches && not (null teacherIdsWithUnrealisableFreePeriodPreference),
show ProblemConfiguration.ProblemValidationSwitches.checkForTeachersWithUnrealisableFreePeriodPreferenceTag ++ ": the preference of some teachers, for the position of free time-slots, within those days on which they're available, can never be realised because of the meeting-times of groups of which they're members; " ++ show teacherIdsWithUnrealisableFreePeriodPreference
)
] where
nTimeslotsPerDay :: Size.NTimeslots
nTimeslotsPerDay = calculateNTimeslotsPerDay timeslotIdBounds
extractResourceIdsWithUnrealisableFreePeriodPreference :: (
Data.HumanResource.HumanResource humanResource,
Data.Resource.Resource humanResource
) => Data.Resource.ResourceMap humanResourceId humanResource -> [humanResourceId]
extractResourceIdsWithUnrealisableFreePeriodPreference = Data.Map.keys . Data.Map.filter (
\profile -> let
meetingTimesByDay = Temporal.Time.categoriseByDay . Aggregate.GroupCatalogue.getMeetingTimes groupCatalogue $ Data.HumanResource.getGroupMembership profile
in Data.Foldable.all (
(
\meetingTimeslotIdSet -> case Data.Maybe.fromJust $ Data.HumanResource.getMaybeFreePeriodPreference profile of
Temporal.FreePeriodPreference.Pre -> Data.Set.member (Factory.Data.Interval.getMinBound timeslotIdBounds) meetingTimeslotIdSet
Temporal.FreePeriodPreference.Post -> all (`Data.Set.member` meetingTimeslotIdSet) $ Factory.Data.Interval.toList timeslotIdBounds
Temporal.FreePeriodPreference.Terminal -> Data.Set.member (Factory.Data.Interval.getMaxBound timeslotIdBounds) meetingTimeslotIdSet
) . Data.Maybe.fromMaybe Data.Set.empty . (`Data.Map.lookup` meetingTimesByDay)
) . Temporal.Availability.deconstruct $ Data.Resource.getAvailability profile
) . Data.Map.filter Data.HumanResource.hasFreePeriodPreference
coursesByTeacherIdBySynchronisationId = Aggregate.TeacherRegister.findCoursesByTeacherIdBySynchronisationId teacherRegister
distinctCoursesBySynchronisationId = Aggregate.TeacherRegister.findDistinctCoursesBySynchronisationId coursesByTeacherIdBySynchronisationId
{- |
* Merges those /student-bodies/ whose /profile/s are identical, into a /student-body/.
* Also writes the list of those students affected.
-}
reduceStudentBodyRegister :: (
Ord level,
Ord stream,
Ord teachingRatio
)
=> Aggregate.StudentClass.MnemonicSeparator
-> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Control.Monad.Writer.Writer [[Aggregate.StudentBody.StudentBody]] (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId)
reduceStudentBodyRegister mnemonicSeparator problemParameters = do
studentBodyRegister <- Aggregate.StudentBodyRegister.reduce mnemonicSeparator $ getStudentBodyRegister problemParameters
return {-to Writer-monad-} problemParameters { getStudentBodyRegister = studentBodyRegister }
{- |
* Removes /course/s from the /teacherRegister/, which aren't required by any /student/.
* Writes an association-list of /teacherId/ & any /knowledge/ they offer for which there's no demand;
where there's a requirement for all a /teacher/'s knowledge, then no data is written for that teacher, rather than writing a null set.
-}
removeRedundantCourses :: (
#if !MIN_VERSION_containers(0,5,2)
Ord synchronisationId,
Ord timeslotId,
#endif
Ord level
)
=> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Control.Monad.Writer.Writer [(teacherId, Data.Subject.Knowledge level)] (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId)
removeRedundantCourses problemParameters = let
(teacherRegister, redundantServiceByTeacherId) = (
Data.Map.map fst &&& Data.Map.filter (not . Data.Set.null) . Data.Map.map snd
) . Data.Map.map (
\profile -> let
(requiredService, redundantService) = Data.Set.partition (
(`Data.Set.member` Aggregate.StudentBodyRegister.extractDistinctSubjects (getStudentBodyRegister problemParameters)) . Data.Course.getSubject
) $ Data.Teacher.getService profile
in (
profile {Data.Teacher.getService = requiredService},
redundantService
) -- Pair.
) $ getTeacherRegister problemParameters
in do
Control.Monad.Writer.tell . Data.Map.toList $ Data.Map.map (Data.Set.map Data.Course.getSubject) redundantServiceByTeacherId
return {-to Writer-monad-} problemParameters {
getTeacherRegister = teacherRegister
}
-- | Remove any /groups/ from the /groupCatalogue/ which have zero /meeting-times/.
removePointlessGroups
:: ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Control.Monad.Writer.Writer [Data.Group.Id] (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId)
removePointlessGroups problemParameters = let
(pointlessGroupMembership, reducedGroupCatalogue) = Control.Arrow.first Data.Map.keysSet . Data.Map.partition (
Data.Set.null . Data.Group.getMeetingTimes
) $ getGroupCatalogue problemParameters
in do
Control.Monad.Writer.tell $ Data.Set.toList pointlessGroupMembership
return {-to Writer-monad-} problemParameters {
getGroupCatalogue = reducedGroupCatalogue,
getStudentBodyRegister = Data.Map.map (Data.Student.unsubscribe pointlessGroupMembership) $ getStudentBodyRegister problemParameters,
getTeacherRegister = Data.Map.map (Data.Teacher.unsubscribe pointlessGroupMembership) $ getTeacherRegister problemParameters
}
-- | Remove any /groups/ from the /groupCatalogue/ to which neither /student-bodies/ nor /teachers/ have subscribed.
removeUnsubscribedGroups
:: RealFrac teachingRatio
=> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Control.Monad.Writer.Writer [Data.Group.Id] (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId)
removeUnsubscribedGroups problemParameters = let
(reducedGroupCatalogue, unsubscribedGroupCatalogue) = Data.Map.partitionWithKey (
\groupId _ -> Data.Set.member groupId . uncurry Data.Set.union $ (
Data.HumanResource.extractCombinedGroupMembership . getStudentBodyRegister &&& Data.HumanResource.extractCombinedGroupMembership . getTeacherRegister
) problemParameters
) $ getGroupCatalogue problemParameters
in do
Control.Monad.Writer.tell $ Data.Map.keys unsubscribedGroupCatalogue
return {-to Writer-monad-} problemParameters {
getGroupCatalogue = reducedGroupCatalogue
}
-- | Proxies request to 'Aggregate.TeacherRegister.mergeConstraintsOnSynchronisedCourses'.
mergeConstraintsOnSynchronisedCourses :: (
Ord level,
Ord synchronisationId,
Ord teacherId,
Ord timeslotId,
Show synchronisationId,
Show timeslotId
) => ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
mergeConstraintsOnSynchronisedCourses problemParameters = problemParameters {
getTeacherRegister = Aggregate.TeacherRegister.mergeConstraintsOnSynchronisedCourses $ getTeacherRegister problemParameters
}
{- |
* When temporary student-body mergers are permitted according to the execution-options, some problem-validation switches are inappropriate & should be disabled to prevent false positives.
* Also writes the names of any disabled switches.
-}
disableAnyValidationInappropriateForTemporaryStudentBodyMerger
:: ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Control.Monad.Writer.Writer [String] (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId)
disableAnyValidationInappropriateForTemporaryStudentBodyMerger problemParameters = foldr (
\(_, disabledSwitchTag) -> (Control.Monad.Writer.tell [disabledSwitchTag] >>) -- Report the disabled problem-validation switch's name, & forward the writer.
) (
return {-to Writer-monad-} problemParameters {
getProblemValidationSwitches = problemValidationSwitches {
ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodiesExceedTeachers = False,
ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodySizeExceedsLocationCapacity = False
} -- Disable potentially conflicting problem-validation switches.
} -- Initial value.
) $ filter (
($ problemValidationSwitches) . fst {-accessor-} -- Select enabled problem-validation switches.
) [
(ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodiesExceedTeachers, ProblemConfiguration.ProblemValidationSwitches.checkIfStudentBodiesExceedTeachersTag),
(ProblemConfiguration.ProblemValidationSwitches.getCheckIfStudentBodySizeExceedsLocationCapacity, ProblemConfiguration.ProblemValidationSwitches.checkIfStudentBodySizeExceedsLocationCapacityTag)
] where
problemValidationSwitches = getProblemValidationSwitches problemParameters
-- | True if any /student-body/ or /teacher/ has a preference for the location of any free timeslots in their working day.
hasAnyFreePeriodPreference :: RealFrac teachingRatio => ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Bool
hasAnyFreePeriodPreference = uncurry (||) . (Aggregate.StudentBodyRegister.hasAnyFreePeriodPreference . getStudentBodyRegister &&& Aggregate.TeacherRegister.hasAnyFreePeriodPreference . getTeacherRegister)
-- | Whether /course/s offer different /minimumConsecutiveLessons/.
hasVariousMinimumConsecutiveLessons ::
#if !MIN_VERSION_containers(0,5,2)
(
Ord level,
Ord synchronisationId,
Ord timeslotId
) =>
#endif
ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Bool
hasVariousMinimumConsecutiveLessons = (> 1) . Data.Set.size . Data.Foldable.foldr (
\teacherProfile -> Data.Set.union (Data.Set.map Data.Course.getMinimumConsecutiveLessons $ Data.Teacher.getService teacherProfile)
) Data.Set.empty . getTeacherRegister
instance (
Enum timeslotId,
Ord level,
Ord synchronisationId,
Ord teacherId,
Ord timeslotId,
RealFrac teachingRatio,
Show level,
Show synchronisationId,
Show teacherId,
Show timeslotId
) => Configuration.Configuration (ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId) where
issueWarnings problemParameters = [
msg | (True, msg) <- [
let
excessTotalWorkloadByStudentBody :: Data.Map.Map Aggregate.StudentBody.StudentBody Size.NTimeslots
excessTotalWorkloadByStudentBody = findExcessTotalWorkloadByStudentBody problemParameters
in (
not $ Data.Map.null excessTotalWorkloadByStudentBody,
"the time required for the total (core & optional) knowledge-requirements of some student-bodies, exceeds that allocated to teaching; " ++ show (
Control.Arrow.first Aggregate.StudentBody.getMnemonic `map` Data.Map.toList excessTotalWorkloadByStudentBody
)
),
let
subjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek = Aggregate.TeacherRegister.findSubjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek $ getTeacherRegister problemParameters
in (
not $ Data.Map.null subjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek,
"some subjects have been offered by more than one teacher, but in courses requiring a different number of lessons per week; " ++ show (
Data.Map.toList $ Data.Map.map Data.Map.toList subjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek
)
), (
ProblemConfiguration.ValidationSwitch.areAllOff $ getProblemValidationSwitches problemParameters,
"all problem-validation switches are off; the problem-parameters may be invalid"
), (
ProblemConfiguration.ValidationSwitch.areAllOff $ getTimetableValidationSwitches problemParameters,
"all timetable-validation switches are off; any imported timetable may be invalid"
),
let
subjectsByTeacherId = Data.Map.map (
map Data.Course.getSubject . Data.Set.toList
) . Data.Map.filter (
not . Data.Set.null
) . Data.Map.map (
Data.Set.filter (
Data.Maybe.maybe False (
null . show -- BODGE: the type is unknown.
) . Data.Course.getMaybeSynchronisationId
) . Data.Teacher.getService
) $ getTeacherRegister problemParameters
in (
not $ Data.Map.null subjectsByTeacherId,
"some teachers have defined courses with a null 'synchronisationId'; " ++ show (Data.Map.toList subjectsByTeacherId)
),
let
synchronousMeetingsByTimeByStudentBodyMnemonic = findSynchronousMeetingsByTimeByStudentBodyMnemonic problemParameters
in (
not $ Data.Map.null synchronousMeetingsByTimeByStudentBodyMnemonic,
"the following student-bodies have synchonous group-meetings; " ++ show (
map (Control.Arrow.first Aggregate.StudentBody.getMnemonic) . Data.Map.toList $ Data.Map.map Data.Map.toList synchronousMeetingsByTimeByStudentBodyMnemonic
)
), -- Pair.
let
synchronousMeetingsByTimeByTeacherId = findSynchronousMeetingsByTimeByTeacherId problemParameters
in (
not $ Data.Map.null synchronousMeetingsByTimeByTeacherId,
"the following teachers have synchonous group-meetings; " ++ show (Data.Map.toList $ Data.Map.map Data.Map.toList synchronousMeetingsByTimeByTeacherId)
), -- Pair.
case calculateNTimeslotsPerDay $ getTimeslotIdBounds problemParameters of
1 -> (
hasAnyFreePeriodPreference problemParameters,
"a " ++ show Temporal.FreePeriodPreference.tag ++ " is meaningless with only one time-slot per day"
) -- Pair.
2 -> (
let
hasTerminalFreePeriodPreference :: Data.HumanResource.HumanResource resource => Data.Resource.ResourceMap resourceId resource -> Bool
hasTerminalFreePeriodPreference = Data.Foldable.any $ (== Just Temporal.FreePeriodPreference.Terminal) . Data.HumanResource.getMaybeFreePeriodPreference
in uncurry (||) $ (
hasTerminalFreePeriodPreference . getStudentBodyRegister &&& hasTerminalFreePeriodPreference . getTeacherRegister
) problemParameters,
"'" ++ Temporal.FreePeriodPreference.tag ++ "=" ++ show Temporal.FreePeriodPreference.Terminal ++ "' is redundant when there are only two time-slots per day"
) -- Pair.
_ -> (False, undefined)
]
]