packages feed

sloane-1.9.2: sloane.hs

-- |
-- Copyright   : Anders Claesson 2012-2014
-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>
-- License     : BSD-3
--
import           Data.Text                    (Text)
import qualified Data.Text                    as T
import qualified Data.Text.IO                 as IO
import           Data.Text.Encoding           (decodeUtf8)
import           Network.HTTP                 (urlEncodeVars)
import           Network.Curl.Download        (openURI)
import           Options.Applicative
import           Sloane.Config
import           Sloane.DB                    hiding (null)
import qualified Sloane.DB                    as DB

type URL = String
type Seq = Text

data Visibility = Visible | Internal

data Options
    = Cmd Command
    | IntOpts SearchOpts -- Internal opts for fallback to lookup

data Command
    = Lookup SearchOpts
    | Grep   SearchOpts
    | Filter FilterOpts
    | Update
    | Version

data SearchOpts = SearchOpts
    { full   :: Bool     -- Print all fields?
    , keys   :: String   -- Keys of fields to print
    , limit  :: Int      -- Fetch at most this many entries
    , url    :: Bool     -- Print URLs of found entries
    , terms  :: [String] -- Search terms
    }

data FilterOpts = FilterOpts
    { invert :: Bool     -- Return sequences NOT in DB
    }

oeisKeys :: String
oeisKeys = "ISTUVWXNDHFYAOEeptoKC" -- Valid OEIS keys

oeisUrls :: Config -> DB -> [URL]
oeisUrls cfg = map ((oeisHost cfg ++) . T.unpack) . aNumbers

oeisLookup :: SearchOpts -> Config -> IO DB
oeisLookup opts cfg =
    (parseOEISEntries . decodeUtf8 . either error id) <$>
    openURI (oeisURL cfg ++ "&" ++ urlEncodeVars [("n", show n), ("q", q)])
  where
    n = limit opts
    q = unwords $ terms opts

grepDB :: SearchOpts -> DB -> DB
grepDB opts = DB.take n . DB.grep (T.pack q)
  where
    n = limit opts
    q = unwords $ terms opts

filterDB :: FilterOpts -> DB -> IO [Seq]
filterDB opts db = filter match . parseSeqs <$> IO.getContents
  where
    match q = (if invert opts then id else not) (DB.null $ DB.grep q db)
    parseSeqs = filter (not . T.null) . map mkSeq . T.lines
    mkSeq = normalize . dropComment
    dropComment = T.takeWhile (/= '#')
    normalize = T.intercalate (T.pack ",") . T.words . clean . T.map tr
    tr c = if c `elem` ";," then ' ' else c
    clean = T.filter (`elem` " 0123456789-")

searchOptionsParser :: Visibility -> Parser SearchOpts
searchOptionsParser visibility = hiddenHelp <*> (SearchOpts
    <$> switch
        ( short 'a'
       <> long "all"
       <> help "Print all fields"
       <> f )
    <*> strOption
        ( short 'k'
       <> metavar "KEYS"
       <> value "SN"
       <> help "Keys of fields to print [default: SN]"
       <> f )
    <*> option auto
        ( short 'n'
       <> metavar "N"
       <> value 5
       <> help "Fetch at most this many entries [default: 5]"
       <> f )
    <*> switch
        ( long "url"
       <> help "Print URLs of found entries"
       <> f )
    <*> some (argument str (metavar "TERMS...")))
  where
    f = case visibility of {Visible -> idm; Internal -> internal}

filterOptionsParser :: Parser FilterOpts
filterOptionsParser = FilterOpts
    <$> switch (long "invert" <> help "Return sequences NOT in the database")

commandParser :: Parser Command
commandParser = subparser
    ( command "lookup" (info (Lookup <$> searchOptionsParser Visible)
      ( progDesc "Lookup a sequence, or other search term, in OEIS" ))
   <> command "grep" (info (Grep <$> searchOptionsParser Visible)
      ( progDesc "Grep for a sequence in the local database" ))
   <> command "filter" (info (Filter <$> filterOptionsParser)
      ( progDesc ("Read sequences from stdin and "
               ++ "return those that are in the local database")))
   <> command "update" (info (pure Update)
      ( progDesc "Update the local database" ))
   <> command "version" (info (pure Version)
      ( progDesc "Show version info" ))
    )

optionsParser :: Parser Options
optionsParser =
    (Cmd <$> commandParser) <|> (IntOpts <$> searchOptionsParser Internal)

runSearch :: (SearchOpts -> Config -> IO DB) -> SearchOpts -> Config -> IO ()
runSearch f opts cfg = f opts cfg >>=
    if url opts
        then putStr . unlines . oeisUrls cfg
        else putDB cfg (if full opts then oeisKeys else keys opts)

runCmd :: Command -> Config -> IO ()
runCmd (Lookup opts) = runSearch oeisLookup opts
runCmd (Grep   opts) = runSearch (\o cfg -> grepDB o <$> readDB cfg) opts
runCmd (Filter opts) = \c -> readDB c >>= filterDB opts >>= mapM_ IO.putStrLn
runCmd Update        = initDB
runCmd Version       = putStrLn . name

hiddenHelp :: Parser (a -> a)
hiddenHelp = abortOption ShowHelpText $ hidden <> short 'h' <> long "help"

main :: IO ()
main = do
    conf <- defaultConfig
    opts <- customExecParser preferences (info parser description)
    case opts of
        (Cmd cmd)   -> runCmd cmd conf
        (IntOpts o) -> runCmd (Lookup o) conf -- Fallback to 'lookup'
  where
    parser = hiddenHelp <*> optionsParser
    preferences = prefs showHelpOnError
    description = fullDesc <> footer
        "Run 'sloane COMMAND --help' for help on a specific command."