packages feed

sloane 2.0.5 → 4.0.0

raw patch · 14 files changed

+1049/−1126 lines, 14 filesdep +aesondep +attoparsecdep +bloomfilterdep −HTTPdep −cerealdep −download-curldep ~basedep ~bytestringdep ~optparse-applicative

Dependencies added: aeson, attoparsec, bloomfilter, conduit, conduit-extra, http-conduit, http-types, transformers

Dependencies removed: HTTP, cereal, download-curl, network, zlib

Dependency ranges changed: base, bytestring, optparse-applicative, stringsearch

Files

README.md view
@@ -1,196 +1,24 @@-----title: SLOANE(1) Sloane User Manual | Version 2.0.5-date: 7 Jan 2015------# NAME--sloane - a command line interface to Sloane's-On-Line Encyclopedia of Integer Sequences <http://oeis.org>--# SYNOPSIS--sloane [-a|--all] [-k KEYS] [-n N] [--url] [--local] TERMS...--sloane -A NUMBER--sloane --filter [--invert]--sloane --transform NAME--sloane (--list-transforms | --update | --version | --help)--# DESCRIPTION--The `sloane` command searches Sloane's On-Line Encyclopedia of Integer-Sequences (OEIS). The search terms are typically the leading term of a-sequence. For example,--    sloane 1,1,2,5,15,52,203,877,4140--returns entry A000110 (Bell numbers), and four more entries.  One can-also search by sequence id (A-number), or even search for arbitrary-words. See the **EXAMPLES** section.--Alternatively, using the `--local` option, the search can be done-locally against a downloaded local database of known sequences. This-mode works by "grepping" for the query in the sequence field.--To check a large number of sequences one can use `--filter`.  When this-option is set, `sloane` reads the standard input line-by-line, if the-sequence read is in the local database, then it is returned to the-standard output; if not, it is ignored. This way one can quickly filter-out the sequences from the input that are in the local database. In-other words, assuming that *FILE* contains one sequence per line,--    sloane --filter <FILE--returns the subset of the sequences in *FILE* that are in the local-database. To also look-up the names of those sequences one could, for-instance, run--    sloane --filter <FILE | xargs -L1 --verbose sloane --local--Sloane normally crops long lines to fit the widths of the terminal. If-this is unwanted, pipe the output through cat or less:--    sloane -a id:A000110 | less -R--# OPTIONS---a, --all-:   Print all fields---k *KEYS*-:   Keys of fields to print (default: SN)---n *N*-:   Fetch at most this many entries (default: 5)----url-:   Print URLs of found entries (but nothing else)----local-:   Grep for a sequence in the local database.---A *NUMBER*-:   Fetch the sequence with this number from the local database. Prints-    the sequence, but nothing else, to sdout.----filter-:   Read sequences from stdin and return those that are in the local-    database.----invert-:   Return sequences *not* in the database. This option has no effect-    unless `--filter` is also set.----transform *NAME*-:   Apply the named transform to the input sequence. If the resulting-    sequence is integral print it to stdout; else print nothing.-    Most of the transforms and their names are taken from-    <https://oeis.org/transforms.txt>.----list-transforms-:   List the names of all transforms.----update-:   Update the local database.----version-:   Print version information.----help-:   Briefly describe the available options.--# EXAMPLES--The most common search is for entries matching a sequence of consecutive terms:--    sloane 1,3,19,183,2371,38703--At the time of writing this particular query would return--    S A006531 1,1,3,19,183,2371,38703,763099,17648823,468603091,14050842303,-    N A006531 Semiorders on n elements.--As this illustrates, the default is to return just the sequence (S) and-the name (N) fields. To override the default one can use the keys-option. For instance, the following search shows the sequence, name,-comments, and formula fields of the sequence whose A-number is A006531:--    sloane -k SNCF id:A006531--The next example returns at most 3 results of a free text search:--    sloane -n 3 "(2+2)-free posets"--To view the full entries of these 3 results in a browser (e.g., Firefox)-one can use the url option:--    firefox `sloane --url -n 3 "(2+2)-free posets"`--To retrieve sequence A022493 from the local database use the `-A` option:--    sloane -A022493--In the final example the local database is used to filter out sequences-from the standard input that are in OEIS:--    sloane --filter <<END-    1,2,3,6,11,23,47,106,235           # Comma separated integers-    1 2 444 90 120                     # Space separated integers-    '(3 9 27 88 123)                   # S-expression-    [2,3,5,7,11,13,17,19,23,29,31,37]  # Haskell list-    [1; 1; 2; 3; 5; 8; 13; 21]         # O'Caml list-    {1, -1, 2, -6, 24, -120, 720}      # Mathematica list-    END--The '#' character and any text after it is ignored by sloane. Note also-that sloane is quite liberal in the formatting of sequences it accepts.--# KEYS--These are the keys used by OEIS <http://oeis.org/eishelp2.html>.--    I  ID number--    S  1st line of unsigned sequence-    T  2nd line of unsigned sequence-    U  3rd line of unsigned sequence--    V  1st line of signed sequence-    W  2nd line of signed sequence-    X  3rd line of signed sequence+# sloane [![Build Status](https://travis-ci.org/akc/sloane.svg)](https://travis-ci.org/akc/sloane) -    N  Name-    C  Comments-    D  References-    H  Links-    F  Formula-    e  Examples+A command line interface to+[The On-Line Encyclopedia of Integer Sequences](http://oeis.org). -    p  Maple program-    t  Mathematica program-    o  Program in other language+![demo](demo.gif) -    Y  Cross-references-    K  Keywords-    O  Offset-    A  Author-    E  Extensions and errors+## Install -# NOTES+If using the [nix](https://nixos.org/nix/) package manager: -Please use this program with moderation as not to overburden the-OEIS-server; see OEIS' policy on searching the database:-<http://oeis.org/wiki/Welcome#Policy_on_Searching_the_Database>.+```+$ nix-env -f "<nixpkgs>" -iA haskellPackages.sloane+``` -# SEE ALSO+Otherwise, use [cabal](https://www.haskell.org/cabal/): -The sloane source code may be downloaded from-<https://github.com/akc/sloane>.+```+$ cabal install sloane+``` -# AUTHOR+## Usage -Anders Claesson <http://akc.is>+See the [man page](sloane.md).
+ Sloane/Bloom.hs view
@@ -0,0 +1,52 @@+-- |+-- Copyright   : Anders Claesson 2015+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--++module Sloane.Bloom+    ( mkBloomFilter+    , isFactorOf+    ) where++import Data.ByteString.Char8 (ByteString)+import Data.BloomFilter (Bloom)+import qualified Data.BloomFilter as F+import Data.BloomFilter.Hash+import Sloane.OEIS+import Sloane.DB++newtype T9 = T9 ( ByteString, ByteString, ByteString+                , ByteString, ByteString, ByteString+                , ByteString, ByteString, ByteString )++type BlF = Bloom T9++instance Hashable T9 where+    hashIO32 (T9 (a,b,c,d,e,f,g,h,i)) salt =+        hashIO32 a salt >>= hashIO32 b >>= hashIO32 c >>= hashIO32 d >>= hashIO32 e+                        >>= hashIO32 f >>= hashIO32 g >>= hashIO32 h >>= hashIO32 i++zip9 :: [a]->[b]->[c]->[d]->[e]->[f]->[g]->[h]->[i] -> [(a,b,c,d,e,f,g,h,i)]+zip9 (a:as) (b:bs) (c:cs) (d:ds) (e:es) (f:fs) (g:gs) (h:hs) (i:is)+                       = (a,b,c,d,e,f,g,h,i) : zip9 as bs cs ds es fs gs hs is+zip9 _ _ _ _ _ _ _ _ _ = []++ninegrams :: [ByteString] -> [T9]+ninegrams xs = map T9 $ zip9 xs (drop 1 xs) (drop 2 xs) (drop 3 xs) (drop 4 xs)+                                (drop 5 xs) (drop 6 xs) (drop 7 xs) (drop 8 xs)++-- | Make a Bloom filter of all nine integer segments of all sequences+-- in the given data base.+mkBloomFilter :: DB Sequences -> BlF+mkBloomFilter (DB db) = F.fromList (cheapHashes numHashes) numBits ts+  where+    ts = ninegrams (parseTermsOfRecords db)+    numHashes = 3+    numBits = 2^(26 :: Int)++-- | Are all the nine element factors of the given (packed) sequence+-- members of the Bloom filter. May give a false positive answer, but+-- never a false negative answer.+isFactorOf :: PackedSeq -> BlF -> Bool+isFactorOf (PSeq s) bf = all (`F.elem` bf) $ ninegrams (parseTermsErr s)
Sloane/Config.hs view
@@ -3,41 +3,41 @@ -- Maintainer  : Anders Claesson <anders.claesson@gmail.com> -- License     : BSD-3 ---module Sloane.Config (Config (..), defaultConfig) where+-- Constants determined at runtime such as the home directory. +module Sloane.Config+    ( Config (..)+    , getConfig+    ) where+ import System.Console.Terminal.Size (width, size) import System.FilePath ((</>)) import System.Directory -type URL = String-+-- | A data type holding "constants" determined at runtime. data Config = Config-    { nameVer   :: String-    , home      :: FilePath-    , sloaneDir :: FilePath-    , sloaneDB  :: FilePath-    , oeisHost  :: URL-    , oeisURL   :: URL-    , sURL      :: URL-    , nURL      :: URL-    , termWidth :: Int+    {+    -- | The home directory+      home        :: FilePath+    -- | Path to the '.sloane' directory.+    , sloaneDir   :: FilePath+    -- | Path to 'stripped' file.+    , seqDBPath   :: FilePath+    -- | Path to 'names' file.+    , namesDBPath :: FilePath+    -- | The width of the terminal.+    , termWidth   :: Int     } -defaultConfig :: IO Config-defaultConfig = do+-- | Get configuration.+getConfig :: IO Config+getConfig = do     w <- maybe maxBound width `fmap` size     h <- getHomeDirectory-    let dsloane = h </> ".sloane"-    return Config-        { nameVer   = "sloane 2.0.5"-        , home      = h-        , sloaneDir = dsloane-        , sloaneDB  = dsloane </> "sloane.db"-        , oeisHost  = oeisorg-        , oeisURL   = oeisorg ++ "search?fmt=text"-        , sURL      = oeisorg ++ "stripped.gz"-        , nURL      = oeisorg ++ "names.gz"-        , termWidth = w-        }-  where-    oeisorg = "https://oeis.org/"+    let c = Config { home        = h+                   , sloaneDir   = h </> ".sloane"+                   , seqDBPath   = sloaneDir c </> "stripped"+                   , namesDBPath = sloaneDir c </> "names"+                   , termWidth   = w+                   }+    return c
Sloane/DB.hs view
@@ -1,160 +1,70 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE PolyKinds #-}+ -- |--- Copyright   : Anders Claesson 2014-2015+-- Copyright   : Anders Claesson 2015 -- Maintainer  : Anders Claesson <anders.claesson@gmail.com> -- License     : BSD-3 --  module Sloane.DB-    ( DB-    , ANumber-    , Seq-    , Reply-    , update-    , read-    , write-    , put-    , null-    , insert-    , lookup-    , lookupSeq+    ( DB (..), Sequences, Names+    , readSeqDB+    , readNamesDB+    , grepN     , grep-    , take-    , aNumbers-    , parseOEISEntries     ) where -import           Prelude                    hiding (lookup, null, take, read)-import qualified Prelude                    as P-import           Data.List                  (intersect)-import qualified Data.ByteString.Search     as Search-import qualified Data.ByteString.Char8      as Ch8-import           Data.ByteString            (ByteString)-import qualified Data.ByteString            as B-import qualified Data.ByteString.Lazy       as BL-import           Data.Map.Strict            (Map, (!))-import qualified Data.Map.Strict            as M-import           Data.Serialize             hiding (put)-import           Data.Text                  (Text)-import qualified Data.Text                  as T-import qualified Data.Text.IO               as IO-import           Data.Text.Encoding         (decodeUtf8)-import           Control.Monad              (forM_, unless)-import qualified Codec.Compression.GZip     as GZ-import           Network.Curl.Download      (openURI)-import           System.Console.ANSI-import           Sloane.Config-import           System.Directory--type ANumber = ByteString-type Seq     = ByteString-type Key     = Char-type Entry   = ByteString-type Reply   = Map Key Entry--type DB      = Map ANumber Reply--gzCompress :: ByteString -> ByteString-gzCompress = BL.toStrict . GZ.compress . BL.fromStrict--compressDB :: DB -> ByteString-compressDB = gzCompress . encode--gzDecompress :: ByteString -> ByteString-gzDecompress = BL.toStrict . GZ.decompress . BL.fromStrict--decompressDB :: ByteString -> Either String DB-decompressDB = decode . gzDecompress--update :: Config -> IO ()-update cfg = do-    createDirectoryIfMissing False (sloaneDir cfg)-    putStrLn $ "Downloading " ++ sURL cfg-    dbS <- either error (return . mkDB 'S') =<< openURI (sURL cfg)-    putStrLn $ "Downloading " ++ nURL cfg-    dbN <- either error (return . mkDB 'N') =<< openURI (nURL cfg)-    putStrLn "Building database"-    write cfg $ unionDB dbS dbN-    putStrLn "Done."-  where-    unionDB = M.unionWith M.union-    mkDB key = mkMap key . gzDecompress-    mkMap key = M.fromList . map (aNumberAndReply key) . drop 4 . Ch8.lines-    mkReply key = M.singleton key . Ch8.dropWhile (==',') . Ch8.drop 8-    aNumberAndReply key line = (Ch8.take 7 line, mkReply key line)--read :: Config -> IO DB-read cfg = doesFileExist (sloaneDB cfg) >>= \updated ->-    if updated-        then B.readFile (sloaneDB cfg) >>= either error return . decompressDB-        else error $ "No local database found. " ++-                     "You need to run \"sloane --update\" first."--write :: Config -> DB -> IO ()-write cfg = B.writeFile (sloaneDB cfg) . compressDB--null :: DB -> Bool-null = M.null--insert :: ANumber -> Reply -> DB -> DB-insert = M.insert--lookup :: ANumber -> DB -> Maybe Reply-lookup = M.lookup--lookupSeq :: ANumber -> DB -> Maybe Seq-lookupSeq anum db = lookup anum db >>= M.lookup 'S'+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif+import Data.Maybe+import Data.List+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Search as S+import System.Directory+import Sloane.OEIS+import Sloane.Config -isInfix :: ByteString -> ByteString -> Bool-isInfix q = not . P.null . Search.indices q+-- | An empty data declaration used with the phantom `DB` data type.+data Sequences -grep :: ByteString -> DB -> DB-grep q = M.filter $ \reply ->-             case M.lookup 'S' reply of-                 Nothing -> False-                 Just r  -> q `isInfix` r+-- | An empty data declaration used with the phantom `DB` data type.+data Names -take :: Int -> DB -> DB-take n = M.fromList . P.take n . M.toList+-- | A data base (DB) is just a wrapped `ByteString`.+newtype DB a = DB {unDB :: ByteString} deriving Show -aNumbers :: DB -> [ANumber]-aNumbers = M.keys+-- | Read the DB at the given location.+readDB :: FilePath -> IO (DB a)+readDB fpath = doesFileExist fpath >>= \b ->+    if b then DB <$> B.readFile fpath+         else error "No local database; run 'sloane --update' first." -unions :: [DB] -> DB-unions = M.unionsWith . M.unionWith $ \s t ->-    (s `B.append` Ch8.pack "\n") `B.append` t+-- | Read the sequence DB (derived from \"stripped.gz\").+readSeqDB :: Config -> IO (DB Sequences)+readSeqDB = readDB . seqDBPath -singleton :: ANumber -> Key -> Entry -> DB-singleton aNum key = M.singleton aNum . M.singleton key+-- | Read the names DB (derived from \"names.gz\").+readNamesDB :: Config -> IO (DB Names)+readNamesDB = readDB . namesDBPath -parseOEISEntries :: ByteString -> DB-parseOEISEntries = unions . map parseLine . trim+-- | Return all A-numbers whose associated sequence contains a given+-- sequence as a factor.+grep :: PackedSeq -> DB Sequences -> [ANum]+grep (PSeq p) (DB bs) = mapMaybe locateANum (S.indices q bs)   where-    trim = map (Ch8.drop 1) . reverse . drop 2 . reverse . drop 5 . Ch8.lines-    parseLine = parseWords . Ch8.words-    parseWords (key:aNum:rest) = singleton aNum (Ch8.head key) (Ch8.unwords rest)-    parseWords _ = M.empty--put :: Config -> [Key] -> DB -> IO ()-put cfg keys db = do-    unless (null db) $ putStrLn ""-    forM_ (M.toList db) $ \(aNum, reply) -> do-        forM_ (keys `intersect` M.keys reply) $ \key -> do-            let entry = reply ! key-            forM_ (Ch8.lines entry) $ \line -> do-                let line' = decodeUtf8 line-                setSGR [ SetColor Foreground Dull Green ]-                putStr [key]-                setSGR [ SetColor Foreground Dull Yellow ]-                putStr " " >> Ch8.putStr aNum-                setSGR []-                putStr " " >> IO.putStrLn (crop key (termWidth cfg - 10) line')-        putStrLn ""+    q = B.snoc (B.cons ',' p) ','+    locateANum i = listToMaybe+        [ ANum (B.take 7 v)+        | j <- [i,i-1..0]+        , B.index bs j == 'A'+        , let (_,v) = B.splitAt j bs+        ] -crop :: Key -> Int -> Text -> Text-crop key =-    let cropText f maxLen s = if maxLen < T.length s then f maxLen s else s-    in if key `elem` ['S'..'X']-           then cropText $ \maxLen ->-               T.reverse . T.dropWhile (/= ',') . T.reverse . T.take maxLen-           else cropText $ \maxLen s ->-               T.take (maxLen-2) s `T.append` T.pack ".."+-- | Similar to `grep` but return at most 'n' unique hits.+grepN :: Int -> PackedSeq -> DB Sequences -> [ANum]+grepN n q db = Prelude.take n $ map head $ group (grep q db)
+ Sloane/Download.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE CPP #-}+-- |+-- Copyright   : Anders Claesson 2015+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--+module Sloane.Download (download, requestPage) where++import Numeric+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as BL+import Data.Conduit hiding (($$))+import Data.Conduit.Zlib (ungzip)+import qualified Data.Conduit.Binary as CB+import Network.HTTP.Conduit hiding (Proxy)+import Network.HTTP.Types (hContentLength)+import Control.Monad.IO.Class (liftIO)+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif+import Sloane.OEIS (URL)+import System.Console.ANSI+import System.IO++-- | Request a page at a given URL with specified key-value pairs (query+-- string).+requestPage :: URL -> [(ByteString, ByteString)] -> IO ByteString+requestPage url kvs = do+    req <- setQueryString [(k, Just v) | (k,v) <- kvs] <$> parseUrl url+    res <- withManager (httpLbs req)+    return $ BL.toStrict (responseBody res)++-- | Download a file at a given URL showing a progress indicator at the+-- given column, and save it at a specified path.+download :: Int -> URL -> FilePath -> IO ()+download col url fpath = withManager $ \manager -> do+    req <- parseUrl url+    res <- http req manager+    let Just cl = lookup hContentLength (responseHeaders res)+    let n = read (B.unpack cl) :: Int+    responseBody res $$+- progress n 0 =$ ungzip =$ CB.sinkFile fpath+  where+    progress total acc = await >>= maybe (return ()) (\chunk -> do+        let acc' = acc + B.length chunk+        let percent = fromIntegral (100*acc') / fromIntegral total :: Double+        liftIO $ setCursorColumn col+        liftIO $ putStr $ showFFloat (Just 2) percent "" ++ "%"+        liftIO $ hFlush stdout+        yield chunk+        progress total acc')
+ Sloane/Entry.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++-- |+-- Copyright   : Anders Claesson 2015+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--++module Sloane.Entry+    ( PackedPrg (..)+    , PackedEntry (..)+    , parsePackedEntry+    , parsePackedEntryErr+    ) where++import GHC.Generics (Generic)+import Data.Maybe+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.Attoparsec.ByteString.Char8 as Ch+import Data.Attoparsec.ByteString.Char8+import Control.Monad+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif+import Sloane.Utils+import Sloane.OEIS++-- | A compact `ByteString` representation of a `Prg`.+newtype PackedPrg = PPrg ByteString deriving (Eq, Show, Generic)++-- | Similary, a packed entry consists of a packed program together with+-- a packed sequence.+data PackedEntry = PackedEntry+    { getPackedPrg :: PackedPrg+    , getPackedSeq :: PackedSeq+    } deriving (Eq, Show, Generic)++packedEntry :: Parser PackedEntry+packedEntry =+    let f w = if B.last w == '=' then return (PPrg (B.init w)) else mzero+    in PackedEntry <$> (Ch.takeWhile1 (/='>') >>= f)+                   <*> (char '>' *> packedSeq)++-- | A parser for packed entries.+parsePackedEntry :: ByteString -> Maybe PackedEntry+parsePackedEntry = parse_ packedEntry . B.filter (/=' ')++-- | Like `parsePackedEntry` but throws an error rather than returning+-- `Nothing` in case the parse fails.+parsePackedEntryErr :: ByteString -> PackedEntry+parsePackedEntryErr = fromMaybe (error "cannot parse input") . parsePackedEntry
− Sloane/GF.hs
@@ -1,159 +0,0 @@--- |--- Copyright   : Anders Claesson 2012-2015--- Maintainer  : Anders Claesson <anders.claesson@gmail.com>--- License     : BSD-3----module Sloane.GF-    ( Series (..)-    , GF-    , imap-    , diff-    , squareRoot-    , (^^^)-    , o-    , ogf-    , egf-    , ogfCoeffs-    , egfCoeffs-    )where--import Data.List-import Data.Maybe-import Data.Ratio---- Reference: M. D. McIlroy, The music of streams,--- Information Processing Letters 77 (2001) 189-195.--newtype Series a = Series { coeffs :: [a] } deriving (Show, Eq)--type GF = Series Rational--instance Functor Series where-    fmap f = Series . fmap f . coeffs--instance (Eq a, Num a) => Num (Series a) where-    (+) = lift2 add-    (*) = lift2 mul-    fromInteger c = Series [fromInteger c]-    negate = fmap negate-    signum = undefined-    abs    = undefined--instance (Eq a, Fractional a) => Fractional (Series a) where-     fromRational c = Series [fromRational c]-     (/) = lift2 divide--degree :: (Num a, Eq a) => Series a -> Int-degree (Series [0]) = -1-degree (Series cs)  = length cs - 1--- degree (Series cs)  = length (dropWhile (==0) (reverse cs)) - 1--o :: (Eq a, Fractional a) => Series a -> Series a -> Series a-o = lift2 compose--lift :: ([a] -> [b]) -> Series a -> Series b-lift f (Series as) = Series (f as)--lift2 :: ([a] -> [b] -> [c]) -> Series a -> Series b -> Series c-lift2 op (Series as) (Series bs) = Series (as `op` bs)--add :: Num a => [a] -> [a] -> [a]-add []     ds     = ds-add cs     []     = cs-add (c:cs) (d:ds) = c+d : add cs ds--sub :: Num a => [a] -> [a] -> [a]-cs `sub` ds = cs `add` map negate ds--(!*!) :: (Eq a, Num a) => a -> a -> a-(!*!) _ 0 = 0-(!*!) 0 _ = 0-(!*!) a b = a*b--mul :: (Eq a, Num a) => [a] -> [a] -> [a]-mul (c:ct) ds@(d:dt) = c!*!d : map (c !*!) dt `add` (ct `mul` ds)-mul _      _         = []--divide :: (Eq a, Fractional a) => [a] -> [a] -> [a]-divide []     (0:_)     = undefined-divide []      _        = []-divide (0:ct) (0:dt)    = ct `divide` dt-divide (c:ct) ds@(d:dt) = q : (ct `sub` ([q] `mul` dt)) `divide` ds where q = c/d-divide _      []        = undefined--diff :: GF -> GF-diff = lift dF where {dF [] = []; dF (_:ct) = zipWith (*) [1..] ct}--nthRootApprox :: Integer -> GF -> [GF]-nthRootApprox n f@(Series (1:_)) =-    iterateUntilFixed (nthRootNext n f) (Series [1])-nthRootApprox _ _ = error "GF has constant term different from 1"--nthRootNext :: Integer -> GF -> GF -> GF-nthRootNext n f g = Series (take (1 + 2*degree f) ds)-  where-    Series ds = (Series [n%1-1] * g^n + f) / (Series [n%1] * g^(n-1))--iterateUntilFixed :: Eq a => (a -> a) -> a -> [a]-iterateUntilFixed f x = x : (if x == y then [] else ys)-  where-    ys@(y:_) = iterateUntilFixed f (f x)--saddlePoint :: Eq a => [a] -> Maybe a-saddlePoint [c] = Just c-saddlePoint cs  = listToMaybe [ c | (c,d) <- zip cs (drop 1 cs), c == d ]--nthRoot1 :: Integer -> GF -> GF-nthRoot1 n f@(Series (1:_)) =-    let css = transpose (map coeffs (nthRootApprox n f))-    in Series $ map fromJust (takeWhile isJust (map saddlePoint css))-nthRoot1 _ _ = error "GF has constant term different from 1"--nthRoot :: Integer -> GF -> GF-nthRoot _ (Series [])       = Series []-nthRoot n (Series cs@(c:_)) = Series [d] * nthRoot1 n (Series $ map (/c) cs)-    where-      d = toRational (fromRational c ** fromRational (1%n) :: Double)--(^^^) :: GF -> Rational -> GF-(^^^) f r = case (numerator r, denominator r) of-              (n, 1) -> f ^^ n-              (0, _) -> ogf [1::Int]-              (n, k) -> nthRoot k f ^^ n---- XXX: Deprecate and use (^^^(1%2)) instead?-squareRoot :: GF -> GF-squareRoot = Series . map toRational . squareRoot' . map fromRational . coeffs---- XXX: Deprecate?-squareRoot' :: [Double] -> [Double]-squareRoot' []     = []-squareRoot' (c:ct) = ds-  where-    ds = d : ct `divide` ([d] `add` ds)-    d  = sqrt c--compose :: (Eq a, Fractional a) => [a] -> [a] -> [a]-[]     `compose` _         = []-(c:_)  `compose` []        = [c]-(c:ct) `compose` ds@(0:dt) = c : dt `mul` (ct `compose` ds)-(c:ct) `compose` ds        = [c] `add` ds `mul` (ct `compose` ds)-                                              -- ct must be finite--imap :: (Integer -> a -> b) -> Series a -> Series b-imap f = Series . zipWith f [0..] . coeffs--factorial :: Integer -> Integer-factorial n = product [1..n]--ogf :: Real a => [a] -> GF-ogf = Series . map toRational--ogfCoeffs :: GF -> [Rational]-ogfCoeffs = coeffs--egf :: Real a => [a] -> GF-egf = Series . zipWith (\n c -> toRational c * (1 % factorial n)) [0..]--egfCoeffs :: GF -> [Rational]-egfCoeffs = ogfCoeffs . imap (\i -> (* (factorial i % 1)))
+ Sloane/OEIS.hs view
@@ -0,0 +1,291 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++-- |+-- Copyright   : Anders Claesson 2015+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--++module Sloane.OEIS+    (+    -- * Types+      URL+    , Key+    , Name+    , ANum (..)+    , PackedSeq (..)+    , Table (..)+    , Reply (..)+    -- * Parse names.gz and stripped.gz+    , parseNames+    , parseStripped+    , parseTermsErr+    , parseTermsOfRecords+    -- * Parse replies from oeis.org+    , oeisKeys+    , parseReplies+    -- * Parse sequences+    , shave+    , parseSeqErr+    , parseIntegerSeq+    , packedSeq+    , packSeq+    -- * Parse A-numbers and B-numbers+    , aNumInt+    , parseANum+    , packANum+    , tag+    ) where++import GHC.Generics (Generic)+import Data.Maybe+#if __GLASGOW_HASKELL__ < 710+import Data.Monoid+#endif+import Data.String+import Data.Ratio+import Data.Map (Map)+import qualified Data.Map as M+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import Data.Aeson+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.ByteString.Char8 as Ch+import Data.Attoparsec.ByteString.Char8+import Control.Monad+import Control.Applicative+import Sloane.Utils++-- | An OEIS key is `Char`.+type Key  = Char++-- | The name of an OEIS entry is a short description of the+-- sequence. Here represented as a `ByteString`.+type Name = ByteString++type Row  = (Key, ANum, ByteString)++-- | A URL is currently just a synonym for `String`.+type URL  = String++-- | An A-number is the character \'A\' followed by a six digit+-- number. Here we represent that by a wrapped (7 character)+-- `ByteString`.+newtype ANum = ANum {unANum :: ByteString} deriving (Eq, Ord, Show, Generic)++-- | A `PackedSeq` is a wrapped `ByteString`.+newtype PackedSeq = PSeq {unPSeq :: ByteString} deriving (Eq, Show, Generic)++instance Monoid PackedSeq where+    mempty = PSeq mempty+    mappend (PSeq x) (PSeq y) = PSeq (mappend x y)++instance IsString PackedSeq where+    fromString = PSeq . fromString++-- | A `Table` represents an OEIS entry. It is a `Map` from OEIS keys to+-- lists of `ByteString`s.+newtype Table = Table (Map Key [ByteString]) deriving Show++-- | A `Reply` is an A-number together with an associated `Table` (OEIS+-- entry).+data Reply = Reply ANum Table deriving Show++instance ToJSON ANum where+    toJSON (ANum bs) = String (decodeUtf8 bs)++instance FromJSON ANum where+    parseJSON (String s) = pure $ ANum (encodeUtf8 s)+    parseJSON _ = mzero++instance ToJSON PackedSeq where+    toJSON (PSeq bs) = String (decodeUtf8 bs)++instance FromJSON PackedSeq where+    parseJSON (String s) = pure $ PSeq (encodeUtf8 s)+    parseJSON _ = mzero++instance ToJSON Table where+    toJSON (Table tbl) =+        object [ T.singleton key .= toJSON (map decodeUtf8 ls)+               | (key, ls) <- M.toList tbl+               ]++instance ToJSON Reply where+    toJSON (Reply anum table) =+        object [ "A-number" .= toJSON anum+               , "table" .= toJSON table+               ]++instance FromJSON Table where+    parseJSON (Object v) =+        let f k = (,) <$> pure k <*> (map encodeUtf8 <$> v .: T.singleton k)+        in Table . M.fromList <$> mapM f oeisKeys+    parseJSON _ = mzero++instance FromJSON Reply where+    parseJSON (Object v) = Reply <$> v .: "A-number" <*> v .: "table"+    parseJSON _ = mzero++spc :: Parser Char+spc = char ' '++aNum :: Parser ANum+aNum = ANum <$> (B.cons <$> char 'A' <*> takeWhile1 isDigit)++-------------------------------------------------------------------------------+-- Parsing names.gz and stripped.gz+-------------------------------------------------------------------------------++dropHeader :: [ByteString] -> [ByteString]+dropHeader = dropWhile (\line -> B.head line == '#')++parseRecords :: ByteString -> [(ANum, ByteString)]+parseRecords = mapMaybe (parse_ record) . dropHeader . B.lines+  where+    record = (,) <$> (aNum <* spc) <*> A.takeByteString++-- | Parse a list of A-number-names pairs and build a `Map` from the+-- result. It's purpose is to parse lines of the @names@ file. A typical+-- line of that file looks like this:+--+-- > A000108 Catalan numbers: C(n) = binomial(2n,n)/(n+1) = (2n)!/(n!(n+1)!).+--+parseNames :: ByteString -> [(ANum, ByteString)]+parseNames = parseRecords++-- | Parse a list of A-number-sequence pairs. It's purpose is to parse+-- lines of the @stripped@ file. A typical line of that file looks like+-- this:+--+-- > A000108 ,1,1,2,5,14,42,132,429,1430,4862,16796,58786,208012,742900,+--+parseStripped :: ByteString -> [(ANum, PackedSeq)]+parseStripped bs = [ (anum, PSeq (shave s)) | (anum, s) <- parseRecords bs ]++-------------------------------------------------------------------------------+-- Building the Bloom filter+-------------------------------------------------------------------------------++terms :: Parser [ByteString]+terms = takeWhile1 (/=',') `sepBy` char ','++-- | Extract the list of terms from a `ByteString` of comma separated+-- terms.+parseTermsErr :: ByteString -> [ByteString]+parseTermsErr = fromMaybe (error "error parsing terms") . parse_ terms++-- | The purpose of this function is extract all terms from the records+-- (lines) of the @stripped@ file (skipping over A-numbers).+parseTermsOfRecords :: ByteString -> [ByteString]+parseTermsOfRecords =+      concat+    . mapMaybe (parse_ ((aNum <* spc) *> char ',' *> terms))+    . dropHeader . B.lines++-------------------------------------------------------------------------------+-- Parsing replies from oeis.org/search?fmt=text+-------------------------------------------------------------------------------++-- | The complete list of keys used by OEIS (<http://oeis.org/eishelp2.html>).+oeisKeys :: String+oeisKeys = "ISTUVWXNDHFYAOEeptoKC"++row :: Parser Row+row = (,,)+    <$> (char '%' *> anyChar)+    <*> (spc *> aNum)+    <*> ((spc *> rest) <|> (endOfLine *> return B.empty))+  where+    rest = A.takeTill isEndOfLine <* endOfLine++rows :: Parser Reply+rows = mkMap1 <$> many1 row+  where+    mkMap2 = Table . M.fromListWith (flip (++))+    mkMap1 rs@((_,a,_):_) = Reply a $ mkMap2 (map (\(key, _, r) -> (key, [r])) rs)+    mkMap1 [] = error "internal error"++noise :: Parser ()+noise = skipMany (notChar '%')++replies :: Parser [Reply]+replies = concat <$> (noise *> (many1 rows `sepBy` many1 endOfLine) <* noise)++-- | Parse OEIS replies as recieved from @oeis.org/search?fmt=text@.+parseReplies :: ByteString -> [Reply]+parseReplies = fromMaybe [] . parse_ replies++-------------------------------------------------------------------------------+-- Parse sequences+-------------------------------------------------------------------------------++rat :: Parser Rational+rat = (%) <$> signed decimal <*> ((char '/' *> decimal) <|> return 1)++ratSeq :: Parser [Rational]+ratSeq = rat `sepBy` char ','++integerSeq :: Parser [Integer]+integerSeq = signed decimal `sepBy` char ','++parseSeq :: ByteString -> Maybe [Rational]+parseSeq = parse_ (ratSeq <* endOfInput) . B.filter (/=' ')++-- | Parse a sequence of `Rational`s.+parseSeqErr :: ByteString -> [Rational]+parseSeqErr = fromMaybe (error "error parsing sequence") . parseSeq++-- | Parse a sequence of `Integer`s.+parseIntegerSeq :: ByteString -> Maybe [Integer]+parseIntegerSeq = parse_ (integerSeq <* endOfInput) . B.filter (/=' ')++-- | Parser for `PackedSeq`.+packedSeq :: Parser PackedSeq+packedSeq = PSeq <$> (char '{' *> Ch.takeWhile (/='}') <* char '}')++-- | Pack a sequence of `Rational`s into a `PackedSeq`. E.g.+--+-- > packSeq [1,1/2,1/3] = PSeq {unPSeq = "1,1/2,1/3"}+--+packSeq :: [Rational] -> PackedSeq+packSeq = PSeq . B.intercalate (B.pack ",") . map (B.pack . f)+  where+    f r = case (numerator r, denominator r) of+            (n, 1) -> show n+            (n, d) -> show n ++ '/':show d++-------------------------------------------------------------------------------+-- Utility functions+-------------------------------------------------------------------------------++-- | \"Shave\" off the first and last element of a `ByteString`. E.g.+--+-- > shave "{1,2,3}" = "1,2,3"+--+shave :: ByteString -> ByteString+shave = B.init . B.tail++-- | A parser for A-numbers as `Int`s.+aNumInt :: Parser Int+aNumInt = char 'A' >> decimal++-- | Run the `aNumInt` parser.+parseANum :: ByteString -> Maybe ANum+parseANum = parse_ (packANum <$> aNumInt)++-- | Pack an A-number given as an `Int` into a wrapped `ByteString`+-- consistsing of an \'A\' followed by six digits. E.g.+--+-- > packANum 1234 = ANum {unANum = "A001234"}+--+packANum :: Int -> ANum+packANum anum = ANum $ B.cons 'A' (pad 6 anum)++-- | A parser for tags (B-numbers) as `Int`s.+tag :: Parser Int+tag = string "TAG" >> decimal
+ Sloane/Options.hs view
@@ -0,0 +1,111 @@+-- |+-- Copyright   : Anders Claesson 2015+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--+-- Command line options for sloane.++module Sloane.Options+    ( Palette+    , monochrome+    , Options (..)+    , getOptions+    ) where++import Options.Applicative+import Sloane.OEIS++-- | A palette is either colorful or monochrome.+data Palette = Colorful | Monochrome deriving (Eq, Enum, Show, Read)++-- | Is the palette monochrome?+monochrome :: Palette -> Bool+monochrome Monochrome = True+monochrome Colorful   = False++-- | Command line options:+data Options = Options+    {+    -- | Search the local DB.+      query          :: Bool+    -- | Search oeis.org.+    , oeis           :: Bool+    -- | Print all fields?+    , longFormat     :: Bool+    -- | Keys of fields to print.+    , keys           :: String+    -- | Fetch at most this many entries.+    , limit          :: Int+    -- | Fetch all entries (that match).+    , limitless      :: Bool+    -- | Should we colorize the output?+    , palette        :: Palette+    -- | Filter out sequences in local DB.+    , tojson         :: Bool+    -- | Return sequences NOT in DB+    , filtr          :: Bool+    -- | Return sequences NOT in DB+    , invert         :: Bool+    -- | Output all the sequences of the local DB+    , update         :: Bool+    -- | Show version info+    , version        :: Bool+    -- Search terms or programs+    , terms          :: [String]+    }++-- | Parse command line options.+optionsParser :: Parser Options+optionsParser =+  abortOption ShowHelpText (long "help") <*> (Options+    <$> switch+        ( short 'q'+       <> long "query"+       <> help "Search the local database" )+    <*> switch+        ( long "oeis"+       <> help "Search oeis.org" )+    <*> switch+        ( long "long"+       <> help "Long format; 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 "all"+       <> help "Fetch all matching entries (equivalent to -n 999999)" )+    <*> (toEnum . fromEnum <$> switch+        ( long "monochrome"+       <> help "Do not colorize the output" ) )+    <*> switch+        ( long "json"+       <> help "Return results is JSON format" )+    <*> switch+        ( long "filter"+       <> help ("Read sequences from stdin and return"+            ++ " those that are in the local database") )+    <*> switch+        ( long "invert"+       <> help "When filtering return sequences NOT in the database." )+    <*> switch+        ( long "update"+       <> help "Update the local database" )+    <*> switch+        ( long "version"+       <> help "Show version info" )+    <*> many (argument str (metavar "TERMS...")))++-- | Run the command line options parser (above).+getOptions :: IO Options+getOptions = updateOpts <$> execParser (info optionsParser fullDesc)+  where+    updateOpts = updateLimit . updateKeys+    updateKeys  opts = if longFormat opts then opts {keys = oeisKeys} else opts+    updateLimit opts = if limitless  opts then opts {limit = 999999}  else opts
− Sloane/Transform.hs
@@ -1,291 +0,0 @@--- |--- Copyright   : Anders Claesson 2012-2015--- Maintainer  : Anders Claesson <anders.claesson@gmail.com>--- License     : BSD-3----module Sloane.Transform-    ( NamedTransform (..)-    , ($$)-    , tLEFT-    , tRIGHT-    , tM2-    , tM2i-    , tBINOMIAL-    , tBINOMIALi-    , tBIN1-    , tBISECT0-    , tBISECT1-    , tCONV-    , tCONVi-    , tEXPCONV-    , tDIFF-    , tMOBIUS-    , tMOBIUSi-    , tEULER-    , tEXP-    , tLOG-    , tNEGATE-    , tPRODS-    , tPSUM-    , tPSUMSIGN-    , tSTIRLING-    , tTRISECT0-    , tTRISECT1-    , tTRISECT2-    , tPOINT-    , tWEIGHT-    , tPARTITION-    , transforms-    , lookupTranform-    , applyAllTransforms-    ) where--import Data.List-import Data.Ratio-import Data.Monoid-import Control.Monad-import Sloane.GF--type Transform = [Rational] -> [Rational]--data NamedTransform = NT-    { name :: String-    , eval :: Transform-    }--instance Show NamedTransform where-    show = ('t':) . name--instance Eq NamedTransform where-    t == s = name t == name s--instance Monoid NamedTransform where-    mempty = NT "" id-    mappend f g = NT tname teval-      where-        tname = name f ++ "." ++ name g-        teval = eval f     .     eval g--infixr 0 $$--isInteger :: Rational -> Bool-isInteger = (1==) . denominator--toIntSeq :: [Rational] -> [Integer]-toIntSeq cs = [ numerator c | c <- cs, all isInteger cs ]--($$) :: NamedTransform -> [Integer] -> [Integer]-f $$ cs = toIntSeq $ take (length cs) (eval f (map toRational cs))--x :: GF-x = ogf [0::Integer, 1]---- addSeq :: [Integer] -> Transform--- addSeq seq0 = zipWith (+) seq0---- mulOGF :: [Integer] -> Transform--- mulOGF seq0 = \cs -> ogfCoeffs (ogf seq0 * ogf cs)---- mulEGF :: [Integer] -> Transform--- mulEGF seq0 = \cs -> egfCoeffs (egf seq0 * egf cs)--geoSeries :: Rational -> GF-geoSeries c = ogf [ c^k | k<-[0::Int ..] ]--expSeries :: Rational -> GF-expSeries c = egf [ c^k | k<-[0::Int ..] ]--bisect0 :: [a] -> [a]-bisect0 [] = []-bisect0 (c:cs) = c : bisect1 cs--bisect1 :: [a] -> [a]-bisect1 [] = []-bisect1 (_:cs) = bisect0 cs--trisect0 :: [a] -> [a]-trisect0 [] = []-trisect0 (c:cs) = c : trisect2 cs--trisect1 :: [a] -> [a]-trisect1 [] = []-trisect1 (_:cs) = trisect0 cs--trisect2 :: [a] -> [a]-trisect2 [] = []-trisect2 (_:cs) = trisect1 cs--signed :: GF -> GF-signed = imap $ \i c -> (-1%1)^i * c--tLEFT :: NamedTransform-tLEFT = NT "LEFT" (drop 1)--tRIGHT :: NamedTransform-tRIGHT = NT "RIGHT" (1:)--tM2 :: NamedTransform-tM2 = NT "M2" f where f [] = []; f (c:cs) = c : map ((2%1)*) cs--tM2i :: NamedTransform-tM2i = NT "M2i" (ogfCoeffs . Series . f)-  where-    f [] = []-    f cs = let (d:ds) = map toRational cs in d : map (/(2%1)) ds--tBINOMIAL :: NamedTransform-tBINOMIAL = NT "BINOMIAL" (\cs -> egfCoeffs (expSeries (1%1) * egf cs))--tBINOMIALi :: NamedTransform-tBINOMIALi = NT "BINOMIALi" (\cs -> egfCoeffs (expSeries ((-1)%1) * egf cs))--tBIN1 :: NamedTransform-tBIN1 = NT "BIN1" (\cs ->-    drop 1 . egfCoeffs $ -expSeries (-1%1) * signed (egf (0:cs)))--tBISECT0 :: NamedTransform-tBISECT0 = NT "BISECT0" bisect0--tBISECT1 :: NamedTransform-tBISECT1 = NT "BISECT1" bisect1--tCONV :: NamedTransform-tCONV = NT "CONV" (\cs -> ogfCoeffs (ogf cs ^ (2::Int)))--tCONVi :: NamedTransform-tCONVi = NT "CONVi" (ogfCoeffs . squareRoot . ogf)--tEXPCONV :: NamedTransform-tEXPCONV = NT "EXPCONV" (\cs -> egfCoeffs (egf cs ^ (2::Int)))--tDIFF :: NamedTransform-tDIFF = NT "DIFF" (\cs -> zipWith (-) (drop 1 cs) cs)---- The Mobius function of the poset of integers under divisibility-mobius :: Integer -> Integer -> Integer-mobius a b-  | a == b         = 1-  | b `rem` a == 0 = -sum [ mobius a c | c <- [a..b-1], b `rem` c == 0 ]-  | otherwise      = 0---- The number theoretical Mobius function-mu :: Integer -> Integer-mu = mobius 1--tMOBIUS :: NamedTransform-tMOBIUS = NT "MOBIUS" $ \cs ->-    [ sum [mu (n `div` k) % 1 * (cs !! (fromInteger k-1))-          | k<-[1..n], n `rem` k == 0-          ]-    | (n,_) <- zip [1..] cs-    ]--tMOBIUSi :: NamedTransform-tMOBIUSi = NT "MOBIUSi" $ \cs ->-    [ sum [ cs !! (fromInteger k-1) | k<-[1..n], n `rem` k == 0 ]-    | (n,_) <- zip [1..] cs-    ]--tEULER :: NamedTransform-tEULER = NT "EULER" (\cs ->-    let f = product $ zipWith (\n c -> (1 - x^n)^^^c) [1::Int ..] cs-    in drop 1 $ ogfCoeffs (1/f))--tEULERi :: NamedTransform-tEULERi = NT "EULERi" undefined---- EXP converts [a_1, a_2, ...] to [b_1, b_2,...] where--- 1 + EGF_B (x) = exp EGF_A (x)-tEXP :: NamedTransform-tEXP = NT "EXP" (\cs -> drop 1 . egfCoeffs $ expSeries 1 `o` egf (0:cs))---- LOG converts [a_1, a_2, ...] to [b_1, b_2,...] where--- 1 + EGF_A (x) = exp EGF_B (x) i.e. EGF_B (x) = log(1 + EGF_A (x)).-tLOG :: NamedTransform-tLOG = NT "LOG" (\cs -> drop 1 $ egfCoeffs (log1 `o` (-1 * egf (0:cs))))-  where-    log1 = Series (0 : [-1 % n | n <- [1..]])--tNEGATE :: NamedTransform-tNEGATE = NT "NEGATE" f where f [] = []; f (c:cs) = c : map negate cs--tPRODS :: NamedTransform-tPRODS = NT "PRODS" (drop 1 . scanl (*) (1%1))--tPSUM :: NamedTransform-tPSUM = NT "PSUM" (drop 1 . scanl (+) (0%1))--tPSUMSIGN :: NamedTransform-tPSUMSIGN = NT "PSUMSIGN" (ogfCoeffs . (geoSeries (-1%1) *) . ogf)--tSTIRLING :: NamedTransform-tSTIRLING = NT "STIRLING" (\cs ->-    drop 1 . egfCoeffs $ egf (0:cs) `o` (expSeries (1%1) - 1))--tTRISECT0 :: NamedTransform-tTRISECT0 = NT "TRISECT0" trisect0--tTRISECT1 :: NamedTransform-tTRISECT1 = NT "TRISECT1" trisect1--tTRISECT2 :: NamedTransform-tTRISECT2 = NT "TRISECT2" trisect2--tPOINT :: NamedTransform-tPOINT = NT "POINT" (zipWith (*) [0..])--tWEIGHT :: NamedTransform-tWEIGHT = NT "WEIGHT" $-    drop 1 . ogfCoeffs . product . zipWith (\n c -> (1 + x^n)^^^c) [1::Int ..]--increasing :: Ord a => [a] -> Bool-increasing cs = and $ zipWith (<=) cs (drop 1 cs)--tPARTITION :: NamedTransform-tPARTITION = NT "PARTITION" $ \cs -> do-    guard $ not (null cs) && all (>0) cs && increasing cs-    let f = product $ map (\c -> 1 - x^^^c) (nub cs)-    drop 1 . ogfCoeffs $ 1/f---- New transform: tSTIELTJES -- continued fraction coefficients---transforms :: [NamedTransform]-transforms =-    [ tLEFT-    , tRIGHT-    , tM2-    , tM2i-    , tBINOMIAL-    , tBINOMIALi-    , tBIN1-    , tBISECT0-    , tBISECT1-    , tCONV-    , tCONVi-    , tEXPCONV-    , tDIFF-    , tMOBIUS-    , tMOBIUSi-    , tEULER---    , tEULERi-    , tEXP-    , tLOG-    , tNEGATE-    , tPRODS-    , tPSUM-    , tPSUMSIGN-    , tSTIRLING-    , tTRISECT0-    , tTRISECT1-    , tTRISECT2-    , tPOINT-    , tWEIGHT-    , tPARTITION-    ]--lookupTranform :: String -> Maybe NamedTransform-lookupTranform tname = lookup tname [ (name f, f) | f <- transforms ]--applyAllTransforms :: [Rational] -> [(NamedTransform, [Rational])]-applyAllTransforms cs = [ (f, eval f cs) | f <- transforms ]
+ Sloane/Utils.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Copyright   : Anders Claesson 2015+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--++module Sloane.Utils+    ( pad+    , parens+    , oneOf+    , chainl1+    , parse_+    ) where++import Data.Monoid+import qualified Data.ByteString.Char8 as B+import Data.ByteString.Char8 (ByteString)+import Data.Attoparsec.ByteString.Char8+import Control.Applicative+++-- | @pad d n@ packs the integer @n@ into a `ByteString` padding with+-- \'0\' on the right to achieve length @d@.+--+-- > pad 6 123 = "000123"+--+pad :: Int -> Int -> ByteString+pad d n = B.replicate (d - B.length s) '0' <> s where s = B.pack (show n)++-- | Parse @p@ enclosed in parenthesis.+parens :: Parser a -> Parser a+parens p = string "(" *> p <* string ")"++-- | Parse one of the words in the given `ByteString`.+oneOf :: ByteString -> Parser ByteString+oneOf s = choice (map string (B.words s))++-- | Parse one or more occurrences of @p@, separated by an operator.+-- Returns a value obtained by a left associative application.+chainl1 :: (Monad f, Alternative f) => f a -> f (a -> a -> a) -> f a+chainl1 p op = p >>= rest+  where+    rest a = do { f <- op; b <- p; rest $! f a b } <|> pure a++-- | Run a parser.+parse_ :: Parser t -> ByteString -> Maybe t+parse_ f = either (const Nothing) Just . parseOnly f
sloane.1 view
@@ -1,156 +1,73 @@-.TH "SLOANE" "1" "7 Jan 2015" "Sloane User Manual" "Version 2.0.4"+.TH "SLOANE" "1" "18 Aug 2015" "User Manual" "Version 4.0.0" .SH NAME .PP-sloane \- a command line interface to Sloane\[aq]s On\-Line Encyclopedia-of Integer Sequences <http://oeis.org>+sloane: A command line interface to Sloane\[aq]s OEIS. .SH SYNOPSIS .PP-sloane [\-a|\-\-all] [\-k KEYS] [\-n N] [\-\-url] [\-\-local] TERMS...-.PP-sloane \-A NUMBER-.PP-sloane \-\-filter [\-\-invert]-.PP-sloane \-\-transform NAME-.PP-sloane (\-\-list\-transforms | \-\-update | \-\-version | \-\-help)+\f[B]sloane\f[] OPTIONS \-\-oeis TERMS...+.PD 0+.P+.PD+\f[B]sloane\f[] OPTIONS (\-q|\-\-query) A\-NUMBER/SEQUENCE...+.PD 0+.P+.PD+\f[B]sloane\f[] [\-\-invert] \-\-filter .SH DESCRIPTION .PP-The \f[C]sloane\f[] command searches Sloane\[aq]s On\-Line Encyclopedia-of Integer Sequences (OEIS).-The search terms are typically the leading term of a sequence.-For example,-.IP-.nf-\f[C]-sloane\ 1,1,2,5,15,52,203,877,4140-\f[]-.fi-.PP-returns entry A000110 (Bell numbers), and four more entries.-One can also search by sequence id (A\-number), or even search for-arbitrary words.-See the \f[B]EXAMPLES\f[] section.-.PP-Alternatively, using the \f[C]\-\-local\f[] option, the search can be-done locally against a downloaded local database of known sequences.-This mode works by "grepping" for the query in the sequence field.-.PP-To check a large number of sequences one can use \f[C]\-\-filter\f[].-When this option is set, \f[C]sloane\f[] reads the standard input-line\-by\-line, if the sequence read is in the local database, then it-is returned to the standard output; if not, it is ignored.-This way one can quickly filter out the sequences from the input that-are in the local database.-In other words, assuming that \f[I]FILE\f[] contains one sequence per-line,-.IP-.nf-\f[C]-sloane\ \-\-filter\ <FILE-\f[]-.fi+The \f[C]sloane\f[] command provides an interface to OEIS (The On\-Line+Encyclopedia of Integer Sequences).+It can be used online with the \f[C]\-\-oeis\f[] operation, and offline+with the \f[C]\-\-query\f[] operation; offline mode is the default.+.SH OPERATION \-\-oeis+.SS Synopsis .PP-returns the subset of the sequences in \f[I]FILE\f[] that are in the-local database.-To also look\-up the names of those sequences one could, for instance,-run-.IP-.nf-\f[C]-sloane\ \-\-filter\ <FILE\ |\ xargs\ \-L1\ \-\-verbose\ sloane\ \-\-local-\f[]-.fi+sloane [\-\-long] [\-k KEYS] [\-n N] [\-\-all] [\-\-monochrome]+[\-\-json] \-\-oeis TERMS...+.SS Description .PP-Sloane normally crops long lines to fit the widths of the terminal.-If this is unwanted, pipe the output through cat or less:-.IP-.nf-\f[C]-sloane\ \-a\ id:A000110\ |\ less\ \-R-\f[]-.fi-.SH OPTIONS+Lookup search terms in Sloane\[aq]s On\-Line Encyclopedia of Integer+Sequences (OEIS).+.SS Options .TP-.B \-a, \-\-all-Print all fields+.B \-\-long+Long format; print all fields. .RS .RE .TP .B \-k \f[I]KEYS\f[]-Keys of fields to print (default: SN)+Keys of fields to print (default: SN). .RS .RE .TP .B \-n \f[I]N\f[]-Fetch at most this many entries (default: 5)-.RS-.RE-.TP-.B \-\-url-Print URLs of found entries (but nothing else)-.RS-.RE-.TP-.B \-\-local-Grep for a sequence in the local database.-.RS-.RE-.TP-.B \-A \f[I]NUMBER\f[]-Fetch the sequence with this number from the local database.-Prints the sequence, but nothing else, to sdout.-.RS-.RE-.TP-.B \-\-filter-Read sequences from stdin and return those that are in the local-database.-.RS-.RE-.TP-.B \-\-invert-Return sequences \f[I]not\f[] in the database.-This option has no effect unless \f[C]\-\-filter\f[] is also set.-.RS-.RE-.TP-.B \-\-transform \f[I]NAME\f[]-Apply the named transform to the input sequence.-If the resulting sequence is integral print it to stdout; else print-nothing.-Most of the transforms and their names are taken from-<https://oeis.org/transforms.txt>.-.RS-.RE-.TP-.B \-\-list\-transforms-List the names of all transforms.+Fetch at most this many entries (default: 5). .RS .RE .TP-.B \-\-update-Update the local database.+.B \-\-all+Fetch all matching entries (equivalent to \-n 999999). .RS .RE .TP-.B \-\-version-Print version information.+.B \-\-monochrome+Do not colorize the output.+Useful when piping the output to another program. .RS .RE .TP-.B \-\-help-Briefly describe the available options.+.B \-\-json+Output results is JSON format. .RS .RE-.SH EXAMPLES+.SS Examples .PP The most common search is for entries matching a sequence of consecutive terms: .IP .nf \f[C]-sloane\ 1,3,19,183,2371,38703+sloane\ \-\-oeis\ 1,3,19,183,2371,38703 \f[] .fi .PP@@ -171,7 +88,7 @@ .IP .nf \f[C]-sloane\ \-k\ SNCF\ id:A006531+sloane\ \-k\ SNCF\ \-\-oeis\ id:A006531 \f[] .fi .PP@@ -179,47 +96,116 @@ .IP .nf \f[C]-sloane\ \-n\ 3\ "(2+2)\-free\ posets"+sloane\ \-n\ 3\ \-\-oeis\ "(2+2)\-free\ posets" \f[] .fi .PP-To view the full entries of these 3 results in a browser (e.g., Firefox)-one can use the url option:+Sloane normally crops long lines to fit the widths of the terminal.+If this is unwanted, pipe the output through cat or less: .IP .nf \f[C]-firefox\ `sloane\ \-\-url\ \-n\ 3\ "(2+2)\-free\ posets"`+sloane\ \-\-long\ \-\-oeis\ id:A000110\ |\ less\ \-R \f[] .fi+.SH OPERATION \-\-query+.SS Synopsis .PP-To retrieve sequence A022493 from the local database use the-\f[C]\-A\f[] option:+sloane [\-\-long] [\-k KEYS] [\-n N] [\-\-all] [\-\-monochrome]+[\-\-json] (\-q|\-\-query) A\-NUMBER/SEQUENCE...+.SS Description+.PP+Search locally against a downloaded local database of known sequences.+This type of query is less flexible, but faster, than using the+\f[C]\-\-oeis\f[] operation: With \f[C]sloane\ \-\-query\f[] one can+only lookup A\-numbers and seqences; free text searches are not+supported.+Also, the fields returned by \f[C]sloane\ \-\-query\f[] are \[aq]S\[aq]+and \[aq]N\[aq].+.SS Examples+.PP+Lookup A\-numbers: .IP .nf \f[C]-sloane\ \-A022493+$\ sloane\ \-\-query\ A000111\ A000112++S\ A000111\ 1,1,1,2,5,16,61,272,1385,7936,50521,353792,2702765,+N\ A000111\ Euler\ or\ up/down\ numbers:\ e.g.f.\ sec(x)\ +\ tan(x)..++S\ A000112\ 1,1,2,5,16,63,318,2045,16999,183231,2567284,46749427,+N\ A000112\ Number\ of\ partially\ ordered\ sets\ ("posets")\ with\ n.. \f[] .fi .PP-In the final example the local database is used to filter out sequences-from the standard input that are in OEIS:+Lookup a sequence: .IP .nf \f[C]-sloane\ \-\-filter\ <<END-1,2,3,6,11,23,47,106,235\ \ \ \ \ \ \ \ \ \ \ #\ Comma\ separated\ integers-1\ 2\ 444\ 90\ 120\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ Space\ separated\ integers-\[aq](3\ 9\ 27\ 88\ 123)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ S\-expression-[2,3,5,7,11,13,17,19,23,29,31,37]\ \ #\ Haskell\ list-[1;\ 1;\ 2;\ 3;\ 5;\ 8;\ 13;\ 21]\ \ \ \ \ \ \ \ \ #\ O\[aq]Caml\ list-{1,\ \-1,\ 2,\ \-6,\ 24,\ \-120,\ 720}\ \ \ \ \ \ #\ Mathematica\ list-END+$\ sloane\ \-n2\ \-q\ 1,1,2,5,15,52,203,877,4140,21147,115975,678570,++S\ A000110\ 1,1,2,5,15,52,203,877,4140,21147,115975,678570,4213597,+N\ A000110\ Bell\ or\ exponential\ numbers:\ number\ of\ ways\ to\ partition..++S\ A192128\ 1,1,2,5,15,52,203,877,4140,21147,115975,678570,4213597,+N\ A192128\ Number\ of\ set\ partitions\ of\ {1,\ ...,\ n}\ that\ avoid.. \f[] .fi+.SS Options .PP-The \[aq]#\[aq] character and any text after it is ignored by sloane.-Note also that sloane is quite liberal in the formatting of sequences it-accepts.+Same as for the \f[C]\-\-oeis\f[] operation.+.SH OPERATION \-\-filter+.SS Synopsis+.PP+sloane [\-\-invert] \-\-filter+.SS Description+.PP+To check a large number of sequences one can use \f[C]\-\-filter\f[].+When this option is set, \f[C]sloane\f[] reads the standard input+line\-by\-line, if the sequence read is in the local database, then it+is returned to the standard output; if not, it is ignored.+This way one can quickly filter out the sequences from the input that+are in the local database.+In other words, assuming that \f[I]FILE\f[] contains one sequence per+line,+.IP+.nf+\f[C]+sloane\ \-\-filter\ <FILE+\f[]+.fi+.PP+returns the subset of the sequences in \f[I]FILE\f[] that are in the+local database.+To also lookup the names of those sequences one could, for instance, run+.IP+.nf+\f[C]+sloane\ \-\-filter\ <FILE\ |\ sloane\ \-q+\f[]+.fi+.SS Flag+.TP+.B \-\-invert+Return sequences \f[I]not\f[] in the database.+.RS+.RE+.SH ADDITIONAL OPERATIONS+.TP+.B \-\-update+Update the local database.+.RS+.RE+.TP+.B \-\-version+Print version information.+.RS+.RE+.TP+.B \-\-help+Briefly describe the available options.+.RS+.RE .SH KEYS .PP These are the keys used by OEIS <http://oeis.org/eishelp2.html>.@@ -256,13 +242,13 @@ .fi .SH NOTES .PP-Please use this program with moderation as not to overburden the-OEIS\-server; see OEIS\[aq] policy on searching the database:+Please use the \f[C]\-\-oeis\f[] option with moderation as not to+overburden the OEIS\-server; see OEIS\[aq] policy on searching the+database: <http://oeis.org/wiki/Welcome#Policy_on_Searching_the_Database>. .SH SEE ALSO .PP-The sloane source code may be downloaded from-<https://github.com/akc/sloane>.+<http://akc.is/src/sloane> .SH AUTHOR .PP Anders Claesson <http://akc.is>
sloane.cabal view
@@ -1,10 +1,10 @@ Name:                sloane-Version:             2.0.5-Synopsis:            A command line interface to Sloane's On-Line Encyclopedia-                     of Integer Sequences-Description:         A command line interface to Sloane's On-Line Encyclopedia-                     of Integer Sequences. For usage see-                     <http://github.com/akc/sloane>.+Version:             4.0.0+Synopsis:            A command line interface to Sloane's OEIS.++Description:         A command line interface to Sloane's On-Line Encyclopedia of+                     Integer Sequences with offline capabilities.+ Homepage:            http://github.com/akc/sloane License:             BSD3 License-file:        LICENSE@@ -22,23 +22,33 @@  Executable sloane   Main-is:             sloane.hs-  Other-Modules:       Sloane.DB++  Other-Modules:       Sloane.Bloom                        Sloane.Config-                       Sloane.GF-                       Sloane.Transform-  ghc-options:         -Wall-  Build-depends:       base >=3 && <5,-                       containers >=0.5,-                       optparse-applicative >=0.10.0,-                       download-curl >= 0.1.4,-                       HTTP >=4000.0.9,-                       cereal >=0.4,-                       network >=2.4,-                       bytestring >=0.9,-                       text >=0.11,+                       Sloane.DB+                       Sloane.Download+                       Sloane.Entry+                       Sloane.OEIS+                       Sloane.Options+                       Sloane.Utils++  ghc-options:         -Wall -fno-warn-unused-do-bind -fno-warn-missing-signatures++  Build-depends:       aeson >=0.8,                        ansi-terminal >=0.6,-                       terminal-size >=0.2,-                       filepath >=1.3,+                       attoparsec >=0.11,+                       base >=4 && <5,+                       bloomfilter >=2.0,+                       bytestring >=0.10,+                       conduit >=1,+                       conduit-extra >=1,+                       containers >=0.5,                        directory >=1.2,-                       zlib >=0.5,-                       stringsearch >=0.3.0+                       filepath >=1.3,+                       http-conduit >=2,+                       http-types >=0.8,+                       optparse-applicative >=0.10,+                       stringsearch >=0.3,+                       terminal-size >=0.2,+                       text >=0.11,+                       transformers >=0.3
sloane.hs view
@@ -1,179 +1,210 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE PolyKinds #-}+ -- | -- Copyright   : Anders Claesson 2012-2015 -- Maintainer  : Anders Claesson <anders.claesson@gmail.com> -- License     : BSD-3 -- -import           Data.List                    (intercalate)-import           Data.Maybe                   (maybeToList)-import           Data.Monoid-import           Data.ByteString              (ByteString)-import qualified Data.ByteString.Char8        as Ch8-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+module Main (main) where -type URL = String+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 -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-    }+nameVer  = "sloane 4.0.0"                 :: String+oeisURL  = "https://oeis.org/search"      :: URL+strpdURL = "https://oeis.org/stripped.gz" :: URL+namesURL = "https://oeis.org/names.gz"    :: URL -oeisKeys :: String-oeisKeys = "ISTUVWXNDHFYAOEeptoKC" -- Valid OEIS keys+type Query  = B.ByteString+type Width  = Int+type Limit  = Int+type View   = (Width, Palette, [Key]) -oeisUrls :: Config -> DB -> [URL]-oeisUrls cfg = map ((oeisHost cfg ++) . Ch8.unpack) . DB.aNumbers+data QA = QA Query [Reply] -oeisLookup :: Options -> Config -> IO DB-oeisLookup opts cfg =-    (DB.parseOEISEntries . either error id) <$>-    openURI (oeisURL cfg ++ "&" ++ urlEncodeVars [("n", show n), ("q", q)])-  where-    n = limit opts-    q = unwords $ terms opts+instance ToJSON QA where+    toJSON (QA q rs) =+        object [ "query" .= String (decodeUtf8 q)+               , "reply" .= toJSON rs+               ] -grepDB :: Options -> DB -> DB-grepDB opts = DB.take n . DB.grep (Ch8.pack q)-  where-    n = limit opts-    q = intercalate "," (terms opts)+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 -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))))+data Output+    = OEISReplies View [QA]+    | OEISRepliesJSON [QA]+    | Entries [PackedEntry]+    | NOP -dropComment :: ByteString -> ByteString-dropComment = Ch8.takeWhile (/= '#')+nonEmptyLines :: BL.ByteString -> [B.ByteString]+nonEmptyLines = map BL.toStrict . filter (not . BL.null) . BL.lines -showSeq :: [Integer] -> String-showSeq = intercalate "," . map show+readStdin :: IO [B.ByteString]+readStdin = nonEmptyLines <$> BL.getContents -mkSeq :: ByteString -> Seq-mkSeq = Ch8.intercalate (Ch8.pack ",") . Ch8.words . clean . dropComment-  where-    clean = Ch8.filter (`elem` " 0123456789-") . Ch8.map tr-    tr c  = if c `elem` ";," then ' ' else c+readInput :: Options -> Config -> IO Input+readInput opts cfg+    | version opts = return Empty -mkANumber :: Int -> ANumber-mkANumber n = let s = show n in Ch8.pack ('A' : replicate (6-length s) '0' ++ s)+    | update opts =+        return $ UpdateDBs (sloaneDir cfg) (seqDBPath cfg) (namesDBPath cfg) -filterDB :: Options -> DB -> IO [Seq]-filterDB opts db = filter match . parseSeqs <$> Ch8.getContents-  where-    match q = (if invert opts then id else not) (DB.null $ DB.grep q db)-    parseSeqs = filter (not . Ch8.null) . map mkSeq . Ch8.lines+    | filtr opts = do+        db <- readSeqDB cfg+        FilterSeqs db (invert opts) . map parsePackedEntryErr <$> readStdin -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...")))+    | oeis opts =+        return $ SearchOEIS (tojson opts) (limit opts) view (unwords (terms opts)) -search :: (Options -> Config -> IO DB) -> Options -> Config -> IO ()-search f opts cfg = f opts cfg >>= put+    | 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-    put | url opts  = putStr . unlines . oeisUrls cfg-        | otherwise = DB.put cfg $ if full opts then oeisKeys else keys opts+    view = (termWidth cfg, palette opts, keys opts) -getTerms :: Options -> IO Options-getTerms opts-    | dont      = 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+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-    dont = or [ filtr opts, anumber opts > 0-              , listTransforms opts, update opts-              , version opts+    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-    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_ Ch8.putStrLn-         | filtr opts = \c -> DB.read c >>= filterDB opts >>= mapM_ Ch8.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+    c <- getConfig+    t <- getOptions+    readInput t c >>= sloane >>= printOutput