packages feed

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)
		]
	 ]