1 {-# LANGUAGE ScopedTypeVariables #-}
2 module Hjugement.Section where
4 import Control.Applicative (Applicative(..))
6 import Data.Either (Either(..))
7 import Data.Eq (Eq(..))
8 import Data.Foldable (Foldable(..), any)
9 import Data.Function (($), (.))
10 import Data.Traversable (Traversable(..))
11 import Data.Functor ((<$>), (<$))
12 import Data.Maybe (Maybe(..), isNothing, maybe, fromMaybe)
13 import Data.Hashable (Hashable(..))
14 import Data.Map.Strict (Map)
15 import Data.Ord (Ord(..))
16 import Data.Ratio ((%))
17 import Data.Semigroup (Semigroup(..))
18 import Prelude (Num(..), Fractional(..), toRational, fromIntegral)
20 import Text.Show (Show(..))
21 import qualified Data.List as List
22 import qualified Data.HashMap.Strict as HM
23 import qualified Data.HashSet as HS
24 import qualified Data.Map.Strict as Map
29 -- | Node of a 'Tree' holding a 'SectionOpinion' per 'judge'.
30 type Section judge grade = HM.HashMap judge (SectionOpinion grade)
32 -- ** Type 'SectionOpinion'
33 data SectionOpinion grade
35 { sectionShare :: Maybe Share
36 -- ^ A 'Share' within the parent 'Section'
37 -- (defaulting to a 'Share' computed as the remaining 'Share' to reach 1
38 -- divided by the number of defaulted 'Share's).
39 , sectionGrade :: Maybe grade
40 -- ^ A 'grade' attributed to the current 'Section'
41 -- (defaulting to the 'grade' set on an ancestor 'Section' if any,
42 -- or the |judge|'s default grade).
45 -- | Compute the 'Opinion's from the |Judges|' evaluation on all or some 'Section's.
54 Tree (Section judge grade) ->
55 Either (ErrorSection judge)
56 (Tree (Opinions judge grade))
57 opinionsBySection js = go (singleGrade <$> js)
59 go defaultGradeByJudge (Node gradeByJudge opinionByJudgeBySection) =
60 let maybeShareBySectionByJudge :: HM.HashMap judge [Maybe Share] =
61 foldr (\section -> HM.unionWith (<>) $ pure . sectionShare <$> rootLabel section)
62 HM.empty opinionByJudgeBySection in
63 let shareBySectionByJudge :: HM.HashMap judge [Share] =
64 (<$> maybeShareBySectionByJudge) $ \maybeShareBySection ->
65 let usedShare = sum $ fromMaybe 0 <$> maybeShareBySection in
66 let unusedShares = toRational $ List.length $ List.filter isNothing maybeShareBySection in
67 let evenShare = (1 - usedShare) / unusedShares in
68 fromMaybe evenShare <$> maybeShareBySection
70 case opinionByJudgeBySection of
71 [] | unknownJudges <- gradeByJudge`HM.difference`defaultGradeByJudge
72 , not $ null unknownJudges ->
73 Left $ ErrorSection_unknown_judges $ HS.fromMap $ (() <$) $ unknownJudges
75 -- NOTE: no Section, current Opinions is derived from current Node and default grades.
76 let opin :: HM.HashMap judge (Map grade Share) =
77 HM.mapWithKey (\judge -> maybe (defaultGradeByJudge HM.! judge) singleGrade . sectionGrade) gradeByJudge in
78 Right $ Node (HM.union opin defaultGradeByJudge) []
79 _ | ko <- HM.filter (\ss -> any (< 0) ss || sum ss /= 1) shareBySectionByJudge
81 Left $ ErrorSection_invalid_shares ko
83 -- NOTE: current Opinions is computed from its sub-'Section's.
84 let defaultGradeByJudge' :: HM.HashMap judge (Map grade Share) =
85 -- NOTE: grades set at current node become the new default grades within its 'Section's.
87 (HM.mapWithKey (\judge ->
88 maybe (defaultGradeByJudge HM.! judge) singleGrade . sectionGrade) gradeByJudge)
90 opins <- traverse (go defaultGradeByJudge') opinionByJudgeBySection
91 let distByJudgeBySection :: [HM.HashMap judge (Map grade Share)] = rootLabel <$> opins
92 let distBySectionByJudge :: HM.HashMap judge [Map grade Share] =
93 foldr (\distByJudge -> HM.unionWith (<>) (pure <$> distByJudge))
94 HM.empty distByJudgeBySection
95 let numSections = List.length opinionByJudgeBySection
96 let defaultShares = List.replicate numSections $ 1 % fromIntegral numSections
97 let opin :: HM.HashMap judge (Map grade Share) =
98 HM.intersectionWith -- NOTE: just used to zip actually
99 (\shareBySection distBySection ->
100 Map.unionsWith (+) $ -- NOTE: merge dists
101 List.zipWith (\share dist -> (share *) <$> dist) -- NOTE: shrink dists
102 shareBySection distBySection)
103 (shareBySectionByJudge`HM.union`(defaultShares <$ defaultGradeByJudge))
105 Right $ Node opin opins
107 -- * Type 'ErrorSection'
108 data ErrorSection judge
109 = ErrorSection_invalid_shares (HM.HashMap judge [Share])
110 -- ^ When at least one of the 'Share's is not positive, or when their sum is not 1.
111 | ErrorSection_unknown_judges (HS.HashSet judge)
112 -- ^ When at least one 'Section'