Ajout : rangement.
[tool/hledger.git] / script / hledger-range-voting.hs
diff --git a/script/hledger-range-voting.hs b/script/hledger-range-voting.hs
new file mode 100755 (executable)
index 0000000..2bcc330
--- /dev/null
@@ -0,0 +1,201 @@
+#!/usr/bin/env runhaskell
+{-|
+hledger-range-voting [-f JOURNALFILE]
+
+Perform range voting calculus
+on the default or specified journal.
+|-}
+
+import Data.List
+--import Data.List (mapAccumL)
+import Data.Maybe
+import Data.Ord
+import Data.Time.Calendar
+import System.Console.CmdArgs.Explicit
+import Text.Printf
+import qualified Data.Map as Map
+
+import Hledger
+import Hledger.Cli
+import Hledger.Cli.Options
+-- import Prelude hiding (putStr)
+import Hledger.Utils.UTF8IOCompat (putStr)
+
+
+argsmode :: Mode RawOpts
+argsmode = (defCommandMode ["range-voting"])
+       { modeHelp = "perform range voting"
+       , modeGroupFlags = Group
+               { groupNamed =
+                       [ ("Input",inputflags)
+                       , ("Reporting",reportflags)
+                       , ("Misc",helpflags)
+                       ]
+               , groupUnnamed = []
+               , groupHidden = []
+               }
+       }
+
+
+-- like Register.summarisePostings
+-- | Print various statistics for the journal.
+main :: IO ()
+main = do
+       opts <- getCliOpts argsmode
+       withJournalDo opts $
+        \CliOpts{reportopts_=reportopts_} j -> do
+               d <- getCurrentDay
+               let q = queryFromOpts d reportopts_
+               let l = ledgerFromJournal q j
+               let reportspan = (ledgerDateSpan l) `spanDefaultsFrom` (queryDateSpan False q)
+               let intervalspans = splitSpan (intervalFromOpts reportopts_) reportspan
+               let s = intercalate "\n" $ map (showRangeVoting reportopts_{empty_=True} l d) intervalspans
+               putStr s
+
+data Stats =
+       Stats
+        { stats_choice      :: String
+        , stats_sum         :: Double
+        , stats_blank_count :: Int
+        , stats_null_count  :: Int
+        , stats_null_value  :: Double
+        , stats_values      :: [(Int,Int)]
+        }
+stats :: Stats
+stats = Stats
+ { stats_choice      = ""
+ , stats_sum         = 0.0
+ , stats_blank_count = 0
+ , stats_null_count  = 0
+ , stats_null_value  = 0.0
+ , stats_values      = []
+ }
+
+
+showRangeVoting :: ReportOpts -> Ledger -> Day -> DateSpan -> String
+showRangeVoting ropts l today span =
+       unlines $ map (\(label,value) -> concatBottomPadded [printf fmt1 label, value]) main
+        where
+               fmt1 = "%-" ++ show w1 ++ "s: "
+               -- fmt2 = "%-" ++ show w2 ++ "s"
+               w1 = maximum $ map (length . fst) main
+               -- w2 = maximum $ map (length . show . snd) main
+               main =
+                concat $
+                [ [("Span vote (official)", printf "%s to %s (%d days)" (show $ (tdate opening)) (show $ (tdate2 opening)) votedays)
+                        ,("Span vote (actual)", printf "%s to %s (%d days)" (start span) (end span) days)
+                        ,("Cardinal", printf "%d" cardinal)
+                        ,("Values", "{"++(intercalate "," $ map (printf "%d") values)++"}")
+                        ,("Quorum", printf "%d/%d (%f%%)" votesnum votersnum (fromIntegral votesnum*100.0/fromIntegral votersnum::Double))
+                        ,("Blanks", printf "%d" $ foldl (\acc t -> acc + stats_blank_count t) 0 stats)
+                        ,("Nulls", printf "%d" $ foldl (\acc t -> acc + stats_null_count t) 0 stats)
+                        ,("Medians", "")
+                        ]
+                , map (\t ->
+                       ( printf "  - %s" (stats_choice t)
+                       , printf "sum: %+.f" (stats_sum t)
+                       ) ) medians
+                , [("Choices", printf "%d" nchoices)
+                        ]
+                , map
+                        (\t ->
+                               ( printf "  - %s" (stats_choice t)
+                               , printf "sum: %+f = %d*(0:blank) + %d*(%+.2f:null) + %s"
+                                        (stats_sum t)
+                                        (stats_blank_count t)
+                                        (stats_null_count t)
+                                        (stats_null_value t)
+                                        (intercalate " + " $ map (\(value,sum) -> printf "%d*(%+d)" sum value) (stats_values t))
+                               )
+                        ) stats
+                --,("Commodities", printf "%s (%s)" (show $ length cs) (intercalate ", " cs))
+                ]
+                where
+                       j = ljournal l
+                       ts = sortBy (comparing tdate) $ filter (spanContainsDate span . tdate) $ jtxns j
+                       as = nub $ map paccount $ concatMap tpostings ts
+                       cs = Map.keys $ canonicalStyles $ concatMap amounts $ map pamount $ concatMap tpostings ts
+                       showelapsed Nothing = ""
+                       showelapsed (Just days) = printf " (%d %s)" days' direction
+                               where days' = abs days
+                                     direction | days >= 0 = "days ago" :: String
+                                               | otherwise = "days from now"
+                       start (DateSpan (Just d) _) = show d
+                       start _ = ""
+                       end (DateSpan _ (Just d)) = show d
+                       end _ = ""
+                       days = fromMaybe 0 $ daysInSpan span
+                       votedays = fromMaybe 0 $ daysInSpan
+                        (DateSpan
+                                (Just (tdate opening))
+                                (maybe Nothing Just (tdate2 opening)))
+                       acctnum = length as
+                       openings = filter ((== "Opening") . tdescription) $ ts
+                       opening | null openings = error' "\"Opening\" transaction is missing"
+                               | otherwise     = head $ openings
+                       cardinals = filter ((== "Cardinal") . fst) $ ttags opening
+                       cardinal | null cardinals = error' "\"Cardinal\" tag is missing on \"Opening\" transaction"
+                                | otherwise      = read $ snd $ head $ cardinals :: Int
+                       values | (cardinal `mod` 2 == 0) = delete 0 [-cardinal`div`2..cardinal`div`2]
+                              | otherwise               = [-(cardinal-1)`div`2..(cardinal-1)`div`2]
+                       choices =
+                               map paccount $
+                               filter (isPrefixOf "Choice:" . paccount) $
+                               tpostings opening
+                       
+                       votes = filter ((== "Vote") . tdescription) $ ts
+                       stats =
+                               sortBy (\a b -> (stats_sum b) `compare` (stats_sum a)) $
+                               map (\s ->
+                                       let mean = fromIntegral (foldl (\acc (v,c) -> acc + (v * c)) 0 (stats_values s)) / fromIntegral (length (stats_values s)) in
+                                       s
+                                        { stats_null_value = mean
+                                        , stats_sum        = (stats_sum s) + (mean * fromIntegral (stats_null_count s))
+                                        }) $
+                               foldl (\s vote ->
+                                       map (\s ->
+                                               case filter ((== stats_choice s) . paccount) (tpostings vote) of
+                                                [] -> s { stats_blank_count = stats_blank_count s + 1 }
+                                                [choice_posting] ->
+                                                       case pamount choice_posting of
+                                                        Mixed [Amount{acommodity="", aquantity=q, aprice=NoPrice}] ->
+                                                               let i = floor q in
+                                                               if snd (properFraction q) == 0.0 && i `elem` values
+                                                               then s
+                                                                        { stats_sum    = stats_sum s + fromIntegral i
+                                                                        , stats_values = map (\(v,c) -> if v == i then (v,c+1) else (v,c)) (stats_values s)
+                                                                        }
+                                                               else s { stats_null_count = stats_null_count s + 1 } -- error' $ printf "TODO: null vote: invalid value: %f" q
+                                                        _ ->  s { stats_null_count = stats_null_count s + 1 } -- error' "TODO: null vote: too much informations"
+                                                _ ->    s { stats_null_count = stats_null_count s + 1 }
+                                        )
+                                        s
+                                )
+                                [ Stats
+                                       { stats_choice      = choice
+                                       , stats_sum         = 0.0
+                                       , stats_blank_count = 0
+                                       , stats_null_count  = 0
+                                       , stats_null_value  = 0.0
+                                       , stats_values      = [(value,0) | value<-values]
+                                       }
+                                | choice<-choices ]
+                                votes
+                       
+                       medians =
+                               filter (\c -> stats_sum c `elem` medians) stats
+                                where medians =
+                                       if length choices `mod` 2 == 0
+                                       then map (stats_sum . (!!) stats) [nchoices `div` 2 - 1, nchoices `div` 2 + 1]
+                                       else map (stats_sum . (!!) stats) [(nchoices - 1) `div` 2]
+                               
+                       nchoices = length choices
+                       
+                       -- choicesbalancesropts = ropts{query_="Choice:"}
+                       -- choicesbalances = tail $ fst $ balanceReport ropts (queryFromOpts today choicesbalancesropts) j
+                       
+                       voteras = filter (isPrefixOf "Voter:" . paccount) $ tpostings opening
+                       votests = filter ((== "Vote") . tdescription) $ ts
+                       votesnum = length votests
+                       votersnum = length voteras
+                       votechoicesps = filter (isPrefixOf "Choice:" . paccount) $ tpostings opening