packages feed

weekdaze-0.0.0.1: src-lib/WeekDaze/Aggregate/TeacherRegister.hs

{-# LANGUAGE CPP, FlexibleContexts #-}
{-
	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@]	Describes all the /teacher/s available to be booked in the timetable, in terms of their name & their attributes.
-}

module WeekDaze.Aggregate.TeacherRegister(
-- * Types
-- ** Type-synonyms
	TeacherRegister,
	CoursesByTeacherId,
	CoursesByTeacherIdBySynchronisationId,
	NTimeslotsByTeacherIdBySubject,
-- * Constants
	tag,
-- * Functions
	calculateWorkloadBoundsBySubject,
	countAvailableTeacherDays,
	extractDistinctCourses,
	extractDistinctOwnLocationIds,
	extractDistinctSubjects,
--	extractDistinctRequiredFacilityNames,
	findSuitableCourseByTeacherId,
	findCoursesByTeacherIdBySynchronisationId,
	findDistinctCoursesBySynchronisationId,
	findSpecifiedTimes,
	findSubjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek,
	mergeConstraintsOnSynchronisedCourses,
	countLessonsPerWeekByFacilityName,
-- ** Accessors
	getTeacherIds,
	getTeacherProfiles,
#ifdef USE_HDBC
-- ** Constructor
	fromDatabase,
#endif
-- ** Predicates
	hasAnyFreePeriodPreference,
	hasAnySynchronisedCourses,
	hasAnyIdealTimeslotRequests,
	hasAnyCourseMaximumClassSizes,
	hasAnySpecificTimeRequests,
	hasAnyTimeslotRequests,
	hasAnySpecialists,
	isInhabited
) where

import qualified	Control.Arrow
import			Control.Arrow((&&&), (***))
import qualified	Data.Foldable
import qualified	Data.Map
import			Data.Map((!))
import qualified	Data.Maybe
import qualified	Data.Set
import qualified	WeekDaze.Data.Course			as Data.Course
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.Subject			as Data.Subject
import qualified	WeekDaze.Data.Teacher			as Data.Teacher
import qualified	WeekDaze.Size				as Size
import qualified	WeekDaze.Temporal.Time			as Temporal.Time
import qualified	WeekDaze.Temporal.TimeslotRequest	as Temporal.TimeslotRequest
import qualified	WeekDaze.Temporal.Workload		as Temporal.Workload

#ifdef USE_HDBC
import qualified	Database.HDBC
import qualified	Data.Convertible
import qualified	Data.IntMap
import qualified	Data.Typeable
import qualified	WeekDaze.Database.Selector		as Database.Selector
import qualified	WeekDaze.Data.Group			as Data.Group
import qualified	WeekDaze.Temporal.Availability		as Temporal.Availability
import qualified	WeekDaze.Temporal.Day			as Temporal.Day
import qualified	WeekDaze.Temporal.FreePeriodPreference	as Temporal.FreePeriodPreference

{- |
	* Construct from the specified database-connection.

	* CAVEAT: though the database may not permit a null value for many fields (applying its own default value when the value is unspecified),
	default values are applied here should the SQL-query return one.
-}
fromDatabase :: (
	Database.HDBC.IConnection	connection,
	Data.Convertible.Convertible	Database.HDBC.SqlValue level,			-- Flexible context.
	Data.Convertible.Convertible	Database.HDBC.SqlValue locationId,		-- 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.Typeable.Typeable		teachingRatio,
	Ord				level,
	Ord				synchronisationId,
	Ord				teacherId,
	Ord				timeslotId,
	RealFrac			teachingRatio,
	Show				level,
	Show				synchronisationId,
	Show				timeslotId
 )
	=> connection			-- ^ An abstract database-connection.
	-> Database.HDBC.SqlValue	-- ^ The project-id.
	-> IO (TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio)
