packages feed

UMM-0.1.1: UMMData.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: UMMData.hs,v 1.30 2009/11/22 22:43:06 uwe Exp $ -}

module UMMData (Name(..), Date(..), Amount(..), start_time,
                Command(..), CmdOpt(..), Record(..), genDate,
                getRecDate, cmpRecDate, getRecName, cmpRecName,
                Ledger(..), runLedger, getResult, getInfo, getErrs,
                recordInfo, recordErr, recordNil, BSE(..),
                CCSAmt(..), cmpCCSAmountName, eqCCSAmountName,
                noName, todoName, joinDrop, isLeap, validDate) where
import Prelude
import Data.Char
import Data.List
import Data.Ratio
import System.Time

-- UMM values: names, dates, amounts, records

newtype Name = Name String deriving (Eq, Ord)
instance Show Name where
  show (Name name) = name

noName = Name ""
todoName = Name " todo "

data Date = Date Int Int Int deriving (Eq)

instance Show Date where
  show (Date y m d) = show y ++ "-" ++ show2 m ++ "-" ++ show2 d
    where show2 n = reverse (take 2 (reverse (show n) ++ "00"))

instance Ord Date where
  compare (Date y1 m1 d1) (Date y2 m2 d2) =
    let dy = compare y1 y2
        dm = compare m1 m2
    in if dy /= EQ then dy else if dm /= EQ then dm else compare d1 d2

genDate (CalendarTime {ctYear = y, ctMonth = m, ctDay = d}) =
  let m2 = case m of
             January   -> 1
             February  -> 2
             March     -> 3
             April     -> 4
             May       -> 5
             June      -> 6
             July      -> 7
             August    -> 8
             September -> 9
             October   -> 10
             November  -> 11
             December  -> 12
  in Date y m2 d

start_time = Date (-4003) 10 23		-- 9 AM (GMT)

newtype Amount = Amount Rational deriving (Eq, Ord)
instance Show Amount where
  show (Amount amt) = if amt < 0 then "-" ++ shRat (-amt) else shRat amt

shRat val =
  let n = numerator val
      d = denominator val
      (p2,r2) = divout d 2
      (p5,r5) = divout r2 5
      e10 = max p2 p5
      vs1 = show (numerator (val * 10^e10))
      l1 = max (length vs1) (1 + e10)
      vs2 = take l1 (reverse vs1 ++ cycle "0")
      vsf = reverse (take e10 vs2)
      vsi = reverse (drop e10 vs2)
  in if r5 == 1
        then if e10 == 0 then show n else vsi ++ "." ++ vsf
        else show n ++ "/" ++ show d

divout n q = doit 0 n
  where doit e v = if rem v q == 0 then doit (e + 1) (quot v q) else (e,v)

data CCSAmt = CCSAmount Name Amount
instance Show CCSAmt where
  show (CCSAmount name amount) = joinDrop [show amount, show name]

-- Compare two CCSAmounts by name

cmpCCSAmountName (CCSAmount n1 _) (CCSAmount n2 _) = compare n1 n2
eqCCSAmountName (CCSAmount n1 _) (CCSAmount n2 _) = n1 == n2

data CmdOpt = COLAll
            | COLCCS
            | COLAccts
            | COLGrps
            | COLIncs
            | COLExps
  deriving (Eq)

instance Show CmdOpt where
  show COLAll = "all"
  show COLCCS = "ccs"
  show COLAccts = "accounts"
  show COLGrps = "groups"
  show COLIncs = "incomes"
  show COLExps = "expenses"

data Command = ListDataCmd CmdOpt
             | ToDoCmd Date
             | BalanceCmd Name Date
             | BasisCmd Name Date
             | RegisterCmd Name Date Date
             | ReconcileCmd Name Date
             | PriceCmd Name Date
             | ChangeCmd Bool Name Date Date

instance Show Command where
  show (ListDataCmd opt) = joinDrop ["list", show opt]
  show (ToDoCmd date) = joinDrop ["todo", show date]
  show (PriceCmd name date) = joinDrop ["price", show name, show date]
  show (BalanceCmd name date) = joinDrop ["balance", show name, show date]
  show (BasisCmd name date) = joinDrop ["basis", show name, show date]
  show (RegisterCmd name date1 date2) =
    joinDrop ["register", show name, show date1, show date2]
  show (ReconcileCmd name date) = joinDrop ["reconcile", show name, show date]
  show (ChangeCmd verbose name date1 date2) =
    joinDrop ["change", (if verbose then "verbose" else ""),
              show name, show date1, show date2]

-- No, not bovine spongiform encephalitis! Disambiguate buy, sell, and
-- exch records: internally, they are all treated as exch, because
-- that makes the code simple, but the user can enter "buy" or "sell",
-- and it seems only polite to echo that back on output.

data BSE = B | S | E deriving (Eq)

instance Show BSE where
  show B = "buy"
  show S = "sell"
  show E = "exch"

