packages feed

sloane-2.0.2: sloane.hs

-- |
-- Copyright   : Anders Claesson 2012-2014
-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>
-- License     : BSD-3
--

import           Data.List                    (intercalate)
import           Data.Maybe                   (maybeToList)
import           Data.Monoid
import           Data.Text                    (Text)
import qualified Data.Text                    as T
import qualified Data.Text.IO                 as IO
import           Data.Text.Encoding           (decodeUtf8)
import           System.IO                    (isEOF)
import           Network.HTTP                 (urlEncodeVars)
import           Network.Curl.Download        (openURI)
import           Options.Applicative
import           Sloane.Config
import           Sloane.DB                    (DB, ANumber, Seq)
import qualified Sloane.DB                    as DB
import           Sloane.Transform

type URL = String

data Options = Options
    { 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
    , local   :: Bool     -- Lookup in local DB
    , anumber :: Int      -- Get the sequence with this number from the local DB
    , filtr   :: Bool     -- Filter out sequences in local DB
    , invert  :: Bool     -- Return sequences NOT in DB
    , transform :: String -- Apply the named transform
    , listTransforms :: Bool -- List the names of all transforms
    , update  :: Bool     -- Updated local DB
    , version :: Bool     -- Show version info
    , terms   :: [String] -- Search terms
    }

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

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

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

grepDB :: Options -> DB -> DB
grepDB opts = DB.take n . DB.grep (T.pack q)
  where
    n = limit opts
    q = intercalate "," (terms opts)

applyTransform :: Options -> String -> IO ()
applyTransform opts tname =
    case lookupTranform tname of
      Nothing -> error "No transform with that name"
      Just f  -> case f $$ input of
                   [] -> return ()
                   cs -> putStrLn (showSeq cs)
  where
    tr c  = if c `elem` ";," then ' ' else c
    input = map read (words (map tr (unwords (terms opts))))


dropComment :: Text -> Text
dropComment = T.takeWhile (/= '#')

showSeq :: [Integer] -> String
showSeq = intercalate "," . map show

mkSeq :: Text -> Seq
mkSeq = T.intercalate (T.pack ",") . T.words . clean . dropComment
  where
    clean = T.filter (`elem` " 0123456789-") . T.map tr
    tr c  = if c `elem` ";," then ' ' else c

mkANumber :: Int -> ANumber
mkANumber n = let s = show n in T.pack ('A' : replicate (6-length s) '0' ++ s)

filterDB :: Options -> 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

optionsParser :: Parser Options
optionsParser =
  (abortOption ShowHelpText $ long "help") <*> (Options
    <$> switch
        ( short 'a'
       <> long "all"
       <> help "Print all fields" )
    <*> strOption
        ( short 'k'
       <> metavar "KEYS"
       <> value "SN"
       <> help "Keys of fields to print [default: SN]" )
    <*> option auto
        ( short 'n'
       <> metavar "N"
       <> value 5
       <> help "Fetch at most this many entries [default: 5]" )
    <*> switch
        ( long "url"
       <> help "Print URLs of found entries" )
    <*> switch
        ( long "local"
       <> help "Use the local database rather than oeis.org" )
    <*> option auto
        ( short 'A'
       <> metavar "NUMBER"
       <> value 0
       <> help "Fetch the sequence with this number from the local database" )
    <*> switch
        ( long "filter"
       <> help ("Read sequences from stdin and return"
            ++ " those that are in the local database") )
    <*> switch
        ( long "invert"
       <> help ("Return sequences NOT in the database;"
            ++ " only relevant when used with --filter") )
    <*> strOption
        ( long "transform"
       <> metavar "NAME"
       <> value ""
       <> help "Apply the named transform to input sequence")
    <*> switch
        ( long "list-transforms"
       <> help "List the names of all transforms" )
    <*> switch
        ( long "update"
       <> help "Update the local database" )
    <*> switch
        ( long "version"
       <> help "Show version info" )
    <*> many (argument str (metavar "TERMS...")))

search :: (Options -> Config -> IO DB) -> Options -> Config -> IO ()
search f opts cfg = f opts cfg >>= put
  where
    put | url opts  = putStr . unlines . oeisUrls cfg
        | otherwise = DB.put cfg $ if full opts then oeisKeys else keys opts

getTerms :: Options -> IO Options
getTerms opts
    | filtr opts = return opts
    | otherwise  = (\xs -> opts {terms = xs}) <$>
        case terms opts of
          [] -> isEOF >>= \b ->
                  if b then error "<stdin>: end of file" else return <$> getLine
          ts -> return ts

main :: IO ()
main = do
    opts <- getTerms =<< execParser (info optionsParser fullDesc)
    let tname = transform opts
    let anum = anumber opts
    let lookupSeq = maybeToList . DB.lookupSeq (mkANumber anum)
    let sloane
         | version opts = putStrLn . nameVer
         | update opts = DB.update
         | listTransforms opts = const $ mapM_ (putStrLn . name) transforms
         | anum > 0 = \c -> lookupSeq <$> DB.read c >>= mapM_ IO.putStrLn
         | filtr opts = \c -> DB.read c >>= filterDB opts >>= mapM_ IO.putStrLn
         | not (null tname) = const $ applyTransform opts tname
         | local opts = search (\o cfg -> grepDB o <$> DB.read cfg) opts
         | otherwise = search oeisLookup opts
    defaultConfig >>= sloane