fromDatabase connection	projectIdSql	= let
	idealTimeslotRequestColumnName, teacherRegisterIdColumnName :: Database.Selector.ColumnName
	idealTimeslotRequestColumnName	= "idealTimeslotRequest"
	teacherRegisterIdColumnName	= showString tag "Id";

	requiredFacilityTableName, serviceTableName, specialtyTopicTableName, specificTimeRequestsTableName, teacherGroupMembershipTableName, teacherRegisterTableName :: Database.Selector.TableName
	[requiredFacilityTableName, serviceTableName, specialtyTopicTableName, specificTimeRequestsTableName, teacherGroupMembershipTableName, teacherRegisterTableName]	= map (showString Database.Selector.tablePrefix) ["requiredFacility", Data.Teacher.serviceTag, Data.Teacher.specialtyTopicTag, "specificTimeRequest", "teacherGroupMembership", tag]
 in do
	facilityNameByFacilityTypeId	<- Data.Location.findFacilityNameByFacilityTypeId connection projectIdSql

#ifdef USE_HDBC_ODBC
	[
		selectRequiredFacilitiesForTeacherRegisterId,
		selectSpecificTimeRequestsForTeacherRegisterId,
		selectServiceForTeacherRegisterId,
		selectGroupIdsForTeacherRegisterId,
		selectSpecialtyTopicForTeacherRegisterId
	 ] <- mapM (
		\(columnNames, tableName)	-> Database.Selector.prepare connection columnNames [tableName] [teacherRegisterIdColumnName]
	 ) [
		(
			[
				Data.Subject.topicTag,
				Data.Subject.levelTag,
				Data.Location.facilityTypeIdTag
			],
			requiredFacilityTableName
		), (
			[
				Data.Subject.topicTag,
				Data.Subject.levelTag,
				Temporal.Day.tag,
				Database.Selector.timeslotIdColumnName
			],
			specificTimeRequestsTableName
		), (
			[
				Data.Subject.topicTag,
				Data.Subject.levelTag,
				Data.Course.requiredLessonsPerWeekTag,
				Data.Course.minimumConsecutiveLessonsTag,
				Data.Course.maximumClassSizeTag,
				Database.Selector.synchronisationIdColumnName,
				idealTimeslotRequestColumnName
			],
			serviceTableName
		), (
			[Data.Group.groupIdTag],
			teacherGroupMembershipTableName
		), (
			[Data.Subject.topicTag],
			specialtyTopicTableName
		)
	 ] -- Prepare statements for execution with each teacherRegisterId.
#endif /* USE_HDBC_ODBC */
	Database.Selector.select connection [
		teacherRegisterIdColumnName,
		Database.Selector.teacherIdColumnName,
		Temporal.Availability.tag,
		Database.Selector.locationIdColumnName,
		Data.Teacher.maximumTeachingRatioTag,
		Temporal.FreePeriodPreference.tag
	 ] [teacherRegisterTableName] [(Database.Selector.projectIdColumnName, projectIdSql)] >>= fmap Data.Map.fromList . mapM (
		\teacherRow -> case teacherRow of
			[
				teacherRegisterIdSql,
				teacherIdSql,
				availabilitySql,
				locationIdSql,
				maximumTeachingRatioSql,
				freePeriodPreferenceSql
			 ] -> do
#ifndef USE_HDBC_ODBC
				let primaryKey	= [(teacherRegisterIdColumnName, teacherRegisterIdSql)]
#endif
				requiredFacilityNamesBySubject	<- (
					Data.Map.fromListWith Data.Set.union . map (
						\requiredFacilityRow -> case requiredFacilityRow of
							[topicSql, levelSql, facilityTypeIdSql]	-> let
								facilityTypeId	= Data.Maybe.fromMaybe (
									error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tnull " $ shows Data.Location.facilityTypeIdTag "."
								 ) . either (
									error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tfailed to parse the value for " . shows Data.Location.facilityTypeIdTag . showString " read from the database; " . show
								 ) id $ Database.HDBC.safeFromSql facilityTypeIdSql
							 in (
								(,) (Data.Subject.mkSubjectFromSql topicSql levelSql) . Data.Set.singleton
							 ) . Data.Maybe.fromMaybe (
								error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tunknown " . showString Data.Location.facilityTypeIdTag . showChar '=' $ shows facilityTypeId "."
							 ) $ Data.IntMap.lookup facilityTypeId facilityNameByFacilityTypeId
							_					-> error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tunexpected number of columns=" . shows (length requiredFacilityRow) . showString " in row of table " $ shows requiredFacilityTableName "."
					)
#ifdef USE_HDBC_ODBC
				 ) `fmap` (
					Database.HDBC.execute selectRequiredFacilitiesForTeacherRegisterId [teacherRegisterIdSql] >> Database.HDBC.fetchAllRows' selectRequiredFacilitiesForTeacherRegisterId
				 )
#else
				 ) `fmap` Database.Selector.select connection [
					Data.Subject.topicTag,
					Data.Subject.levelTag,
					Data.Location.facilityTypeIdTag
				 ] [requiredFacilityTableName] primaryKey
