packages feed

sloane-4.0.2: sloane.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}

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

module Main (main) where

import Data.List
import Data.Aeson
import Data.Bits (xor)
import Data.Maybe
import Data.Monoid
import Data.Map (Map, (!))
import qualified Data.Map as M
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text as T
import qualified Data.Text.IO as IO
import Control.Applicative
import Control.Monad
import System.Directory
import System.Console.ANSI
import System.IO
import Sloane.OEIS
import Sloane.Entry
import Sloane.Options
import Sloane.Config
import Sloane.Download
import Sloane.Bloom
import Sloane.DB

nameVer  = "sloane 4.0.2"                 :: String
oeisURL  = "https://oeis.org/search"      :: URL
strpdURL = "https://oeis.org/stripped.gz" :: URL
namesURL = "https://oeis.org/names.gz"    :: URL

type Query  = B.ByteString
type Width  = Int
type Limit  = Int
type View   = (Width, Palette, [Key])

data QA = QA Query [Reply]

instance ToJSON QA where
    toJSON (QA q rs) =
        object [ "query" .= String (decodeUtf8 q)
               , "reply" .= toJSON rs
               ]

data Input
    = SearchLocalDB (DB Sequences) (DB Names) Bool Limit View [Either ANum PackedSeq]
    | SearchOEIS Bool Limit View String
    | FilterSeqs (DB Sequences) Bool [PackedEntry]
    | UpdateDBs FilePath FilePath FilePath
    | Empty

data Output
    = OEISReplies View [QA]
    | OEISRepliesJSON [QA]
    | Entries [PackedEntry]
    | NOP

nonEmptyLines :: BL.ByteString -> [B.ByteString]
nonEmptyLines = map BL.toStrict . filter (not . BL.null) . BL.lines

readStdin :: IO [B.ByteString]
readStdin = nonEmptyLines <$> BL.getContents

readInput :: Options -> Config -> IO Input
readInput opts cfg
    | version opts = return Empty

    | update opts =
        return $ UpdateDBs (sloaneDir cfg) (seqDBPath cfg) (namesDBPath cfg)

    | filtr opts = do
        db <- readSeqDB cfg
        FilterSeqs db (invert opts) . map parsePackedEntryErr <$> readStdin

    | oeis opts =
        return $ SearchOEIS (tojson opts) (limit opts) view (unwords (terms opts))

    | query opts = do
        sdb <- readSeqDB cfg
        ndb <- readNamesDB cfg
        let parseInp t = fromMaybe (error "cannot parse input")
                       $  (Right . getPackedSeq <$> parsePackedEntry t)
                      <|> (Left <$> parseANum t)
                      <|> (Right . packSeq . map fromIntegral <$> parseIntegerSeq t)
        SearchLocalDB sdb ndb (tojson opts) (limit opts) view . map parseInp
            <$> case map B.pack (terms opts) of
                  [] -> readStdin
                  ts -> return ts
    | otherwise = readInput (opts {query = True}) cfg
  where
    view = (termWidth cfg, palette opts, keys opts)

few :: [a] -> Bool
few (_:_:_) = False
few _       = True

allANum :: [QA] -> Bool
allANum= all (\(QA q _) -> B.head q == 'A')

printReply :: View -> Reply -> IO ()
printReply (width, pal, ks) (Reply (ANum anum) (Table table)) =
    forM_ (ks `intersect` M.keys table) $ \key -> do
        let entry = table ! key
        let crop' = crop key (width - 10)
        let mono  = monochrome pal
        forM_ entry $ \line -> do
            unless mono $ setSGR [ SetColor Foreground Dull Green ]
            putStr [key]
            unless mono $ setSGR [ SetColor Foreground Dull Yellow ]
            B.putStr $ indent anum
            unless mono $ setSGR []
            IO.putStrLn $ indent (crop' (decodeUtf8 line))
  where
    indent s = " " <> s
    dropLastField = T.reverse . T.dropWhile (/= ',') . T.reverse
    cropText f cap s = if cap < T.length s then f cap s else s
    crop key = cropText $ \cap s ->
        if key `elem` ['S'..'X']
            then dropLastField (T.take cap s)
            else T.take (cap-2) s <> ".."

printOutput :: Output -> IO ()
printOutput NOP = return ()
printOutput (Entries es) =
    forM_ es $ \(PackedEntry (PPrg p) (PSeq s)) ->
        B.putStrLn $ p <> " => {" <> s <> "}"

printOutput (OEISRepliesJSON rss) = mapM_ (BL.putStrLn . encode) rss

printOutput (OEISReplies view rss) = do
    let hideQuery = few rss || allANum rss
    let noReplies = null $ concat [ rs | QA _ rs <- rss ]
    forM_ (zip [1::Int ..] rss) $ \(i, QA q replies) -> do
        unless hideQuery $ do
            when (i > 1) $ putStrLn ""
            B.putStrLn ("query: " <> q)
        forM_ replies $ \r -> do
            putStrLn ""
            printReply view r
    unless noReplies $ putStrLn ""

-- Construct a list of replies associated with a list of A-numbers.
mkReplies :: Map ANum PackedSeq -> Map ANum Name -> [ANum] -> [Reply]
mkReplies s n anums =
    [ Reply k (Table $ M.fromList [('S', [unPSeq (s!k)]), ('N', [n!k])])
    | k <- anums
    , M.member k s && M.member k n
    ]

sloane :: Input -> IO Output
sloane inp =
    case inp of

      SearchLocalDB sdb ndb jsonflag maxReplies view ts -> do
          let sm = M.fromList $ parseStripped (unDB sdb)
          let nm = M.fromList $ parseNames (unDB ndb)
          let qas = [ QA q (mkReplies sm nm ks)
                    | (q, ks) <-
                        [ case t of
                            Right s@(PSeq r) -> (r, grepN maxReplies s sdb)
                            Left (ANum anum) -> (anum, [ANum anum])
                        | t <- ts
                        ]
                    ]
          return $ if jsonflag then OEISRepliesJSON qas else OEISReplies view qas

      SearchOEIS jsonflag lim view q -> do
          let kvs = [("n", B.pack (show lim)), ("q", B.pack q), ("fmt", "text")]
          replies <- requestPage oeisURL kvs
          let qas = [QA (B.pack q) (parseReplies replies)]
          return $ if jsonflag then OEISRepliesJSON qas else OEISReplies view qas

      FilterSeqs db invFlag es -> do
          let bloom = mkBloomFilter db
          return $ Entries
              [ e | e@(PackedEntry _ s) <- es
              , not (B.null (unPSeq s))
              , invFlag `xor` (s `isFactorOf` bloom && not (null (grep s db)))
              ]

      UpdateDBs sloanedir sdbPath ndbPath -> do
          createDirectoryIfMissing False sloanedir
          let msg1 = "Downloading " ++ strpdURL ++ ": "
          let msg2 = "Downloading " ++ namesURL ++ ": "
          putStr msg1 >> hFlush stdout
          download (length msg1) strpdURL sdbPath >> putStrLn ""
          putStr msg2 >> hFlush stdout
          download (length msg2) namesURL ndbPath >> putStrLn ""
          return NOP

      Empty -> putStrLn nameVer >> return NOP

-- | Main function and entry point for sloane.
main :: IO ()
main = do
    c <- getConfig
    t <- getOptions
    readInput t c >>= sloane >>= printOutput