{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StrictData #-} module Main where import Control.Monad (when) import Control.Monad.Writer hiding (getFirst) import Data.Aeson import Data.Aeson.Types (Parser) import qualified Data.ByteString.Lazy.Char8 import qualified Data.Char import Data.List (isInfixOf) import qualified Data.Map import Data.Maybe (fromMaybe) import qualified Data.Text import GHC.Generics import Network.Curl import System.Exit (exitFailure) import System.IO (hPutStrLn, stderr) import Text.Printf (printf) -- Date time that easily comparable. data SillyTime = SillyTime { sillyTimeDate :: !String, sillyTimeAMPM :: !String, sillyTimeTime :: !String } deriving (Ord, Eq, Show) data CurrentCondition = CurrentCondition { feelsLikeF :: !String, cloudCover :: !String, humidity :: !String, tempF :: !String, weatherDesc :: !String, windspeedMiles :: !String, winddir :: !String, observationTime :: !SillyTime } deriving (Generic, Show) newtype NearestArea = NearestArea { areaName :: String } deriving (Generic, Show) data Astronomy = Astronomy { sunrise :: !SillyTime, sunset :: !SillyTime } deriving (Generic, Show) data Weather = Weather { currentCondition :: !CurrentCondition, nearestArea :: !NearestArea, astronomy :: !Astronomy } deriving (Generic, Show) getFirst :: Parser [a] -> Parser a getFirst ls = do l <- ls case l of (a : _) -> return a [] -> fail "No current conditions" instance FromJSON CurrentCondition where parseJSON = withObject "CurrentCondition" $ \v -> CurrentCondition <$> v .: "FeelsLikeF" <*> v .: "cloudcover" <*> v .: "humidity" <*> v .: "temp_F" <*> ( withArray "WeatherDescriptions" ( \vs -> do concat <$> mapM (withObject "Description" (.: "value")) vs ) =<< (v .: "weatherDesc") ) <*> v .: "windspeedMiles" <*> v .: "winddir16Point" <*> ( withText "observationTime" ( \txt -> case Data.Text.unpack <$> Data.Text.words txt of [a, b, c] -> return (SillyTime a c b) _ -> fail $ printf "1 Failed to parse SillyTime [%s]" (Data.Text.unpack txt) ) =<< (v .: "localObsDateTime") ) instance FromJSON NearestArea where parseJSON = withObject "NearestArea" $ \v -> fmap NearestArea $ do (areaNames :: [Object]) <- v .: "areaName" concat <$> mapM (.: "value") areaNames parseAstronomy :: String -> Value -> Parser Astronomy parseAstronomy date = withObject "Astronomy" $ \v -> Astronomy <$> (withText "sunrise" toSillyTime =<< (v .: "sunrise")) <*> (withText "sunset" toSillyTime =<< (v .: "sunset")) where toSillyTime str = case Data.Text.unpack <$> Data.Text.words str of [a, b] -> return $ SillyTime date b a _ -> fail $ printf "2 Failed to parse SillyTime [%s]" (Data.Text.unpack str) instance FromJSON Weather where parseJSON = withObject "Weather" $ \v -> Weather <$> getFirst (v .: "current_condition") <*> getFirst (v .: "nearest_area") <*> ( withObject "Timeline" ( \v -> do d <- v .: "date" parseAstronomy d =<< getFirst (v .: "astronomy") ) =<< getFirst (v .: "weather") ) conditionsIconDay :: [(String -> Bool, String)] conditionsIconDay = [ ((== "overcast"), fc "#808080" "\63070"), ((== "fair"), fc "a0a0a0" "🌑"), ((== "clear"), fc "#ddcf04" "\58125"), ((== "sunny"), fc "#ddcf04" "\58125"), ((== "mostly clear"), fc "#00a3c4" "\57894"), ((== "mostly sunny"), fc "#ddcf04" "\58124"), ((== "partly sunny"), fc "#ddcf04" "\58124"), ((== "fair"), fc "#a0a0a0" "\127761"), ((== "cloudy"), fc "#a0a0a0" "\64143"), ((== "overcast"), fc "#808080" "\63070"), ((== "partly cloudy"), fc "#a0a0a0" "\64148"), ((== "mostly cloudy"), fc "#808080" "\63070"), ((== "considerable cloudiness"), fc "#a0a0a0" "\64381"), (("snow" `isInfixOf`), fc "#a0a0f0" "\58138") ] conditionsIconNight :: [(String -> Bool, String)] conditionsIconNight = [ ((== "clear"), fc "#00a3c4" "\61830"), ((== "sunny"), fc "#00a3c4" "\61830"), ((== "mostly clear"), fc "#00a3c4" "\57894"), ((== "mostly sunny"), fc "#00a3c4" "\57894"), ((== "partly sunny"), fc "#00a3c4" "\57894"), ((== "fair"), fc "#808080" "\127761"), ((== "cloudy"), fc "#808080" "\64143"), ((== "overcast"), fc "#404040" "\63070"), ((== "partly cloudy"), fc "#a0a0a0" "\57894"), ((== "mostly cloudy"), fc "#808080" "\63070"), ((== "considerable cloudiness"), fc "#a0a0a0" "\64381") ] handleWeather :: Weather -> String handleWeather w = execWriter $ do tell $ lightGrey $ fn 3 $ areaName (nearestArea w) tell " " tell $ lightGrey $ fn 3 $ case winddir (currentCondition w) of "NE" -> "\62785" "NNE" -> "\62785" "ENE" -> "\62785" "SE" -> "\62810" "SSE" -> "\62810" "ESE" -> "\62810" "NW" -> "\62786" "NNW" -> "\62786" "WNW" -> "\62786" "SW" -> "\62811" "SSW" -> "\62811" "WSW" -> "\62811" "N" -> "\62788" "S" -> "\62812" "W" -> "\62803" "E" -> "\62796" _ -> "?" tell " " tell $ lightGrey $ fn 3 $ windspeedMiles (currentCondition w) ++ "mph" tell " " let conditions = if isNight then conditionsIconNight else conditionsIconDay tell $ fn 5 $ fromMaybe "?" $ findMatch (map Data.Char.toLower $ weatherDesc (currentCondition w)) conditions tell " " tell $ lightGrey $ fn 3 $ printf "%s°F" (tempF $ currentCondition w) where isNight = observationTime (currentCondition w) < sunrise (astronomy w) || observationTime (currentCondition w) > sunset (astronomy w) lightGrey = fc "#a0a0a0" fn :: Int -> String -> String fn = printf "%s" fc :: String -> String -> String fc = printf "%s" findMatch :: a -> [(a -> Bool, b)] -> Maybe b findMatch a ((f, b) : fs) | f a = Just b findMatch a (_ : fs) = findMatch a fs findMatch _ [] = Nothing main :: IO () main = do (code, resp) <- curlGetString "https://wttr.in?format=j2" [] when (code /= CurlOK) exitFailure case eitherDecode (Data.ByteString.Lazy.Char8.pack resp) of Left err -> hPutStrLn stderr $ printf "Failure to parse [%s]" err Right weather -> putStrLn $ handleWeather weather