packages feed

sloane-1.8: sloane.hs

-- |
-- Copyright   : Anders Claesson 2012-2014
-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>
-- License     : BSD-3
--
import qualified Codec.Compression.GZip       as GZip
import           Control.Monad                (unless, when)
import qualified Data.ByteString.Char8        as B
import qualified Data.ByteString.Lazy.Char8   as BL
import qualified Data.ByteString.Lazy.Search  as Search
import           Data.Maybe                   (fromJust, maybe)
import           Data.Time                    (diffUTCTime, getCurrentTime)
import           Network.HTTP
import           Network.URI                  (parseURI)
import           Options.Applicative
import           System.Console.ANSI
import           System.Console.Terminal.Size (Window (..), size)
import           System.Directory
import           System.FilePath              ((</>))
import           System.IO                    (stderr, stdin)

type OEISEntries = [String]
type ANumbers = [String]
type Query = String
type Keys = String

data Args = Args
    { all    :: Bool
    , keys   :: String
    , limit  :: Int
    , update :: Bool
    , url    :: Bool
    , ver    :: Bool
    , terms  :: [String]
    }

version   = "sloane 1.8"

oeisHost  = "http://oeis.org/"
oeisURL   = oeisHost ++ "search?fmt=text"
oeisKeys  = "ISTUVWXNDHFYAOEeptoKC"

cacheDir  = ".sloane"
cacheFile = "stripped.gz"
cacheURL  = oeisHost ++ cacheFile

msgDownloadingCache = unlines
    [ "Downloading " ++ cacheURL
    , "This may take a minute or two ..."
    ]
msgCacheIsUpToDate = unlines
    [ "The sequence cache is now up-to-date"
    ]
msgNoCache = unlines
    [ "No sequence cache found. You need to run \"sloane --update\""
    ]
msgOldCache = unlines
    [ "The sequence cache is more than 100 days old"
    , "You may want to run \"sloane --update\""
    ]

select :: Keys -> OEISEntries -> OEISEntries
select ks = filter (\line -> null line || head line `elem` ks)

aNumbers :: OEISEntries -> ANumbers
aNumbers es = [ words ids !! 1 | ids@(_:_) <- select "I" es ]

urls :: OEISEntries -> String
urls = unlines . map (oeisHost ++ ) . aNumbers

get :: HStream b => String -> IO b
get uri = simpleHTTP (defaultGETRequest_ uri') >>= getResponseBody
  where
    uri' = fromJust $ parseURI uri

updateCache :: FilePath -> IO ()
updateCache home = do
    let dir = home </> cacheDir
    createDirectoryIfMissing False dir
    put msgDownloadingCache
    cache <- get cacheURL
    B.writeFile (dir </> cacheFile) cache
    put msgCacheIsUpToDate

searchOEIS :: Int -> Query -> IO OEISEntries
searchOEIS n s = trim `fmap` get uri
  where
    trim = map (drop 1) . reverse . drop 2 . reverse . drop 5 . lines
    uri = oeisURL ++ "&" ++ urlEncodeVars [("n", show n), ("q", s)]

cropStr :: (Int -> String -> String) -> Int -> String -> String
cropStr f maxLen s = if maxLen < length s then f maxLen s else s

cropSeq :: Int -> String -> String
cropSeq = cropStr $ \maxLen ->
              reverse . dropWhile (/= ',') . reverse . take maxLen

cropLine :: Int -> String -> String
cropLine = cropStr $ \maxLen s -> take (maxLen-2) s ++ ".."

getWidth :: IO Int
getWidth = maybe maxBound width `fmap` size

put = B.putStr . B.pack
putErr = B.hPutStr stderr . B.pack
newline = B.putStrLn B.empty

putEntries :: Int -> OEISEntries -> IO ()
putEntries width = mapM_ $ \line ->
    case words line of
        [] -> newline
        (key:aNum:rest) -> do
            setSGR [ SetColor Foreground Dull Green ]
            put key
            setSGR [ SetColor Foreground Dull Yellow ]
            put $ ' ' : aNum
            setSGR []
            let crop = if key == "S" then cropSeq else cropLine
            put $ ' ' : crop width (unwords rest) ++ "\n"

isInfixOf :: B.ByteString -> BL.ByteString -> Bool
isInfixOf q = not . null . Search.indices q

dropPreamble :: BL.ByteString -> BL.ByteString
dropPreamble = BL.unlines . drop 4 . BL.lines

readCache :: FilePath -> IO BL.ByteString
readCache home = do
    let name = home </> cacheDir </> cacheFile
    updated <- doesFileExist name
    if updated
        then do
            c <- getCurrentTime
            m <- getModificationTime name
            let day = 60*60*24
            let expired = c `diffUTCTime` m > 100*day
            when expired $ putErr msgOldCache
            (dropPreamble . GZip.decompress) `fmap` BL.readFile name
        else
            error msgNoCache

seqs :: B.ByteString -> [B.ByteString]
seqs = filter (not . B.null) . map mkSeq . B.lines
  where
    mkSeq = normalize . dropComment
    dropComment = B.takeWhile (/= '#')
    normalize   = B.intercalate (B.pack ",") . B.words . clean . B.map tr
    tr c  = if c `elem` ";," then ' ' else c
    clean = B.filter (\c -> B.elem c (B.pack " 0123456789-"))

filterSeqs :: FilePath -> IO ()
filterSeqs home = do
    cache <- readCache home
    B.getContents >>= mapM_ B.putStrLn . filter (`isInfixOf` cache) . seqs

args :: Parser Args
args = Args
    <$> 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
        ( short 'n'
       <> metavar "N"
       <> value 5
       <> help "Fetch at most this many entries [default: 5]" )
    <*> switch (long "update" <> help "Update the local sequence cache")
    <*> switch (long "url" <> help "Print URLs of found entries")
    <*> switch (hidden <> long "version")
    <*> many (argument str (metavar "TERMS..."))

sloane :: Args -> IO ()
sloane (Args all keys n update url True terms) = put version >> newline
sloane (Args all keys n True   url ver  terms) = getHomeDirectory >>= updateCache
sloane (Args all keys n update url ver  []   ) = getHomeDirectory >>= filterSeqs
sloane (Args all keys n update url ver  terms) = do
    ncols <- getWidth
    hits  <- searchOEIS n (unwords terms)
    let pick = if all then id else select keys
    unless (null hits) $ do
        newline
        if url
            then put (urls hits)
            else putEntries (ncols - 10) (pick hits)
        newline

main = execParser (info (h <*> args) (fullDesc <> header version)) >>= sloane
  where
    h = abortOption ShowHelpText $ hidden <> long "help"