packages feed

sloane-4.0.0: Sloane/OEIS.hs

{-# 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