weekdaze-0.0.0.1: src-lib/WeekDaze/ProblemConfiguration/ProblemAnalysis.hs
{-
Copyright (C) 2013-2015 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@]
* Loosely connected utilities, which rely only on the 'ProblemConfiguration.ProblemParameters.ProblemParameters',
rather than on the dynamically changing state of the /timetable/.
* As a result, some of these functions can effectively be cached, after the problem has been defined.
-}
module WeekDaze.ProblemConfiguration.ProblemAnalysis(
-- * Types
-- ** Type-synonyms
-- CoursesByTeacherIdBySubjectByStudentBody,
-- DistinctTimeslotRunlengthsByCourse,
-- MeetingResourcesByTime,
-- LessonsByStudentBody,
-- LessonsByStudentBodyByDay,
-- StudentBodiesBySynchronisationId,
-- ** Data-types
ProblemAnalysis(
-- MkProblemAnalysis,
getCoursesByTeacherIdBySynchronisationId,
getCoursesRequestingSeparatedLessonsWithinAnyDay,
getDistinctRunlengthsOfSpecifiedTimesByCourse,
-- getDistinctSubjects,
getFreeLocationViewTimetable,
getFreeStudentViewTimetable,
getFreeTeacherViewTimetable,
getMeetingResourcesByTime,
getMeetingsByTime,
getNAvailableDaysPerStudentTimetable,
getNAvailableDaysPerTeacherTimetable,
getNStudents,
getNTeachers,
-- getNTeacherInternalAvailabilityGaps,
-- getSpecifiedTimes,
getNTimeslotsPerDay,
getRequiredLessonCombinationsByStudentBody,
getRequiredLessonCombinationsByStudentBodyAvailableByDay,
getStudentBodiesBySynchronisationId,
getSubjectsWithAnIdealTimeslotRequest,
getSuitableCoursesByTeacherIdBySubjectByStudentBody,
getTimeslotIdRange,
getTotalLessonsPerWeekByFacilityName
),
-- * Functions
countStudentPlaces,
-- findCoursesFor,
findCourseFor,
findCourseForLocationViewLesson,
findCourseForTeacherViewLesson,
findDistinctSynchronisationIds,
-- findMeetingsByTime,
-- findMeetingResourcesByTime,
-- findCoursesRequestingSeparatedLessonsWithinAnyDay,
-- findSubjectsWithAnIdealTimeslotRequest,
-- findDistinctRunlengthsOfSpecifiedTimesByCourse,
-- findStudentBodiesBySynchronisationId,
-- findSuitableCoursesByTeacherIdBySubjectByStudentBody,
-- findSuitableLocations,
lookupCourseFor,
-- generateRequiredLessonCombinationsByStudentBody,
-- generateRequiredLessonCombinationsByStudentBodyAvailableByDay,
-- ** Constructor
mkProblemAnalysis,
-- ** Predicates
areAnyResourcesBookedForGroupMeeting,
areAvailableResources,
doAllCoursesSatifyingAnotherKnowledgeRequirementSpecifyThisTime,
isFree,
isValidTimeslotId
) where
import Control.Arrow((&&&), (***))
import Data.Map((!))
import qualified Data.Array.IArray
import qualified Data.Foldable
import qualified Data.Map
import qualified Data.Maybe
import qualified Data.Set
import qualified Factory.Data.Interval
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.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.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.LocationView.Lesson as LocationView.Lesson
import qualified WeekDaze.LocationView.Timetable as LocationView.Timetable
import qualified WeekDaze.Model.Lesson as Model.Lesson
import qualified WeekDaze.Model.Meeting as Model.Meeting
import qualified WeekDaze.Model.Timetable as Model.Timetable
import qualified WeekDaze.ProblemConfiguration.ProblemParameters as ProblemConfiguration.ProblemParameters
import qualified WeekDaze.Size as Size
import qualified WeekDaze.StudentView.Lesson as StudentView.Lesson
import qualified WeekDaze.StudentView.LessonResourceIds as StudentView.LessonResourceIds
import qualified WeekDaze.StudentView.Timetable as StudentView.Timetable
import qualified WeekDaze.StudentView.TimetableCoordinates as StudentView.TimetableCoordinates
import qualified WeekDaze.TeacherView.Lesson as TeacherView.Lesson
import qualified WeekDaze.TeacherView.Timetable as TeacherView.Timetable
import qualified WeekDaze.Temporal.Availability as Temporal.Availability
import qualified WeekDaze.Temporal.Day as Temporal.Day
import qualified WeekDaze.Temporal.Time as Temporal.Time
import qualified WeekDaze.Temporal.TimeslotRequest as Temporal.TimeslotRequest
{- |
* A map by /student-body/, of maps by /subject/, of maps by /teacherId/, of /course/s.
* CAVEAT: it is assumed that no /teacher/ can offer more than one suitable /course/.
-}
type CoursesByTeacherIdBySubjectByStudentBody synchronisationId level teacherId timeslotId = Data.Map.Map Aggregate.StudentBody.StudentBody (Data.Map.Map (Data.Subject.Subject level) (Data.Map.Map teacherId (Data.Course.Course synchronisationId level timeslotId)))
-- | Sets of distinct /time-slot/ run-lengths, for each /course/.
type DistinctTimeslotRunlengthsByCourse synchronisationId level timeslotId = Data.Map.Map (Data.Course.Course synchronisationId level timeslotId) (Data.Set.Set Size.NTimeslots)
-- | A map by /time/, of sets of /resource/s.
type MeetingResourcesByTime timeslotId locationId teacherId = Data.Map.Map (Temporal.Time.Time timeslotId) (Data.Set.Set locationId, Data.Set.Set Aggregate.StudentBody.StudentBody, Data.Set.Set teacherId)
-- | A map by /student-body/, of /lesson/s; as returned by 'generateRequiredLessonCombinationsByStudentBody'.
type LessonsByStudentBody locationId teacherId level = Data.Map.Map Aggregate.StudentBody.StudentBody [StudentView.Lesson.Lesson locationId teacherId level]
-- | A map by /day/, of maps by /student-body/, of /lesson/s; as returned by 'generateRequiredLessonCombinationsByStudentBodyAvailableByDay'.
type LessonsByStudentBodyByDay locationId teacherId level = Data.Map.Map Temporal.Day.Day (LessonsByStudentBody locationId teacherId level)
-- | A map by /synchronisationId/, of /student-bodies/ who require a /subject/ from one of the corresponding /course/s.
type StudentBodiesBySynchronisationId synchronisationId = Data.Map.Map synchronisationId (Data.Set.Set Aggregate.StudentBody.StudentBody)
-- | The results of the analysis of 'ProblemConfiguration.ProblemParameters.ProblemParameters'.
data ProblemAnalysis level locationId synchronisationId teacherId timeslotId = MkProblemAnalysis {
getCoursesByTeacherIdBySynchronisationId :: Aggregate.TeacherRegister.CoursesByTeacherIdBySynchronisationId synchronisationId teacherId level timeslotId, -- ^ Index /course/s by the /teacherId/ who offers them, & by their /synchronisationId/.
getCoursesRequestingSeparatedLessonsWithinAnyDay :: Data.Set.Set (Data.Course.Course synchronisationId level timeslotId), -- ^ Identifies /course/s which specify /time/s, separated within a single /day/.
getDistinctRunlengthsOfSpecifiedTimesByCourse :: DistinctTimeslotRunlengthsByCourse synchronisationId level timeslotId, -- ^ The distinct runlengths of consecutive specified /time/s, per /course/.
getDistinctSubjects :: Data.Subject.Knowledge level, -- ^ The distinct /subject/s amongst the /service/s offered by /teacher/s.
getFreeLocationViewTimetable :: LocationView.Timetable.Timetable locationId timeslotId teacherId level, -- ^ An unallocated /timetable/ indexed by /location/s.
getFreeStudentViewTimetable :: StudentView.Timetable.Timetable timeslotId locationId teacherId level, -- ^ An unallocated /timetable/ indexed by /student-bodies/.
getFreeTeacherViewTimetable :: TeacherView.Timetable.Timetable teacherId timeslotId locationId level, -- ^ An unallocated /timetable/ indexed by /teacher/s.
getMeetingResourcesByTime :: MeetingResourcesByTime timeslotId locationId teacherId, -- ^ A map by /time/, of the /resource/s used by the /meeting/s of /group/s. CAVEAT: /time/s at which zero /meeting/s are booked, don't exist in the map.
getMeetingsByTime :: Model.Meeting.MeetingsByTime timeslotId locationId teacherId, -- ^ A map by /time/, of the /meeting/s of /group/s with at least one member.
getNAvailableDaysPerStudentTimetable :: Size.NDays, -- ^ The number of /day/s in a /student-timetable/, each multiplied by the size of the /student-body/ for which it's applicable.
getNAvailableDaysPerTeacherTimetable :: Size.NDays, -- ^ The number of /day/s in a /teacher-timetable/.
getNStudents :: Size.NStudents, -- ^ Counts the number of /student/s on the register.
getNTeachers :: Size.NTeachers, -- ^ Counts the number of /teacher/s (excluding purely administrative staff) on the register.
getNTeacherInternalAvailabilityGaps :: Size.NDays, -- ^ Counts the non-terminal (/available/ <-> un/available/)-day boundaries, over all /teacher/s.
getNTimeslotsPerDay :: Size.NTimeslots, -- ^ The number of /time-slot/s in any /day/.
getRequiredLessonCombinationsByStudentBodyAvailableByDay:: LessonsByStudentBodyByDay locationId teacherId level, -- ^ The map by /day/, of maps by /student-body/, of combinations of possible /lesson/s.
getRequiredLessonCombinationsByStudentBody :: LessonsByStudentBody locationId teacherId level, -- ^ The map by /student-body/, of combinations of possible /lesson/s.
getSpecifiedTimes :: Temporal.Time.TimeSet timeslotId, -- ^ The set of all specified /time/s, for any /course/, offered by any /teacher/.
getStudentBodiesBySynchronisationId :: StudentBodiesBySynchronisationId synchronisationId, -- ^ A map by /synchronisationId/, of /student-bodies/ who require a /subject/ from one of the corresponding /course/s.
getSubjectsWithAnIdealTimeslotRequest :: Data.Set.Set (Data.Subject.Subject level), -- ^ The /subject/s, of the subset of all /course/s, which have an /ideal timeslot-request/.
getSuitableCoursesByTeacherIdBySubjectByStudentBody :: CoursesByTeacherIdBySubjectByStudentBody synchronisationId level teacherId timeslotId, -- ^ Catalogues all the /course/s, which match each of the /knowledge-requirement/s, of each /student-body/.
getTimeslotIdRange :: [timeslotId], -- ^ The range of /time-slot/s in any /day/.
getTotalLessonsPerWeekByFacilityName :: Data.Map.Map Data.Location.FacilityName Size.NTimeslots -- ^ The total /lesson/s per week, required by all those /course/s needing a specified /facility/. CAVEAT: only the subset of /facilities/ offered, which are also required, are indexed.
}
-- | Constructor.
mkProblemAnalysis :: (
Data.Array.IArray.Ix timeslotId,
Enum timeslotId,
Ord level,
Ord locationId,
Ord synchronisationId,
Ord teacherId,
RealFrac teachingRatio,
Show locationId,
Show teacherId
) => ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> ProblemAnalysis level locationId synchronisationId teacherId timeslotId
mkProblemAnalysis problemParameters = problemAnalysis where
timeslotIdBounds = ProblemConfiguration.ProblemParameters.getTimeslotIdBounds problemParameters
teacherRegister = ProblemConfiguration.ProblemParameters.getTeacherRegister problemParameters
studentBodies = Aggregate.StudentBodyRegister.getStudentBodies $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters
problemAnalysis = MkProblemAnalysis {
getCoursesByTeacherIdBySynchronisationId = Aggregate.TeacherRegister.findCoursesByTeacherIdBySynchronisationId teacherRegister,
getCoursesRequestingSeparatedLessonsWithinAnyDay = findCoursesRequestingSeparatedLessonsWithinAnyDay problemParameters,
getDistinctRunlengthsOfSpecifiedTimesByCourse = findDistinctRunlengthsOfSpecifiedTimesByCourse problemParameters,
getDistinctSubjects = Aggregate.TeacherRegister.extractDistinctSubjects teacherRegister,
getFreeLocationViewTimetable = Model.Timetable.mkFreeTimetable (Aggregate.LocationCatalogue.getLocationIds $ ProblemConfiguration.ProblemParameters.getLocationCatalogue problemParameters) timeslotIdBounds,
getFreeStudentViewTimetable = Model.Timetable.mkFreeTimetable studentBodies timeslotIdBounds,
getFreeTeacherViewTimetable = Model.Timetable.mkFreeTimetable (Aggregate.TeacherRegister.getTeacherIds teacherRegister) timeslotIdBounds,
getMeetingResourcesByTime = findMeetingResourcesByTime problemParameters,
getMeetingsByTime = findMeetingsByTime problemParameters,
getNAvailableDaysPerStudentTimetable = Aggregate.StudentBodyRegister.countAvailableStudentDays $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters,
getNAvailableDaysPerTeacherTimetable = Aggregate.TeacherRegister.countAvailableTeacherDays $ ProblemConfiguration.ProblemParameters.getTeacherRegister problemParameters,
getNStudents = Aggregate.StudentClass.getSize studentBodies,
getNTeachers = Data.Map.size $ Data.Map.filter Data.Teacher.offersService teacherRegister,
getNTeacherInternalAvailabilityGaps = Data.Map.foldr ((+) . Temporal.Availability.countInternalAvailabilityGaps . Data.Resource.getAvailability) 0 teacherRegister,
getNTimeslotsPerDay = ProblemConfiguration.ProblemParameters.calculateNTimeslotsPerDay timeslotIdBounds,
getRequiredLessonCombinationsByStudentBodyAvailableByDay = generateRequiredLessonCombinationsByStudentBodyAvailableByDay problemParameters problemAnalysis {-recurse-},
getRequiredLessonCombinationsByStudentBody = generateRequiredLessonCombinationsByStudentBody problemParameters,
getSpecifiedTimes = Aggregate.TeacherRegister.findSpecifiedTimes teacherRegister,
getStudentBodiesBySynchronisationId = findStudentBodiesBySynchronisationId problemParameters problemAnalysis {-recurse-},
getSubjectsWithAnIdealTimeslotRequest = findSubjectsWithAnIdealTimeslotRequest problemParameters,
getSuitableCoursesByTeacherIdBySubjectByStudentBody = findSuitableCoursesByTeacherIdBySubjectByStudentBody problemParameters,
getTimeslotIdRange = Factory.Data.Interval.toList timeslotIdBounds,
getTotalLessonsPerWeekByFacilityName = Aggregate.TeacherRegister.countLessonsPerWeekByFacilityName teacherRegister
}
{- |
* Catalogue by /time/, the /meeting/s of /group/s which have at least one member.
* CAVEAT: the map doesn't necessarily contain a key for each possible /time/.
-}
findMeetingsByTime :: (
Ord locationId,
Ord teacherId,
Ord timeslotId,
RealFrac teachingRatio,
Show locationId,
Show teacherId
) => ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Model.Meeting.MeetingsByTime timeslotId locationId teacherId
findMeetingsByTime problemParameters = Data.Set.foldr (
\groupId m -> let
(meetingTimes, maybeLocationId) = Data.Group.getMeetingTimes &&& Data.Group.getMaybeLocationId $ ProblemConfiguration.ProblemParameters.getGroupCatalogue problemParameters ! groupId
getResourceIdSet :: Aggregate.GroupCatalogue.ResourceIdsByGroupId resourceId -> Data.Set.Set resourceId
getResourceIdSet = Data.Maybe.fromMaybe Data.Set.empty . Data.Map.lookup groupId
in Data.Set.foldr (
(
$ (
Data.Set.singleton . uncurry (
Model.Meeting.mkMeeting groupId maybeLocationId
) . (
getResourceIdSet {-studentBody-set-} *** getResourceIdSet {-teacherId-set-}
) $ ProblemConfiguration.ProblemParameters.findHumanResourceIdsByGroupId problemParameters
) -- Section.
) . Data.Map.insertWith Data.Set.union
) m meetingTimes
) Data.Map.empty $ ProblemConfiguration.ProblemParameters.extractDistinctGroupMembership problemParameters
{- |
* Catalogues by /time/, the /resource/s used for /meeting/s of /group/s.
* CAVEAT: the map doesn't necessarily contain a key for each possible /time/.
-}
findMeetingResourcesByTime :: (
Ord locationId,
Ord teacherId,
Ord timeslotId,
RealFrac teachingRatio,
Show locationId,
Show teacherId
) => ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> MeetingResourcesByTime timeslotId locationId teacherId
findMeetingResourcesByTime problemParameters = Data.Map.map (
Data.Set.foldr (
\meeting (l, s, t) -> (
Data.Maybe.maybe l (`Data.Set.insert` l) $ Model.Meeting.getMaybeLocationId meeting,
s `Data.Set.union` Model.Meeting.getStudentClass meeting,
t `Data.Set.union` Model.Meeting.getTeacherIds meeting
) -- Triple
) (
Data.Set.empty,
Data.Set.empty,
Data.Set.empty
{-
) . Data.Set.filter (
Data.Group.getMandatesAttendance . (ProblemConfiguration.ProblemParameters.getGroupCatalogue problemParameters !) . Model.Meeting.getGroupId
-}
)
) $ findMeetingsByTime problemParameters
-- | True if any of the required /resource/s, are already required at the specified /time/, for a /meeting/ of a /group/.
areAnyResourcesBookedForGroupMeeting :: (
Ord locationId,
Ord teacherId,
Ord timeslotId
)
=> ProblemAnalysis level locationId synchronisationId teacherId timeslotId
-> Temporal.Time.Time timeslotId
-> Aggregate.StudentBody.StudentBody
-> locationId
-> teacherId
-> Bool
areAnyResourcesBookedForGroupMeeting problemAnalysis time studentBody locationId teacherId = Data.Maybe.maybe False (
\(locationIdSet, studentBodySet, teacherIdSet) -> or [
Data.Set.member locationId locationIdSet,
Data.Set.member studentBody studentBodySet, -- CAVEAT: performance-hotspot.
Data.Set.member teacherId teacherIdSet
]
) . Data.Map.lookup time $ getMeetingResourcesByTime problemAnalysis
-- | Catalogues by /teacherId/, all the /course/s which match each of the /knowledge-requirement/s, of each /student-body/.
findSuitableCoursesByTeacherIdBySubjectByStudentBody
:: Ord level
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> CoursesByTeacherIdBySubjectByStudentBody synchronisationId level teacherId timeslotId
findSuitableCoursesByTeacherIdBySubjectByStudentBody problemParameters = Data.Map.mapWithKey (
\studentBody studentProfile -> Data.Map.fromList . map (
\subject -> (
subject,
Aggregate.TeacherRegister.findSuitableCourseByTeacherId (Aggregate.StudentBody.getSize studentBody) subject . Data.Map.filter (
Data.Resource.isAvailable . (,) studentProfile -- Check that they're simultaneously available.
) $ ProblemConfiguration.ProblemParameters.getTeacherRegister problemParameters
) -- Pair.
) . Data.Set.toList $ Data.Student.deriveAmalgamatedKnowledgeRequirement studentProfile
) $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters
{- |
* Checks for the proposed /booking/, whether all suitable /course/s, for any one of the /student-body/'s other knowledge-requirements, specify the proposed /time/.
* CAVEAT: doesn't check whether the specified /subject/ can only be satisfied by a /synchronised course/,
& if so, whether any /student-body/ who requires a different /course/ from the same synchronised set,
has any other /knowledge-requirement/ which can only be satisfied by a /course/ which specifies the proposed /time/.
This tricky scenario is dealt with in the /lesson-criterion/ 'minimiseBookingAtAnotherCoursesSpecifiedTime' where the specific /course/ is known.
-}
doAllCoursesSatifyingAnotherKnowledgeRequirementSpecifyThisTime
:: (Ord level, Ord timeslotId)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> ProblemAnalysis level locationId synchronisationId teacherId timeslotId
-> Data.Subject.Subject level -- ^ The /subject/ of the proposed /lesson/.
-> StudentView.TimetableCoordinates.Coordinates timeslotId -- ^ The /student-body/ & /time/, of the proposed /booking/.
-> Bool
doAllCoursesSatifyingAnotherKnowledgeRequirementSpecifyThisTime problemParameters problemAnalysis subject (studentBody, time) = Data.Set.member time (
getSpecifiedTimes problemAnalysis
) {-required for efficiency-} && (
Data.Foldable.any (
Data.Foldable.all (
Data.Course.isASpecifiedTime time
) . (
getSuitableCoursesByTeacherIdBySubjectByStudentBody problemAnalysis ! studentBody !
) -- Check whether all possible courses matching this other subject-requirement, specify the same booking-time.
) . Data.Set.filter (
/= subject -- Find the other subjects required by this student-body.
) . Data.Student.deriveAmalgamatedKnowledgeRequirement $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters ! studentBody
)
{- |
* If the specified /teacher/ has a suitable personal /location/, then that is prefered.
* Otherwise, returns a list of /locationId/s, of sufficient /capacity/, & offering the /facilities/ required by the /course/ corresponding to the specified /subject/.
* CAVEAT: the list of /locationId/s returned isn't ordered by suitability.
-}
findSuitableLocations :: (
Eq level,
Ord locationId,
Ord teacherId
)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Size.NStudents -- ^ Required /capacity/.
-> Data.Subject.Subject level -- ^ The /subject/ to be taught.
-> teacherId -- ^ The /teacher/ who will be taking the /class/.
-> [locationId]
findSuitableLocations ProblemConfiguration.ProblemParameters.MkProblemParameters {
ProblemConfiguration.ProblemParameters.getLocationCatalogue = locationCatalogue,
ProblemConfiguration.ProblemParameters.getTeacherRegister = teacherRegister
} requiredCapacity subject teacherId
| Just ownLocationId <- maybeOwnLocationId
, Data.Location.isSuitable requiredCapacity requiredFacilityNames $ locationCatalogue ! ownLocationId = [ownLocationId] -- Confirm suitability of teacher's own location.
| otherwise = filter (
`Data.Set.notMember` Aggregate.TeacherRegister.extractDistinctOwnLocationIds teacherRegister -- Prevent squatting on another teacher's own location.
) . Aggregate.LocationCatalogue.getLocationIds $ Aggregate.LocationCatalogue.findSuitableLocations requiredCapacity requiredFacilityNames locationCatalogue
where
(maybeOwnLocationId, requiredFacilityNames) = Data.Teacher.getMaybeOwnLocationId . (teacherRegister !) &&& Data.Course.getRequiredFacilityNames . (Aggregate.TeacherRegister.findSuitableCourseByTeacherId requiredCapacity subject teacherRegister !) $ teacherId
{- |
* Gets the combinations of possible /lesson/s, required by each /student-body/.
* CAVEAT: though several /booking/s of a /lesson/ are typically required to satisfy the requirements of a single /course/,
only one instance for each viable combination of locationId & teacherId, is returned.
-}
generateRequiredLessonCombinationsByStudentBody :: (
Ord level,
Ord locationId,
Ord teacherId
)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> LessonsByStudentBody locationId teacherId level
generateRequiredLessonCombinationsByStudentBody problemParameters = Data.Map.mapWithKey (
\studentBody studentProfile -> let
requiredCapacity = Aggregate.StudentBody.getSize studentBody
in [
Model.Lesson.MkLesson {
Model.Lesson.getResourceIds = StudentView.LessonResourceIds.MkLessonResourceIds locationId teacherId,
Model.Lesson.getSubject = subject
} |
subject <- Data.Set.toList $ Data.Student.deriveAmalgamatedKnowledgeRequirement studentProfile,
(teacherId, teacherProfile) <- Data.Map.toList . Data.Map.filter (Data.Teacher.offersSuitableCourse requiredCapacity subject) $ ProblemConfiguration.ProblemParameters.getTeacherRegister problemParameters,
locationId <- findSuitableLocations problemParameters requiredCapacity subject teacherId,
Data.Resource.isAvailable (
studentProfile,
teacherProfile,
ProblemConfiguration.ProblemParameters.getLocationCatalogue problemParameters ! locationId
) -- Confirm mutual availability at some unspecified time.
] -- A list-comprehension, which composes lessons from combinations of subjects, teacherIds & locationIds.
) $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters
{- |
* Calls 'generateRequiredLessonCombinationsByStudentBody' to get the combinations of possible /lesson/s, required by each /student-body/.
* The list of /lesson/s for each /student-body/, is replicated & filtered for compatibility with each specific /day/.
* /Day/s on which there're zero available /student-bodies/, & /student-bodies/ for which there're zero /lesson/s, are included to facilitate lookup.
-}
generateRequiredLessonCombinationsByStudentBodyAvailableByDay :: (
Ord level,
Ord locationId,
Ord synchronisationId,
Ord teacherId,
Ord timeslotId
)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> ProblemAnalysis level locationId synchronisationId teacherId timeslotId
-> LessonsByStudentBodyByDay locationId teacherId level
generateRequiredLessonCombinationsByStudentBodyAvailableByDay problemParameters problemAnalysis = Data.Map.fromList $ map (
\day -> (
day,
Data.Map.mapWithKey (
\studentBody -> filter (
uncurry (&&) . (
areAvailableResources problemParameters day studentBody &&& Data.Maybe.maybe False {-no course found for lesson-} (
uncurry (&&) . (
Data.Maybe.maybe True {-not a synchronised course-} (
\synchronisationId -> Data.Foldable.all (
uncurry (||) . (
not . Data.Student.requiresAnySubjectBy (
`Data.Set.member` Data.Set.map Data.Course.getSubject (
Aggregate.TeacherRegister.findDistinctCoursesBySynchronisationId (getCoursesByTeacherIdBySynchronisationId problemAnalysis) ! synchronisationId
)
) &&& Data.Resource.isAvailableOn day
) -- Check that all students who require this synchronised course are available.
) $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters
) . Data.Course.getMaybeSynchronisationId &&& uncurry (||) . (
not . Data.Course.hasRigidlySpecifiedDays &&& Data.Course.isASpecifiedDay day
) -- Ensure that the course can be booked today.
) {-course-}
) . lookupCourseFor problemParameters
) {-lesson-}
) -- Filter the list of lessons for this student-body.
) $ generateRequiredLessonCombinationsByStudentBody problemParameters
) -- Pair.
) Temporal.Day.range
-- | True if the specified /student-body/, & the /location/ & /teacher/ referenced in the proposed /lesson/, are regularly available on the specified /day/.
areAvailableResources
:: (Ord locationId, Ord teacherId)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Temporal.Day.Day -- ^ The /day/ on which we want to book a /lesson/.
-> Aggregate.StudentBody.StudentBody
-> StudentView.Lesson.Lesson locationId teacherId level -- ^ The rendezvous we want to book.
-> Bool
areAvailableResources problemParameters day studentBody studentLesson = Data.Resource.isAvailableOn day (
ProblemConfiguration.ProblemParameters.getTeacherRegister problemParameters ! StudentView.LessonResourceIds.getTeacherId resourceIds,
ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters ! studentBody,
ProblemConfiguration.ProblemParameters.getLocationCatalogue problemParameters ! StudentView.LessonResourceIds.getLocationId resourceIds
) where
resourceIds = Model.Lesson.getResourceIds studentLesson
-- | True if the specified /human-resource/ is /available/ & not booked for a /meeting/ at the specified /time/.
isFree :: (
Data.HumanResource.HumanResource humanResource,
Data.Resource.Resource humanResource,
Ord timeslotId
)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Temporal.Time.Time timeslotId
-> humanResource
-> Bool
isFree problemParameters time = uncurry (&&) . (
Data.Resource.isAvailableOn (
Temporal.Time.getDay time
) &&& Data.Set.notMember time . Aggregate.GroupCatalogue.getMeetingTimes (
ProblemConfiguration.ProblemParameters.getGroupCatalogue problemParameters
) . Data.HumanResource.getGroupMembership
)
-- | Get the number of /student/-places, when accounting for the capacity-limits imposed by both the /course/ & /location/; but not for any /student/s already booked.
countStudentPlaces
:: Ord locationId
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> Data.Course.Course synchronisationId level timeslotId -- ^ The /course/ one wishes to query.
-> locationId -- ^ A specific /location/ in which the /course/ may be held.
-> Size.NStudents
countStudentPlaces problemParameters course = Data.Course.countStudentPlaces course . (ProblemConfiguration.ProblemParameters.getLocationCatalogue problemParameters !)
-- | Lookup the /course/ corresponding to the specified /lesson/.
lookupCourseFor
:: (Eq level, Ord teacherId)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> StudentView.Lesson.Lesson locationId teacherId level
-> Maybe (Data.Course.Course synchronisationId level timeslotId) -- ^ The /course/ of which the /lesson/ may be a part.
lookupCourseFor problemParameters = uncurry Data.Teacher.lookupCourseIn . (
Model.Lesson.getSubject &&& (ProblemConfiguration.ProblemParameters.getTeacherRegister problemParameters !) . StudentView.LessonResourceIds.getTeacherId . Model.Lesson.getResourceIds
)
{- |
* Find the unique /course/ corresponding to the specified /studentViewLesson/.
* CAVEAT: the specified /lesson/ is assumed to exist in the /timetable/, & therefore must have a corresponding /course/.
-}
findCourseFor
:: (Eq level, Ord teacherId)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> StudentView.Lesson.Lesson locationId teacherId level -- ^ The /studentViewLesson/ extracted from a /studentViewTimetable/.
-> Data.Course.Course synchronisationId level timeslotId -- ^ The /course/ of which the /lesson/ was a part.
findCourseFor problemParameters = Data.Maybe.fromJust . lookupCourseFor problemParameters
-- | Find the unique /course/ corresponding to the specified /locationViewLesson/.
findCourseForLocationViewLesson
:: (Eq level, Ord teacherId)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> locationId
-> LocationView.Lesson.Lesson teacherId level
-> Data.Course.Course synchronisationId level timeslotId
findCourseForLocationViewLesson problemParameters locationId = findCourseFor problemParameters . LocationView.Lesson.toStudentView locationId
-- | Find the unique /course/ corresponding to the specified /teacherViewLesson/.
findCourseForTeacherViewLesson
:: (Eq level, Ord teacherId)
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> teacherId
-> TeacherView.Lesson.Lesson locationId level
-> Data.Course.Course synchronisationId level timeslotId
findCourseForTeacherViewLesson problemParameters teacherId = findCourseFor problemParameters . TeacherView.Lesson.toStudentView teacherId
-- | Find the set of distinct run-lengths of consecutive specified /time/s, occuring within any single /day/, for each distinct /course/.
findDistinctRunlengthsOfSpecifiedTimesByCourse :: (
Enum timeslotId,
Ord level,
Ord synchronisationId,
Ord timeslotId
) => ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> DistinctTimeslotRunlengthsByCourse synchronisationId level timeslotId
findDistinctRunlengthsOfSpecifiedTimesByCourse = Data.Map.fromList . map (
id &&& Temporal.TimeslotRequest.findDistinctRunlengthsOfSpecifiedTimes . Data.Course.getTimeslotRequest
) . Data.Set.toList . Aggregate.TeacherRegister.extractDistinctCourses . ProblemConfiguration.ProblemParameters.getTeacherRegister
{- |
Some /course/s may specify /time/s separated within a single /day/,
which must be discovered to avoid erroneously downgrading those /timetable/s correctly booking /lesson/s in these /course/s.
-}
findCoursesRequestingSeparatedLessonsWithinAnyDay :: (
Enum timeslotId,
Ord level,
Ord synchronisationId,
Ord timeslotId
) => ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Data.Set.Set (Data.Course.Course synchronisationId level timeslotId)
findCoursesRequestingSeparatedLessonsWithinAnyDay = Data.Map.foldr (
Data.Set.union . Data.Set.filter Data.Course.requestsSeparatedTimelotsWithinAnyDay . Data.Teacher.getService
) Data.Set.empty . ProblemConfiguration.ProblemParameters.getTeacherRegister
-- | Find the /subject/s offered by those /course/s which have an /ideal timeslot-request/.
findSubjectsWithAnIdealTimeslotRequest :: (
Ord level,
Ord synchronisationId,
Ord timeslotId
) => ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId -> Data.Set.Set (Data.Subject.Subject level) {-coincidentally similar to Data.Subject.Knowledge-}
findSubjectsWithAnIdealTimeslotRequest = Data.Set.map Data.Course.getSubject . Data.Set.filter (
Temporal.TimeslotRequest.isIdeally . Data.Course.getTimeslotRequest
) . Aggregate.TeacherRegister.extractDistinctCourses . ProblemConfiguration.ProblemParameters.getTeacherRegister
{- |
* Returns a map by /synchronisationId/, of /student-bodies/ who require a /subject/ from one of the corresponding /course/s.
* If zero /student-bodies/ require any of the /subject/s offered with a specific /synchronisationId/, then an empty set exists in the returned map, rather than no entry at all.
-}
findStudentBodiesBySynchronisationId
:: Ord level
=> ProblemConfiguration.ProblemParameters.ProblemParameters campus level locationId stream synchronisationId teacherId teachingRatio timeslotId
-> ProblemAnalysis level locationId synchronisationId teacherId timeslotId
-> StudentBodiesBySynchronisationId synchronisationId
findStudentBodiesBySynchronisationId problemParameters = Data.Map.map (
\synchronisedCoursesByTeacherId -> Data.Map.keysSet $ Data.Map.filter (
Data.Foldable.any (
`Data.Set.member` Data.Set.fromList (map Data.Course.getSubject $ Data.Map.elems synchronisedCoursesByTeacherId)
) . Data.Student.deriveAmalgamatedKnowledgeRequirement
) $ ProblemConfiguration.ProblemParameters.getStudentBodyRegister problemParameters
) . getCoursesByTeacherIdBySynchronisationId
-- | True if the specified /timeslotId/ falls within the range defined for a /day/.
isValidTimeslotId
:: Eq timeslotId
=> ProblemAnalysis level locationId synchronisationId teacherId timeslotId
-> timeslotId
-> Bool
isValidTimeslotId problemAnalysis = (`elem` getTimeslotIdRange problemAnalysis)
-- | Returns the list of distinct /synchronisationId/s, specified amongst all /course/s.
findDistinctSynchronisationIds :: ProblemAnalysis level locationId synchronisationId teacherId timeslotId -> [synchronisationId]
findDistinctSynchronisationIds = Data.Map.keys . getCoursesByTeacherIdBySynchronisationId