data Record = CCSRec Name String
            | IncomeRec Name String
            | ExpenseRec Name String
            | AccountRec Name Date String
            | GroupRec Name [Name]
            | PriceRec Date Bool CCSAmt CCSAmt
            | XferRec Date Bool Name Name CCSAmt String Int
            | ExchRec BSE Date Bool Name CCSAmt CCSAmt String
            | SplitRec Date Name Amount Amount
            | CommentRec String
            | ToDoRec Date Bool String
            | ErrorRec String

instance Show Record where
  show = showR

-- We use "show str" here to add quotes and escape any escapables

optStr str = if str == "" then "" else show str

shRec rec = if rec then "*" else ""

joinDrop ss = intercalate " " (filter (\s -> s /= "") ss)

showR (CCSRec n d) = joinDrop ["ccs", show n, optStr d]
showR (IncomeRec n d) = joinDrop ["income", show n, optStr d]
showR (ExpenseRec n d) = joinDrop ["expense", show n, optStr d]
showR (AccountRec n da de) = joinDrop ["account", show n, show da, optStr de]
showR (GroupRec n as) = joinDrop (["group", show n] ++ map show as)
showR (PriceRec d imp c1 c2) =
  joinDrop ["price", shRec imp, show d, show c1, show c2]

showR (XferRec d r a1 a2 c memo chknum) =
  joinDrop ["xfer", shRec r, show d, show a1, show a2,
            show c, optStr memo, (if chknum == 0 then "" else show chknum)]

showR (ExchRec t d r a c1 c2 memo) =
  let hs = if t == S then [show c2, show c1] else [show c1, show c2]
  in joinDrop ([show t, shRec r, show d, show a] ++ hs ++ [optStr memo])

showR (SplitRec d n a1 a2) =
  joinDrop ["split", show d, show n, show a1, show a2]

showR (CommentRec c) = if c == "" then "" else joinDrop ["#", c]

showR (ToDoRec d r memo) =
  joinDrop ["todo", shRec r, show d,
            (if memo == "" then "something! but what?" else memo)]

showR (ErrorRec str) = joinDrop ["#err", str]

-- Get the date (or at any rate /some/ date) from a Record

getRecDate (AccountRec _ d _) =      d
getRecDate (PriceRec d _ _ _) =      d
getRecDate (XferRec d _ _ _ _ _ _) = d
getRecDate (ExchRec _ d _ _ _ _ _) = d
getRecDate (SplitRec d _ _ _) =      d
getRecDate (ToDoRec d _ _) =         d
getRecDate _ = start_time	-- so it works for every Record

-- Get the name (or at any rate /some/ name) from a Record

getRecName (CCSRec n _) =       n
getRecName (IncomeRec n _) =    n
getRecName (ExpenseRec n _) =   n
getRecName (AccountRec n _ _) = n
getRecName (GroupRec n _) =     n
getRecName _ = Name " nil "	-- so it works for every Record

-- Compare two Records by date

cmpRecDate v1 v2 = compare (getRecDate v1) (getRecDate v2)

-- Compare two Records by name

cmpRecName v1 v2 = compare (getRecName v1) (getRecName v2)

-- Ledger monad: essentially a parametrized bi-level version of the Logger
-- monad from RWH. The first component of the tuple is "the final result",
-- whatever that may be, the second component is a list of ordinary bits
-- of information, and the third component is a list of extraordinary bits
-- of information, aka errors.

newtype Ledger e i r = Ledger (r, [i], [e]) deriving (Show)

runLedger (Ledger a) = a
getResult (Ledger (r,_,_)) = r
getInfo   (Ledger (_,i,_)) = i
getErrs   (Ledger (_,_,e)) = e

-- Record a message

recordInfo i = Ledger ((), [i], [])

-- Record an error

recordErr e = Ledger ((), [], [e])

-- Record nothing: a placeholder for conditionals:
-- if someCond then recordErr "someErr" else recordNil

recordNil = Ledger ((), [], [])

-- Note that because we use append (++) for lists here, this is fairly
-- low-performance: it's entirely ok for this application, where we
-- toss around a few to a few thousand list entries, but for longer
-- lists it might not be suitable. <shrug>

instance Monad (Ledger e i) where
  return a = Ledger (a, [], [])
  (>>=) m k =
    let (a,li1,le1) = runLedger m
        n = k a
        (b,li2,le2) = runLedger n
    in Ledger (b, li1 ++ li2, le1 ++ le2)
  (>>) a f = a >>= \_ -> f

-- Some miscellany

-- divisible by 4 except if divisible by 100 except if divisible by 400
isLeap y = rem y 4 == 0 && (rem y 100 /= 0 || rem y 400 == 0)

validDate (Date y m d) =
  let lim = [ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ] !! m
  in (m >= 1 && m <= 12 && d >= 1 &&
       (d <= lim || m == 2 && isLeap y && d <= 29))