#endif
				specificTimeRequestsBySubject	<- (
					Data.Map.fromListWith Data.Set.union . map (
						\specificTimeRow -> case specificTimeRow of
							[topic, level, day, timeslotId]	-> (
								Data.Subject.mkSubjectFromSql topic level,
								Data.Set.singleton $ Temporal.Time.mkTimeFromSql day timeslotId
							 ) -- Pair
							_				-> error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tunexpected number of columns=" . shows (length specificTimeRow) . showString " in row of table " $ shows specificTimeRequestsTableName "."
					)
#ifdef USE_HDBC_ODBC
				 ) `fmap` (
					Database.HDBC.execute selectSpecificTimeRequestsForTeacherRegisterId [teacherRegisterIdSql] >> Database.HDBC.fetchAllRows' selectSpecificTimeRequestsForTeacherRegisterId
				 )
#else
				 ) `fmap` Database.Selector.select connection [
					Data.Subject.topicTag,
					Data.Subject.levelTag,
					Temporal.Day.tag,
					Database.Selector.timeslotIdColumnName
				 ] [specificTimeRequestsTableName] primaryKey
#endif
				service	<- (
					Data.Set.fromList . map (
						\courseRow -> case courseRow of
							[topicSql, levelSql, requiredLessonsPerWeekSql, minimumConsecutiveLessonsSql, maximumClassSizeSql, synchronisationIdSql, idealTimeslotRequestSql]	-> let
								subject	= Data.Subject.mkSubjectFromSql topicSql levelSql
							 in Data.Course.mkCourse subject (
								Data.Maybe.fromMaybe (
									error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tnull " $ shows Data.Course.requiredLessonsPerWeekTag "."
								) . either (
									error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tfailed to parse the value for " . shows Data.Course.requiredLessonsPerWeekTag . showString " read from the database; " . show
								) id $ Database.HDBC.safeFromSql requiredLessonsPerWeekSql
							 ) (
								Data.Maybe.fromMaybe Data.Set.empty $ Data.Map.lookup subject requiredFacilityNamesBySubject
							 ) (
								Data.Maybe.maybe (
									Temporal.TimeslotRequest.Specifically . Data.Maybe.fromMaybe Data.Set.empty $ Data.Map.lookup subject specificTimeRequestsBySubject
								) Temporal.TimeslotRequest.Ideally $ Database.HDBC.fromSql idealTimeslotRequestSql
							 ) (
								Data.Maybe.fromMaybe Data.Course.defaultMinimumConsecutiveLessons . either (
									error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tfailed to parse the value for " . shows Data.Course.minimumConsecutiveLessonsTag . showString " read from the database; " . show
								) id $ Database.HDBC.safeFromSql minimumConsecutiveLessonsSql
							 ) (
								either (
									error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tfailed to parse the value for " . shows Data.Course.maximumClassSizeTag . showString " read from the database; " . show
								) id $ Database.HDBC.safeFromSql maximumClassSizeSql	-- Returns Nothing for SqlNull.
							 ) $ Database.HDBC.fromSql synchronisationIdSql	-- Returns Nothing for SqlNull.
							_	-> error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tunexpected number of columns=" . shows (length courseRow) . showString " in row of table " $ shows serviceTableName "."
					)
#ifdef USE_HDBC_ODBC
				 ) `fmap` (
					Database.HDBC.execute selectServiceForTeacherRegisterId [teacherRegisterIdSql] >> Database.HDBC.fetchAllRows' selectServiceForTeacherRegisterId
				 )
