I've been learning some Haskell as an amateur (to be precise: I started programming with this language, and it has been a year or less since I started seriously). So far, I have realised only small projects and find myself unable to judge my code (quality, style, ideas, ...)
For example, do you have some suggestion about this small module?
{- cabal:
build-depends: base, extra, scalpel, parsec
-}
module LibraryScraper where
import Data.Either (fromRight)
import Data.List.Extra (trim)
import Text.HTML.Scalpel
import Text.Parsec
import Text.Parsec.String
--
-- DESCRIPTION
--
-- This library provides a scraper for retrieving some information about
-- the public libraries in Milan (https://milano.biblioteche.it/). The
-- information we care here are: for each day say when they open and when
-- they close or if they are closed for the entire day.
--
-- The libraries supported here.
data Library = Accursio
| Affori
| CassinaAnna
| DerganoBovisa
| QuartoOggiaro
| Villapizzone
deriving (Eq, Show)
-- Assign to each library the homepage url, according to the association
-- list below.
--
-- TODO: The list is to be updated, as the type above.
--
libraryURL :: Library -> Maybe URL
libraryURL name = lookup name libraryURLs
libraryURLs :: [(Library, URL)]
libraryURLs =
map (fmap wrap)
[ (Accursio , "accursio")
, (Affori , "affori")
, (CassinaAnna , "cassinaanna")
, (DerganoBovisa, "dergano")
, (QuartoOggiaro, "quartooggiaro")
, (Villapizzone , "villapizzone")
]
where
wrap :: String -> URL
wrap s = "https://milano.biblioteche.it/library/" ++ s ++ "/timetable/"
-- A date is just a string. For example: "Fri 02 August".
type Date = String
-- The library status.
data Status = Open String String
-- if open, the interval for which is open
| Closed
-- closed the whole day
| Unknown
-- who knows? (for parsing purposes see below)
deriving (Show)
type LibraryCalendar = [(Date, Status)]
-- Provided a library, visit the corresponding web page
-- https://milano.biblioteche.it/library/.../timetable/
-- and return the calendar of the openings you can scrape.
getLibraryCalendar :: Library -> IO (Maybe LibraryCalendar)
getLibraryCalendar name =
maybe (return Nothing) calendarScraper (libraryURL name)
calendarScraper :: URL -> IO (Maybe LibraryCalendar)
calendarScraper url =
scrapeURL url $
chroot (TagString "table" @: [hasClass "library-timetable"]) $
chroots (TagString "tr" @: []) $ do
day:interval:_ <- texts $ TagString "td" @: []
return (day, parseStatus $ trim interval)
-- If the string is "Chiusa" then give Closed, if the string has the form
-- "hh1:mm1 - hh2:mm2" then output Open "hh1:mm1" "hh2:mm2". Unknown is for
-- any other string.
parseStatus :: String -> Status
parseStatus = fromRight Unknown . parse parser ""
where
parser :: Parser Status
parser = try (string "Chiusa" >> return Closed)
<|> try (Open <$> (hour <* string " - ") <*> hour)
<?> "\"Chiusa\" or \"hh:mm - hh:mm\""
hour :: Parser String
hour = do
hh <- count 2 digit
_ <- char ':'
mm <- count 2 digit
return $ hh ++ ':' : mm
```