module Penny.Cabin.Allocate ( Allocation, allocation, unAllocation, allocate) where import qualified Control.Monad.Trans.State as St import qualified Data.Foldable as F import qualified Data.Traversable as T import qualified Data.Map as M newtype Allocation = Allocation { unAllocation :: Int } deriving (Show, Eq, Ord) allocation :: Int -> Allocation allocation i = if i < 1 then error "Allocations must be at least 1" else Allocation i -- | Divide up a whole number proportionally into several parts. The -- sum of the parts is guaranteed to add up to original number. allocate :: Ord k => M.Map k Allocation -- ^ Maps arbitrary unique values to the allocations, so that you -- can look up the corresponding allocations. -> Int -- ^ Allocated values will add up to this number. -> M.Map k Int -- ^ The given number, allocated into proportional parts. allocate m t = let tot = F.sum . fmap (toDouble . unAllocation) $ m ratios = fmap ((/tot) . toDouble . unAllocation) m rounded = fmap (round . (* (toDouble t))) ratios toDouble = fromIntegral :: Int -> Double in if M.null m then M.empty else adjust rounded t adjust :: Ord k => M.Map k Int -> Int -> M.Map k Int adjust ws w = let wsInts = fmap fromIntegral ws diff = (fromIntegral w) - F.sum wsInts in if M.null ws then M.empty else if diff == 0 then ws else let ws' = St.evalState (T.mapM adjustMap ws) diff in adjust ws' w -- | The state is the target number minus the current actual total. adjustMap :: Int -> St.State Int Int adjustMap w = do diff <- St.get case compare diff 0 of EQ -> return w GT -> do St.put (pred diff) return (succ w) LT -> do St.put (succ diff) return (pred w)