#else
				 ) `fmap` Database.Selector.select connection [
					Data.Subject.topicTag,
					Data.Subject.levelTag,
					Data.Course.requiredLessonsPerWeekTag,
					Data.Course.minimumConsecutiveLessonsTag,
					Data.Course.maximumClassSizeTag,
					Database.Selector.synchronisationIdColumnName,
					idealTimeslotRequestColumnName
				 ] [serviceTableName] primaryKey
#endif
				groupMembership	<- (
					Data.Set.fromList . map (
						Data.Maybe.fromMaybe (
							error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tnull " $ shows Data.Group.groupIdTag "."
						) . Database.HDBC.fromSql . head {-select the only column-}
					)
#ifdef USE_HDBC_ODBC
				 ) `fmap` (
					Database.HDBC.execute selectGroupIdsForTeacherRegisterId [teacherRegisterIdSql] >> Database.HDBC.fetchAllRows' selectGroupIdsForTeacherRegisterId
				 )
#else
				 ) `fmap` Database.Selector.select connection [Data.Group.groupIdTag] [teacherGroupMembershipTableName] primaryKey
#endif
				maybeSpecialtyTopic	<- (
					Data.Maybe.listToMaybe . map (
						Database.HDBC.fromSql . head {-select the only column-}
					)
#ifdef USE_HDBC_ODBC
				 ) `fmap` (
					Database.HDBC.execute selectSpecialtyTopicForTeacherRegisterId [teacherRegisterIdSql] >> Database.HDBC.fetchAllRows' selectSpecialtyTopicForTeacherRegisterId
				 )
#else
				 ) `fmap` Database.Selector.select connection [Data.Subject.topicTag] [specialtyTopicTableName] primaryKey
#endif
				return {-to IO-monad-} (
					Data.Maybe.fromMaybe (
						error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tnull " $ shows Database.Selector.teacherIdColumnName "."
					) $ Database.HDBC.fromSql teacherIdSql,
					Data.Teacher.mkProfile service (
						Database.HDBC.fromSql locationIdSql	-- Returns 'Nothing' for SqlNull.
					) (
						Data.Maybe.fromMaybe (
							error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tnull " $ shows Temporal.Availability.tag "."
						) $ Database.HDBC.fromSql availabilitySql
					) (
						Database.Selector.fromSqlFractional (
							error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tnull " $ shows Data.Teacher.maximumTeachingRatioTag "."
						) maximumTeachingRatioSql
					) groupMembership maybeSpecialtyTopic $ Database.HDBC.fromSql freePeriodPreferenceSql	-- Returns Nothing for SqlNull.
				 ) -- Pair.
			_ -> error . showString "WeekDaze.Aggregate.TeacherRegister.fromDatabase:\tunexpected number of columns=" . shows (length teacherRow) . showString " in row of table " $ shows teacherRegisterTableName "."
	 )
#endif /* USE_HDBC */

-- | Used to qualify XML.
tag :: String
tag	= "teacherRegister"

-- | The complete set of 'Data.Teacher.Profile', indexed by 'teacherId'.
type TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio	= Data.Resource.ResourceMap teacherId (Data.Teacher.Profile synchronisationId level timeslotId locationId teachingRatio)

-- | Accessor.
getTeacherIds :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> [teacherId]
getTeacherIds	= Data.Map.keys

