packages feed

weekdaze-0.0.0.1: src-test/WeekDaze/Test/QuickCheck/StudentView/Timetable.hs

{-
	Copyright (C) 2013-2015 Dr. Alistair Ward

	This file is part of WeekDaze.

	WeekDaze is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	WeekDaze is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with WeekDaze.  If not, see <http://www.gnu.org/licenses/>.
-}
{- |
 [@AUTHOR@]	Dr. Alistair Ward

 [@DESCRIPTION@]	Defines /QuickCheck/-properties for 'StudentView.Timetable.Timetable'.
-}

module WeekDaze.Test.QuickCheck.StudentView.Timetable(
-- * Types
-- ** Type-synonyms
--	Timetable,
-- * Constants
	results,
-- * Functions
--	removeIncompatibleLessons,
--	reindexStudentBodyRegister
) where

import			Control.Arrow((***), (&&&))
import			Data.Map((!))
import			Data.Set((\\))
import qualified	Control.Arrow
import qualified	Data.Array.IArray
import qualified	Data.Foldable
import qualified	Data.List
import qualified	Data.Map
import qualified	Data.Maybe
import qualified	Data.Set
import qualified	Data.Tuple
import qualified	Test.QuickCheck
import			Test.QuickCheck((==>))
import qualified	ToolShed.Data.List
import qualified	ToolShed.Data.List.Runlength
import			ToolShed.Test.QuickCheck.Arbitrary.Array()
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.Data.Resource					as Data.Resource
import qualified	WeekDaze.Identifiers.Level				as Identifiers.Level
import qualified	WeekDaze.Identifiers.LocationId				as Identifiers.LocationId
import qualified	WeekDaze.Identifiers.TeacherId				as Identifiers.TeacherId
import qualified	WeekDaze.Identifiers.TimeslotId				as Identifiers.TimeslotId
import qualified	WeekDaze.LinearModel.Timetable				as LinearModel.Timetable
import qualified	WeekDaze.LocationView.Timetable				as LocationView.Timetable
import qualified	WeekDaze.Model.GeneralisedBooking			as Model.GeneralisedBooking
import qualified	WeekDaze.Model.Lesson					as Model.Lesson
import qualified	WeekDaze.Model.ResourceUser				as Model.ResourceUser
import qualified	WeekDaze.Model.Timetable				as Model.Timetable
import qualified	WeekDaze.Model.TimetableCoordinates			as Model.TimetableCoordinates
import qualified	WeekDaze.Model.TimetableForWeek				as Model.TimetableForWeek
import qualified	WeekDaze.StudentView.LessonResourceIds			as StudentView.LessonResourceIds
import qualified	WeekDaze.StudentView.Timetable				as StudentView.Timetable
import qualified	WeekDaze.TeacherView.Timetable				as TeacherView.Timetable
import qualified	WeekDaze.Temporal.Day					as Temporal.Day
import qualified	WeekDaze.Temporal.Time					as Temporal.Time
import qualified	WeekDaze.Test.QuickCheck.Aggregate.StudentBodyRegister	as Test.Aggregate.StudentBodyRegister
import qualified	WeekDaze.Test.QuickCheck.StudentView.Lesson		as Test.StudentView.Lesson
import qualified	WeekDaze.Test.QuickCheck.Temporal.Time			as Test.Temporal.Time

-- | Remove incompatible /lesson/s.
removeIncompatibleLessons :: (
	Data.Array.IArray.Ix	timeslotId,
	Eq			locationId,
	Eq			level,
	Eq			teacherId
 ) => StudentView.Timetable.Timetable timeslotId locationId teacherId level -> StudentView.Timetable.Timetable timeslotId locationId teacherId level
removeIncompatibleLessons studentViewTimetable	= foldr (
	\(coordinates, _) studentViewTimetable'	-> if Model.ResourceUser.areMutuallyMergeable $ Model.Timetable.extractSynchronousLessonsAt (
		Model.TimetableCoordinates.getTime coordinates
	) studentViewTimetable'
		then studentViewTimetable'
		else Model.Timetable.undefineTimeslot coordinates studentViewTimetable'
 ) studentViewTimetable {-initial value-} $ LinearModel.Timetable.fromTimetable studentViewTimetable

