sloane-1.7: sloane.hs
{-# LANGUAGE DeriveDataTypeable #-}
-- |
-- Copyright : Anders Claesson 2012-2014
-- Maintainer : Anders Claesson <anders.claesson@gmail.com>
-- License : BSD-3
--
import Prelude hiding (all)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Lazy.Search as Search
import qualified Codec.Compression.GZip as GZip
import System.IO (stdin, stderr)
import System.Directory
import System.FilePath ((</>))
import System.Console.ANSI
import System.Console.CmdArgs
import System.Console.Terminal.Size (Window(..), size)
import Data.Time (getCurrentTime, diffUTCTime)
import Data.Maybe (maybe, fromJust)
import Control.Monad (when, unless)
import Network.HTTP
import Network.URI (parseURI)
type OEISEntries = [String]
type ANumbers = [String]
type Query = String
type Keys = String
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 two weeks old"
, "You may want to run \"sloane --update\""
]
data Sloane = Sloane { all :: Bool
, keys :: String
, limit :: Int
, update :: Bool
, url :: Bool
, terms :: [String]
}
deriving (Data, Typeable)
sloane = cmdArgsMode $ Sloane
{ all = False &= name "a" &= help "Print all fields"
, keys = "SN" &= typ "KEYS" &= help "Keys of fields to print (default: SN)"
, limit = 5 &= name "n" &= help "Fetch at most this many entries (default: 5)"
, update = False &= help "Update the local sequence cache"
, url = False &= name "u" &= help "Print urls of found entries"
, terms = def &= args &= typ "SEARCH-TERMS"
}
&= versionArg [summary "sloane 1.7"]
&= summary "Search Sloane's On-Line Encyclopedia of Integer Sequences"
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
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 week = 60*60*24*7
let expired = c `diffUTCTime` m > 2*week
when expired $ putErr msgOldCache
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
main = do
home <- getHomeDirectory
args <- cmdArgsRun sloane
let query = unwords $ terms args
case (update args, null query) of
(True, _) -> updateCache home
(_, True) -> filterSeqs home
_ -> do
ncols <- getWidth
hits <- searchOEIS (limit args) query
let pick = if all args then id else select (keys args)
unless (null hits) $ do
newline
if url args
then put (urls hits)
else putEntries (ncols - 10) (pick hits)
newline