{-# LANGUAGE CPP #-} #ifdef MIN_TOOL_VERSION_ghc /* CAVEAT: early versions of Cabal don't define this */ #if MIN_TOOL_VERSION_ghc(8,0,1) {-# OPTIONS_GHC -freduction-depth=22 #-} #endif #else {-# OPTIONS_GHC -fcontext-stack=22 #-} #endif {- 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 . -} {- | [@AUTHOR@] Dr. Alistair Ward [@DESCRIPTION@] * Reads the configuration, either from the specified local XML file, or from the specified database on the referenced DBMS. * Override this, by parsing the remainder of the command-line arguments, to determine transient configuration. * Attempts to solves the configured problem. * Present the results in each of the specified output file-formats. * . -} module Main(main) where import Control.Arrow((***), (&&&)) import Control.Category((>>>)) import qualified Control.Arrow import qualified Control.Exception import qualified Control.Monad import qualified Control.Monad.Writer -- The lazy instance. import qualified Data.Default import qualified Data.List import qualified Data.Map import qualified Data.Maybe import qualified Data.Set import qualified Data.Time.Clock import qualified Data.Version import qualified Distribution.Verbosity import qualified Factory.Data.Interval import qualified Paths_weekdaze as Paths -- Either local stub, or package-instance autogenerated by 'Setup build'. import qualified System.Console.GetOpt as G import qualified System.Directory import qualified System.Environment import qualified System.Exit import qualified System.FilePath import System.FilePath((), (<.>)) import qualified System.IO import qualified System.Info import qualified System.Random import qualified Text.Printf import qualified Text.XML.HXT.Core as HXT import qualified ToolShed.Data.List import qualified ToolShed.SelfValidate import qualified ToolShed.System.File import qualified WeekDaze.Aggregate.StudentBody as Aggregate.StudentBody import qualified WeekDaze.Configuration as Configuration import qualified WeekDaze.Data.Group as Data.Group import qualified WeekDaze.Data.Subject as Data.Subject import qualified WeekDaze.Dynamic.StudentViewTimetableUtilities as Dynamic.StudentViewTimetableUtilities -- import WeekDaze.Enhanced.EnhancedDouble() -- Required when some of the 'Rational' types below are replaced by 'Double'. import WeekDaze.Enhanced.EnhancedRatio() import qualified WeekDaze.ExecutionConfiguration.EvolutionStrategies as ExecutionConfiguration.EvolutionStrategies import qualified WeekDaze.ExecutionConfiguration.ExecutionOptions as ExecutionConfiguration.ExecutionOptions import qualified WeekDaze.ExecutionConfiguration.OptimiseLessonCriteriaWeights as ExecutionConfiguration.OptimiseLessonCriteriaWeights import qualified WeekDaze.Identifiers.Campus as Identifiers.Campus import qualified WeekDaze.Identifiers.Level as Identifiers.Level import qualified WeekDaze.Identifiers.LocationId as Identifiers.LocationId import qualified WeekDaze.Identifiers.Stream as Identifiers.Stream import qualified WeekDaze.Identifiers.SynchronisationId as Identifiers.SynchronisationId import qualified WeekDaze.Identifiers.TeacherId as Identifiers.TeacherId import qualified WeekDaze.Identifiers.TimeslotId as Identifiers.TimeslotId import qualified WeekDaze.Implementation.EvolutionaryAlgorithm as Implementation.EvolutionaryAlgorithm import qualified WeekDaze.Implementation.TimetableFitness as Implementation.TimetableFitness import qualified WeekDaze.Input.CommandLineOption as Input.CommandLineOption import qualified WeekDaze.Input.ConfigVersion as Input.ConfigVersion import qualified WeekDaze.Input.Options as Input.Options import qualified WeekDaze.LocationView.LessonResourceIds as LocationView.LessonResourceIds import qualified WeekDaze.LocationView.Timetable as LocationView.Timetable import qualified WeekDaze.Model.Timetable as Model.Timetable import qualified WeekDaze.OutputConfiguration.FileFormat as OutputConfiguration.FileFormat import qualified WeekDaze.OutputConfiguration.Format as OutputConfiguration.Format import qualified WeekDaze.OutputConfiguration.Options as OutputConfiguration.Options import qualified WeekDaze.OutputConfiguration.Style as OutputConfiguration.Style import qualified WeekDaze.OutputConfiguration.Verbosity as OutputConfiguration.Verbosity import qualified WeekDaze.OutputConfiguration.View as OutputConfiguration.View import qualified WeekDaze.OutputFormat.XHTMLFormat as OutputFormat.XHTMLFormat import qualified WeekDaze.OutputFormat.XHTMLFormatLocationViewTimetable as OutputFormat.XHTMLFormatLocationViewTimetable import qualified WeekDaze.OutputFormat.XHTMLFormatStudentViewTimetable as OutputFormat.XHTMLFormatStudentViewTimetable import qualified WeekDaze.OutputFormat.XHTMLFormatTeacherViewTimetable as OutputFormat.XHTMLFormatTeacherViewTimetable import qualified WeekDaze.OutputFormat.DeterministicStudentViewTimetableSelection as OutputFormat.DeterministicStudentViewTimetableSelection import qualified WeekDaze.OutputFormat.EvolutionStrategyStatistics as OutputFormat.EvolutionStrategyStatistics import qualified WeekDaze.ProblemConfiguration.ProblemAnalysis as ProblemConfiguration.ProblemAnalysis import qualified WeekDaze.ProblemConfiguration.ProblemParameters as ProblemConfiguration.ProblemParameters import qualified WeekDaze.StudentView.LessonResourceIds as StudentView.LessonResourceIds import qualified WeekDaze.TeacherView.LessonResourceIds as TeacherView.LessonResourceIds import qualified WeekDaze.TeacherView.Timetable as TeacherView.Timetable import qualified WeekDaze.Text.CSS as Text.CSS #ifdef TOOL_VERSION_ghc import qualified GHC.Conc #endif #ifdef USE_HDBC import qualified Crypto.Hash import qualified Database.HDBC import qualified Data.Byteable import qualified Data.ByteString import qualified Data.ByteString.Char8 import qualified WeekDaze.Database.Selector as Database.Selector #ifdef USE_HDBC_ODBC import qualified Database.HDBC.ODBC import qualified WeekDaze.Database.ODBC as Database.ODBC #endif #ifdef USE_HDBC_MYSQL import qualified WeekDaze.Database.MySQL as Database.MySQL #endif #endif /* USE_HDBC */ #ifdef USE_UNIX import qualified WeekDaze.SignalHandlers as SignalHandlers #endif -- | Define a concrete type for the value of a criterion. type CriterionValue = Rational -- CAVEAT: avoid arithmetic on bounded rationals, which can easily overflow. -- | Define a concrete type for the weighting applied to criteria, used to bias their significance in the selection of /lesson/s or /timetable/s. type CriterionWeight = CriterionValue -- There's no obvious reason for it to differ from this. -- | Define a concrete type for the factor by which the fecundity of the breeding-program is amended, when the diversity of the population of candidates falls beneath the minimum acceptable. type FecundityDecayRatio = Rational -- CAVEAT: avoid arithmetic on bounded rationals, which can easily overflow. -- | Define a concrete type for the diversity-ratio in the population of candidates. type PopulationDiversityRatio = FecundityDecayRatio -- There's no obvious reason for it to differ from this. {- | * The concrete type used for IO of Fractional values. * This allows one to read 'Rational' values from the command-line, into this intermediate type, before conversion to the correct internal type, -} type FractionalIOFormat = Double -- | Define a concrete type for the ratio of a /teacher/'s working-week which is actually devoted to teaching. type TeachingRatio = Rational -- CAVEAT: avoid arithmetic on bounded rationals, which can easily overflow. -- | Define a concrete type for the weighted mean of; the criteria by which /lesson/s or /timetable/s, are selected. type WeightedMean = CriterionWeight -- There's no obvious reason for it to differ from this. -- | Define a concrete type for the mean of the values of /lesson-criteria/. type Mean = CriterionValue -- There's no obvious reason for it to differ from this. -- | Define a concrete type for the standard deviation of the criteria by which /lesson/s, are selected. type StandardDeviation = Double -- There's little choice, since it must implement 'Floating'. -- | Define a concrete instance of the underlying polymorphic type. type ProblemParameters = ProblemConfiguration.ProblemParameters.ProblemParameters Identifiers.Campus.Campus Identifiers.Level.Level Identifiers.LocationId.LocationId Identifiers.Stream.Stream Identifiers.SynchronisationId.SynchronisationId Identifiers.TeacherId.TeacherId TeachingRatio Identifiers.TimeslotId.TimeslotId -- | Define a concrete instance of the underlying polymorphic type. type ExecutionOptions = ExecutionConfiguration.ExecutionOptions.ExecutionOptions CriterionWeight FecundityDecayRatio PopulationDiversityRatio -- | Define a concrete type for the minimum acceptable contrast-ratio between the automatically generated foreground & complementary background-colours. type MinimumContrastRatio = Rational -- | Define a concrete instance of the underlying polymorphic type. type Options = Input.Options.Options Identifiers.Campus.Campus CriterionWeight FecundityDecayRatio Identifiers.Level.Level Identifiers.LocationId.LocationId MinimumContrastRatio PopulationDiversityRatio Identifiers.Stream.Stream Identifiers.SynchronisationId.SynchronisationId Identifiers.TeacherId.TeacherId TeachingRatio Identifiers.TimeslotId.TimeslotId -- | Define a concrete instance of the underlying polymorphic type. type ProblemAnalysis = ProblemConfiguration.ProblemAnalysis.ProblemAnalysis Identifiers.Level.Level Identifiers.LocationId.LocationId Identifiers.SynchronisationId.SynchronisationId Identifiers.TeacherId.TeacherId Identifiers.TimeslotId.TimeslotId -- | Express the specified file in XML, citing the referenced DTD (whether it exists or not), & write it to the file-system. writeXMLToFile :: HXT.XmlPickler a => System.FilePath.FilePath -> String -> System.FilePath.FilePath -> a -- ^ The pickleable. -> IO () writeXMLToFile destinationFilePath rootElementTag dtdFilePath x = do -- System.IO.hPutStrLn System.IO.stderr . showString "INFO:\twriting " . showString rootElementTag . showString "-configuration to file=" $ shows destinationFilePath "." System.IO.withFile destinationFilePath System.IO.WriteMode ( `System.IO.hPutStrLn` ( showString "\n\n" ( HXT.showPickled [ HXT.withIndent HXT.yes -- CAVEAT: corresponding input-option 'HXT.withRemoveWS' may subsequently be required. ] x ) ) ) -- Section. {- | * Defines the permissible command-line arguments. * Parses the XML configuration-file, to determine the persistent configuration. * Override this, by parsing the command-line arguments, to determine transient configuration. * Solve the resulting problem. * Present the results in each of the specified output file-formats. -} main :: IO () main = do -- Inspect the environment. startUTCTime <- Data.Time.Clock.getCurrentTime progName <- System.Environment.getProgName args <- System.Environment.getArgs dataDir <- fmap System.FilePath.normalise Paths.getDataDir -- Defined using 'cabal configure --datadir="" --datasubdir=""'. appUserDataDir <- System.Directory.getAppUserDataDirectory progName -- CAVEAT: on Unix/Gnu-Linux, requires the '$HOME' environment-variable to have been defined. let packageIdentifier, author :: String packageIdentifier = showString progName . showChar '-' $ showString (Input.ConfigVersion.toString $ Data.Version.versionBranch Paths.version) "" author = "Dr. Alistair Ward" inputConfigFilePathFlag :: Input.CommandLineOption.Flag inputConfigFilePathFlag = "inputConfigFilePath" #ifdef USE_HDBC #ifdef USE_HDBC_ODBC dsnFlag :: Input.CommandLineOption.Flag dsnFlag = "dsn" defaultDSN :: String defaultDSN = progName #else _x :: [Input.CommandLineOption.Flag] _x@[dataServerHostFlag, dataServerPortFlag, dataServerUserIdFlag, dataServerPasswordFlag, databaseNameFlag] = ["dataServerHost", "dataServerPort", "dataServerUserId", "dataServerPassword", "databaseName"] defaultDataServerUserId, defaultDatabaseName :: String defaultDataServerUserId = "root" defaultDatabaseName = progName #endif _y :: [Input.CommandLineOption.Flag] _y@[databaseUserIdFlag, databasePasswordFlag, databaseProjectNameFlag] = ["databaseUserId", "databasePassword", "databaseProjectName"] mandatoryDBFlags :: [Input.CommandLineOption.Flag] mandatoryDBFlags@[databaseUserIdFlag', databaseProjectNameFlag'] = map (showString Input.CommandLineOption.longFlagPrefix) [databaseUserIdFlag, databaseProjectNameFlag] #endif /* USE_HDBC */ odbcPackageName :: String odbcPackageName = "HDBC-odbc" mySQLPackageName :: String mySQLPackageName = "HDBC-mysql" {- Define all permissible command-line options. This specification is fairly complicated: Each command-line option is categorised as one of: an "IO-action", which is subsequently sequenced with other IO-actions, before exiting; a "Config-location Parameter", which if specified, locates a file or database containing more options; an "Options-mutator", which if specified, defines a function used to transform 'Input.CommandLineOption.CommandLineOption'. Additionally, the command-line options are recorded as they're processed, to enable display of the command-line in the results; if one merely displays 'args', then security-sensitive details may be revealed, & it's difficult to find & remove such options, since the associated flag may be truncated to a minimum unique string. -} optDescrList :: [ G.OptDescr ( Maybe (String {-flag-}, Maybe String {-optional value-}), -- Record command-line specifications. Input.CommandLineOption.CommandLineOption Options -- Defines the action to take on receipt of a command-line option. ) -- Pair. ] optDescrList = [ G.Option "?" ["help"] ( G.NoArg (Nothing {-N/A-}, Input.CommandLineOption.mkIOAction printHelp) ) $ showString infoString "print this help, & then exit.", G.Option "v" ["version"] ( G.NoArg (Nothing {-N/A-}, Input.CommandLineOption.mkIOAction printVersion) ) $ showString infoString "print version-information, & then exit.", G.Option "i" [inputConfigFilePathFlag] ( G.ReqArg (Just . (,) inputConfigFilePathFlag . Just &&& Input.CommandLineOption.mkConfigLocationParameter inputConfigFilePathFlag) $ mkTypeString filePathString ) ( showString inputString $ showString "define the path to an XML-file from which the configuration can be read" #ifdef USE_HDBC . showString ", as an alternative to " $ shows mandatoryDBFlags #endif "." ), #ifdef USE_HDBC #ifdef USE_HDBC_ODBC G.Option "" [dsnFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter dsnFlag) $ mkTypeString stringString ) . showString inputString . showString "define the data-source name from which to read ODBC DBMS connection-parameters; default " $ shows defaultDSN ".", #else G.Option "" [dataServerHostFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter dataServerHostFlag) $ mkTypeString stringString ) . showString inputString . showString "define the name or IP-address, of the host on which the RDBMS is running; default " $ shows Database.MySQL.defaultHost ".", G.Option "" [dataServerPortFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter dataServerPortFlag) $ mkTypeString intString ) . showString inputString . showString "define the port on which the RDBMS is listening; default '" $ shows Database.MySQL.defaultPort "'.", G.Option "" [dataServerUserIdFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter dataServerUserIdFlag) $ mkTypeString stringString ) . showString inputString . showString "define the user-id with which to authenticate with the referenced RDBMS; default " $ shows defaultDataServerUserId ".", G.Option "" [dataServerPasswordFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter dataServerPasswordFlag) $ mkTypeString stringString ) . showString inputString . showString "define the password common to all users, required for authentication with the referenced RDBMS; cf. " $ shows databasePasswordFlag ".", G.Option "" [databaseNameFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter databaseNameFlag) $ mkTypeString stringString ) . showString inputString . showString "define the name of the database on the referenced RDBMS; default " $ shows defaultDatabaseName ".", #endif G.Option "" [databaseProjectNameFlag] ( G.ReqArg (Just . (,) databaseProjectNameFlag . Just &&& Input.CommandLineOption.mkConfigLocationParameter databaseProjectNameFlag) $ mkTypeString stringString ) $ showString inputString "define the name of the project in the database from which to read the configuration.", G.Option "" [databaseUserIdFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter databaseUserIdFlag) $ mkTypeString stringString ) $ showString inputString "used to identify a user's configuration amongst all those in the database; typically one's email-address, since this is naturally unique.", G.Option "" [databasePasswordFlag] ( G.ReqArg ((,) Nothing {-security-sensitive-} . Input.CommandLineOption.mkConfigLocationParameter databasePasswordFlag) $ mkTypeString stringString ) $ showString inputString "define the password required to grant access to the user's personal configuration in the database.", #endif /* USE_HDBC */ G.Option "" [ExecutionConfiguration.EvolutionStrategies.fecundityDecayRatioTag] ( G.ReqArg (Just . (,) ExecutionConfiguration.EvolutionStrategies.fecundityDecayRatioTag . Just &&& Input.CommandLineOption.mkOptionsMutator . setFecundityDecayRatio) $ mkTypeString floatString ) . showString executionString $ "define the factor (in the closed unit-interval) by which the fecundity of the breeding-program for future generations, is multiplied (& thus reduced), when the population-diversity ratio falls beneath " ++ show ExecutionConfiguration.EvolutionStrategies.minimumPopulationDiversityRatioTag ++ "; default '" ++ Text.Printf.printf "%.*f" defaultNDecimalDigits ( ExecutionConfiguration.EvolutionStrategies.getFecundityDecayRatio defaultEvolutionStrategies ) ++ "'.", G.Option "" [ExecutionConfiguration.ExecutionOptions.inputStudentViewTimetableTag] ( G.ReqArg (Just . (,) ExecutionConfiguration.ExecutionOptions.inputStudentViewTimetableTag . Just &&& Input.CommandLineOption.mkOptionsMutator . setInputStudentViewTimetableFilePath) $ mkTypeString filePathString ) $ showString executionString "define the path to a file, from which an initial timetable (viewed from the perspective of student-bodies, & formatted in XML), can be read.", G.Option "" [ExecutionConfiguration.EvolutionStrategies.minimumPopulationDiversityRatioTag] ( G.ReqArg (Just . (,) ExecutionConfiguration.EvolutionStrategies.minimumPopulationDiversityRatioTag . Just &&& Input.CommandLineOption.mkOptionsMutator . setMinimumPopulationDiversityRatio) $ mkTypeString floatString ) . showString executionString $ "define the population-diversity ratio (in the closed unit-interval), beneath which a reduction in the fecundity of the breeding-program for future generations, by a factor of " ++ show ExecutionConfiguration.EvolutionStrategies.fecundityDecayRatioTag ++ ", is triggered; default '" ++ Text.Printf.printf "%.*f" defaultNDecimalDigits ( ExecutionConfiguration.EvolutionStrategies.getMinimumPopulationDiversityRatio defaultEvolutionStrategies ) ++ "'.", G.Option "" [ExecutionConfiguration.EvolutionStrategies.nInitialScoutsTag] ( G.OptArg (Just . (,) ExecutionConfiguration.EvolutionStrategies.nInitialScoutsTag &&& Input.CommandLineOption.mkOptionsMutator . setNInitialScouts) $ mkTypeString intString ) $ showString executionString "define the initial number of candidates to select from each generation in the evolution of the timetable; if the argument is unspecified then the number of scouts varies depending on the quality of the candidates; which is also the default.", G.Option "" [ExecutionConfiguration.OptimiseLessonCriteriaWeights.tag] ( G.ReqArg (Just . (,) ExecutionConfiguration.OptimiseLessonCriteriaWeights.tag . Just &&& Input.CommandLineOption.mkOptionsMutator . setOptimiseLessonCriteriaWeights) "" ) . showString executionString $ "optimise the weights of lesson-criteria, using the weighted mean over timetable-criteria, over all specified raster-scans of the initial deterministic timetable; default '" ++ show ( let defaultOptimiseLessonCriteriaWeights = ExecutionConfiguration.ExecutionOptions.getOptimiseLessonCriteriaWeights defaultExecutionOptions in ExecutionConfiguration.OptimiseLessonCriteriaWeights.mkOptimiseLessonCriteriaWeights ( ExecutionConfiguration.OptimiseLessonCriteriaWeights.getNTrials defaultOptimiseLessonCriteriaWeights ) ( realToFrac $ ExecutionConfiguration.OptimiseLessonCriteriaWeights.getChangeMagnitude defaultOptimiseLessonCriteriaWeights :: FractionalIOFormat ) ( realToFrac $ ExecutionConfiguration.OptimiseLessonCriteriaWeights.getReductionFactor defaultOptimiseLessonCriteriaWeights ) ( ExecutionConfiguration.OptimiseLessonCriteriaWeights.getUseMeanOverRasterScans defaultOptimiseLessonCriteriaWeights ) ) ++ "'.", G.Option "" [ExecutionConfiguration.ExecutionOptions.permitTemporaryStudentBodyMergerTag] ( G.OptArg (Just . (,) ExecutionConfiguration.ExecutionOptions.permitTemporaryStudentBodyMergerTag &&& Input.CommandLineOption.mkOptionsMutator . setPermitTemporaryStudentBodyMerger) $ mkTypeString boolString ) . showString executionString . showString "permit a temporary merger between student-bodies, to permit a teacher & location to be shared for the duration of a course; default '" $ shows ( ExecutionConfiguration.ExecutionOptions.getPermitTemporaryStudentBodyMerger defaultExecutionOptions ) "'. If the argument is unspecified, then 'True' will be inferred.", G.Option "r" [ExecutionConfiguration.ExecutionOptions.randomSeedTag] ( G.OptArg (Just . (,) ExecutionConfiguration.ExecutionOptions.randomSeedTag &&& Input.CommandLineOption.mkOptionsMutator . setRandomSeed) $ mkTypeString intString ) . showString executionString . showString "seed the pseudo-random number-generator with the specified integer, to produce a repeatable sequence; if this option is unspecified then the seed is unpredictable, but if only its argument is unspecified then the seed defaults to '" $ shows defaultRandomSeed "'.", G.Option "" [ExecutionConfiguration.ExecutionOptions.reduceStudentBodyRegisterTag] ( G.OptArg (Just . (,) ExecutionConfiguration.ExecutionOptions.reduceStudentBodyRegisterTag &&& Input.CommandLineOption.mkOptionsMutator . setReduceStudentBodyRegister) $ mkTypeString boolString ) . showString executionString . showString "permit a permanent merger between student-bodies with identical profiles, to reduce the number of independently schedulable entities; default '" $ shows ( ExecutionConfiguration.ExecutionOptions.getReduceStudentBodyRegister defaultExecutionOptions ) "'. If the argument is unspecified, then 'True' will be inferred.", G.Option "" [ExecutionConfiguration.ExecutionOptions.removeRedundantCoursesTag] ( G.OptArg (Just . (,) ExecutionConfiguration.ExecutionOptions.removeRedundantCoursesTag &&& Input.CommandLineOption.mkOptionsMutator . setRemoveRedundantCourses) $ mkTypeString boolString ) . showString executionString . showString "define whether to automatically remove any courses which aren't required by any student-body; default '" $ shows ( ExecutionConfiguration.ExecutionOptions.getRemoveRedundantCourses defaultExecutionOptions ) "'. If the argument is unspecified, then 'True' will be inferred.", G.Option "" [OutputConfiguration.Verbosity.tag] ( G.ReqArg (Just . (,) OutputConfiguration.Verbosity.tag . Just &&& Input.CommandLineOption.mkOptionsMutator . setVerbosity) "" ) . showString outputString . showString "define the log-level; default '" $ shows (OutputConfiguration.Options.getVerbosity defaultOutputOptions) "'.", let tag :: String tag = "displayRuntimeLog" in G.Option "" [tag] ( G.OptArg (Just . (,) tag &&& Input.CommandLineOption.mkOptionsMutator . setDisplayRuntimeInformation) $ mkTypeString boolString ) . showString outputString . showString "requests that the runtime-log is rendered in all XHTML output-streams; default '" $ shows (OutputConfiguration.Style.getDisplayRuntimeInformation defaultStyle) "'. If the argument is unspecified, then 'True' will be inferred.", G.Option "" [OutputConfiguration.Options.nDecimalDigitsTag] ( G.ReqArg (Just . (,) OutputConfiguration.Options.nDecimalDigitsTag . Just &&& Input.CommandLineOption.mkOptionsMutator . setNDecimalDigits) $ mkTypeString intString ) . showString outputString . showString "define the precision with which fractional auxiliary data is displayed; default '" $ shows defaultNDecimalDigits "'.", G.Option "" [OutputConfiguration.Options.outputConfigFilePathTag] ( G.ReqArg (Just . (,) OutputConfiguration.Options.outputConfigFilePathTag . Just &&& Input.CommandLineOption.mkOptionsMutator . setOutputConfigFilePath) $ mkTypeString filePathString ) $ showString outputString "define the path to a file into which the unprocessed configuration, formatted in XML, should be written.", let tag :: String tag = "outputStudentViewTimetable" in G.Option "" [tag] ( G.ReqArg (Just . (,) tag . Just &&& Input.CommandLineOption.mkOptionsMutator . appendOutputStudentViewTimetableFilePath) $ mkTypeString filePathString ) $ showString outputString "define the path to a file into which the resulting timetable (viewed from the perspective of student-bodies & formatted in XML), should be written.", G.Option "" ["printInputOptionsXMLDTD"] ( G.NoArg (Nothing {-N/A-}, Input.CommandLineOption.mkIOAction printInputOptionsXMLDTD) ) $ showString outputString "generate a rough Document Type Definition (DTD), defining the XML-format of the configuration-file, & then exit. CAVEAT: it must be manually amended to identify 'IMPLIED', 'ID', & 'IDREF' attributes.", G.Option "" ["printTimetableXMLDTD"] ( G.ReqArg ((,) Nothing {-N/A-} . Input.CommandLineOption.mkIOAction . printTimetableXMLDTD) "" ) $ showString outputString "generate the Document Type Definition (DTD), defining the XML-format of the resulting timetable viewed from the specified perspective, & then exit." ] where mkTypeString :: ShowS mkTypeString s = showChar '<' $ showString s ">" boolString, floatString, intString, filePathString, executionString, infoString, inputString, outputString :: String boolString = "Bool" floatString = "Float" intString = "Int" filePathString = "File-path" executionString = "Execution: " infoString = "Info: " inputString = "Input: " outputString = "Output: " #ifdef USE_HDBC stringString :: String stringString = "String" #endif defaultEvolutionStrategies :: ExecutionConfiguration.EvolutionStrategies.EvolutionStrategies FractionalIOFormat FractionalIOFormat defaultExecutionOptions :: ExecutionOptions defaultOutputOptions :: OutputConfiguration.Options.Options MinimumContrastRatio defaultStyle :: OutputConfiguration.Style.Style MinimumContrastRatio ((defaultEvolutionStrategies, defaultExecutionOptions), (defaultOutputOptions, defaultStyle)) = Data.Default.def defaultRandomSeed :: ExecutionConfiguration.ExecutionOptions.RandomSeed defaultRandomSeed = 0 defaultNDecimalDigits :: OutputConfiguration.Options.NDecimalDigits defaultNDecimalDigits = OutputConfiguration.Options.getNDecimalDigits defaultOutputOptions -- Unary options-mutators. appendOutputStudentViewTimetableFilePath, setFecundityDecayRatio, setInputStudentViewTimetableFilePath, setMinimumPopulationDiversityRatio, setNDecimalDigits, setOptimiseLessonCriteriaWeights, setOutputConfigFilePath, setVerbosity :: String -> Options -> Options appendOutputStudentViewTimetableFilePath = Input.Options.appendOutputStudentViewTimetableFilePath setFecundityDecayRatio = Input.Options.setFecundityDecayRatio . realToFrac . (Input.CommandLineOption.readArg :: String -> FractionalIOFormat) setInputStudentViewTimetableFilePath = Input.Options.setInputStudentViewTimetableFilePath -- CAVEAT: this can over-ride a specific raster-scan. setMinimumPopulationDiversityRatio = Input.Options.setMinimumPopulationDiversityRatio . realToFrac . (Input.CommandLineOption.readArg :: String -> FractionalIOFormat) setNDecimalDigits = Input.Options.setNDecimalDigits . Input.CommandLineOption.readBoundedIntegral setOptimiseLessonCriteriaWeights = Input.Options.setOptimiseLessonCriteriaWeights . ( \optimiseLessonCriteriaWeights -> ExecutionConfiguration.OptimiseLessonCriteriaWeights.mkOptimiseLessonCriteriaWeights ( ExecutionConfiguration.OptimiseLessonCriteriaWeights.getNTrials optimiseLessonCriteriaWeights ) ( realToFrac (ExecutionConfiguration.OptimiseLessonCriteriaWeights.getChangeMagnitude optimiseLessonCriteriaWeights :: FractionalIOFormat) ) ( realToFrac (ExecutionConfiguration.OptimiseLessonCriteriaWeights.getReductionFactor optimiseLessonCriteriaWeights :: FractionalIOFormat) ) ( ExecutionConfiguration.OptimiseLessonCriteriaWeights.getUseMeanOverRasterScans optimiseLessonCriteriaWeights ) ) . Input.CommandLineOption.readArg setOutputConfigFilePath = Input.Options.setMaybeOutputConfigFilePath . Just setVerbosity = Input.Options.setVerbosity . Input.CommandLineOption.readArg setDisplayRuntimeInformation, setNInitialScouts, setPermitTemporaryStudentBodyMerger, setRandomSeed, setReduceStudentBodyRegister, setRemoveRedundantCourses :: Maybe String -> Options -> Options setDisplayRuntimeInformation = Input.Options.setDisplayRuntimeInformation . Data.Maybe.maybe True {-default-} Input.CommandLineOption.readArg setNInitialScouts = Input.Options.setMaybeNInitialScouts . fmap Input.CommandLineOption.readBoundedIntegral setPermitTemporaryStudentBodyMerger = Input.Options.setPermitTemporaryStudentBodyMerger . Data.Maybe.maybe True {-default-} Input.CommandLineOption.readArg setRandomSeed = Input.Options.setMaybeRandomSeed . Just . Data.Maybe.maybe defaultRandomSeed Input.CommandLineOption.readBoundedIntegral setReduceStudentBodyRegister = Input.Options.setReduceStudentBodyRegister . Data.Maybe.maybe True {-default-} Input.CommandLineOption.readArg setRemoveRedundantCourses = Input.Options.setRemoveRedundantCourses . Data.Maybe.maybe True {-default-} Input.CommandLineOption.readArg -- Nullary I/O-actions. printHelp, printVersion, printInputOptionsXMLDTD :: IO () printHelp = Control.Monad.void $ Text.Printf.printf "Usage:\t%s\nEBNF argument-format:\n\t%-9s = %-40s%s;\n\t%-9s = %s;\n\t%-9s = %s;\n\t%-9s = %s;\n\t%-9s = %s;\n\t%-9s = %-40s%s;\n\t%-9s = %-40s%s;\n" ( G.usageInfo progName optDescrList ) boolString ( ToolShed.Data.List.showListWith listDelimiters [minBound :: Bool .. maxBound] "" ) "(* Case-sensitive *)" filePathString ( "File-name ('" ++ [System.FilePath.pathSeparator] ++ "' File-name)*" ) floatString "Int ('.' Int)?" "Int" "[0-9]+" "Triple" "'(', Int, ',', Float, ',', Float, ')'" "Verbosity" ( ToolShed.Data.List.showListWith listDelimiters OutputConfiguration.Verbosity.range "" ) "(* Case-sensitive *)" "View" ( ToolShed.Data.List.showListWith listDelimiters OutputConfiguration.View.range "" ) "(* Case-sensitive *)" {-CAVEAT: requires increase to default context-stack-} where listDelimiters = ('(', '|', ')') printVersion = putStrLn ( Data.List.intercalate "\n" [ s | (True, s) <- [ ( True, packageIdentifier ), ( True, showString "Compiled by " . shows ( showString System.Info.compilerName . showChar '-' . Data.List.intercalate "." . map show $ Data.Version.versionBranch System.Info.compilerVersion ) . showString ", with Cabal-flags " $ shows [ showChar #ifdef USE_HDBC_MYSQL '+' #else '-' #endif mySQLPackageName, showChar #ifdef USE_HDBC_ODBC '+' #else '-' #endif odbcPackageName, showChar #ifdef PRINT_DB_QUERIES '+' #else '-' #endif "printDBQueries", showChar #ifdef PRINT_DB_ROWS '+' #else '-' #endif "printDBRows", showChar #ifdef QUERY_DB_CONCURRENTLY '+' #else '-' #endif "queryDBConcurrently", showChar #ifdef USE_UNIX '+' #else '-' #endif "unix" ] "." ), ( True, showString "Written by " $ shows author "." ), ( True, showString "Copyright (C) 2013-2015 " $ showString author "." ), ( True, "This program comes with ABSOLUTELY NO WARRANTY." ), ( False, "This is free software, and you are welcome to redistribute it under certain conditions." -- TODO: review. ) ] ] -- List-comprehension. ) >> System.Exit.exitSuccess outputSysConfig :: HXT.SysConfigList outputSysConfig = [HXT.withTrace 0 {-valid values in range [0 .. 4]-}] printInputOptionsXMLDTD = Control.Monad.void . HXT.runX $ HXT.constA ( undefined :: Options ) >>> HXT.xpickleWriteDTD HXT.xpickle outputSysConfig OutputConfiguration.FileFormat.stdoutProxy -- CAVEAT: this DTD requires manual correction of defaulted attributes, which are erroneously defined as 'REQUIRED' rather than 'IMPLIED'. -- Unary I/O-actions. printTimetableXMLDTD :: String -> IO () printTimetableXMLDTD = Control.Monad.void . HXT.runX . ( \view -> case view of OutputConfiguration.View.LocationView -> HXT.constA ( undefined :: Model.Timetable.Wrapper Identifiers.LocationId.LocationId Identifiers.TimeslotId.TimeslotId ( LocationView.LessonResourceIds.LessonResourceIds Identifiers.TeacherId.TeacherId ) Identifiers.Level.Level ) >>> HXT.xpickleWriteDTD HXT.xpickle outputSysConfig OutputConfiguration.FileFormat.stdoutProxy OutputConfiguration.View.StudentView -> HXT.constA ( undefined :: Model.Timetable.Wrapper Aggregate.StudentBody.StudentBody Identifiers.TimeslotId.TimeslotId ( StudentView.LessonResourceIds.LessonResourceIds Identifiers.LocationId.LocationId Identifiers.TeacherId.TeacherId ) Identifiers.Level.Level ) >>> HXT.xpickleWriteDTD HXT.xpickle outputSysConfig OutputConfiguration.FileFormat.stdoutProxy OutputConfiguration.View.TeacherView -> HXT.constA ( undefined :: Model.Timetable.Wrapper Identifiers.TeacherId.TeacherId Identifiers.TimeslotId.TimeslotId ( TeacherView.LessonResourceIds.LessonResourceIds Identifiers.LocationId.LocationId ) Identifiers.Level.Level ) >>> HXT.xpickleWriteDTD HXT.xpickle outputSysConfig OutputConfiguration.FileFormat.stdoutProxy ) . Input.CommandLineOption.readArg #ifdef USE_UNIX SignalHandlers.handleSignals #endif -- Use the list of possible options, to process the actual list of arguments, & to record those we want to display in the XHTML-results. case G.getOpt G.RequireOrder optDescrList args of (commandLineOptions, [{-non-options-}], [{-errors-}]) -> let visibleArgs :: [(String, Maybe String)] ioActions :: [IO ()] optionsMutators :: [Options -> Options] configLocationParameters :: Data.Map.Map Input.CommandLineOption.Flag String (visibleArgs, (ioActions, optionsMutators, configLocationParameters)) = Data.Maybe.catMaybes *** Input.CommandLineOption.partition3 $ unzip commandLineOptions -- Categorise the specified command-line options. in if not $ null ioActions then do sequence_ ioActions if null optionsMutators && Data.Map.null configLocationParameters then System.Exit.exitSuccess else error "incompatible command-line options." else if Data.Map.null configLocationParameters then error . shows (Input.CommandLineOption.longFlagPrefix ++ inputConfigFilePathFlag) #ifdef USE_HDBC . showString " or " . shows mandatoryDBFlags #endif $ " must be specified." else {-config-location specified-} let {- The strategy is to either parse XML or query the database, for the input-options, then over-ride them with each of the command-line options-mutators, but regrettably "verbosity" (a command-line option) is needed while doing so. This is resolved by reading the "verbosity" from the command-line first. -} preVerbosity :: Distribution.Verbosity.Verbosity preVerbosity | null verbosityArgs = Data.Default.def | otherwise = Input.CommandLineOption.readArg $ head verbosityArgs where verbosityArgs :: [String] verbosityArgs = Input.CommandLineOption.getArgs ( map (showString Input.CommandLineOption.longFlagPrefix) . drop 4 {-those prefixes ambiguous with "version"-} $ Data.List.inits OutputConfiguration.Verbosity.tag -- Generate all unique abbreviations. ) args inputSysConfig :: HXT.SysConfigList inputSysConfig = [ HXT.withRemoveWS HXT.yes, -- Remove white-space, e.g. any indentation which might have been introduced by 'HXT.withIndent'. HXT.withStrictInput HXT.yes, -- Read the input file strictly (cf. lazily), this ensures file-closure even if not completely read. HXT.withValidate HXT.yes -- Validate against any DTD referenced from the specified XML-file. ] processInputOptions :: Options -> IO () processInputOptions inputOptions | null configVersion = error . showString "undefined " $ shows Input.ConfigVersion.tag "." | not $ Factory.Data.Interval.elem' configVersion tolerableConfigVersionInterval = error . showString Input.ConfigVersion.tag . showChar '=' . shows (Input.ConfigVersion.toString configVersion) . showString ", falls outside tolerable bounds " $ shows (Input.ConfigVersion.toString *** Input.ConfigVersion.toString $ tolerableConfigVersionInterval) "." | not $ ToolShed.SelfValidate.isValid inputOptions'' = error $ ToolShed.SelfValidate.getFirstError inputOptions'' -- Now that the input-options have been amended according to the command-line options, check that they're valid. | otherwise = do #ifdef TOOL_VERSION_ghc numProcessors <- GHC.Conc.getNumProcessors Control.Monad.when (verbosity /= minBound && numProcessors > 1 && GHC.Conc.numCapabilities == 1) . System.IO.hPutStrLn System.IO.stderr . showString "WARNING:\tthis application would benefit from " . shows numProcessors . showString " CPU-cores; try '" . showString progName . showChar ' ' $ showString (unwords args) " +RTS -N'." #endif Control.Monad.when (verbosity == maxBound) $ fmap ( showString "INFO:\tconfiguration read after " . (`showString` ".") . show . (`Data.Time.Clock.diffUTCTime` startUTCTime) ) Data.Time.Clock.getCurrentTime >>= System.IO.hPutStrLn System.IO.stderr -- CAVEAT: this is only meaningful when there's no password-dialogue. Control.Monad.when (Data.Maybe.isJust maybeOutputConfigFilePath) $ let outputConfigFilePath = Data.Maybe.fromJust maybeOutputConfigFilePath in Control.Exception.catch ( writeXMLToFile outputConfigFilePath Input.Options.tag ( dtdDir progName <.> dtdSuffix ) inputOptions {-the original-} ) $ \e -> System.IO.hPutStrLn System.IO.stderr . showString "WARNING:\tfailed to write the configuration in XML, to " . showString OutputConfiguration.Options.outputConfigFilePathTag . showChar '=' . shows outputConfigFilePath . showString "; " $ shows (e :: Control.Exception.SomeException) "." -- This error isn't fatal, so continue. Control.Monad.unless (verbosity == minBound) $ mapM_ (System.IO.hPutStrLn System.IO.stderr) warnings -- Issue any warnings regarding inputOptions'. -- Construct an initial deterministic timetable. let maybeHint = ExecutionConfiguration.ExecutionOptions.getMaybeHint executionOptions maybeStudentViewTimetables <- Data.Maybe.maybe ( return {-to IO-monad-} [Nothing] -- Neither a traversalOrder nor an input-filePath were specified. ) ( const ( return {-to IO-monad-} [Nothing] -- A traversalOrder was specified. ) `either` ( \studentViewTimetableFilePath -> HXT.runX $ HXT.xunpickleDocument HXT.xpickle ( HXT.withTrace (fromEnum verbosity `min` 2) : inputSysConfig -- CAVEAT: the maximum trace-level is actually 4; but that's VERY verbose. ) studentViewTimetableFilePath >>> HXT.arrIO ( \wrapper -> let studentViewTimetable = Model.Timetable.deconstruct wrapper maybeErrorMessage = Dynamic.StudentViewTimetableUtilities.isTimetableValid problemParameters executionOptions problemAnalysis studentViewTimetable in Data.Maybe.maybe ( return {-to IO-monad-} $ Just studentViewTimetable ) ( error . shows studentViewTimetableFilePath . showString " failed validation; " ) maybeErrorMessage ) ) ) maybeHint randomGen <- Data.Maybe.maybe ( do Control.Monad.when (verbosity > Data.Default.def) $ System.IO.hPutStrLn System.IO.stderr "WARNING:\tseeding the pseudo-random number-generator from the operating-system; the result will not typically be repeatable." System.Random.getStdGen ) ( return {-to IO-monad-} . System.Random.mkStdGen -- Seed the pseudo-random number-generator with the specified integer. ) $ ExecutionConfiguration.ExecutionOptions.getMaybeRandomSeed executionOptions if null maybeStudentViewTimetables then error . showString "failed to parse a StudentView-timetable from " . showString ExecutionConfiguration.ExecutionOptions.inputStudentViewTimetableTag . showChar '=' $ show maybeHint else let deterministicStudentViewTimetableSelection :: OutputFormat.DeterministicStudentViewTimetableSelection.DeterministicStudentViewTimetableSelection CriterionValue CriterionWeight Identifiers.Level.Level Identifiers.LocationId.LocationId Mean StandardDeviation Identifiers.TeacherId.TeacherId Identifiers.TimeslotId.TimeslotId WeightedMean ( (executionOptions', deterministicStudentViewTimetableSelection), optimiseLessonCriteriaWeightsResults ) = Control.Monad.Writer.runWriter $ Implementation.EvolutionaryAlgorithm.optimiseLessonCriteriaWeights problemParameters executionOptions problemAnalysis ( head {-it's a singleton-} maybeStudentViewTimetables ) randomGen {-used if ExecutionConfiguration.OptimiseLessonCriteriaWeights.isRequired-} deterministicStudentViewTimetable = OutputFormat.DeterministicStudentViewTimetableSelection.getStudentViewTimetable deterministicStudentViewTimetableSelection -- Access. evolutionStrategyStatisticsList :: [OutputFormat.EvolutionStrategyStatistics.EvolutionStrategyStatistics WeightedMean CriterionValue] (studentViewTimetable, evolutionStrategyStatisticsList) = Control.Monad.Writer.runWriter $ Implementation.EvolutionaryAlgorithm.evolveStudentViewTimetable randomGen problemParameters executionOptions' problemAnalysis deterministicStudentViewTimetable -- Evolve the initial deterministic timetable. -- Analyse the results. in do if verbosity == Data.Default.def -- Print either a progress-bar or the 'runtimeLog'. then let stagingPost :: Char stagingPost = '.' in System.IO.hPutStrLn System.IO.stderr $ seq deterministicStudentViewTimetable stagingPost : map ( (`seq` stagingPost) . OutputFormat.EvolutionStrategyStatistics.getWeightedMeanOfTimetableCriteria ) evolutionStrategyStatisticsList -- Print progress-bar. else Control.Monad.when (verbosity > Data.Default.def) . mapM_ (System.IO.hPutStrLn System.IO.stderr) $ ( "INFO:\t" ++ Data.Maybe.maybe "a variable number of" ( (++ " initial") . show ) ( ExecutionConfiguration.EvolutionStrategies.getMaybeNInitialScouts $ ExecutionConfiguration.ExecutionOptions.getEvolutionStrategies executionOptions -- CAVEAT: unnecessary use of "executionOptions'" here, will block output until it's evaluated. ) ++ " scouts will be dispatched during evolution." ) : map ( \(key, value) -> key ++ ": " ++ value ) ( OutputFormat.EvolutionStrategyStatistics.composeRuntimeLog executionOptions' nDecimalDigits ( ExecutionConfiguration.ExecutionOptions.hintWasSpecified executionOptions {- CAVEAT: unnecessary use of executionOptions' here, will block output until it's evaluated-} ) ( ExecutionConfiguration.EvolutionStrategies.areAllZero $ ExecutionConfiguration.ExecutionOptions.getEvolutionStrategies executionOptions ) lessonCriteriaWeights ( ExecutionConfiguration.OptimiseLessonCriteriaWeights.getUseMeanOverRasterScans $ ExecutionConfiguration.ExecutionOptions.getOptimiseLessonCriteriaWeights executionOptions -- CAVEAT: unnecessary use of executionOptions' here, will block output until it's evaluated. ) optimiseLessonCriteriaWeightsResults deterministicStudentViewTimetableSelection evolutionStrategyStatisticsList . snd {-[Maybe timetableCriterion]-} . Control.Arrow.first ( \x -> x :: WeightedMean -- Define the type of the discarded datum. ) . Control.Monad.Writer.runWriter $ Implementation.TimetableFitness.evaluateFitness problemParameters executionOptions' problemAnalysis studentViewTimetable ) -- Present the 'timetable' in each of the requested output-formats. mapM_ ( \fileFormat -> let filePath = OutputConfiguration.FileFormat.getFilePath fileFormat in Control.Exception.catch ( case OutputConfiguration.FileFormat.getFormat fileFormat of OutputConfiguration.Format.XHTML style -> let description, documentTitle :: String description = "The proposed solution to the specified school-timetable problem" documentTitle = unwords $ progName : map ( uncurry showString . ( showString Input.CommandLineOption.longFlagPrefix *** Data.Maybe.maybe "" (showString "='" . (`showString` "'")) ) ) visibleArgs in do Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . showString "INFO:\tdataDir=" . shows dataDir . showString ", appUserDataDir=" $ shows appUserDataDir "." maybeCSSURL <- Data.Maybe.maybe ( let cssFileName :: System.FilePath.FilePath cssFileName = progName <.> Text.CSS.cssSuffix -- The default CSS-filename. searchPath = [ ".", -- CWD. appUserDataDir Text.CSS.cssSuffix, -- Under the user's home-directory. dataDir Text.CSS.cssSuffix -- Under the installation-directory defined during 'cabal configure'. ] -- CAVEAT: not "css/", i.e. relative to the root of the source-tree, which one wouldn't typically install. in ToolShed.System.File.locate cssFileName searchPath >>= Data.Maybe.maybe ( do Control.Monad.unless (verbosity == minBound) . System.IO.hPutStrLn System.IO.stderr . showString "WARNING:\tthe default CSS-filename " . shows cssFileName . showString ", wasn't found in any of " . (`showString` ".") $ show searchPath return {-to IO-monad-} Nothing ) ( \cssFilePath -> do Control.Monad.when (verbosity > Data.Default.def) . System.IO.hPutStrLn System.IO.stderr . showString "INFO:\twriting the CSS-filepath " $ shows cssFilePath " into the XHTML-output." return {-to IO-monad-} $ Just cssFilePath ) . Data.Maybe.listToMaybe ) ( return {-to IO-monad-} . Just ) $ OutputConfiguration.Style.getMaybeCSSURL style Control.Exception.catch ( ( \document -> if filePath == OutputConfiguration.FileFormat.stdoutProxy then putStrLn document else System.IO.withFile filePath System.IO.WriteMode (`System.IO.hPutStrLn` document) ) . OutputFormat.XHTMLFormat.renderDocument maybeCSSURL author description ( Data.List.intercalate ", " [packageIdentifier, "Timetable", "Haskell", "Evolutionary algorithm"] -- Key-words. ) progName documentTitle $ OutputFormat.XHTMLFormat.composeBody problemParameters executionOptions' outputOptions problemAnalysis ( if OutputConfiguration.Style.getDisplayRuntimeInformation style then Just ( warnings, OutputFormat.EvolutionStrategyStatistics.toHtml executionOptions' nDecimalDigits lessonCriteriaWeights optimiseLessonCriteriaWeightsResults deterministicStudentViewTimetableSelection evolutionStrategyStatisticsList ) else Nothing ) ( OutputConfiguration.Style.getDisplayViews style ) ( OutputConfiguration.Style.getDisplaySupplementaryInformation style ) ( OutputConfiguration.Style.getMergeDuplicateTimeslots style ) ( OutputConfiguration.Style.getDisplayAxisLabels style ) ( OutputConfiguration.Style.getWeekend style ) ( OutputConfiguration.Style.getMaybeGenerateLessonColour style ) studentViewTimetable ) $ \e -> System.IO.hPutStrLn System.IO.stderr . showString "WARNING:\tfailed to render the results in XHTML; \"" $ shows (e :: Control.Exception.SomeException) "\"." -- There may be other file-formats to process, so continue. OutputConfiguration.Format.XML view -> let permitTemporaryStudentBodyMerger :: Bool permitTemporaryStudentBodyMerger = ExecutionConfiguration.ExecutionOptions.getPermitTemporaryStudentBodyMerger executionOptions' in Control.Exception.catch ( ( case view of OutputConfiguration.View.LocationView -> writeXMLToFile filePath Model.Timetable.tag ( dtdDir OutputFormat.XHTMLFormatLocationViewTimetable.locationViewTimetableTag <.> dtdSuffix -- Reference the corresponding packaged DTD. ) . Model.Timetable.MkWrapper . LocationView.Timetable.fromStudentViewTimetable permitTemporaryStudentBodyMerger (ProblemConfiguration.ProblemAnalysis.getFreeLocationViewTimetable problemAnalysis) OutputConfiguration.View.StudentView -> writeXMLToFile filePath Model.Timetable.tag ( dtdDir OutputFormat.XHTMLFormatStudentViewTimetable.studentViewTimetableTag <.> dtdSuffix -- Reference the corresponding packaged DTD. ) . Model.Timetable.MkWrapper OutputConfiguration.View.TeacherView -> writeXMLToFile filePath Model.Timetable.tag ( dtdDir OutputFormat.XHTMLFormatTeacherViewTimetable.teacherViewTimetableTag <.> dtdSuffix -- Reference the corresponding packaged DTD. ) . Model.Timetable.MkWrapper . TeacherView.Timetable.fromStudentViewTimetable permitTemporaryStudentBodyMerger (ProblemConfiguration.ProblemAnalysis.getFreeTeacherViewTimetable problemAnalysis) ) studentViewTimetable ) $ \e -> System.IO.hPutStrLn System.IO.stderr . showString "WARNING:\tfailed to write the solution in XML, to path=" . shows filePath . showString "; \"" $ shows (e :: Control.Exception.SomeException) "\"." -- There may be other file-formats to process, so continue. ) $ \e -> System.IO.hPutStrLn System.IO.stderr . showString "WARNING:\tfailed to process " . shows fileFormat . showString "; \"" $ shows (e :: Control.Exception.SomeException) "\"." -- There may be other file-formats to process, so continue. ) outputFileFormats Control.Monad.when (verbosity == maxBound) $ fmap ( showString "INFO:\ttotal elapsed time " . (`showString` ".") . show . (`Data.Time.Clock.diffUTCTime` startUTCTime) ) Data.Time.Clock.getCurrentTime >>= System.IO.hPutStrLn System.IO.stderr where configVersion :: Input.ConfigVersion.ConfigVersion configVersion = Input.ConfigVersion.normalise $ Input.Options.getConfigVersion inputOptions tolerableConfigVersionInterval :: Factory.Data.Interval.Interval Input.ConfigVersion.ConfigVersion tolerableConfigVersionInterval = Factory.Data.Interval.precisely $ Input.ConfigVersion.normalise [1, 0] -- Only version "1.0" is tolerable. inputOptions' :: Options inputOptions' = foldr ($) inputOptions {-initial value-} optionsMutators -- Sequentially mutate the configuration, using each of the options-mutators specified on the command-line. outputOptions = Input.Options.getOutputOptions inputOptions' -- Deconstruct. nDecimalDigits = OutputConfiguration.Options.getNDecimalDigits outputOptions -- Amend the problemParameters according to the executionOptions, logging any changes. problemParameters :: ProblemParameters mergedStudentBodies :: [[Aggregate.StudentBody.StudentBody]] pointlessGroupIds, unsubscribedGroupIds :: [Data.Group.Id] redundantKnowledgeByTeacherId :: [(Identifiers.TeacherId.TeacherId, Data.Subject.Knowledge Identifiers.Level.Level)] disabledProblemValidationSwitches :: [String] (((((problemParameters, mergedStudentBodies), pointlessGroupIds), unsubscribedGroupIds), redundantKnowledgeByTeacherId), disabledProblemValidationSwitches) = Control.Arrow.first ( Control.Arrow.first ( Control.Arrow.first ( Control.Arrow.first ( Control.Monad.Writer.runWriter . ( if ExecutionConfiguration.ExecutionOptions.getReduceStudentBodyRegister e then ProblemConfiguration.ProblemParameters.reduceStudentBodyRegister (OutputConfiguration.Options.getStudentBodyMnemonicSeparator outputOptions) -- Which returns a Writer. else return {-to Writer-monad-} ) . ProblemConfiguration.ProblemParameters.mergeConstraintsOnSynchronisedCourses ) . Control.Monad.Writer.runWriter . ( if ExecutionConfiguration.ExecutionOptions.getRemovePointlessGroups e then ProblemConfiguration.ProblemParameters.removePointlessGroups -- Which returns a Writer. else return {-to Writer-monad-} ) ) . Control.Monad.Writer.runWriter . ( if ExecutionConfiguration.ExecutionOptions.getRemoveUnsubscribedGroups e then ProblemConfiguration.ProblemParameters.removeUnsubscribedGroups -- Which returns a Writer. else return {-to Writer-monad-} ) ) . Control.Monad.Writer.runWriter . ( if ExecutionConfiguration.ExecutionOptions.getRemoveRedundantCourses e then ProblemConfiguration.ProblemParameters.removeRedundantCourses -- Which returns a Writer. else return {-to Writer-monad-} ) ) . Control.Monad.Writer.runWriter . ( if ExecutionConfiguration.ExecutionOptions.getPermitTemporaryStudentBodyMerger e then ProblemConfiguration.ProblemParameters.disableAnyValidationInappropriateForTemporaryStudentBodyMerger -- Which returns a Writer. else return {-to Writer-monad-} ) $ Input.Options.getProblemParameters inputOptions' where e = Input.Options.getExecutionOptions inputOptions' -- Amend the executionOptions according to the problemParameters, gathering the list of changes & concatenating it with other warnings. (executionOptions, warnings) = let showListWithQuotesAndSpaces :: Show a => [a] -> String showListWithQuotesAndSpaces = Data.List.intercalate ", " . map show in Control.Arrow.second ( \((maybeLessonCriteriaWeightsWarning, maybeTimetableCriteriaWeightsWarning), maybeEvolutionStrategyFecundityWarnings) -> map ( showString "WARNING:\t" . (`showString` ".") ) $ Data.Maybe.catMaybes [ maybeLessonCriteriaWeightsWarning, maybeTimetableCriteriaWeightsWarning, maybeEvolutionStrategyFecundityWarnings ] ++ Configuration.issueWarnings inputOptions' ++ [ msg | (True, msg) <- [ ( not $ null disabledProblemValidationSwitches, "the following problem-validation switches are inappropriate when temporary student-body mergers are permissible, & have been disabled; " ++ showListWithQuotesAndSpaces disabledProblemValidationSwitches ), ( not $ null redundantKnowledgeByTeacherId, "those subjects for which there's zero demand have been removed; " ++ show ( Control.Arrow.second Data.Set.toList `map` redundantKnowledgeByTeacherId ) ), ( not $ null unsubscribedGroupIds, "those groups to which neither student-bodies nor teachers have subscribed, have been removed from the group-catalogue; " ++ showListWithQuotesAndSpaces unsubscribedGroupIds ), ( not $ null pointlessGroupIds, "those groups for which zero meetings have been scheduled, have been removed from the group-catalogue; " ++ showListWithQuotesAndSpaces pointlessGroupIds ), ( not $ null mergedStudentBodies, "those student-bodies whose profiles are identical, have been merged; " ++ showListWithQuotesAndSpaces ( map Aggregate.StudentBody.getMnemonic `map` mergedStudentBodies ) ), case ( ExecutionConfiguration.ExecutionOptions.getMaybeHint $ Input.Options.getExecutionOptions inputOptions', ExecutionConfiguration.ExecutionOptions.getMaybeHint executionOptions {-lazy evaluation-} ) of (Just hint, Just hint') -> ( hint /= hint', showString "the command-line specification of '" . showString ExecutionConfiguration.ExecutionOptions.inputStudentViewTimetableTag . showChar '=' . either shows shows hint' . showString "', overrides the configured " $ either ( showString "raster-scan " . show ) ( showString "input student-view timetable " . show ) hint ) -- Pair. _ -> (False, undefined) ] ] -- List-comprehension. ) . ( \e -> let maybeFormatTags :: String -> [String] -> Maybe String maybeFormatTags _ [] = Nothing maybeFormatTags s l = Just $ s ++ showListWithQuotesAndSpaces l composeZeroedWeightsStatement :: String -> [String] -> Maybe String composeZeroedWeightsStatement s = maybeFormatTags $ "the following " ++ s ++ "-criteria are inappropriate for the specified problem-parameters, & consequently their weights have been zeroed; " in Control.Arrow.second ( ( composeZeroedWeightsStatement "lesson" *** composeZeroedWeightsStatement "timetable" ) *** maybeFormatTags "the following evolution-strategies are inappropriate for the specified problem-parameters, & consequently their fecundities have been zeroed; " ) $ Input.Options.zeroInappropriateExecutionOptions (ExecutionConfiguration.ExecutionOptions.getZeroInappropriateOptions e) problemParameters e -- CAVEAT: actions aren't printed until after checking the validity of the resulting structure. ) $ Input.Options.getExecutionOptions inputOptions' lessonCriteriaWeights = ExecutionConfiguration.ExecutionOptions.getLessonCriteriaWeights executionOptions problemAnalysis :: ProblemAnalysis problemAnalysis = ProblemConfiguration.ProblemAnalysis.mkProblemAnalysis problemParameters inputOptions'' :: Options inputOptions'' = inputOptions' { Input.Options.getExecutionOptions = executionOptions, -- CAVEAT: not including any optimisation of lesson-criteria weights in executionOptions'. Input.Options.getProblemParameters = problemParameters } -- Re-integrate the amended configuration-parameters. dtdSuffix, dtdDir :: System.FilePath.FilePath dtdSuffix = "dtd" dtdDir = dataDir dtdSuffix {-name the directory the same as the file-suffixes it contains-} maybeOutputConfigFilePath = OutputConfiguration.Options.getMaybeOutputConfigFilePath outputOptions outputFileFormats = OutputConfiguration.Options.getFileFormats outputOptions verbosity = OutputConfiguration.Options.getVerbosity outputOptions in case Data.Map.lookup inputConfigFilePathFlag configLocationParameters of -- Determine whether the configuration is filed locally, or in a database. Just inputConfigFilePath -> do fileExists <- System.Directory.doesFileExist inputConfigFilePath if not fileExists then error . showString "no such file " $ shows inputConfigFilePath "." -- This error would be trapped by HXT.xunpickleDocument, but it doesn't set the exit-status. else let maximumTraceLevel :: Int maximumTraceLevel = 2 -- CAVEAT: HXT trace-levels 3 & 4 are too verbose. in Control.Monad.void {-discard the return-value of processInputOptions-} . HXT.runX $ HXT.setTraceLevel ( fromEnum preVerbosity `min` maximumTraceLevel ) >>> HXT.xunpickleDocument HXT.xpickle inputSysConfig inputConfigFilePath >>> HXT.traceMsg maximumTraceLevel "XML-configuration parsed" >>> HXT.arrIO processInputOptions #ifdef USE_HDBC _ {-connect to the RDBMS-} -> case Data.Map.lookup databaseUserIdFlag configLocationParameters of Nothing -> error . showString "command-line option " $ shows databaseUserIdFlag' ", must be specified." Just databaseUserId -> case Data.Map.lookup databaseProjectNameFlag configLocationParameters of Nothing -> error . showString "command-line option " $ shows databaseProjectNameFlag' ", must be specified." Just databaseProjectName -> let verbose :: Bool verbose = preVerbosity > Data.Default.def withEcho :: Bool -> IO a -> IO a withEcho echo action = do oldPolicy <- System.IO.hGetEcho System.IO.stdin Control.Exception.bracket_ (System.IO.hSetEcho System.IO.stdin echo) (System.IO.hSetEcho System.IO.stdin oldPolicy) action readPassword :: String -> IO String readPassword prompt = do Control.Monad.unless ( preVerbosity == minBound -- This might appear to make the process hang, but might be useful when the passwords have already been piped into stdin. ) . System.IO.hPutStr System.IO.stderr . showString "Enter the " $ shows prompt ": " line <- withEcho False getLine System.IO.hPutChar System.IO.stderr '\n' return {-to IO-monad-} line in #ifdef USE_HDBC_MYSQL /* This seems to increase the speed of ODBC, but isn't essential */ Database.MySQL.withRTSSignalsBlocked -- CAVEAT: MySQL doesn't re-start actions after interruption. #endif ( do #ifdef USE_HDBC_ODBC dbConnection <- Database.HDBC.ODBC.connectODBC . Database.ODBC.buildConnectionString Nothing {-ServerName-} Nothing {-Port-} Nothing {-UId-} Nothing {-Data-server password-} Nothing {-Database-} . Data.Maybe.fromMaybe defaultDSN $ Data.Map.lookup dsnFlag configLocationParameters #elif USE_HDBC_MYSQL dataServerPassword <- Data.Maybe.maybe (readPassword dataServerPasswordFlag) return {-to IO-monad-} $ Data.Map.lookup dataServerPasswordFlag configLocationParameters dbConnection <- Database.MySQL.connect ( Data.Maybe.fromMaybe Database.MySQL.defaultHost $ Data.Map.lookup dataServerHostFlag configLocationParameters ) ( Data.Maybe.maybe Database.MySQL.defaultPort read $ Data.Map.lookup dataServerPortFlag configLocationParameters ) ( Data.Maybe.fromMaybe defaultDataServerUserId $ Data.Map.lookup dataServerUserIdFlag configLocationParameters ) dataServerPassword ( Data.Maybe.fromMaybe defaultDatabaseName $ Data.Map.lookup databaseNameFlag configLocationParameters ) #else /* Currently there aren't any alternative DBMS-interfaces */ dbConnection <- error "unknown HDBC-backend." #endif Control.Monad.when verbose . System.IO.hPutStrLn System.IO.stderr . showString "INFO:\tconnection authorised; database-client=" . shows ( showString (Database.HDBC.proxiedClientName dbConnection) . showChar '-' $ Database.HDBC.proxiedClientVer dbConnection ) . showString ", version of database-server=" $ shows (Database.HDBC.dbServerVer dbConnection) "." let executableColumnName, emailAddressColumnName, saltColumnName, saltedPasswordHashColumnName, userIdColumnName :: Database.Selector.ColumnName executableColumnName = "executable" emailAddressColumnName = "emailAddress" saltColumnName = "salt" saltedPasswordHashColumnName = "saltedPasswordHash" userIdColumnName = "userId" userTableName :: Database.Selector.TableName userTableName = Database.Selector.tablePrefix ++ "user" Control.Exception.catch ( do -- Retrieve security-details from the RDBMS. userRows <- Database.Selector.select dbConnection [ userIdColumnName, saltColumnName, saltedPasswordHashColumnName, executableColumnName ] [userTableName] [ ( emailAddressColumnName, Database.HDBC.toSql (databaseUserId :: String) ) -- CAVEAT: this column is defined as an email-address in the database (to ensure uniqueness), but merely as 'databaseUserId' here. ] -- Authenticate the user. case userRows of [] -> error . showString databaseUserIdFlag . showChar '(' . showString emailAddressColumnName . showString ")=" $ shows databaseUserId " wasn't found in the database." [userRow] -> case userRow of [userSql, saltSql, saltedPasswordHashSql, executableSql] -> do authorised <- case either ( error . showString "failed to parse the value for " . shows saltedPasswordHashColumnName . showString " read from the database; " . show ) id $ Database.HDBC.safeFromSql saltedPasswordHashSql of -- Retrieve the recorded raw binary hash, for comparison. Nothing -> return {-to IO-monad-} True -- A salted password-hash hasn't been recorded. Just recordedSaltedPasswordHash -> do saltedPasswordHash <- ( Crypto.Hash.hash . ( either ( error . showString "ERROR:\tfailed to parse the value for " . shows saltColumnName . showString " read from the database; " . show ) id ( Database.HDBC.safeFromSql saltSql ) `Data.ByteString.append` ) . Data.ByteString.Char8.pack ) `fmap` Data.Maybe.maybe ( readPassword databasePasswordFlag ) return {-to IO-monad-} ( Data.Map.lookup databasePasswordFlag configLocationParameters ) -- Generate a salted password-hash for comparison with the recorded value. System.IO.hClose System.IO.stdin -- Any passwords required, have now been read. Control.Monad.when (preVerbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . showString "INFO:\thash generated from salted " . shows databasePasswordFlag . showString "; 0x" $ shows saltedPasswordHash "." return {-to IO-monad-} $ Data.Byteable.toBytes (saltedPasswordHash :: Crypto.Hash.Digest Crypto.Hash.SHA512) == recordedSaltedPasswordHash -- Authenticate by comparing a hash of the concatenated recorded binary salt & the specified ASCII password, against the recorded binary hash. if not authorised then error . showString "incorrect " . shows databasePasswordFlag . showString " for " . showString databaseUserIdFlag . showChar '=' $ shows databaseUserId "; database-access denied." else {-authorised-} let userId :: Int -- Arbitrarily. userId = either ( error . showString "ERROR:\tfailed to parse the value for " . shows userIdColumnName . showString " read from the database; " . show ) id $ Database.HDBC.safeFromSql userSql in do Control.Monad.when verbose $ fmap ( showString "INFO:\tauthenticated as " . showString progName . showChar '-' . showString userIdColumnName . showChar '=' . shows userId . showString ", after " . (`showString` ".") . show . (`Data.Time.Clock.diffUTCTime` startUTCTime) ) Data.Time.Clock.getCurrentTime >>= System.IO.hPutStrLn System.IO.stderr -- Query the RDBMS to confirm that the user is active. if either ( error . showString "ERROR:\tfailed to parse the value for " . shows executableColumnName . showString " read from the database; " . show ) not $ Database.HDBC.safeFromSql executableSql then error . showString "the account for " . showString databaseUserIdFlag . showChar '(' . showString emailAddressColumnName . showString ")=" $ shows databaseUserId " isn't executable." else {-executable-} let projectNameColumnName :: Database.Selector.ColumnName projectNameColumnName = "projectName" projectsTableName :: Database.Selector.TableName projectsTableName = showString Database.Selector.tablePrefix "project" in do -- Query the database to construct the input-options. projectIdRows <- map head {-select the single column requested from each row-} `fmap` Database.Selector.select dbConnection [ Database.Selector.projectIdColumnName ] [projectsTableName] [ (userIdColumnName, userSql), (projectNameColumnName, Database.HDBC.toSql databaseProjectName) ] case projectIdRows of [] -> error . showString projectNameColumnName . showChar '=' . shows databaseProjectName . showString " wasn't found, where " . showString databaseUserIdFlag . showChar '(' . showString emailAddressColumnName . showString ")=" $ show databaseUserId [projectIdSql] -> let projectId :: Int -- Arbitrarily. projectId = either ( error . showString "ERROR:\tfailed to parse the value for " . shows Database.Selector.projectIdColumnName . showString " read from the database; " . show ) id $ Database.HDBC.safeFromSql projectIdSql in do Control.Monad.when verbose . System.IO.hPutStrLn System.IO.stderr . showString "INFO:\t" . showString Database.Selector.projectIdColumnName . showChar '=' $ shows projectId " selected." inputOptions <- Database.Selector.fromDatabase dbConnection projectIdSql Control.Monad.when verbose . System.IO.hPutStrLn System.IO.stderr $ "INFO:\tproject-configuration read from database." Database.HDBC.disconnect dbConnection return {-to IO-monad-} inputOptions _ -> error . showString "unexpected number of rows=" . shows (length projectIdRows) . showString " selected, where " . showString userIdColumnName . showChar '=' . shows userId . showString " & " . showString projectNameColumnName . showChar '=' $ showString databaseProjectName "." _ -> error . showString "unexpected number of columns=" . shows (length userRow) . showString " selected, where " . showString databaseUserIdFlag . showChar '(' . showString emailAddressColumnName . showString ")=" $ showString databaseUserId "." _ -> error . showString "unexpected number of rows=" . shows (length userRows) . showString " selected, where " . showString databaseUserIdFlag . showChar '(' . showString emailAddressColumnName . showString ")=" $ showString databaseUserId "." ) $ \e -> do Database.HDBC.disconnect dbConnection System.IO.hPutStrLn System.IO.stderr "ERROR:\tfailed to read all the required configuration from the database." Control.Exception.throwIO (e :: Control.Exception.SomeException) ) >>= processInputOptions #else /* no HDBC */ _ {-connect to the RDBMS-} -> error . showString "one or more of the database-backend Haskell packages " $ shows [odbcPackageName, mySQLPackageName] " must be installed before database-connectivity can be established." #endif /* USE_HDBC */ (_, nonOptions, []) -> error . showString "unexpected command-line arguments; " $ shows nonOptions "." (_, _, errors) -> error $ concat errors