packages feed

UMM-0.1.0: UMMParser.hs

{- Copyright 2009 Uwe Hollerbach <uh@alumni.caltech.edu>

This file is part of umm, Uwe's Money Manager.

umm is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your
option) any later version.

umm is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.

You should have received a copy of the GNU General Public License
along with umm; if not, write to the Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

$Id: UMMParser.hs,v 1.28 2009/11/14 22:41:36 uwe Exp $ -}

-- TODO: template := <to be determined>

module UMMParser (parseURecord, parseUDate, parseUCommand) where
import Prelude
import Data.Char
import Data.Ratio
import Text.ParserCombinators.Parsec as TPCP hiding (spaces)

import UMMData

readInt s = foldl ma 0 s
  where ma v1 v2 = 10*v1 + (toInteger (digitToInt v2))

readAmt ip fp =
  let l = length fp
      e = 10^l
      iv = readInt ip
      fv = readInt fp
  in (e*iv + fv) % e

trimspace str = f (f str)
  where f = dropWhile isSpace . reverse

-- Hide parsec's spaces and substitute this one:
-- want "spaces" to mean an actual something there

spaces = many1 space

escChar =
  do char '\\'
     c <- anyChar
     return (case c of
             'a' -> chr 7
             'b' -> chr 8
             't' -> '\t'
             'n' -> '\n'
             'v' -> chr 11
             'f' -> chr 12
             'r' -> '\r'
             _ -> c)

-- The usual quoted-string with escaped quotes inside

parseString =
  do spaces
     char '"'
     str <- many (escChar <|> (noneOf "\""))
     char '"'
     return str

parseOptionalString = option "" (TPCP.try parseString)

-- TODO: are there some here that we need to add or remove?

symbol = oneOf "!$%&*+-/:<=>?@^_~"

parseName =
  do spaces
     first <- letter
     rest <- many (letter <|> digit <|> symbol)
     return (Name (first:rest))

parseOptionalName = option (Name "") (TPCP.try parseName)

parseInt = many1 digit >>= return . readInt

-- Parse first alternative for amounts: \d+(\.\d*)?

parseAmt1 =
  do ip <- many1 digit
     fp <- option "" (char '.' >> many digit)
     return (readAmt ip fp)

-- Parse second alternative for amounts: \.\d+

parseAmt2 =
  do char '.'
     fp <- many1 digit
     return (readAmt "0" fp)

-- Parse third alternative for amounts: \d+/\d+: this is how rationals
-- get displayed if they're not integers scaled by a power of 10, so
-- we'd better be able to handle them...

parseAmt3 =
  do n <- parseInt
     char '/'
     d <- parseInt
     return (n%d)

-- Parse a floating-point-formatted number as either \d+(\.\d*)? or \.\d+

parseAmount =
  do many space
     s <- option '+' (oneOf "+-")
     v <- (TPCP.try parseAmt3) <|>
           parseAmt1 <|>
           parseAmt2
     return (Amount (if s == '+' then v else negate v))

parseOptionalAmount = option (Amount 1) (TPCP.try parseAmount)

-- A date: three groups of digits separated by '/' or '-' (but not mixed)
-- where the first group of digits is the year, the second the month, and
-- the third the day: YYYY/MM/DD or YYYY-MM-DD: no messing around with
-- whether days come first or months, and also no implicit 19xx or 20xx
-- or whatever.

parseDate =
  do spaces
     y <- parseInt
     m <- pMS '/' <|> pMS '-'
     d <- parseInt
     return (Date (fromInteger y) (fromInteger m) (fromInteger d))
  where pMS s =
          do char s
             m <- parseInt
             char s
             return m

parseReconcile =
  option False (TPCP.try (many space >> char '*' >> return True))

-- The top-level record parsers

parseCIE =
  do rtype <- string "ccs" <|> string "income" <|> string "expense"
     name <- parseName
     desc <- parseOptionalString
     return (case rtype of
               "ccs" -> CCSRec name desc
               "income" -> IncomeRec name desc
               "expense" -> ExpenseRec name desc)

parseAccount =
  do string "account"
     name <- parseName
     date <- parseDate
     desc <- parseOptionalString
     return (AccountRec name date desc)

parseGroup =
  do string "group"
     names <- many1 parseName
     return (GroupRec (head names) (tail names))

parsePrice =
  do string "price"
     date <- parseDate
     amt1 <- parseOptionalAmount
     name1 <- parseName
     amt2 <- parseAmount
     name2 <- parseOptionalName
     return (PriceRec date False (CCSAmount name1 amt1) (CCSAmount name2 amt2))

parseXfer =
  do string "xfer"
     rec <- parseReconcile
     date <- parseDate
     acc1 <- parseName
     acc2 <- parseName
     amt <- parseAmount
     ccs <- parseOptionalName
     memo <- parseOptionalString
     chknum <- option 0 (TPCP.try (spaces >> parseInt))
     return (XferRec date rec acc1 acc2 (CCSAmount ccs amt)
                     memo (fromInteger chknum))

