summaryrefslogtreecommitdiff
path: root/src/main.hs
blob: c66dc6e4daefd810c19b3ba4ce85ebdbdbdebbfe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211




--  This source is licensed under Creative Commons CC0 v1.0.

--  To read the full text, see license.txt in the main directory of this repository
--  or go to https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt

--  For a human readable summary, go to https://creativecommons.org/publicdomain/zero/1.0/




import qualified System.Environment as Env
import qualified System.Console.GetOpt as Opt
import qualified System.Exit as Ex
import qualified System.Directory as Dir
import qualified System.IO as IO
import qualified Control.Monad as Con
import qualified Data.Time.Clock as Time
import qualified Data.Maybe as Maybe
import qualified Counter as Sen
import qualified Candidate as Cand
import qualified Election as Elt
import qualified Miscellaneous as Misc




data Options = Options
    { isVerbose     :: Bool
    , isVersion     :: Bool
    , isHelp        :: Bool
    , getCandFile   :: Maybe FilePath
    , getPrefFile   :: Maybe FilePath
    , getOutDir     :: Maybe FilePath
    , getNumToElect :: Maybe Int
    , getState      :: Maybe String }
        deriving Show




defaultOptions = Options
    { isVerbose     = False
    , isVersion     = False
    , isHelp        = False
    , getCandFile   = Nothing
    , getPrefFile   = Nothing
    , getOutDir     = Nothing
    , getNumToElect = Nothing
    , getState      = Nothing }




electOpt :: String -> (Options -> Options)
electOpt str =
    let r = Misc.readMaybe str :: Maybe Int
        jr = if (Maybe.isJust r && Maybe.fromJust r > 0) then r else Nothing
    in (\opts -> opts { getNumToElect = jr })




stateOpt :: String -> (Options -> Options)
stateOpt str =
    let validStates = ["NSW", "VIC", "TAS", "QLD", "SA", "WA", "NT", "ACT"]
        sr = if (str `elem` validStates) then Just str else Nothing
    in (\opts -> opts { getState = sr } )




optionHeader =
    "Usage: stv [OPTION...]\n\n" ++
    "Note that the -c, -p, -o, -e, -s options are all\n" ++
    "required for normal operation.\n"

furtherHelp =
    "Please be sure to provide all required options to run the election counter.\n" ++
    "For further information consult '--help'.\n"

optionData :: [Opt.OptDescr (Options -> Options)]
optionData =
    [ Opt.Option ['v'] ["verbose"]
        (Opt.NoArg (\opts -> opts { isVerbose = True}) )
        "chatty output on stderr"

    , Opt.Option ['V'] ["version"]
        (Opt.NoArg (\opts -> opts { isVersion = True}) )
        "show version number"

    , Opt.Option ['h'] ["help"]
        (Opt.NoArg (\opts -> opts { isHelp = True }) )
        "show this help information"

    , Opt.Option ['c'] ["candidates"]
        (Opt.ReqArg (\c opts -> opts { getCandFile = Just c }) "FILE")
        ".csv file containing AEC candidate data"

    , Opt.Option ['p'] ["preferences"]
        (Opt.ReqArg (\p opts -> opts { getPrefFile = Just p}) "FILE")
        ".csv file containing AEC formal preferences"

    , Opt.Option ['o'] ["outdir"]
        (Opt.ReqArg (\d opts -> opts { getOutDir = Just d}) "DIR")
        "new directory to output count logging"

    , Opt.Option ['e'] ["elect"]
        (Opt.ReqArg electOpt "INT")
        "number of candidates to elect"

    , Opt.Option ['s'] ["state"]
        (Opt.ReqArg stateOpt "STATE")
        "state or territory the data corresponds to" ]




getOpts :: [String] -> IO (Options, [String])
getOpts argv =
    case Opt.getOpt Opt.Permute optionData argv of
        (o,n, [] ) -> return (foldl (flip id) defaultOptions o, n)
        (_,_,errs) -> ioError (userError (concat errs ++ Opt.usageInfo optionHeader optionData))




main = do
    rawArgs <- Env.getArgs
    (options, arguments) <- getOpts rawArgs


    --  options that abort the main program
    Con.when (isHelp options) $ do
        putStrLn (Opt.usageInfo optionHeader optionData)
        Ex.exitFailure

    Con.when (isVersion options) $ do
        putStrLn "Australian STV Counter v0.1"
        Ex.exitFailure


    --  check that all necessary parameters are
    --  both present and valid
    let candidateFile = Maybe.fromJust (getCandFile options)
    Con.when (Maybe.isNothing (getCandFile options)) $
        Ex.die ("Candidate data file not provided.\n\n" ++ furtherHelp)
    doesExist <- Dir.doesFileExist candidateFile
    Con.when (not doesExist) $
        Ex.die ("Candidate data file does not exist.\n\n" ++ furtherHelp)

    let preferenceFile = Maybe.fromJust (getPrefFile options)
    Con.when (Maybe.isNothing (getPrefFile options)) $
        Ex.die ("Formal preference data file not provided.\n\n" ++ furtherHelp)
    doesExist <- Dir.doesFileExist preferenceFile
    Con.when (not doesExist) $
        Ex.die ("Formal preference data file does not exist.\n\n" ++ furtherHelp)

    let outputDir = Maybe.fromJust (getOutDir options)
    Con.when (Maybe.isNothing (getOutDir options)) $
        Ex.die ("Output logging directory not provided.\n\n" ++ furtherHelp)
    doesExist <- Dir.doesDirectoryExist outputDir
    Con.when doesExist $
        Ex.die ("Output directory already exists.\n\n" ++ furtherHelp)

    let numToElect = Maybe.fromJust (getNumToElect options)
    Con.when (Maybe.isNothing (getNumToElect options)) $
        Ex.die ("Invalid number of candidates to elect or number not provided.\n\n" ++ furtherHelp)

    let state = Maybe.fromJust (getState options)
    Con.when (Maybe.isNothing (getState options)) $
        Ex.die ("Invalid state/territory or state/territory not provided.\n\n" ++ furtherHelp)


    --  set up logging
    Dir.createDirectory outputDir
    startTime <- Time.getCurrentTime
    let mainLog = outputDir ++ "/" ++ "log.txt"
        startmsg = "Started election count at " ++ show startTime ++ "\n"
    IO.appendFile mainLog startmsg
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr startmsg


    --  set up the election processing
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr "Reading candidate data..."
    (aboveBallot, belowBallot) <- Cand.readCandidates candidateFile state
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr "Reading preference data..."
    counter <- Sen.createSenateCounter preferenceFile aboveBallot belowBallot
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr "Done.\n"
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr "Setting up election..."
    election <- Elt.createElection outputDir mainLog counter numToElect (isVerbose options)
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr "Done.\n"


    --  run the show
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr "Running...\n"
    Elt.doCount election
    Con.when (isVerbose options) $ IO.hPutStr IO.stderr "\n"


    --  finish up logging
    endTime <- Time.getCurrentTime
    let endmsg = "Finished election count at " ++ show endTime ++ "\n"
        elapsedmsg = show (Time.diffUTCTime endTime startTime) ++ " elapsed\n"
    IO.appendFile mainLog (endmsg ++ elapsedmsg)
    Con.when (isVerbose options) $ IO.hPutStrLn IO.stderr (endmsg ++ elapsedmsg)