-- | Change the keys (/student-bodies/) of an arbitrary /studentBodyRegister/ to match those of the specified /studentViewTimetable/.
reindexStudentBodyRegister
	:: StudentView.Timetable.Timetable timeslotId locationId teacherId level
	-> Aggregate.StudentBodyRegister.StudentBodyRegister level stream teachingRatio
	-> Aggregate.StudentBodyRegister.StudentBodyRegister level stream teachingRatio
reindexStudentBodyRegister studentViewTimetable studentBodyRegister
	| Data.Map.size studentViewTimetable > Data.Map.size studentBodyRegister	= error "WeekDaze.Test.QuickCheck.StudentView.Timetable.reindexStudentBodyRegister:\tStudentBodyRegister too small."
	| otherwise	= Data.Map.fromList $ zipWith (
		\(k, _) (_, v)	-> (k, v)
	) (
		Data.Map.toList studentViewTimetable
	) (
		Data.Map.toList studentBodyRegister
	)

-- | Defines a concrete type for testing.
type Timetable	= StudentView.Timetable.Timetable Identifiers.TimeslotId.TimeslotId Identifiers.LocationId.LocationId Identifiers.TeacherId.TeacherId Identifiers.Level.Level

-- | The constant test-results for this data-type.
results :: IO [Test.QuickCheck.Result]
results = sequence [
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_purge,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_areBookedResourcesAt,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_teacherView,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_locationView,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_breaksRoutine,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_extractStudentClassAt,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_findStudentClassesByLesson,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_weightedMeanStudentBodyCombinationsPerLesson1,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_findSeparatedEqualLessonRunlengthsByStartingTimeslotIdByLessonByDayByObserverId,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_findStudentClassesByLessonFor,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_countLessons,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_weightedMeanStudentBodyCombinationsPerLesson2,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_extractSynchronousLessonsAt,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_findGeneralisedLessonRunlengthsByCoordinates,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_findSeparatedEqualLessonsWithinAnyDayByObserverId,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_weightedMeanStudentBodyCombinationsPerLesson3,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_calculateMeanStudentClassSize,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay,
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay',
	Test.QuickCheck.quickCheckWithResult Test.QuickCheck.stdArgs {Test.QuickCheck.maxSuccess = 256} prop_extractDistinctLessons
 ] where
	prop_purge, prop_areBookedResourcesAt, prop_teacherView, prop_locationView, prop_breaksRoutine, prop_extractStudentClassAt, prop_findStudentClassesByLesson, prop_weightedMeanStudentBodyCombinationsPerLesson1, prop_findSeparatedEqualLessonRunlengthsByStartingTimeslotIdByLessonByDayByObserverId :: Timetable -> Test.QuickCheck.Property

	prop_purge studentViewTimetable	= Test.QuickCheck.label "prop_purge" $ Model.Timetable.purge studentViewTimetable == Model.Timetable.undefineTimeslots studentViewTimetable (Model.Timetable.extractCoordinates studentViewTimetable)

	prop_areBookedResourcesAt studentViewTimetable	= Test.QuickCheck.label "prop_areBookedResourcesAt" $ all (
		($ studentViewTimetable) . StudentView.Timetable.areBookedResourcesAt . Control.Arrow.first Model.TimetableCoordinates.getTime
	 ) $ LinearModel.Timetable.fromTimetable studentViewTimetable

	prop_teacherView studentViewTimetable	= Test.QuickCheck.label "prop_teacherView" $ TeacherView.Timetable.toStudentViewTimetable (
		Model.Timetable.mkFreeTimetable (StudentView.Timetable.getStudentBodies validStudentViewTimetable) timeslotIdBounds
	 ) (
		TeacherView.Timetable.fromStudentViewTimetable True (
			Model.Timetable.mkFreeTimetable (
				Data.Set.toList . foldr (
					Data.Set.insert . StudentView.LessonResourceIds.getTeacherId . Model.Lesson.getResourceIds
				) Data.Set.empty $ Model.Timetable.extractLessons validStudentViewTimetable
			) timeslotIdBounds
		) validStudentViewTimetable
	 ) == validStudentViewTimetable where
		timeslotIdBounds		= (minBound, maxBound)
		validStudentViewTimetable	= removeIncompatibleLessons studentViewTimetable

	prop_locationView studentViewTimetable	= Test.QuickCheck.label "prop_locationView" $ LocationView.Timetable.toStudentViewTimetable (
		Model.Timetable.mkFreeTimetable (StudentView.Timetable.getStudentBodies validStudentViewTimetable) timeslotIdBounds
	 ) (
		LocationView.Timetable.fromStudentViewTimetable True (
			Model.Timetable.mkFreeTimetable (
				Data.Set.toList . foldr (
					Data.Set.insert . StudentView.LessonResourceIds.getLocationId . Model.Lesson.getResourceIds
				) Data.Set.empty $ Model.Timetable.extractLessons validStudentViewTimetable
			) timeslotIdBounds
		) validStudentViewTimetable
	 ) == validStudentViewTimetable where
		timeslotIdBounds		= (minBound, maxBound)
		validStudentViewTimetable	= removeIncompatibleLessons studentViewTimetable

	prop_breaksRoutine studentViewTimetable	= Test.QuickCheck.label "prop_breaksRoutine" . not . any (
		uncurry (StudentView.Timetable.breaksRoutine studentViewTimetable)
	 ) . map (
		Control.Arrow.first Model.TimetableCoordinates.getObserverId
	 ) $ LinearModel.Timetable.fromTimetable studentViewTimetable

	prop_extractStudentClassAt studentViewTimetable	= Test.QuickCheck.label "prop_extractStudentClassAt" . all (
		\(time, lesson) -> uncurry (&&) . (
			Data.Foldable.all (
				\studentBody -> Model.Timetable.getMaybeLesson (studentBody, time) studentViewTimetable == Just lesson
			) &&& Data.Foldable.all (
				\studentBody -> Model.Timetable.getMaybeLesson (studentBody, time) studentViewTimetable /= Just lesson
			) . (Data.Map.keysSet studentViewTimetable \\)
		) $ StudentView.Timetable.extractStudentClassAt (time, lesson) studentViewTimetable
	 ) . map (
		Control.Arrow.first Model.TimetableCoordinates.getTime
	 ) $ LinearModel.Timetable.fromTimetable studentViewTimetable

	prop_findStudentClassesByLesson studentViewTimetable	= Test.QuickCheck.label "prop_findStudentClassesByLesson" $ Data.Map.unionsWith Data.Set.union (
		map (StudentView.Timetable.findStudentClassesByLessonFor studentViewTimetable) $ StudentView.Timetable.getStudentBodies studentViewTimetable
	 ) == StudentView.Timetable.findStudentClassesByLesson studentViewTimetable

	prop_weightedMeanStudentBodyCombinationsPerLesson1 studentViewTimetable	= Test.QuickCheck.label "prop_weightedMeanStudentBodyCombinationsPerLesson1" $ StudentView.Timetable.calculateWeightedMeanStudentBodyCombinationsPerLesson studentViewTimetable == (calculateWeightedMeanStudentBodyCombinationsPerLesson' studentViewTimetable :: Rational) where
		calculateWeightedMeanStudentBodyCombinationsPerLesson' timetable
			| denominator == 0	= 1	-- (zero / zero) -> 1, at the limit of only one lesson-definition in the timetable.
			| otherwise		= fromIntegral numerator / fromIntegral denominator
			where
				(numerator, denominator)	= foldr (
					\(nStudentBodyCombinations, nStudents)	-> ((nStudentBodyCombinations * nStudents {-weighting-}) +) *** (nStudents +)
				 ) (0, 0) [
					(Data.Set.size studentBodyCombinations, Aggregate.StudentBody.getSize studentBody) |
						studentBody		<- StudentView.Timetable.getStudentBodies timetable,
						studentBodyCombinations	<- Data.Map.elems $ StudentView.Timetable.findStudentClassesByLessonFor timetable studentBody
				 ] -- List-comprehension. Scan the domain of all student-bodies & all the distinct lessons booked for them

	prop_findStudentClassesByLessonFor, prop_countLessons, prop_weightedMeanStudentBodyCombinationsPerLesson2, prop_extractSynchronousLessonsAt :: Timetable -> [(Test.Temporal.Time.Time, Test.StudentView.Lesson.Lesson)] -> Test.QuickCheck.Property
	prop_findStudentClassesByLessonFor studentViewTimetable linearTimetableForWeek	= Test.QuickCheck.label "prop_findStudentClassesByLessonFor" . (
		\studentViewTimetable'	-> all (
			Data.Foldable.all (== Data.Set.singleton (Data.Set.fromList studentBodies)) . StudentView.Timetable.findStudentClassesByLessonFor studentViewTimetable'
		) studentBodies
	 ) $ foldr Model.Timetable.defineTimeslot (
		Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
	 ) [
		((studentBody, time), Just lesson) |
			(time, lesson)	<- take Test.Temporal.Time.nTimeslotsPerWeek linearTimetableForWeek,
			studentBody	<- studentBodies
	 ] {-list-comprehension-} where
		studentBodies	= StudentView.Timetable.getStudentBodies studentViewTimetable

	prop_countLessons studentViewTimetable linearTimetableForWeek	= not (Data.Map.null studentViewTimetable) ==> Test.QuickCheck.label "prop_countLessons" $ Model.Timetable.countLessons (
		foldr Model.Timetable.defineTimeslot (Model.Timetable.purge studentViewTimetable) bookings	-- Start with a blank canvas.
	 ) == length bookings where
		bookings	= Data.List.nubBy (
			ToolShed.Data.List.equalityBy Model.GeneralisedBooking.getCoordinates	-- Ensure no bookings are overwritten.
		 ) $ zipWith (
			\studentBody (time, lesson) -> ((studentBody, time), Just lesson)
		 ) (cycle $ StudentView.Timetable.getStudentBodies studentViewTimetable) linearTimetableForWeek

	prop_weightedMeanStudentBodyCombinationsPerLesson2 studentViewTimetable linearTimetableForWeek	= Test.QuickCheck.label "prop_weightedMeanStudentBodyCombinationsPerLesson2" . (
		== (1 :: Rational)
	 ) . StudentView.Timetable.calculateWeightedMeanStudentBodyCombinationsPerLesson $ foldr Model.Timetable.defineTimeslot (
		Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
	 ) [
		((studentBody, time), Just lesson) |
			(time, lesson)	<- take Test.Temporal.Time.nTimeslotsPerWeek linearTimetableForWeek,
			studentBody	<- StudentView.Timetable.getStudentBodies studentViewTimetable
	 ] -- List-comprehension. Define lines, each using a constant lesson, through all student-bodies, at several times.

	prop_extractSynchronousLessonsAt studentViewTimetable linearTimetableForWeek	= Data.Map.size studentViewTimetable > 1 ==> Test.QuickCheck.label "prop_extractSynchronousLessonsAt" . all (
		(== 2) . length . (
			`Model.Timetable.extractSynchronousLessonsAt` (
				foldr Model.Timetable.defineTimeslot (
					Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
				) $ concatMap (
					\pair -> map ($ pair) [
						(,) studentBodyF *** Just,
						(,) studentBodyL *** Just
					] -- Construct a booking for the same lesson, for two different student-bodies.
				) linearTimetableForWeek
			)
		) -- Section.
	 ) $ map Model.TimetableForWeek.getBookedTime linearTimetableForWeek where
		(studentBodyF, studentBodyL)	= head &&& last $ StudentView.Timetable.getStudentBodies studentViewTimetable

	prop_weightedMeanStudentBodyCombinationsPerLesson3 :: Test.Temporal.Time.Time -> Test.StudentView.Lesson.Lesson -> Timetable -> Test.QuickCheck.Property
	prop_weightedMeanStudentBodyCombinationsPerLesson3 time lesson studentViewTimetable	= Data.Map.size studentViewTimetable > 1 ==> Test.QuickCheck.label "prop_weightedMeanStudentBodyCombinationsPerLesson3" . (
		== result
	 ) . StudentView.Timetable.calculateWeightedMeanStudentBodyCombinationsPerLesson . foldr Model.Timetable.defineTimeslot (
		Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
	 ) . uncurry (++) . (
		(`zip` lessons) . (`zip` repeat time) *** (`zip` lessons) . (`zip` repeat (rotate (Temporal.Time.getDay time) `Temporal.Time.mkTime` Temporal.Time.getTimeslotId time))
	 ) $ (
		init &&& tail	-- Create two classes.
	 ) studentBodies where
		rotate :: (Bounded a, Enum a, Eq a) => a -> a
		rotate x
			| x == maxBound	= minBound
			| otherwise	= succ x

		lessons :: [Maybe Test.StudentView.Lesson.Lesson]
		lessons	= repeat $ Just lesson

		studentBodies :: [Aggregate.StudentBody.StudentBody]
		studentBodies	= StudentView.Timetable.getStudentBodies studentViewTimetable

		result	= (/ fromIntegral (Aggregate.StudentClass.getSize studentBodies)) . toRational $ uncurry (+) (
			Aggregate.StudentBody.getSize . head &&& Aggregate.StudentBody.getSize . last $ studentBodies	-- Two student-bodies are members of only one class.
		 ) + (
			(* 2) . Aggregate.StudentClass.getSize . init $ tail studentBodies	-- All but two student-bodies, are members of two classes, for the single lesson-definition.
		 )

	prop_findGeneralisedLessonRunlengthsByCoordinates :: Timetable -> [Temporal.Day.Day] -> [Test.StudentView.Lesson.Lesson] -> Test.QuickCheck.Property
	prop_findGeneralisedLessonRunlengthsByCoordinates studentViewTimetable days lessons	= Test.QuickCheck.label "prop_findGeneralisedLessonRunlengthsByCoordinates" . all (
		(== Test.Temporal.Time.nTimeslotsPerDay) . ToolShed.Data.List.Runlength.getLength . snd {-runlength-code-}
	 ) $ Model.Timetable.findGeneralisedLessonRunlengthsByCoordinates (
		foldr Model.Timetable.defineTimeslot (
			Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
		) [
			((studentBody, Temporal.Time.mkTime day timeslotId), Just lesson) |
				studentBody	<- StudentView.Timetable.getStudentBodies studentViewTimetable,	-- Range over all student-bodies.
				(day, lesson)	<- Data.List.nub days `zip` lessons,				-- Assign one type of lesson to each day.
				timeslotId	<- [minBound .. maxBound]					-- Range over all timeslots.
		] -- List-comprehension. Book surfaces of constant lesson, on a variety of days.
	 )

	prop_findSeparatedEqualLessonsWithinAnyDayByObserverId :: Timetable -> [Temporal.Day.Day] -> [Test.StudentView.Lesson.Lesson] -> Test.QuickCheck.Property
	prop_findSeparatedEqualLessonsWithinAnyDayByObserverId studentViewTimetable days lessons	= not (null lessons) ==> Test.QuickCheck.label "prop_findSeparatedEqualLessonsWithinAnyDayByObserverId" . Data.Foldable.all (
		all ((== 2) . ToolShed.Data.List.Runlength.getLength)
	 ) $ Model.Timetable.findSeparatedEqualLessonsWithinAnyDayByObserverId (
		foldr Model.Timetable.defineTimeslot (
			Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
		 ) [
			((studentBody, Temporal.Time.mkTime day timeslotId), Just lesson) |
				studentBody	<- StudentView.Timetable.getStudentBodies studentViewTimetable,	-- Range over all student-bodies.
				(day, lesson)	<- Data.List.nub days `zip` cycle lessons,			-- Assign one type of lesson to each day
				timeslotId	<- [minBound, maxBound]						-- Duplicate the lesson at the beginning & the end of the day.
		 ] -- List-comprehension.
	 )

	prop_findSeparatedEqualLessonRunlengthsByStartingTimeslotIdByLessonByDayByObserverId studentViewTimetable	= Test.QuickCheck.label "prop_findSeparatedEqualLessonRunlengthsByStartingTimeslotIdByLessonByDayByObserverId" $ Data.Map.map (
		Data.Foldable.concatMap (
			map (
				Control.Arrow.first length . Data.Tuple.swap	-- Discard the startingTimeslotId & individual runlengths.
			)
		) -- Discard the 'day'.
	 ) (
		Model.Timetable.findSeparatedEqualLessonRunlengthsByStartingTimeslotIdByLessonByDayByObserverId studentViewTimetable
	 ) == Model.Timetable.findSeparatedEqualLessonsWithinAnyDayByObserverId studentViewTimetable

	prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay :: Test.StudentView.Lesson.Lesson -> Timetable -> Test.Aggregate.StudentBodyRegister.StudentBodyRegister -> Test.QuickCheck.Property
	prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay lesson studentViewTimetable studentBodyRegister	= Data.Map.size studentBodyRegister >= Data.Map.size studentViewTimetable ==> Test.QuickCheck.label "prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay" . (== (0 :: Rational)) . Model.Timetable.calculateAverageAbsoluteDeviationOfFreeLessonsPerDay (

		reindexStudentBodyRegister studentViewTimetable studentBodyRegister
	 ) . foldr Model.Timetable.defineTimeslot studentViewTimetable . zip (Model.Timetable.extractCoordinates studentViewTimetable) . map Just $ repeat lesson

	prop_calculateMeanStudentClassSize :: Test.StudentView.Lesson.Lesson -> Timetable -> Test.QuickCheck.Property
	prop_calculateMeanStudentClassSize lesson studentViewTimetable = not (Data.Map.null studentViewTimetable) ==> Test.QuickCheck.label "prop_calculateMeanStudentClassSize" . (
		== toRational (Aggregate.StudentClass.getSize $ StudentView.Timetable.getStudentBodies studentViewTimetable)
	 ) . TeacherView.Timetable.calculateMeanStudentClassSize . TeacherView.Timetable.fromStudentViewTimetable True (
		Model.Timetable.mkFreeTimetable [
			StudentView.LessonResourceIds.getTeacherId $ Model.Lesson.getResourceIds lesson
		] (minBound, maxBound)
	 ) . foldr Model.Timetable.defineTimeslot (Model.Timetable.purge studentViewTimetable) . zip (Model.Timetable.extractCoordinates studentViewTimetable) . map Just $ repeat lesson

	prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay' :: Test.StudentView.Lesson.Lesson -> Timetable -> Test.Aggregate.StudentBodyRegister.StudentBodyRegister -> Test.QuickCheck.Property
	prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay' lesson studentViewTimetable studentBodyRegister	= Data.Map.size studentBodyRegister >= Data.Map.size studentViewTimetable && not (Data.Map.null studentViewTimetable') ==> Test.QuickCheck.label "prop_calculateAverageAbsoluteDeviationOfFreeLessonsPerDay'" . (
		== (1 :: Rational)
	 ) . Model.Timetable.calculateAverageAbsoluteDeviationOfFreeLessonsPerDay studentBodyRegister' $ Data.Map.mapWithKey (
		\studentBody timetableForWeek	-> foldr Model.TimetableForWeek.defineTimeslot timetableForWeek . (
			`zip` repeat (Just lesson)	-- Construct a booking.
		) . concatMap (
			\l -> let
				(day1, day2)	= head &&& last $ l
			in Temporal.Time.mkTime day1 maxBound : map (Temporal.Time.mkTime day2) (take 3 [minBound ..])
		) . ToolShed.Data.List.chunk 2 $ filter (`Data.Resource.isAvailableOn` (studentBodyRegister' ! studentBody)) Temporal.Day.range
	 ) studentViewTimetable' where
		studentBodyRegister'	= reindexStudentBodyRegister studentViewTimetable studentBodyRegister

		studentViewTimetable'	= Data.Map.filterWithKey (
			\studentBody _ -> even . Data.Resource.countDaysPerWeekAvailable $ studentBodyRegister' ! studentBody
		 ) $ Model.Timetable.purge studentViewTimetable	-- Select student-bodies who're available on an even number of days.

	prop_extractDistinctLessons :: [Test.Temporal.Time.Time] -> [Test.StudentView.Lesson.Lesson] -> Timetable -> Test.QuickCheck.Property
	prop_extractDistinctLessons times lessons studentViewTimetable	= Test.QuickCheck.label "prop_extractDistinctLessons" . (== Data.Set.fromList (Data.Maybe.mapMaybe Model.GeneralisedBooking.getMaybeLesson generalisedBookings)) . Model.Timetable.extractDistinctLessons $ foldr Model.Timetable.defineTimeslot (
		Model.Timetable.purge studentViewTimetable	-- Start with a blank canvas.
	 ) generalisedBookings where
		generalisedBookings	= zip (zip (StudentView.Timetable.getStudentBodies studentViewTimetable) $ Data.List.nub times) . map Just $ Data.List.nub lessons