-- | -- Copyright : Anders Claesson 2012-2014 -- Maintainer : Anders Claesson -- 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"