1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE ScopedTypeVariables #-}
5 module Hcompta.Format.Ledger.Read where
7 import Control.Applicative ((<*), (<$>))
8 import qualified Control.Exception as Exn
9 import Control.Monad (guard)
10 -- import Control.Monad.Error
11 import qualified Data.Char
13 import qualified Data.Decimal
14 import qualified Data.List
15 -- import Data.List.Split (wordsBy)
16 import qualified Data.Map
18 import qualified Data.Time.Calendar as Time
19 import qualified Data.Time.Clock as Time
20 import qualified Data.Time.LocalTime as Time
21 import Data.Typeable ()
22 import Safe (headDef, lastDef)
23 import qualified Text.Parsec as P
24 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
27 import qualified Hcompta.Model as Model
28 import qualified Hcompta.Model.Account as Account
29 import Hcompta.Model.Account (Account)
30 import qualified Hcompta.Model.Amount as Amount
31 import Hcompta.Model.Amount (Amount, Conversion, Style, Unit)
32 import qualified Hcompta.Model.Amount.Conversion as Conversion
33 import qualified Hcompta.Model.Amount.Quantity as Quantity
34 import qualified Hcompta.Model.Amount.Style as Style
35 import qualified Hcompta.Model.Amount.Unit as Unit
36 import qualified Hcompta.Model.Date as Date
37 import Hcompta.Format.Ledger.Journal as Journal
41 { account_prefix :: !Account
42 --, context_aliases :: ![AccountAlias]
43 , unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
46 } deriving (Data, Eq, Read, Show, Typeable)
52 , unit_and_style = Nothing
53 , journal = Journal.nil
54 , year = (\(year, _ , _) -> year) $
55 Time.toGregorian $ Time.utctDay $
56 Journal.last_read_time Journal.nil
63 -- | Like 'Text.Parsec.choice' but with 'Text.Parsec.try' on each case.
64 choice_try :: Stream s m t => [ParsecT s st m a] -> ParsecT s st m a
65 choice_try = P.choice . Data.List.map P.try
69 -- | Return the 'Integer' obtained by multiplying the given digits
70 -- with the power of the given base respective to their rank.
73 -> [Char] -- ^ Digits (MUST be recognised by 'Data.Char.digitToInt').
75 integer_of_digits base =
76 Data.List.foldl (\x d ->
77 base*x + toInteger (Data.Char.digitToInt d)) 0
79 decimal :: Stream [Char] m Char => ParsecT [Char] st m Integer
80 decimal = integer 10 P.digit
81 hexadecimal :: Stream [Char] m Char => ParsecT [Char] st m Integer
82 hexadecimal = P.oneOf "xX" >> integer 16 P.hexDigit
83 octal :: Stream [Char] m Char => ParsecT [Char] st m Integer
84 octal = P.oneOf "oO" >> integer 8 P.octDigit
86 -- | Parse an 'Integer'.
87 integer :: Stream [Char] m Char
88 => Integer -> ParsecT [Char] st m Char
89 -> ParsecT [Char] st m Integer
90 integer base digit = do
91 digits <- P.many1 digit
92 let n = integer_of_digits base digits
95 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
96 sign :: (Stream [Char] m Char, Num i) => ParsecT [Char] st m (i -> i)
98 (P.char '-' >> return negate) <|>
99 (P.char '+' >> return id) <|>
104 -- | Return 'True' if and only if the given 'Char' is an horizontal space.
105 is_space :: Char -> Bool
106 is_space c = c /= '\n' && c /= '\r' && Data.Char.isSpace c
108 space :: Stream [Char] m Char => ParsecT [Char] st m Char
109 space = P.satisfy is_space
111 -- * Parsing 'Account'.
113 -- | Parse an 'Account'.
114 account :: Stream [Char] m Char => ParsecT [Char] st m Account
116 P.notFollowedBy $ P.satisfy is_space
117 P.sepBy1 account_name account_separator
119 -- | Parse an Account.'Account.Name'.
120 account_name :: Stream [Char] m Char => ParsecT [Char] st m Account.Name
124 <* (P.lookAhead $ P.satisfy (not . Data.Char.isSpace))
125 <|> (P.notFollowedBy account_separator >> P.anyChar)
127 account_separator :: Stream [Char] m Char => ParsecT [Char] st m Char
128 account_separator = P.char ':'
130 -- * Parsing 'Amount'.
132 -- | Parse an 'Amount'.
133 amount :: Stream [Char] m Char => ParsecT [Char] st m Amount
137 P.option Nothing $ do
139 s <- P.many $ P.satisfy is_space
140 return $ Just $ (u, not $ null s)
141 (quantity_, style) <- do
148 , grouping_fractional
151 [ quantity '_' ',' '_' <* (P.notFollowedBy $ P.oneOf ",._")
152 , quantity '_' '.' '_' <* (P.notFollowedBy $ P.oneOf ",._")
153 , quantity ',' '.' '_' <* (P.notFollowedBy $ P.oneOf ",._")
154 , quantity '.' ',' '_' <* (P.notFollowedBy $ P.oneOf ",._")
156 let int = Data.List.concat integral
157 let frac_flat = Data.List.concat fractional
158 let frac = reverse $ Data.List.dropWhile (== '0') $ reverse $ frac_flat
159 let place = length frac
161 let mantissa = integer_of_digits 10 $ int ++ frac
163 ( Data.Decimal.Decimal
167 { Style.fractioning = fractioning
168 , Style.grouping_integral = grouping_integral
169 , Style.grouping_fractional = grouping_fractional
170 , Style.precision = fromIntegral $ length frac_flat
173 (unit_, side, spaced) <-
176 return (u, Just Style.Side_Left, Just s)
178 P.option (Unit.nil, Nothing, Nothing) $ do
179 s <- P.many $ P.satisfy is_space
181 return $ (u, Just Style.Side_Right, Just $ not $ null s)
184 { Amount.conversion = Conversion.nil -- TODO
185 , Amount.quantity = left_signing $ quantity_
186 , Amount.style = style
187 { Style.unit_side = side
188 , Style.unit_spaced = spaced
190 , Amount.unit = unit_
195 { integral :: [String]
196 , fractional :: [String]
197 , fractioning :: Maybe Style.Fractioning
198 , grouping_integral :: Maybe Style.Grouping
199 , grouping_fractional :: Maybe Style.Grouping
202 -- | Parse a 'Quantity'.
204 :: Stream [Char] m Char
205 => Char -- ^ Integral grouping separator.
206 -> Char -- ^ Fractioning separator.
207 -> Char -- ^ Fractional grouping separator.
208 -> ParsecT [Char] st m Quantity
209 quantity int_group_sep frac_sep frac_group_sep = do
210 (integral, grouping_integral) <- do
213 [] -> return ([], Nothing)
215 t <- P.many $ P.char int_group_sep >> P.many1 P.digit
217 return (digits, grouping_of_digits int_group_sep digits)
218 (fractional, fractioning, grouping_fractional) <-
221 _ -> P.option ([], Nothing, Nothing)) $ do
222 fractioning <- P.char frac_sep
224 t <- P.many $ P.char frac_group_sep >> P.many1 P.digit
226 return (digits, Just fractioning
227 , grouping_of_digits frac_group_sep $ reverse digits)
234 , grouping_fractional
237 grouping_of_digits :: Char -> [String] -> Maybe Style.Grouping
238 grouping_of_digits group_sep digits =
243 Style.Grouping group_sep $
244 canonicalize_grouping $
246 canonicalize_grouping :: [Int] -> [Int]
247 canonicalize_grouping groups =
248 Data.List.foldl -- NOTE: remove duplicates at begining and reverse.
249 (\acc l0 -> case acc of
250 l1:_ -> if l0 == l1 then acc else l0:acc
252 case groups of -- NOTE: keep only longer at begining.
253 l0:l1:t -> if l0 > l1 then groups else l1:t
256 -- | Parse an 'Unit'.
257 unit :: Stream [Char] m Char => ParsecT [Char] st m Unit
259 (quoted <|> unquoted) <?> "unit"
261 unquoted :: Stream [Char] m Char => ParsecT [Char] st m Unit
265 case Data.Char.generalCategory c of
266 Data.Char.CurrencySymbol -> True
267 Data.Char.LowercaseLetter -> True
268 Data.Char.ModifierLetter -> True
269 Data.Char.OtherLetter -> True
270 Data.Char.TitlecaseLetter -> True
271 Data.Char.UppercaseLetter -> True
273 quoted :: Stream [Char] m Char => ParsecT [Char] st m Unit
275 P.between (P.char '"') (P.char '"') $