#!/usr/bin/runhaskell I've always liked the idea of not having to explicitly compile a Haskell program before using it. I always forget to recompile a C program before running it, and then can't figure out why I don't see the result of my changes. Of course, it's slower every time you run it, which gets annoying if the program is pretty stable.... > module Main where > import Data.Time -- for parseTime, etc. > import List -- for sort > import Maybe -- for fromMaybe > import System.Console.SimpleLineEditor as RL -- because getLine sucks > import System.Environment -- for getArgs > import System.Exit -- for exitWith > import System.IO -- for openFile, hGetContents, hSeek, etc. > import System.Locale -- for defaultTimeLocale What data is included in a posted score? When you played, where you played, what that course's rating and slope are, and how well you did. From that you can calculate the posted score's differential, which will be used for getting the handicap later. > data PostedScore = PostedScore { > day :: Day, > place :: String, > rating :: Float, > slope :: Int, > score :: Int > } deriving (Show, Read) There are going to be times we're going to need to sort the differentials in reverse order by date (most recent at the front of the list). > compareByDay :: PostedScore -> PostedScore -> Ordering > compareByDay a b = compare (day b) (day a) If we want to save and restore the differentials to a file, it might be helpful to convert them from an array to a String and vice versa. > readDiffs :: String -> [PostedScore] > readDiffs = sortBy compareByDay . map read . lines > showDiffs :: [PostedScore] -> String > showDiffs = unlines . map show . sortBy compareByDay This is how you calculate the 'differential' for a given round. I have no idea where the number 113 comes from. > scoreDifferential :: PostedScore -> Float > scoreDifferential posted = (playerScore - courseRating) * 113 / courseSlope > where playerScore = fromIntegral (score posted) > courseRating = rating posted > courseSlope = fromIntegral (slope posted) This is the formula to compute your handicap. Given an array of posted scores, of the 20 most recent, take .96 of the average of the 10 with the lowest differential. If you have fewer than 20 scores, then there's a rule for how many of the lowest differentials to use. > handicap :: [PostedScore] -> Maybe Float > handicap xs = if num > 0 then Just (0.96 * (averageOf recentBest)) else Nothing > where averageOf x = (sum x) / (fromIntegral $ length x) > recentBest = take num $ sort $ map scoreDifferential $ take 20 $ sortBy compareByDay xs > num = numUsed $ length xs This is the table of how many differentials to use based on the number of recent scores available. > numUsed :: Int -> Int > numUsed x | x < 5 = 0 > numUsed x | x >= 5 && x <= 17 = (x - 1) `div` 2 > numUsed 18 = 8 > numUsed 19 = 9 > numUsed x | x >= 20 = 10 Everything from here down is in the IO Monad, either because it's main, or it asks the user for information, or it gets information from a file. > huh :: String > huh = "Sorry, I didn't get that." > promptDay :: String -> IO Day > promptDay str = do > date <- (RL.getLineEdited str >>= return . parseTime defaultTimeLocale "%m/%d/%Y" . fromMaybe "") > if isNothing date > then do > putStrLn huh > promptDay str > else return (fromJust date) > promptNum :: (Num a, Read a, Ord a) => String -> a -> a -> IO a > promptNum str min max = do > maybeNum <- (RL.getLineEdited str >>= return . reads . fromMaybe "") > if null maybeNum > then do > putStrLn huh > promptNum str min max > else do > let num = fst $ head maybeNum > if num >= min && num <= max > then return num > else do > putStrLn "Sorry, that's out of range." > promptNum str min max > promptScore :: IO PostedScore > promptScore = do > date <- promptDay "What day did you play (mm/dd/yyyy)? " > course <- RL.getLineEdited "Where did you play? " > rated <- promptNum "What was the rating (67.0-77.0)? " 67.0 77.0 > slop <- promptNum "And the slope (105-155)? " 105 155 > gross <- promptNum "How'd you shoot? " 18 140 > let ps = PostedScore { day = date, > place = fromMaybe "" course, > rating = rated, > slope = slop, > score = gross > } > let diff = scoreDifferential ps > putStrLn $ "The differential for that round was " ++ (show diff) > return ps > promptPost :: IO Bool > promptPost = do > post <- RL.getLineEdited "Post a score [y/n]? " > case post of > Just "y" -> return True > Just "n" -> return False > _ -> promptPost > addScores :: IO [PostedScore] > addScores = do > post <- promptPost > if post > then do > score <- promptScore > more <- addScores > return (score:more) > else return [] > usage :: IO a > usage = do > prog <- getProgName > putStrLn $ "Usage: " ++ prog ++ " " > exitWith (ExitSuccess) > main :: IO () > main = do > RL.initialise > args <- getArgs > if null args then usage else return () > let filename = head args > handle <- catch (openFile filename ReadWriteMode) (\ e -> putStrLn ("Cannot open " ++ filename ++ ": " ++ (show e)) >> exitWith (ExitFailure 1)) > previous <- (hGetContents handle >>= return . readDiffs) > newDiffs <- addScores > let combined = newDiffs ++ previous > let index = handicap combined > if isNothing index > then putStrLn $ "You have not posted enough scores to generate your index yet." > else putStrLn $ "Your handicap index is " ++ (show (fromJust index)) > hClose handle > newh <- openFile filename WriteMode > hPutStr newh $ showDiffs combined > hClose newh vim:set sw=2 ts=2 et: