Actually implement inventory handling
This commit is contained in:
parent
1eb6df9261
commit
9f50e5d3bb
7 changed files with 155 additions and 18 deletions
|
@ -17,12 +17,17 @@ cabal-version: >=1.10
|
||||||
|
|
||||||
executable pilu
|
executable pilu
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
other-modules: CSV
|
other-modules: CLI
|
||||||
|
, CSV
|
||||||
, Event
|
, Event
|
||||||
, Medicine
|
, Medicine
|
||||||
|
, Paths_pilu
|
||||||
|
, Timeline
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
build-depends: base >=4.9 && <4.10
|
build-depends: base >=4.9 && <4.13
|
||||||
, containers
|
, containers
|
||||||
|
, mtl
|
||||||
|
, optparse-applicative
|
||||||
, parsec
|
, parsec
|
||||||
, time
|
, time
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
|
|
29
src/CLI.hs
Normal file
29
src/CLI.hs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module CLI (
|
||||||
|
Invocation(..)
|
||||||
|
, invoked
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Options.Applicative (
|
||||||
|
Parser, execParser, flag, flag', fullDesc, header, help, helper, info, long
|
||||||
|
)
|
||||||
|
import Control.Applicative ((<**>), (<|>))
|
||||||
|
import Data.Version (showVersion)
|
||||||
|
import qualified Paths_pilu as Pilu (version)
|
||||||
|
|
||||||
|
data Invocation = Inventory | Schedule | Version String
|
||||||
|
|
||||||
|
versionStr :: String
|
||||||
|
versionStr = showVersion Pilu.version
|
||||||
|
|
||||||
|
invocation :: Parser Invocation
|
||||||
|
invocation =
|
||||||
|
flag' Schedule
|
||||||
|
(long "schedule" <> help "Show when next to go for provisioning")
|
||||||
|
<|> flag Inventory (Version versionStr)
|
||||||
|
(long "version" <> help "Show the version number")
|
||||||
|
|
||||||
|
invoked :: IO Invocation
|
||||||
|
invoked = execParser $
|
||||||
|
info
|
||||||
|
(invocation <**> helper)
|
||||||
|
(fullDesc <> header ("Pilu v" ++ versionStr))
|
|
@ -6,7 +6,7 @@ module CSV (
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
import Data.Map (Map)
|
import Data.Map (Map)
|
||||||
import qualified Data.Map as Map (fromList, keys)
|
import qualified Data.Map as Map (fromList, elems)
|
||||||
import Text.Parsec.Pos (newPos)
|
import Text.Parsec.Pos (newPos)
|
||||||
import Text.ParserCombinators.Parsec ((<?>), Parser, ParseError, char, endBy, many, noneOf, sepBy, string, try)
|
import Text.ParserCombinators.Parsec ((<?>), Parser, ParseError, char, endBy, many, noneOf, sepBy, string, try)
|
||||||
import Text.ParserCombinators.Parsec.Error (Message(..), newErrorMessage)
|
import Text.ParserCombinators.Parsec.Error (Message(..), newErrorMessage)
|
||||||
|
@ -16,7 +16,7 @@ class Row a where
|
||||||
fromRow :: Map String String -> Either Message a
|
fromRow :: Map String String -> Either Message a
|
||||||
|
|
||||||
instance Row [String] where
|
instance Row [String] where
|
||||||
fromRow = return . Map.keys
|
fromRow = return . Map.elems
|
||||||
|
|
||||||
csvFile :: Char -> Parser [[String]]
|
csvFile :: Char -> Parser [[String]]
|
||||||
csvFile separator = line `endBy` eol
|
csvFile separator = line `endBy` eol
|
||||||
|
|
15
src/Event.hs
15
src/Event.hs
|
@ -1,4 +1,6 @@
|
||||||
module Event (
|
module Event (
|
||||||
|
Event(..)
|
||||||
|
, EventType(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import CSV (Row(..))
|
import CSV (Row(..))
|
||||||
|
@ -7,14 +9,15 @@ import Data.List (isSuffixOf, takeWhile, sortOn)
|
||||||
import Data.Map (Map)
|
import Data.Map (Map)
|
||||||
import qualified Data.Map as Map (delete, empty, insert, lookup, toList)
|
import qualified Data.Map as Map (delete, empty, insert, lookup, toList)
|
||||||
import Data.Time (Day)
|
import Data.Time (Day)
|
||||||
|
import Medicine (MedicineName)
|
||||||
import Text.ParserCombinators.Parsec.Error (Message(..))
|
import Text.ParserCombinators.Parsec.Error (Message(..))
|
||||||
|
|
||||||
data EventType = Prescription | Provisioning deriving (Eq, Show)
|
data EventType = Prescription | Provisioning deriving (Eq, Show)
|
||||||
|
|
||||||
data Event = Event {
|
data Event = Event {
|
||||||
date :: Day
|
date :: Day
|
||||||
, amounts :: Map String Int
|
, amounts :: Map MedicineName Float
|
||||||
, eventtype :: EventType
|
, eventType :: EventType
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance Row Event where
|
instance Row Event where
|
||||||
|
@ -30,13 +33,13 @@ instance Row Event where
|
||||||
unexpected = Left . UnExpect . show
|
unexpected = Left . UnExpect . show
|
||||||
addAmount (evType, ints) (key, val) = do
|
addAmount (evType, ints) (key, val) = do
|
||||||
(newEvType, amount) <- readAmount val
|
(newEvType, amount) <- readAmount val
|
||||||
let newInts = Map.insert key amount ints
|
let newFloats = Map.insert key amount ints
|
||||||
case evType of
|
case evType of
|
||||||
Nothing -> Right (Just newEvType, newInts)
|
Nothing -> Right (Just newEvType, newFloats)
|
||||||
Just currentEvType ->
|
Just currentEvType ->
|
||||||
if currentEvType == newEvType then Right (evType, newInts) else unexpected newEvType
|
if currentEvType == newEvType then Right (evType, newFloats) else unexpected newEvType
|
||||||
|
|
||||||
readAmount :: String -> Either Message (EventType, Int)
|
readAmount :: String -> Either Message (EventType, Float)
|
||||||
readAmount [] = Left $ Expect "prescription or provisioning"
|
readAmount [] = Left $ Expect "prescription or provisioning"
|
||||||
readAmount ('+':n) = Right (Provisioning, read n)
|
readAmount ('+':n) = Right (Provisioning, read n)
|
||||||
readAmount s =
|
readAmount s =
|
||||||
|
|
32
src/Main.hs
32
src/Main.hs
|
@ -1,4 +1,34 @@
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
|
import CLI (Invocation(..), invoked)
|
||||||
|
import CSV (Row, parse)
|
||||||
|
import Data.Time (UTCTime(..), getCurrentTime)
|
||||||
|
import Event (Event)
|
||||||
|
import Medicine (Medicine, Pharmacy, pharmacy)
|
||||||
|
import System.Exit (exitFailure, exitSuccess)
|
||||||
|
import Timeline (State, currentState)
|
||||||
|
|
||||||
|
readCSV :: Row a => FilePath -> IO [a]
|
||||||
|
readCSV filePath = do
|
||||||
|
parsed <- parse filePath '\t' <$> readFile filePath
|
||||||
|
case parsed of
|
||||||
|
Left e -> (putStrLn $ show e) >> exitFailure
|
||||||
|
Right rows -> return rows
|
||||||
|
|
||||||
|
getPharmacy :: String -> IO Pharmacy
|
||||||
|
getPharmacy = fmap pharmacy . readCSV
|
||||||
|
|
||||||
|
getCurrentState :: IO State
|
||||||
|
getCurrentState =
|
||||||
|
currentState
|
||||||
|
<$> (utctDay <$> getCurrentTime)
|
||||||
|
<*> getPharmacy "medicine.csv"
|
||||||
|
<*> readCSV "timeline.csv"
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = putStrLn "Hello, Haskell!"
|
main = do
|
||||||
|
invocation <- invoked
|
||||||
|
case invocation of
|
||||||
|
Inventory -> show <$> getCurrentState >>= putStrLn
|
||||||
|
Schedule -> putStrLn "schedule"
|
||||||
|
Version version -> putStrLn version >> exitSuccess
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
module Medicine (
|
module Medicine (
|
||||||
Medicine(..)
|
Medicine(..)
|
||||||
|
, MedicineName
|
||||||
, Pharmacy
|
, Pharmacy
|
||||||
|
, Stock
|
||||||
, pharmacy
|
, pharmacy
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
@ -10,22 +12,22 @@ import Data.Map (Map)
|
||||||
import qualified Data.Map as Map (empty, insert, lookup)
|
import qualified Data.Map as Map (empty, insert, lookup)
|
||||||
import Text.ParserCombinators.Parsec.Error (Message(..))
|
import Text.ParserCombinators.Parsec.Error (Message(..))
|
||||||
|
|
||||||
|
type MedicineName = String
|
||||||
|
|
||||||
data Medicine = Medicine {
|
data Medicine = Medicine {
|
||||||
name :: String
|
name :: MedicineName
|
||||||
, content :: Int
|
, minStock :: Float
|
||||||
, minStock :: Int
|
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance Row Medicine where
|
instance Row Medicine where
|
||||||
fromRow assoc = Medicine <$>
|
fromRow assoc = Medicine <$>
|
||||||
get "name"
|
get "name"
|
||||||
<*> (read <$> get "content")
|
|
||||||
<*> (read <$> get "minStock")
|
<*> (read <$> get "minStock")
|
||||||
where
|
where
|
||||||
get key = maybe (Left $ Expect key) Right $ Map.lookup key assoc
|
get key = maybe (Left $ Expect key) Right $ Map.lookup key assoc
|
||||||
|
|
||||||
type Pharmacy = Map String Medicine
|
type Pharmacy = Map MedicineName Medicine
|
||||||
|
type Stock = Map MedicineName Float
|
||||||
|
|
||||||
pharmacy :: [Medicine] -> Pharmacy
|
pharmacy :: [Medicine] -> Pharmacy
|
||||||
pharmacy [] = Map.empty
|
pharmacy = foldr (\medicine -> Map.insert (name medicine) medicine) Map.empty
|
||||||
pharmacy (medicine:medicines) = Map.insert (name medicine) medicine $ pharmacy medicines
|
|
||||||
|
|
68
src/Timeline.hs
Normal file
68
src/Timeline.hs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Timeline (
|
||||||
|
State
|
||||||
|
, currentState
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad (foldM, join)
|
||||||
|
import Control.Monad.Reader (Reader, runReader, ask)
|
||||||
|
import Data.List (sortOn)
|
||||||
|
import Data.Map ((!), Map, mapWithKey, toList)
|
||||||
|
import Data.Time (Day, diffDays)
|
||||||
|
import Event (Event(..), EventType(..))
|
||||||
|
import Medicine (Pharmacy, Stock)
|
||||||
|
|
||||||
|
type ConsumptionRate = Map String Float
|
||||||
|
|
||||||
|
data State = State {
|
||||||
|
day :: Day
|
||||||
|
, stock :: Stock
|
||||||
|
, consumptionRate :: ConsumptionRate
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Show State where
|
||||||
|
show (State {day, stock, consumptionRate}) = unlines $
|
||||||
|
("day: " ++ show day)
|
||||||
|
: ("stock:" : (indent <$> showAssoc stock) :: [String])
|
||||||
|
++ ("consumptionRate:" : (indent <$> showAssoc consumptionRate))
|
||||||
|
where
|
||||||
|
indent :: String -> String
|
||||||
|
indent = ('\t' :)
|
||||||
|
showAssoc :: Show a => Map String a -> [String]
|
||||||
|
showAssoc = fmap (\(key, val) -> key ++ ": " ++ show val) . toList
|
||||||
|
|
||||||
|
initState :: Reader Pharmacy State
|
||||||
|
initState = do
|
||||||
|
pharmacy <- ask
|
||||||
|
return $ State {
|
||||||
|
day = toEnum 0
|
||||||
|
, stock = const 0 <$> pharmacy
|
||||||
|
, consumptionRate = const 0 <$> pharmacy
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEvent :: State -> Event -> State
|
||||||
|
applyEvent state (Event {date, amounts, eventType}) =
|
||||||
|
case eventType of
|
||||||
|
Prescription -> newState {consumptionRate = amounts}
|
||||||
|
Provisioning -> newState {stock = mapWithKey addAmount $ stock newState}
|
||||||
|
where
|
||||||
|
newState = setDay date state
|
||||||
|
addAmount medicineName = (+ (amounts ! medicineName))
|
||||||
|
|
||||||
|
setDay :: Day -> State -> State
|
||||||
|
setDay newDay state@(State {day, stock, consumptionRate}) = state {
|
||||||
|
day = newDay
|
||||||
|
, stock = mapWithKey consume stock
|
||||||
|
}
|
||||||
|
where
|
||||||
|
duration = toEnum . fromEnum $ newDay `diffDays` day
|
||||||
|
consume medicineName initialAmount =
|
||||||
|
max 0 $ initialAmount - duration * (consumptionRate ! medicineName)
|
||||||
|
|
||||||
|
lastState :: [Event] -> Reader Pharmacy State
|
||||||
|
lastState events =
|
||||||
|
foldl applyEvent <$> initState <*> (return $ sortOn date events)
|
||||||
|
|
||||||
|
currentState :: Day -> Pharmacy -> [Event] -> State
|
||||||
|
currentState targetDay pharmacy events =
|
||||||
|
setDay targetDay (runReader (lastState events) pharmacy)
|
Loading…
Reference in a new issue