-- | Accessor.
getTeacherProfiles :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> [Data.Teacher.Profile synchronisationId level timeslotId locationId teachingRatio]
getTeacherProfiles	= Data.Map.elems

-- | Extracts the set of all 'Data.Course.Course's, from the /teacher-register/.
extractDistinctCourses :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Data.Teacher.Service synchronisationId level timeslotId
extractDistinctCourses	= Data.Map.foldr (Data.Set.union . Data.Teacher.getService) Data.Set.empty

-- | Extracts the set of distinct 'locationId's, from the /teacher-register/.
extractDistinctOwnLocationIds :: Ord locationId => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Data.Location.Locus locationId
extractDistinctOwnLocationIds	= Data.Map.foldr (\profile locus -> Data.Maybe.maybe locus (`Data.Set.insert` locus) $ Data.Teacher.getMaybeOwnLocationId profile) Data.Set.empty

-- | Extracts the set of all 'Data.Subject.Subject's, from the /teacher-register/.
extractDistinctSubjects :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Data.Subject.Knowledge level
extractDistinctSubjects	= Data.Set.map Data.Course.getSubject . extractDistinctCourses

-- | Extracts the set of distinct required 'Data.Location.Facility's, from the /teacher-register/.
extractDistinctRequiredFacilityNames :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Data.Location.FacilityNames
extractDistinctRequiredFacilityNames	= Data.Set.foldr (Data.Set.union . Data.Course.getRequiredFacilityNames) Data.Set.empty . extractDistinctCourses

{- |
	* Determine the total number of /lesson/s required for each /subject/, according to the 'Data.Course.Course's offered.

	* Since each /course/ may be offered by more than one 'teacherId', the actual workload can only be constrained within bounds.
-}
calculateWorkloadBoundsBySubject :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Data.Map.Map (Data.Subject.Subject level) Temporal.Workload.Bounds
calculateWorkloadBoundsBySubject	= Data.Map.map (minimum &&& maximum) . Data.Map.fromListWith (++) . map (Data.Course.getSubject &&& return {-to List-monad-} . Data.Course.getRequiredLessonsPerWeek) . Data.Set.toList . extractDistinctCourses

-- | Find those /teacher/s offering a /course/ in the specified /subject/.
findSuitableCourseByTeacherId
	:: Eq level
	=> Size.NStudents	-- ^ The number of /student/s, who need to be placed.
	-> Data.Subject.Subject level
	-> TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio
	-> Data.Map.Map teacherId (Data.Course.Course synchronisationId level timeslotId)
findSuitableCourseByTeacherId nStudents subject	= Data.Map.mapMaybe (Data.Teacher.lookupSuitableCourse nStudents subject)

-- | The sum of the number of /day/s worked by each /teacher/.
countAvailableTeacherDays :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Size.NDays
countAvailableTeacherDays	= Data.Map.foldr ((+) . Data.Resource.countDaysPerWeekAvailable) 0

{- |
	* The total number of /lesson/s per week, required by all those /course/s requiring each /facility/.

	* CAVEAT: all /course/s are counted, not just those for which there's some demand from /student/s.
-}
countLessonsPerWeekByFacilityName :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Data.Map.Map Data.Location.FacilityName Size.NTimeslots
countLessonsPerWeekByFacilityName	= Data.Map.foldr (
	\profile m	-> Data.Set.foldr (
		\course m'	-> Data.Set.foldr (
			Data.Map.insertWith (+) `flip` Data.Course.getRequiredLessonsPerWeek course
		) m' $ Data.Course.getRequiredFacilityNames course
	) m $ Data.Teacher.getService profile
 ) Data.Map.empty

-- | True if a /teacher/ typically inhabits the specified 'locationId'.
isInhabited :: Eq locationId => locationId -> TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
isInhabited locationId	= Data.Foldable.any (Data.Teacher.inhabits locationId)

-- | A map indexed by /teacherId/, of /course/s.
type CoursesByTeacherId synchronisationId teacherId level timeslotId	= Data.Map.Map teacherId (Data.Course.Course synchronisationId level timeslotId)

