packages feed

cobot-io-0.1.4.2: src/Bio/GB/Parser.hs

{-# LANGUAGE OverloadedStrings #-}

module Bio.GB.Parser
  ( genBankP
  , rangeP
  ) where

import           Bio.GB.Type                (Feature (..), Form (..), GenBankSequence (..),
                                             Locus (..), Meta (..), Parser, Reference (..),
                                             Source (..), Version (..))
import           Bio.Sequence               (Border (..), MarkedSequence, Range (..),
                                             RangeBorder (..), markedSequence, shiftRange)
import           Control.Monad.Combinators  (many, manyTill, optional, some, (<|>))
import           Data.Char                  (isAlphaNum, isSpace, isUpper)
import           Data.Functor               (($>))
import           Data.Text                  (Text, intercalate, pack, splitOn, unpack)
import qualified Data.Text                  as T
import           Text.Megaparsec            (notFollowedBy, option, satisfy, sepBy1, takeWhile1P,
                                             takeWhileP, try, (<?>))
import           Text.Megaparsec.Char       (char, digitChar, eol, letterChar, string)
import           Text.Megaparsec.Char.Lexer (decimal)

-- | Parser of .gb file.
--
genBankP :: Parser GenBankSequence
genBankP =  GenBankSequence
        <$> (metaP <?> "Meta parser")
        <*> (gbSeqP <?> "GB sequence parser")
        <*  string "//" <* eolSpaceP 

--------------------------------------------------------------------------------
-- Block with meta-information.
--------------------------------------------------------------------------------

metaP :: Parser Meta
metaP = do
  locus'      <- locusP <?> "Locus parser"

  definitionM <- optional definitionP <?> "Definition parser"
  accessionM  <- optional accessionP <?> "Accession parser"
  versionM    <- optional versionP <?> "Version parser"
  keywordsM   <- optional keywordsP <?> "Keywords parser"
  sourceM     <- optional sourceP <?> "Source parser"
  referencesL <- many referenceP <?> "References parser"
  commentsL   <- many commentP <?> "Comments parser"

  pure $ Meta locus' definitionM accessionM versionM keywordsM sourceM referencesL commentsL

locusP :: Parser Locus
locusP = string "LOCUS" *> space *> (Locus
       <$> textP <* space                                      -- name
       <*> decimal <* space <* string "bp" <* space            -- sequence length
       <*> textP <* space                                      -- molecule type
       <*> optional formP <* space                               -- form of sequence
       <*> optional (pack <$> some (satisfy isUpper)) <* space   -- GenBank division
       <*> textP                                               -- modification date
       <*  eolSpaceP)
  where
    textP = takeWhile1P Nothing $ not . isSpace

    formP :: Parser Form
    formP = try (string "linear" $> Linear) <|> (string "circular" $> Circular)

definitionP :: Parser Text
definitionP =  string "DEFINITION" *> space *> (try emptyP <|> someLinesP)

accessionP :: Parser Text
accessionP =  string "ACCESSION" *> space *> (try emptyP <|> (pack
          <$> some (try alphaNumChar <|> char '_')
          <*  eolSpaceP))

versionP :: Parser Version
versionP =  string "VERSION" *> space
         *> ((Version <$> emptyP <*> pure Nothing) <|> (Version
        <$> (pack <$> some versionP')
        <*> optional (pack <$> (space *> string "GI:" *> some versionP'))
        <*  eolSpaceP))
  where
    versionP' = try alphaNumChar <|> try (char '_') <|> char '.'

keywordsP :: Parser Text
keywordsP =  string "KEYWORDS"
          *> (try emptyP
         <|> (space *> textWithSpacesP <* eolSpaceP))

sourceP :: Parser Source
sourceP =  string "SOURCE" *> space
        *> (Source
       <$> someLinesP
       <*> optional organismP)
  where
    organismP = string "  ORGANISM" *> space *> someLinesP

referenceP :: Parser Reference
referenceP = string "REFERENCE" *> space
           *> (((\x -> Reference x Nothing Nothing Nothing Nothing) <$> emptyP) <|> (Reference
          <$> someLinesP
          <*> optional (string "  AUTHORS" *> space *> someLinesP)
          <*> optional (string "  TITLE" *> space *> someLinesP)
          <*> optional (string "  JOURNAL" *> space *> someLinesP)
          <*> optional (string "  PUBMED" *> space *> someLinesP)))

commentP :: Parser Text
commentP = string "COMMENT" *> (try emptyP <|> (many (char ' ') *> someLinesP))

--------------------------------------------------------------------------------
-- Block with FEATURES table.
--------------------------------------------------------------------------------

featuresP :: Parser [(Feature, Range)]
featuresP = -- skip unknown fields and stop on line with "FEATURES" 
          manyTill (textWithSpacesP <* eolSpaceP) (string "FEATURES") *> space
          *> textWithSpacesP <* eolSpaceP
          *> some (featureP <?> "Single feature parser")

featureP :: Parser (Feature, Range)
featureP = do
    _ <- string featureIndent1

    featureName'      <- takeWhileP Nothing (not . isSpace) <* space
    range <- rangeP <* eolSpaceP

    props <- some propsP

    -- Ranges are 1-based, but the underlying Vector in the Feature is 0-based.
    -- We shift the range left so the numberings match.
    --
    pure (Feature featureName' props, shiftRange (-1) range)

rangeP :: Parser Range
rangeP =  try spanP 
      <|> try betweenP 
      <|> try pointP
      <|> try joinP
      <|> complementP
  where
    spanP :: Parser Range
    spanP = do
        lowerBorderType <- option Precise (try $ char '<' *> pure Exceeded)
        lowerBorderLocation <- decimal
        _ <- string ".."
        upperBorderType <- option Precise (try $ char '>' *> pure Exceeded)
        upperBorderLocation <- decimal
        pure $ Span (RangeBorder lowerBorderType lowerBorderLocation) (RangeBorder upperBorderType upperBorderLocation) 
                
    betweenP :: Parser Range
    betweenP = do
        before <- decimal
        _ <- char '^'
        after <- decimal
        pure $ Between before after

    pointP :: Parser Range
    pointP = fmap Point decimal
   
    joinP :: Parser Range
    joinP = string "join(" *> fmap Join (rangeP `sepBy1` char ',') <* char ')'

    complementP :: Parser Range
    complementP = fmap Complement $ string "complement(" *> rangeP <* char ')'
        

propsP :: Parser (Text, Text)
propsP = do
    _ <- string featureIndent2
    _ <- char '/'
    propName <- takeWhile1P Nothing (/= '=')
    _ <- char '='

    propText <- try ((char '\"' *> takeWhile1P Nothing (/= '\"') <* char '\"' <* eolSpaceP)
             <|> multiLineProp)

    let propTextCorrect = mconcat $ filter (/= featureIndent2) $ splitOn featureIndent2 propText

    pure (propName, propTextCorrect)
  where
    indLine :: Parser Text
    indLine = do
        _ <- string featureIndent2
        notFollowedBy (char '/')
        text <- textWithSpacesP 
        eolSpaceP
        pure text

    multiLineProp :: Parser Text
    multiLineProp = do
        fstText <- textWithSpacesP <* eolSpaceP 
        rest <- many (try indLine)
        pure $ T.concat (fstText : rest) 

    

-- | First level of identation in FEATURES table file.
--
featureIndent1 :: Text
featureIndent1 = pack $ replicate 5 ' '

-- | Second level of identation in FEATURES table file.
--
featureIndent2 :: Text
featureIndent2 = pack $ replicate 21 ' '

--------------------------------------------------------------------------------
-- Block with ORIGIN table.
--------------------------------------------------------------------------------

originP :: Parser String
originP =  (string "ORIGIN" <?> "String ORIGIN") *> eolSpaceP
        *> pure toText
       <*> some (space *> some digitChar *> space1
        *> some (some letterChar <* (try space1 <|> eolSpaceP)))
  where
    toText :: [[String]] -> String
    toText = concat . fmap concat

--------------------------------------------------------------------------------
-- Parser of 'GenBankSequence' from FEATURES and ORIGIN tables.
--------------------------------------------------------------------------------
gbSeqP :: Parser (MarkedSequence Feature Char)
gbSeqP = do
    features <- (featuresP <?> "Features parser")

    -- An extract from the GB specification (https://www.ncbi.nlm.nih.gov/genbank/release/current/):
    --    NOTE: The BASE COUNT linetype is obsolete and was removed
    --    from the GenBank flatfile format in October 2003.
    --  Anyway, here, in 2021, we still might get plasmids with the BASE COUNT line present.
    --
    _ <- optional $ try (string "BASE COUNT" *> textWithSpacesP *> eol)

    origin   <- (originP <?> "Origin parser")

    either (fail . unpack) pure (markedSequence origin features)

--------------------------------------------------------------------------------
-- Utility functions.
--------------------------------------------------------------------------------

-- | First level of identation in .gb file.
--
firstIndent :: Text
firstIndent = pack $ replicate 12 ' '

eolSpaceP :: Parser ()
eolSpaceP = () <$ many (char ' ') <* eol

emptyP :: Parser Text
emptyP = many (char ' ') *> char '.' *> eolSpaceP *> pure "."

textWithSpacesP :: Parser Text
textWithSpacesP = takeWhileP Nothing (`notElem` ['\n', '\r'])

someLinesP :: Parser Text
someLinesP = intercalate "\n" <$> someLinesIndentP firstIndent

someLinesIndentP :: Text -> Parser [Text]
someLinesIndentP indent =  (:) <$> textWithSpacesP <* eolSpaceP
                       <*> (many (string indent *> textWithSpacesP <* eolSpaceP))

space :: Parser ()
space = () <$ (many $ satisfy isSpace)

space1 :: Parser ()
space1 = () <$ (some $ satisfy isSpace)

alphaNumChar :: Parser Char
alphaNumChar = satisfy isAlphaNum