-- 'buy' and 'sell' are purely syntactic sugar for 'exch': 'buy' is
-- exactly the same, and 'sell' is the same as if {amt1,ccs1} and
-- {amt2,ccs2} were swapped in an exch

parseEBS =
  do rtype <- string "buy" <|> string "sell" <|> string "exch"
     rec <- parseReconcile
     date <- parseDate
     acct <- parseName
     amt1 <- parseAmount
     name1 <- parseName
     amt2 <- parseAmount
     name2 <- parseOptionalName
     memo <- parseOptionalString
     let et = case rtype of
                "buy" -> B
                "sell" -> S
                "exch" -> E
         ccsa1 = CCSAmount name1 amt1
         ccsa2 = CCSAmount name2 amt2
     if et == S
        then return (ExchRec et date rec acct ccsa2 ccsa1 memo)
        else return (ExchRec et date rec acct ccsa1 ccsa2 memo)

parseSplit =
  do string "split"
     date <- parseDate
     name <- parseName
     amt1 <- parseAmount
     amt2 <- parseAmount
     return (SplitRec date name amt1 amt2)

parseTodo =
  do string "todo"
     rec <- parseReconcile
     date <- parseDate
     spaces
     memo <- many anyChar
     return (ToDoRec date rec (trimspace memo))

parseComment =
  do many1 (char '#')
     many space
     comment <- many anyChar
     return (CommentRec (trimspace comment))

parseBlank = many space >> return (CommentRec "")

parseRecord =
  do many space
     record <- parsePrice
           <|> parseXfer
           <|> (TPCP.try parseCIE)
           <|> (TPCP.try parseEBS)
           <|> parseSplit
           <|> parseTodo
           <|> (TPCP.try parseAccount)
           <|> parseGroup
           <|> parseComment
           <|> parseBlank	-- this must be last, as it can match nothing
     many space
     eof
     return record

parseURecord input =
  case parse parseRecord "umm record" input of
       Left _ -> ErrorRec input
       Right val -> val

parseUDate input = parse parseDate "umm date" (" " ++ input)

parsePrefixOf n str =
  string (take n str) >> opts (drop n str) >> return str
  where opts [] = return ()
        opts (c:cs) = optional (char c >> opts cs)

parseCmdBalance now =
  do parsePrefixOf 3 "balance"
     name <- parseOptionalName
     date <- option now (TPCP.try parseDate)
     return (BalanceCmd name date)

parseCmdBasis now =
  do parsePrefixOf 3 "basis"
     name <- parseName
     date <- option now (TPCP.try parseDate)
     return (BasisCmd name date)

-- TODO: make verbose/nonverbose work... somehow add optional verbosity
-- ok, this is really cheesy! make it better!

parseCmdChange now =
  do parsePrefixOf 1 "change"
     name <- parseName
     date1 <- option now parseDate
     date2 <- option start_time parseDate
     if compare date2 date1 == LT
        then return (ChangeCmd True name date2 date1)
        else return (ChangeCmd False name date1 date2)

parseCmdList =
  do parsePrefixOf 1 "list"
     many space
     w <- option "all" (parsePrefixOf 1 "ccs" <|>
                        parsePrefixOf 1 "groups" <|>
                        parsePrefixOf 1 "incomes" <|>
                        parsePrefixOf 1 "expenses" <|>
                        TPCP.try (parsePrefixOf 2 "all") <|>
                        parsePrefixOf 2 "accounts")
     return (ListDataCmd (case w of
                            "all" -> COLAll
                            "ccs" -> COLCCS
                            "accounts" -> COLAccts
                            "groups" -> COLGrps
                            "incomes" -> COLIncs
                            "expenses" -> COLExps))

parseCmdPrice now =
  do parsePrefixOf 1 "price"
     name <- parseName
     date <- option now (TPCP.try parseDate)
     return (PriceCmd name date)

parseCmdReconcile now =
  do parsePrefixOf 3 "reconcile"
     name <- parseOptionalName
     date <- option now (TPCP.try parseDate)
     return (ReconcileCmd name date)

parseCmdRegister now =
  do parsePrefixOf 3 "register"
     name <- parseName
     date1 <- option now (TPCP.try parseDate)
     date2 <- option start_time (TPCP.try parseDate)
     if compare date2 date1 == LT
        then return (RegisterCmd name date2 date1)
        else return (RegisterCmd name date1 date2)

parseCmdToDo now =
  do parsePrefixOf 1 "todo"
     date <- option now (TPCP.try parseDate)
     return (ToDoCmd date)

parseCommand date =
  do cmd <- parseCmdChange date
        <|> parseCmdList
        <|> parseCmdPrice date
        <|> parseCmdToDo date
        <|> TPCP.try (parseCmdBalance date)
        <|> parseCmdBasis date
        <|> TPCP.try (parseCmdRegister date)
        <|> parseCmdReconcile date
     eof
     return cmd

parseUCommand now input =
  case parse (parseCommand now) "umm command" input of
       Left err -> error (show err)
       Right val -> val