-- | A map indexed by /synchronisationId/, of 'CoursesByTeacherId'.
type CoursesByTeacherIdBySynchronisationId synchronisationId teacherId level timeslotId	= Data.Map.Map synchronisationId (CoursesByTeacherId synchronisationId teacherId level timeslotId)

{- |
	* Returns /course/s which reference a /synchronisationId/, indexed by that /synchronisationId/, sub-indexed by the /teacher-Id/ who offers them.

	* CAVEAT: If a /teacher/ erroneously offers two /course/s with the same /synchronisationId/, then the second will overwrite the first.

	* CAVEAT: /course/s are returned irrespective of whether any /student/ requires them.
-}
findCoursesByTeacherIdBySynchronisationId :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	teacherId,
	Ord	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> CoursesByTeacherIdBySynchronisationId synchronisationId teacherId level timeslotId
findCoursesByTeacherIdBySynchronisationId	= Data.Map.foldrWithKey (
	\teacherId -> flip $ Data.Set.foldr (
		uncurry (Data.Map.insertWith Data.Map.union) . Control.Arrow.second (Data.Map.singleton teacherId)
	)
 ) Data.Map.empty . Data.Map.map (
	Data.Set.map (
		Data.Maybe.fromJust . Data.Course.getMaybeSynchronisationId &&& id
	) . Data.Set.filter Data.Course.isSynchronised . Data.Teacher.getService
 )

-- | Returns the set of /course/s corresponding to each /synchronisationId/, irrespective of whether they're required by any /student/.
findDistinctCoursesBySynchronisationId :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	timeslotId
 ) => CoursesByTeacherIdBySynchronisationId synchronisationId teacherId level timeslotId -> Data.Map.Map synchronisationId (Data.Set.Set (Data.Course.Course synchronisationId level timeslotId))
findDistinctCoursesBySynchronisationId	= Data.Map.map $ Data.Set.fromList . Data.Map.elems

-- | True if any /teacher/ has specified a /free-period preference/.
hasAnyFreePeriodPreference :: RealFrac teachingRatio => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnyFreePeriodPreference	= Data.Foldable.any Data.HumanResource.hasFreePeriodPreference

-- | True if any /teacher/ has offered a /synchronised course/ in their /service/.
hasAnySynchronisedCourses :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnySynchronisedCourses	= Data.Foldable.any Data.Teacher.offersAnySynchronisedCourse

-- | True if any /teacher/ has requested an /ideal timeslot/ in their /service/.
hasAnyIdealTimeslotRequests :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnyIdealTimeslotRequests	= Data.Foldable.any Data.Teacher.hasAnyIdealTimeslotRequest

-- | True if any /teacher/ has specified a /maximum class-size/ in their /service/.
hasAnyCourseMaximumClassSizes :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnyCourseMaximumClassSizes	= Data.Foldable.any (Data.Maybe.isJust . Data.Course.getMaybeMaximumClassSize) . extractDistinctCourses

-- | True if any /teacher/ has requested a /specific time/ in their /service/.
hasAnySpecificTimeRequests :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnySpecificTimeRequests	= Data.Foldable.any Data.Teacher.hasAnySpecificTimeRequest

-- | True if any /teacher/ has requested a /specific time/ in their /service/.
hasAnyTimeslotRequests :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnyTimeslotRequests	= Data.Foldable.any (uncurry (||) . (Data.Teacher.hasAnyIdealTimeslotRequest &&& Data.Teacher.hasAnySpecificTimeRequest))

-- | True if any /teacher/ has requested an /ideal timeslot/ in their /service/.
hasAnySpecialists :: TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Bool
hasAnySpecialists	= Data.Foldable.any Data.Teacher.hasSpecialtyTopic

-- | Find the set of all specified /time/s, for any /course/, offered by any /teacher/.
findSpecifiedTimes :: Ord timeslotId => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> Temporal.Time.TimeSet timeslotId
findSpecifiedTimes	= Data.Map.foldr (Data.Set.union . Data.Teacher.findSpecifiedTimes) Data.Set.empty

-- | The type returned by 'subjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek'.
type NTimeslotsByTeacherIdBySubject level teacherId	= Data.Map.Map (Data.Subject.Subject level) (Data.Map.Map teacherId Size.NTimeslots)

-- | Finds /subject/s offered by more than one /teacher/, but as /course/s requiring different /lesson/s per week.
findSubjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek
	:: (Ord level, Ord teacherId)
	=> TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio
	-> NTimeslotsByTeacherIdBySubject level teacherId
findSubjectsOfferedInMultipleCoursesRequiringDifferentLessonsPerWeek	= Data.Map.filter (
	(> 1) . Data.Set.size . Data.Set.fromList . Data.Map.elems {-lessonsPerWeek-}
 ) . Data.Map.foldrWithKey (
	\teacherId teacherProfile m	-> Data.Set.foldr (
		\course -> uncurry (
			Data.Map.insertWith Data.Map.union
		) (
			Data.Course.getSubject &&& Data.Map.singleton teacherId . Data.Course.getRequiredLessonsPerWeek $ course
		)
	) m $ Data.Teacher.getService teacherProfile
 ) Data.Map.empty

-- | Tightens the constraints on each member of a set of synchronised /course/s, to a level compatible with the requirements of other members.
mergeConstraintsOnSynchronisedCourses :: (
	Ord	level,
	Ord	synchronisationId,
	Ord	teacherId,
	Ord	timeslotId,
	Show	synchronisationId,
	Show	timeslotId
 ) => TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio -> TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio
mergeConstraintsOnSynchronisedCourses teacherRegister	= Data.Map.map (
	\profile -> profile {
		Data.Teacher.getService	= Data.Set.map (
			\course -> Data.Maybe.maybe course (
				\synchronisationId -> let
					distinctCourses	= findDistinctCoursesBySynchronisationId (findCoursesByTeacherIdBySynchronisationId teacherRegister) ! synchronisationId
				in course {
					Data.Course.getMinimumConsecutiveLessons	= Data.Set.findMax $ Data.Set.map Data.Course.getMinimumConsecutiveLessons distinctCourses,
					Data.Course.getTimeslotRequest			= let
						timeslotRequests			= Data.Set.map Data.Course.getTimeslotRequest distinctCourses
						(idealTimeslotIds, specifiedTimes)	= (
							Data.Set.map (Data.Maybe.fromJust . Temporal.TimeslotRequest.getMaybeIdealTimeslotId) *** Data.Set.unions . map Temporal.TimeslotRequest.getSpecifiedTimes . Data.Set.toList
						 ) $ Data.Set.partition Temporal.TimeslotRequest.isIdeally timeslotRequests

						hasZeroIdealTimeslotIds, hasZeroSpecifiedTimes :: Bool
						hasZeroIdealTimeslotIds	= Data.Set.null idealTimeslotIds
						hasZeroSpecifiedTimes	= Data.Set.null specifiedTimes
					in if hasZeroIdealTimeslotIds && hasZeroSpecifiedTimes
						then Data.Course.getTimeslotRequest course	-- Unchanged.
						else if Data.Set.size idealTimeslotIds == 1 && hasZeroSpecifiedTimes
							then Temporal.TimeslotRequest.Ideally $ Data.Set.findMin idealTimeslotIds
							else if hasZeroIdealTimeslotIds && not hasZeroSpecifiedTimes
								then Temporal.TimeslotRequest.Specifically specifiedTimes
								else error . showString "WeekDaze.Aggregate.TeacherRegister.mergeConstraintsOnSynchronisedCourses:\tincompatible TimeslotRequests; " $ shows (synchronisationId, Data.Set.toList timeslotRequests) "."
				}
			 ) $ Data.Course.getMaybeSynchronisationId course
		) $ Data.Teacher.getService profile
	}
 ) teacherRegister