1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE AllowAmbiguousTypes #-}
3 {-# LANGUAGE DeriveFunctor #-}
5 {-# LANGUAGE StandaloneDeriving #-}
6 {-# LANGUAGE UndecidableInstances #-}
7 {-# OPTIONS_GHC -fno-warn-orphans #-}
8 -- {-# OPTIONS_GHC -freduction-depth=0 #-}
9 module Hcompta.LCC.Read where
11 -- import Data.Functor.Identity (Identity(..))
12 -- import Data.String (IsString(..))
13 -- import Data.String (String, fromString)
15 -- import qualified Control.Monad.Classes as MC
16 import Control.Arrow (left)
17 import Control.Applicative (Applicative(..), liftA2)
18 import Control.Monad (Monad(..), void)
19 import Control.Monad.IO.Class (MonadIO(..))
21 import Data.Char (Char)
23 import Data.Eq (Eq(..))
25 import Data.Functor.Compose (Compose(..))
26 import Data.List.NonEmpty (NonEmpty(..), (<|))
27 import Data.Map.Strict (Map)
28 import Data.Maybe (Maybe(..), maybe, isJust)
29 import Data.Monoid (Monoid(..))
30 import Data.NonNull (NonNull)
31 import Data.Ord (Ord(..))
32 import Data.Semigroup (Semigroup(..))
33 import Data.Text (Text)
34 import Data.Time.LocalTime (TimeZone(..))
35 import Data.Typeable ()
36 import Language.Symantic.Parsing hiding (LR(..), At(..))
37 import Prelude (Int, Integer, Num(..), Integral(..), fromIntegral)
38 import Prelude hiding (any, (^), exp, read)
39 import System.FilePath ((</>))
40 import Text.Megaparsec.Pos (SourcePos)
41 import Text.Show (Show)
42 import qualified Control.Applicative as Alt
43 import qualified Control.Exception.Safe as Exn
44 import qualified Control.Monad.Classes as MC
45 -- import qualified Control.Monad.Classes.Run as MC
46 import qualified Control.Monad.Trans.State.Strict as SS
47 import qualified Data.ByteString as BS
48 import qualified Data.Char as Char
49 import qualified Data.List as List
50 import qualified Data.List.NonEmpty as NonEmpty
51 import qualified Data.Map.Strict as Map
52 import qualified Data.NonNull as NonNull
53 import qualified Data.Strict as S
54 import qualified Data.Text as Text
55 import qualified Data.Text.Encoding as Enc
56 import qualified Data.Time.Calendar as Time
57 import qualified Data.Time.LocalTime as Time
58 import qualified Data.TreeMap.Strict as TreeMap
59 import qualified Hcompta as H
60 import qualified Language.Symantic as Sym
61 -- import qualified Language.Symantic.Lib as Sym
62 import qualified Language.Symantic.Grammar as Gram
63 import qualified System.Directory as IO
64 import qualified System.FilePath as FilePath
65 import qualified Text.Megaparsec as P
66 import qualified Text.Megaparsec.Prim as P
68 import Hcompta.LCC.Account
69 import Hcompta.LCC.Name
70 import Hcompta.LCC.Tag
71 import Hcompta.LCC.Amount
72 import Hcompta.LCC.Chart
73 import Hcompta.LCC.Posting
74 import Hcompta.LCC.Transaction
75 import Hcompta.LCC.Journal
77 import qualified Hcompta.LCC.Lib.Strict as S
79 -- * Type 'Gram_Reader'
80 class Gram_Reader ctx g where
81 g_ask :: g (ctx -> a) -> g a
82 g_ask_before :: g (ctx -> a) -> g a
83 deriving instance Gram_Reader ctx g => Gram_Reader ctx (CF g)
86 , MC.MonadReader ctx (P.ParsecT e s m)
87 ) => Gram_Reader ctx (P.ParsecT e s m) where
97 -- * Type 'Gram_State'
98 class Gram_State st g where
99 g_get :: g (st -> a) -> g a
100 g_state :: g (st -> (st, a)) -> g a
101 g_put :: g (st, a) -> g a
102 deriving instance Gram_State st g => Gram_State st (CF g)
105 , MC.MonadState st (P.ParsecT e s m)
106 ) => Gram_State st (P.ParsecT e s m) where
125 { atBegin :: !(NonEmpty SourcePos)
126 , atEnd :: !SourcePos
128 } deriving (Eq, Functor, Ord, Show)
131 class Gram_At g where
132 g_at :: g ((err -> At err) -> a) -> g a
133 deriving instance Gram_At g => Gram_At (CF g)
134 instance ParsecC e s => Gram_At (P.ParsecT e s m) where
136 ps <- P.statePos <$> P.getParserState
139 return $ fa (At ps p)
141 nonEmpty :: NonNull [a] -> NonEmpty a
142 nonEmpty n = x :| xs where (x, xs) = NonNull.splitFirst n
143 nonNull :: NonEmpty a -> NonNull [a]
144 nonNull n = NonNull.ncons x xs where x :| xs = n
146 instance Monad m => MC.MonadReaderN 'MC.Zero
148 (S.StateT (Context_Read j) m) where
149 askN _px = MC.gets $ \(x::Context_Read j) -> context_read_canonfiles x
152 type ParsecC e s = (P.Token s ~ Char, P.Stream s, P.ErrorComponent e)
153 type instance MC.CanDo (P.ParsecT e s m) (MC.EffState a) = 'False
154 type instance MC.CanDo (P.ParsecT e s m) (MC.EffReader P.SourcePos) = 'True
155 type instance MC.CanDo (P.ParsecT e s m) (MC.EffReader (S.Either Exn.IOException CanonFile)) = 'True
157 instance ParsecC e s => MC.MonadReaderN 'MC.Zero P.SourcePos (P.ParsecT e s m) where
158 askN _px = P.getPosition
159 instance ParsecC e s => MC.MonadReaderN 'MC.Zero (NonEmpty P.SourcePos) (P.ParsecT e s m) where
160 askN _px = P.statePos <$> P.getParserState
161 instance (ParsecC e s, MonadIO m) => MC.MonadReaderN 'MC.Zero
162 (S.Either Exn.IOException CanonFile)
163 (P.ParsecT e s m) where
165 sn <- P.sourceName <$> P.getPosition
167 (S.Right . CanonFile . PathFile <$> IO.canonicalizePath sn)
168 (\exn -> return $ S.Left exn)
169 -- instance ParsecC e s => IsString (P.ParsecT e s m [Char]) where
170 -- fromString = P.string
171 instance ParsecC e s => Gram_Rule (P.ParsecT e s m) where
172 rule = P.label . Text.unpack
173 instance ParsecC e s => Gram_Terminal (P.ParsecT e s m) where
178 unicat cat = P.satisfy $ (`elem` cats) . Char.generalCategory
179 where cats = unicode_categories cat
180 range (l, h) = P.satisfy $ \c -> l <= c && c <= h
181 but (Terminal f) (Terminal p) = Terminal $ P.notFollowedBy (P.try p) *> f
182 instance ParsecC e s => Alter (P.ParsecT e s m) where
186 -- choice = foldr ((Alt.<|>) . P.try) Alt.empty
187 -- choice = foldr (Alt.<|>) Alt.empty
188 instance ParsecC e s => Try (P.ParsecT e s m) where
190 instance ParsecC e s => Gram_RegR (P.ParsecT e s m) where
191 Terminal f .*> Reg x = Reg $ f <*> x
192 instance ParsecC e s => Gram_RegL (P.ParsecT e s m) where
193 Reg f <*. Terminal x = Reg $ f <*> x
194 instance ParsecC e s => App (P.ParsecT e s m)
195 instance ParsecC e s => Alt (P.ParsecT e s m) where
199 optional g = P.option Nothing (Just <$> g)
200 instance ParsecC e s => Gram_CF (P.ParsecT e s m) where
201 CF f <& Reg p = CF $ P.lookAhead f <*> p
202 Reg f &> CF p = CF $ P.lookAhead f <*> p
203 CF f `minus` Reg p = CF $ P.notFollowedBy (P.try p) *> f
204 instance ParsecC e s => Gram_Meta P.SourcePos (P.ParsecT e s m) where
208 instance ParsecC e s => Gram_Lexer (P.ParsecT e s m)
209 instance ParsecC e s => Gram_Count (P.ParsecT e s m)
210 instance ParsecC e s => Gram_Op (P.ParsecT e s m)
211 instance ParsecC e s => Gram_Char (P.ParsecT e s m)
212 instance ParsecC e s => Gram_Comment (P.ParsecT e s m)
213 instance ParsecC e s => Gram_Tag (P.ParsecT e s m)
214 instance ParsecC e s => Gram_Account (P.ParsecT e s m)
215 instance ParsecC e s => Gram_Amount (P.ParsecT e s m)
216 instance ParsecC e s => Gram_File (P.ParsecT e s m)
217 instance -- Gram_Date
219 , MC.MonadState Year (P.ParsecT e s m)
220 ) => Gram_Date (P.ParsecT e s m) where
221 instance -- Gram_Posting
223 , MC.MonadState Chart (P.ParsecT e s m)
224 , MC.MonadState Style_Amounts (P.ParsecT e s m)
225 , MC.MonadState (S.Maybe Unit) (P.ParsecT e s m)
226 ) => Gram_Posting (P.ParsecT e s m)
227 instance -- Gram_Transaction
229 , MC.MonadState (S.Maybe Unit) (P.ParsecT e s m)
230 , MC.MonadState Chart (P.ParsecT e s m)
231 , MC.MonadState Section (P.ParsecT e s m)
232 , MC.MonadState Style_Amounts (P.ParsecT e s m)
233 , MC.MonadState Year (P.ParsecT e s m)
234 ) => Gram_Transaction (P.ParsecT e s m)
235 instance -- Gram_Chart
237 , MC.MonadState Chart (P.ParsecT e s m)
238 , MC.MonadState Section (P.ParsecT e s m)
239 ) => Gram_Chart (P.ParsecT e s m)
242 , MC.MonadState (S.Maybe Unit) (P.ParsecT e s m)
243 , MC.MonadState Chart (P.ParsecT e s m)
244 , MC.MonadState Style_Amounts (P.ParsecT e s m)
245 , MC.MonadState Year (P.ParsecT e s m)
246 , P.MonadParsec e Text (P.ParsecT e s m)
248 ) => Gram_IO (P.ParsecT e s m) where
250 pf@(PathFile fp) <- g
251 liftIO $ (pf,) <$> Exn.catch
252 (Right . CanonFile . PathFile <$> IO.canonicalizePath fp)
258 S.Left e -> return $ \at -> S.Left $ at e
259 S.Right (PathFile fp) ->
261 (const . S.Right . (fp,) . Enc.decodeUtf8 <$> BS.readFile fp)
262 (\exn -> return $ \at -> S.Left $ at $ Error_Journal_Read (PathFile fp) exn)
266 S.Right (fp_new, s_new) -> do
267 P.pushPosition $ P.initialPos fp_new
268 s_old <- P.getInput; P.setInput s_new
272 P.observing g >>= \case
277 P.updateParserState (\st -> st{P.statePos=P.errorPos $ trace ("ERROR: " <> show err) err})
279 (P.errorUnexpected err)
280 (P.errorExpected err)
289 instance -- Gram_Journal
291 , Sym.Gram_Term_AtomsR Meta is is (P.ParsecT e s m)
292 , MC.MonadReader (S.Either Exn.IOException CanonFile) (P.ParsecT e s m)
293 , MC.MonadState (Context_Read j) (P.ParsecT e s m)
294 , MC.MonadState (Journal j) (P.ParsecT e s m)
295 , MC.MonadState (Journals j) (P.ParsecT e s m)
296 , MC.MonadState (S.Maybe Unit) (P.ParsecT e s m)
297 , MC.MonadState Chart (P.ParsecT e s m)
298 , MC.MonadState Section (P.ParsecT e s m)
299 , MC.MonadState Style_Amounts (P.ParsecT e s m)
300 , MC.MonadState Year (P.ParsecT e s m)
301 , MC.MonadState (Env cs is) m
302 , MC.MonadState (Sym.Tokenizers Meta is) m
303 , P.MonadParsec e Text (P.ParsecT e s m)
307 , Sym.Inj_Token Meta is (->)
309 ) => Gram_Journal cs is j (P.ParsecT e s m) where
310 instance -- Gram_Term
312 , Gram.Gram_Meta meta (P.ParsecT e s m)
313 , MC.MonadState (Sym.Tokenizers meta ts) (P.ParsecT e s m)
314 , Sym.Gram_Term_AtomsR meta ts ts (P.ParsecT e s m)
315 ) => Sym.Gram_Term ts meta (P.ParsecT e s m) where
316 term_tokenizers (Gram.CF mf) = Gram.CF $ mf >>= MC.gets
317 g_term_abst_args_body (Gram.CF args) (Gram.CF body) = Gram.CF $ do
320 toks :: Sym.Tokenizers meta ts <- MC.get
323 { Sym.tokenizers_prefix = del (Sym.tokenizers_prefix toks) as
324 , Sym.tokenizers_infix = del (Sym.tokenizers_infix toks) as
325 , Sym.tokenizers_postfix = del (Sym.tokenizers_postfix toks) as
329 where del = foldr $ \(n, _) -> Map.adjust (Map.delete n) []
330 instance -- Gram_Error
332 Sym.Gram_Error (P.ParsecT e s m) where
333 term_unError (Gram.CF me) = Gram.CF $ do
336 Left err -> fail $ show err
338 instance -- Gram_Meta
340 Sym.Gram_Meta Meta (P.ParsecT e s m) where
342 instance -- Gram_Type
344 , Gram_Meta meta (P.ParsecT e s m)
345 ) => Sym.Gram_Type meta (P.ParsecT e s m)
346 instance -- Gram_Name
348 Sym.Gram_Name (P.ParsecT e s m)
349 instance -- Gram_Term_Type
351 , Gram.Gram_Meta meta (P.ParsecT e s m)
352 ) => Sym.Gram_Term_Type meta (P.ParsecT e s m)
354 -- * Type 'Context_Read'
357 { context_read_year :: !Year
358 , context_read_style_amounts :: !Style_Amounts
359 , context_read_chart :: !Chart
360 , context_read_unit :: !(S.Maybe Unit)
361 , context_read_journals :: !(Journals j)
362 , context_read_journal :: !(NonEmpty (Journal j))
363 , context_read_canonfiles :: !(NonEmpty CanonFile)
364 , context_read_warnings :: ![At Warning_Journal]
365 , context_read_section :: !Section
366 } deriving (Eq, Show)
368 type instance MC.CanDo (S.StateT (Context_Read j) m)
369 (MC.EffState (Sym.Tokenizers Meta is)) = 'False
371 context_read :: Monoid j => Context_Read j
374 { context_read_year = Year $ H.date_year (H.date_epoch::Date)
375 , context_read_style_amounts = mempty
376 , context_read_chart = mempty
377 , context_read_unit = S.Nothing
378 , context_read_journals = Journals Map.empty
379 , context_read_journal = journal :| []
380 , context_read_canonfiles = CanonFile "" :| []
381 , context_read_warnings = []
382 , context_read_section = Section_Chart
385 -- * Type 'Context_Sym'
386 data Context_Sym cs is
388 { context_sym_tokenizers :: !(Sym.Tokenizers Meta is)
389 , context_sym_env :: !(Env cs is)
394 :: Sym.Tokenize Meta is
398 { context_sym_tokenizers = Sym.tokenizers
399 , context_sym_env = Map.empty
403 type instance MC.CanDo (S.StateT (Context_Sym cs is) m)
404 (MC.EffState (Sym.Tokenizers Meta is)) = 'True
405 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Tokenizers Meta is) (S.StateT (Context_Sym cs is) m) where
406 stateN _px f = S.StateT $ SS.state $ \ctx ->
407 (\a -> ctx{context_sym_tokenizers = a})
408 <$> f (context_sym_tokenizers ctx)
411 type instance MC.CanDo (S.StateT (Context_Read j) m)
412 (MC.EffState (Env cs is)) = 'False
413 type instance MC.CanDo (S.StateT (Context_Sym cs is) m) (MC.EffState (Env cs is)) = 'True
414 instance Monad m => MC.MonadStateN 'MC.Zero (Env cs is) (S.StateT (Context_Sym cs is) m) where
415 stateN _px f = S.StateT $ SS.state $ \ctx ->
416 (\a -> ctx{context_sym_env = a})
417 <$> f (context_sym_env ctx)
420 type instance MC.CanDo (S.StateT (Context_Read j) m)
421 (MC.EffState (Context_Read j)) = 'True
422 instance Monad m => MC.MonadStateN 'MC.Zero (Context_Read j) (S.StateT (Context_Read j) m) where
423 stateN _px = S.StateT . SS.state
425 type instance MC.CanDo (S.StateT (Context_Read j) m)
426 (MC.EffState (S.Maybe Unit)) = 'True
427 instance Monad m => MC.MonadStateN 'MC.Zero (S.Maybe Unit) (S.StateT (Context_Read j) m) where
428 stateN _px f = S.StateT $ SS.state $ \ctx ->
429 (\a -> ctx{context_read_unit = a})
430 <$> f (context_read_unit ctx)
432 type instance MC.CanDo (S.StateT (Context_Read j) m)
433 (MC.EffState Chart) = 'True
434 instance Monad m => MC.MonadStateN 'MC.Zero Chart (S.StateT (Context_Read j) m) where
435 stateN _px f = S.StateT $ SS.state $ \ctx ->
436 (\a -> ctx{context_read_chart = a})
437 <$> f (context_read_chart ctx)
439 newtype Year = Year (H.Date_Year Date)
441 type instance MC.CanDo (S.StateT (Context_Read j) m)
442 (MC.EffState Year) = 'True
443 instance Monad m => MC.MonadStateN 'MC.Zero Year (S.StateT (Context_Read j) m) where
444 stateN _px f = S.StateT $ SS.state $ \ctx ->
445 (\a -> ctx{context_read_year = a})
446 <$> f (context_read_year ctx)
450 | Section_Transaction
452 type instance MC.CanDo (S.StateT (Context_Read j) m)
453 (MC.EffState Section) = 'True
454 instance Monad m => MC.MonadStateN 'MC.Zero Section (S.StateT (Context_Read j) m) where
455 stateN _px f = S.StateT $ SS.state $ \ctx ->
456 (\a -> ctx{context_read_section = a})
457 <$> f (context_read_section ctx)
459 type instance MC.CanDo (S.StateT (Context_Read j) m)
460 (MC.EffState (Journals j)) = 'True
461 instance Monad m => MC.MonadStateN 'MC.Zero (Journals j) (S.StateT (Context_Read j) m) where
462 stateN _px f = S.StateT $ SS.state $ \ctx ->
463 (\a -> ctx{context_read_journals = a})
464 <$> f (context_read_journals ctx)
466 type instance MC.CanDo (S.StateT (Context_Read j) m)
467 (MC.EffState (Journal j)) = 'True
468 instance Monad m => MC.MonadStateN 'MC.Zero (Journal j) (S.StateT (Context_Read j) m) where
469 stateN _px f = S.StateT $ SS.state $ \ctx@Context_Read{context_read_journal = j:|js} ->
470 (\a -> ctx{context_read_journal = a:|js}) <$> f j
472 type instance MC.CanDo (S.StateT (Context_Read j) m)
473 (MC.EffState Style_Amounts) = 'True
474 instance Monad m => MC.MonadStateN 'MC.Zero Style_Amounts (S.StateT (Context_Read j) m) where
475 stateN _px f = S.StateT $ SS.state $ \ctx ->
476 (\s -> ctx{context_read_style_amounts = s})
477 <$> f (context_read_style_amounts ctx)
479 -- * Type 'Error_Date'
481 = Error_Date_Day_invalid (Integer, Int, Int)
482 | Error_Date_TimeOfDay_invalid (Int, Int, Int)
483 | Error_Date_TimeZone_unknown Text
485 -- * Type 'Error_Posting'
487 = Error_Posting_Account_Ref_unknown Tag_Path
488 | Error_Posting_Account_Ref_multiple Tag_Path (Map Account ())
489 | Error_Postings_not_equilibrated Postings
491 -- * Type 'Error_Transaction'
492 data Error_Transaction
493 = Error_Transaction_Date Error_Date
494 | Error_Transaction_Posting Error_Posting
495 | Error_Transaction_not_equilibrated
498 , H.SumByUnit (NonNull [Account_Section]) (H.Polarized Quantity)
501 -- * Type 'Error_Chart'
505 -- * Type 'Error_Journal'
506 data Error_Journal cs is
507 = Error_Journal_Transaction Error_Transaction
508 | Error_Journal_Read PathFile Exn.IOException
509 | Error_Journal_Include_loop CanonFile
510 | Error_Journal_Chart Error_Chart
511 | Error_Journal_Section Section Section
512 | Error_Journal_Term (Sym.Error_Term Meta cs is)
513 deriving instance Eq_Token Meta is => Eq (Error_Journal cs is)
514 deriving instance -- Show
516 , Sym.Show_TyConst cs
517 ) => Show (Error_Journal cs is)
518 -- * Type 'Warning_Journal'
520 = Warning_Journal_Include_multiple CanonFile
524 class Gram_IO g where
527 -> g (PathFile, Either Exn.IOException CanonFile)
529 :: g (S.Either (Error_Journal cs is) PathFile)
530 -> g (S.Either [At (Error_Journal cs is)] (CanonFile, a))
531 -> g (S.Either [At (Error_Journal cs is)] (CanonFile, a))
532 deriving instance Gram_IO g => Gram_IO (CF g)
534 -- * Class 'Gram_Count'
538 ) => Gram_Count g where
539 count :: Int -> CF g a -> CF g [a]
542 | otherwise = sequenceA $ List.replicate n p
543 count' :: Int -> Int -> CF g a -> CF g [a]
545 | n <= 0 || m > n = pure []
546 | m > 0 = (:) <$> p <*> count' (pred m) (pred n) p
548 let f t ts = maybe [] (:ts) t
549 in f <$> optional p <*> count' 0 (pred n) p
551 -- * Class 'Gram_Char'
555 ) => Gram_Char g where
557 g_eol = rule "EOL" $ void (char '\n') <+> void (string "\r\n")
559 g_tab = rule "Tab" $ void $ char '\t'
561 g_space = rule "Space" $ char ' '
562 g_spaces :: CF g Text
563 g_spaces = Text.pack <$> many g_space
565 g_spaces1 = void $ some g_space
567 g_char = g_char_passive <+> g_char_active
568 g_char_passive :: CF g Char
569 g_char_passive = choice $ unicat <$> [Unicat_Letter, Unicat_Number, Unicat_Mark]
570 g_char_active :: CF g Char
571 g_char_active = choice $ unicat <$> [Unicat_Punctuation, Unicat_Symbol]
572 g_char_attribute :: Reg lr g Char
573 g_char_attribute = choice $ char <$> "#/:;@~="
575 g_word = rule "Word" $ Text.pack <$> some g_char
577 g_words = Text.concat <$> many (try $ (<>) <$> g_spaces <*> g_word)
579 g_09 = range ('0', '9')
581 g_19 = range ('1', '9')
582 g_sign :: Num int => CF g (int -> int)
584 (negate <$ char '-') <+>
587 -- * Class 'Gram_Date'
594 ) => Gram_Date g where
595 g_date :: CF g (S.Either (At Error_Date) Date)
596 g_date = rule "Date" $
597 (liftA2 $ \day (tod, tz) ->
598 Time.localTimeToUTC tz $
599 Time.LocalTime day tod)
602 (S.Right (Time.midnight, Time.utc))
606 <*> option (S.Right Time.utc) g_timezone)
607 g_ymd :: CF g (S.Either (At Error_Date) Time.Day)
618 <$> g_get (pure $ \(Year y) -> y)
624 case Time.fromGregorianValid y m d of
625 Nothing -> S.Left $ at $ Error_Date_Day_invalid (y, m, d)
626 Just day -> S.Right day
627 g_tod :: CF g (S.Either (At Error_Date) Time.TimeOfDay)
628 g_tod = rule "TimeOfDay" $
631 case Time.makeTimeOfDayValid hr mn (fromInteger $ toInteger sc) of
632 Nothing -> S.Left $ at $ Error_Date_TimeOfDay_invalid (hr, mn, sc)
633 Just tod -> S.Right $ tod)
637 <$> (char char_tod_sep *> g_minute)
638 <*> option 0 (char char_tod_sep *> g_second))
639 g_year :: CF g Integer
640 g_year = rule "Year" $
641 (\sg y -> sg $ integer_of_digits 10 y)
642 <$> option id (negate <$ char '-')
645 g_month = rule "Month" $ int_of_digits 10 <$> count' 1 2 g_09
647 g_dom = rule "DayOfMonth" $ int_of_digits 10 <$> count' 1 2 g_09
649 g_hour = rule "Hour" $ int_of_digits 10 <$> count' 1 2 g_09
651 g_minute = rule "Minute" $ int_of_digits 10 <$> count' 1 2 g_09
653 g_second = rule "Second" $ int_of_digits 10 <$> count' 1 2 g_09
655 g_timezone :: CF g (S.Either (At Error_Date) TimeZone)
656 g_timezone = rule "TimeZone" $
657 -- DOC: http://www.timeanddate.com/time/zones/
658 -- TODO: only a few time zones are suported below.
659 -- TODO: check the timeZoneSummerOnly values
660 (S.Right <$> g_timezone_digits) <+>
661 (g_at $ read_tz <$ char '_' <*> some (range ('A', 'Z')))
663 read_tz n at = case n of
664 "AST" -> S.Right $ TimeZone (- 4 * 60) False n
665 "ADT" -> S.Right $ TimeZone (- 3 * 60) True n
666 "A" -> S.Right $ TimeZone (- 1 * 60) False n
667 "BST" -> S.Right $ TimeZone (-11 * 60) False n
668 "BDT" -> S.Right $ TimeZone (-10 * 60) True n
669 "CET" -> S.Right $ TimeZone ( 1 * 60) True n
670 "CEST" -> S.Right $ TimeZone ( 2 * 60) False n
671 "CST" -> S.Right $ TimeZone (- 6 * 60) False n
672 "CDT" -> S.Right $ TimeZone (- 5 * 60) True n
673 "EST" -> S.Right $ TimeZone (- 5 * 60) False n
674 "EDT" -> S.Right $ TimeZone (- 4 * 60) True n
675 "GMT" -> S.Right $ TimeZone 0 False n
676 "HST" -> S.Right $ TimeZone (-10 * 60) False n
677 "HDT" -> S.Right $ TimeZone (- 9 * 60) True n
678 "MST" -> S.Right $ TimeZone (- 7 * 60) False n
679 "MDT" -> S.Right $ TimeZone (- 6 * 60) True n
680 "M" -> S.Right $ TimeZone (-12 * 60) False n
681 "NST" -> S.Right $ TimeZone (- 3 * 60 - 30) False n
682 "N" -> S.Right $ TimeZone ( 1 * 60) False n
683 "PST" -> S.Right $ TimeZone (- 8 * 60) False n
684 "PDT" -> S.Right $ TimeZone (- 7 * 60) True n
685 "YST" -> S.Right $ TimeZone (- 9 * 60) False n
686 "YDT" -> S.Right $ TimeZone (- 8 * 60) True n
687 "Y" -> S.Right $ TimeZone ( 12 * 60) False n
688 "Z" -> S.Right $ TimeZone 0 False n
689 _ -> S.Left $ at $ Error_Date_TimeZone_unknown (Text.pack n)
690 g_timezone_digits :: CF g TimeZone
691 g_timezone_digits = do
695 { timeZoneMinutes = sg $ hr * 60 + mn
696 , timeZoneSummerOnly = False
697 , timeZoneName = Time.timeZoneOffsetString tz
702 <*> option 0 (optional (char char_tod_sep) *> g_minute)
704 -- * Class 'Gram_Tag'
709 ) => Gram_Tag g where
712 <$ char char_tag_prefix
714 <*> option (Tag_Data "")
716 *> char char_tag_data_prefix
719 g_tag_path :: CF g Tag_Path
721 (\x xs -> Tag_Path $ NonNull.ncons x xs)
723 <*> many (try $ char char_tag_sep *> g_tag_section)
724 g_tag_section :: CF g Tag_Path_Section
727 <$> some (g_char `minus` g_char_attribute)
728 g_tag_value :: CF g Tag_Data
729 g_tag_value = Tag_Data <$> g_words
731 -- * Class 'Gram_Comment'
735 ) => Gram_Comment g where
736 g_comment :: CF g Comment
737 g_comment = rule "Comment" $
738 Comment <$ char ';' <* g_spaces <*> g_words
740 -- * Class 'Gram_Account'
747 ) => Gram_Account g where
748 g_account_section :: CF g Account_Section
751 <$> some (g_char `minus` g_char_attribute)
752 g_account :: CF g Account
753 g_account = rule "Account" $
754 Account . NonNull.impureNonNull
755 <$> some (try $ char '/' *> g_account_section)
756 g_account_tag :: CF g Account_Tag
760 <$ char char_account_tag_prefix
762 <*> option (Tag_Data "")
764 *> char char_tag_data_prefix
767 g_account_tag_path :: CF g Tag_Path
768 g_account_tag_path = rule "Tag_Path" $
769 char char_account_tag_prefix
772 g_anchor_section :: CF g Anchor_Section
773 g_anchor_section = rule "Anchor_Section" $
775 <$> some (g_char `minus` g_char_attribute)
778 -- * Class 'Gram_Amount'
782 ) => Gram_Amount g where
784 g_unit = rule "Unit" $
785 Unit . Text.singleton
786 <$> unicat (Unicat Char.CurrencySymbol)
787 g_quantity :: CF g (Quantity, Style_Amount)
788 g_quantity = rule "Quantity" $
789 (\(i, f, fr, gi, gf) ->
790 let int = concat i in
791 let frac = concat f in
792 let precision = length frac in
793 -- guard (precision <= 255)
794 let mantissa = integer_of_digits 10 $ int <> frac in
796 (fromIntegral precision)
799 { style_amount_fractioning=fr
800 , style_amount_grouping_integral=gi
801 , style_amount_grouping_fractional=gf
805 [ g_qty '_' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
806 , g_qty '_' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
807 , g_qty ',' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
808 , g_qty '.' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
811 :: Char -- ^ Integral grouping separator.
812 -> Char -- ^ Fractioning separator.
813 -> Char -- ^ Fractional grouping separator.
815 ( [String] -- integral
816 , [String] -- fractional
817 , S.Maybe Style_Amount_Fractioning -- fractioning
818 , S.Maybe Style_Amount_Grouping -- grouping_integral
819 , S.Maybe Style_Amount_Grouping -- grouping_fractional
821 g_qty int_group_sep frac_sep frac_group_sep = do
828 , grouping_of_digits int_group_sep int
831 Just (fractioning, frac) ->
835 , grouping_of_digits int_group_sep int
836 , grouping_of_digits frac_group_sep $ List.reverse frac
840 <*> option [] (many $ try $ char int_group_sep *> some g_09))
841 <*> option Nothing (Just <$> ((,)
845 <*> option [] (many $ try $ char frac_group_sep *> some g_09))))
847 grouping_of_digits :: Char -> [String] -> S.Maybe Style_Amount_Grouping
848 grouping_of_digits group_sep digits =
853 Style_Amount_Grouping group_sep $
854 canonicalize_grouping $
856 canonicalize_grouping :: [Int] -> [Int]
857 canonicalize_grouping groups =
858 foldl' -- NOTE: remove duplicates at beginning and reverse.
859 (\acc l0 -> case acc of
860 l1:_ -> if l0 == l1 then acc else l0:acc
862 case groups of -- NOTE: keep only longer at beginning.
863 l0:l1:t -> if l0 > l1 then groups else l1:t
866 g_amount :: CF g (Styled_Amount Amount)
867 g_amount = rule "Amount" $ g_amount_minus <+> g_amount_plus
868 g_amount_minus :: CF g (Styled_Amount Amount)
872 <$> ((,) <$> g_unit <*> g_spaces)
877 <*> option ("", H.unit_empty)
878 (try $ flip (,) <$> g_spaces <*> g_unit) )
881 <$> ((,) <$> g_unit <*> g_spaces)
885 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
886 mk_amount side (unit, sp) (qty, sty) =
890 { style_amount_unit_side = S.Just side
891 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
894 { amount_quantity = negate qty
898 g_amount_plus :: CF g (Styled_Amount Amount)
902 <$> ((,) <$> g_unit <*> g_spaces)
907 <*> option ("", H.unit_empty)
908 (try $ flip (,) <$> g_spaces <*> g_unit) )
911 <$> ((,) <$> g_unit <*> g_spaces)
912 <* optional (char '+')
917 <*> option ("", H.unit_empty)
918 (try $ flip (,) <$> g_spaces <*> g_unit)
920 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
921 mk_amount side (unit, sp) (qty, sty) =
925 { style_amount_unit_side = S.Just side
926 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
929 { amount_quantity = qty
934 -- * Class 'Gram_Posting'
940 , Gram_Reader P.SourcePos g
941 , Gram_State (S.Maybe Unit) g
943 , Gram_State Style_Amounts g
945 ) => Gram_Posting g where
946 g_postings :: CF g (S.Either (At Error_Posting) [Posting])
950 many (try $ g_spaces *> g_eol) *>
951 g_spaces1 *> g_posting
952 g_posting :: CF g (S.Either (At Error_Posting) Posting)
953 g_posting = rule "Posting" $
954 g_state $ g_get $ g_ask_before $
957 posting_sourcepos ctx_unit
958 (Style_Amounts ctx_stys) -> do
959 let (posting_tags, posting_comments) = attrs
960 let (stys, posting_amounts) =
962 Nothing -> (Style_Amounts ctx_stys, mempty)
966 Map.insertWith (flip (<>))
970 case amount_unit amt of
971 u | u == H.unit_empty -> S.fromMaybe u ctx_unit
978 (posting_account, posting_account_ref) <- lr_acct
982 , posting_account_ref
989 <$> g_posting_account
990 <*> optional (try $ g_spaces1 *> g_amount)
993 :: CF g (S.Either (At Error_Posting)
994 (Account, S.Maybe (S.Pair Tag_Path (S.Maybe Account))))
995 g_posting_account = rule "Posting_Account" $
996 (S.Right . (, S.Nothing) <$> g_account) <+>
998 <$> (g_at $ g_get $ expand_tag_path <$> g_account_tag_path)
999 <*> option S.Nothing (S.Just <$> g_account))
1001 mk_posting_account path acct =
1004 (S.maybe a (a <>) acct)
1005 (S.Just (p S.:!: acct)) )
1007 expand_tag_path tag chart at =
1008 case Map.lookup tag $ chart_tags chart of
1009 Just accts | Map.size accts > 0 ->
1010 if Map.size accts == 1
1012 let acct = fst $ Map.elemAt 0 accts in
1014 else S.Left $ at $ Error_Posting_Account_Ref_multiple tag accts
1015 _ -> S.Left $ at $ Error_Posting_Account_Ref_unknown tag
1016 g_posting_tag :: CF g Posting_Tag
1017 g_posting_tag = rule "Posting_Tag" $ Posting_Tag <$> g_tag
1018 g_posting_attrs :: CF g (Posting_Tags, [Comment])
1020 foldr ($) mempty . Compose
1022 many (try $ g_spaces *> g_eol *> g_spaces1) *>
1026 [ add_tag <$> g_posting_tag
1027 , add_comment <$> g_comment
1030 add_tag (Posting_Tag (Tag (Tag_Path p) v)) =
1031 \(Posting_Tags (Tags tags), cmts) ->
1032 ( Posting_Tags (Tags (TreeMap.insert (<>) p [v] tags))
1038 -- * Class 'Gram_Transaction'
1047 , Gram_State Section g
1048 ) => Gram_Transaction g where
1049 g_transaction :: CF g (S.Either (At Error_Transaction) Transaction)
1050 g_transaction = rule "Transaction" $
1051 g_put $ ((Section_Transaction,) <$>) $
1052 g_state $ (update_year <$>) $
1053 g_at $ g_ask_before $
1057 , transaction_comments )
1059 transaction_sourcepos at -> do
1060 date <- fmap Error_Transaction_Date `S.left` lr_date
1061 posts <- fmap Error_Transaction_Posting `S.left` lr_posts
1062 let postsByAcct = postings_by_account posts
1066 , transaction_comments
1067 , transaction_dates = NonNull.ncons date []
1068 , transaction_wording
1069 , transaction_postings = Postings postsByAcct
1070 , transaction_sourcepos
1072 case H.equilibrium postsByAcct of
1073 (_, Left ko) -> S.Left $ at $ Error_Transaction_not_equilibrated txn ko
1074 (_bal, Right ok) -> S.Right txn{transaction_postings = Postings ok}
1079 <*> g_transaction_attrs
1082 update_year lr_txn y =
1085 S.Right txn -> Year $ H.date_year $ NonNull.head $ transaction_dates txn
1087 g_wording :: CF g Wording
1088 g_wording = rule "Wording" $
1089 Wording . Text.concat
1094 <$> some (g_char `minus` char char_tag_prefix)))
1095 g_transaction_tag :: CF g Transaction_Tag
1096 g_transaction_tag = rule "Transaction_Tag" $ Transaction_Tag <$> g_tag
1097 g_transaction_attrs :: CF g (Transaction_Tags, [Comment])
1098 g_transaction_attrs =
1102 [ add_tag <$ many (try $ g_spaces *> g_eol *> g_spaces1) <*> g_transaction_tag
1103 , add_comment <$ many (try $ g_spaces *> g_eol *> g_spaces) <*> g_comment
1106 add_tag (Transaction_Tag (Tag (Tag_Path p) v)) =
1107 \(Transaction_Tags (Tags tags), cmts) ->
1108 ( Transaction_Tags (Tags (TreeMap.insert (<>) p [v] tags))
1114 -- * Class 'Gram_File'
1120 ) => Gram_File g where
1121 g_pathfile :: CF g PathFile
1122 g_pathfile = rule "PathFile" $
1124 <$> some (try $ (:) <$> char '/' <*> some (g_char `minus` char '/'))
1126 -- * Class 'Gram_Chart'
1132 , Gram_State Chart g
1133 , Gram_State Section g
1135 ) => Gram_Chart g where
1136 g_chart_entry :: CF g (S.Either (At (Error_Journal cs is)) Chart)
1137 g_chart_entry = rule "Chart" $
1139 (\acct attrs at section ->
1140 let (tags, tags2, _comments) = attrs in
1142 Section_Transaction -> False
1143 Section_Chart -> True
1146 { chart_accounts = TreeMap.singleton (H.get acct) tags
1147 , chart_tags = Map.singleton acct () <$ tags2
1149 else S.Left $ at $ Error_Journal_Section section Section_Chart
1153 g_chart_attrs :: CF g (Account_Tags, Map Tag_Path (), [Comment])
1157 many (try $ g_spaces *> g_eol) *>
1159 [ add_tag <$ g_spaces1 <*> g_account_tag
1160 , add_comment <$ g_spaces <*> g_comment
1163 add_tag (Account_Tag (Tag (Tag_Path p) v)) =
1164 \(Account_Tags (Tags tags), tags2, cmts) ->
1165 ( Account_Tags (Tags (TreeMap.insert (<>) p [v] tags))
1166 , Map.insert (Tag_Path p) () tags2
1169 \(tags, tags2, cmts) ->
1170 (tags, tags2, c:cmts)
1173 tokenizer :: forall is m.
1174 ( Inj_Tokens Meta is [Proxy (->), Proxy Integer]
1175 , Sym.Gram_Term is Meta (P.ParsecT P.Dec Text (SS.StateT (Sym.Tokenizers Meta is) m))
1176 , Sym.Tokenize Meta is
1178 ) => Text -> m (Either (P.ParseError Char P.Dec) (EToken Meta is))
1181 MC.evalStateStrict (Sym.tokenizers::Sym.Tokenizers Meta is) $
1182 P.runParserT g "" inp
1183 where g = Gram.unCF $ Sym.g_term <* Gram.eoi
1186 -- * Class 'Gram_Term'
1188 ( Sym.Gram_Term is Meta g
1190 , Sym.Inj_Token Meta is (->)
1192 , Gram_State (Env cs is) g
1193 ) => Gram_Term cs is {-meta-} g where
1195 :: CF g ( Sym.TeName
1196 , Either (At (Sym.Error_Term Meta cs is))
1204 Sym.withContext (Map.toList (env::Env cs is)) $
1208 -- TODO: <*> many Sym.term_abst_decl
1212 instance -- Gram_Term
1214 , MC.MonadState (Sym.Tokenizers Meta is) m
1215 , MC.MonadState (Env cs is) m
1216 , P.MonadParsec e Text (P.ParsecT e s m)
1217 , Sym.Gram_Term_AtomsR Meta is is (P.ParsecT e s m)
1219 , Sym.Inj_Token Meta is (->)
1221 ) => Gram_Term cs is (P.ParsecT e s m) where
1224 type Env cs is = Map Sym.TeName (Sym.ETerm cs is)
1226 -- * Class 'Gram_Journal'
1234 , Gram_Reader (S.Either Exn.IOException CanonFile) g
1235 , Gram_State (Context_Read j) g
1236 , Gram_State (Journal j) g
1237 , Gram_State (Journals j) g
1238 , Gram_State (Env cs is) g
1239 , Gram_Transaction g
1242 , Sym.Inj_Token Meta is (->)
1245 ) => Gram_Journal cs is j g where
1247 :: (Transaction -> j -> j)
1248 -> CF g (S.Either [At (Error_Journal cs is)]
1249 (CanonFile, Journal j))
1250 g_journal cons_txn = rule "Journal" $
1251 g_state $ g_ask_before $
1253 <$> (g_state $ g_at $ g_ask_before $ g_ask_before $ pure init_journal)
1255 [ g_state $ mk_include <$> g_include @cs @is cons_txn
1256 -- NOTE: g_include must be the first choice
1257 -- in order to have Megaparsec reporting the errors
1258 -- of the included journal.
1259 , g_state $ mk_transaction <$> g_transaction
1260 , g_state $ mk_chart <$> g_chart_entry
1261 , g_state $ mk_term <$> g_term
1262 , [] <$ try (g_spaces <* g_eol)
1266 P.SourcePos{P.sourceName=jf} lr_cf at
1268 { context_read_journals = Journals js
1269 , context_read_journal = jnls
1270 , context_read_canonfiles = cfs
1271 }::Context_Read j) =
1273 S.Left e -> (ctx, S.Left $ at $ Error_Journal_Read (PathFile jf) e)
1275 let jnl = journal{journal_file=PathFile jf} in
1278 { context_read_journals = Journals $ Map.insert cf jnl js
1279 , context_read_journal = jnl <| jnls
1280 , context_read_canonfiles = cf <| cfs
1283 P.SourcePos{P.sourceName=jf}
1285 { context_read_journals = Journals js
1286 , context_read_journal = jnl :| jnls
1287 , context_read_canonfiles = cf :| cfs
1288 }::Context_Read j) =
1289 case concat $ S.either (pure . pure) (const []) err <> errs of
1291 let jnl' = jnl{journal_file=PathFile jf} in
1292 (,S.Right (cf, jnl'))
1294 { context_read_journals = Journals $ Map.insert cf jnl' js
1295 , context_read_journal = NonEmpty.fromList jnls
1296 , context_read_canonfiles = NonEmpty.fromList cfs
1298 es -> (ctx, S.Left es)
1299 mk_transaction lr_txn jnl@Journal{journal_content=j} =
1301 S.Left err -> (jnl, [Error_Journal_Transaction <$> err])
1302 S.Right txn -> (jnl{ journal_content = txn `cons_txn` j }, [])
1303 mk_include lr_inc (jnl::Journal j) =
1305 S.Left errs -> (jnl, errs)
1306 S.Right (cf, _inc) -> (jnl{ journal_includes = journal_includes jnl <> [cf] }, [])
1307 mk_chart lr_ch chart =
1309 S.Left err -> (chart, [err])
1310 S.Right ch -> (chart <> ch, [])
1311 mk_term (n, lr_te) terms =
1313 Left err -> (terms, [Error_Journal_Term <$> err])
1314 Right (te::Sym.ETerm cs is) -> (insert_term n te terms, [])
1321 insert_term = Map.insert
1323 :: (Transaction -> j -> j)
1324 -> CF g (S.Either [At (Error_Journal cs is)]
1325 (CanonFile, Journal j))
1326 g_include cons_txn = rule "Include" $
1327 g_read g_path (g_journal @cs @is cons_txn <* eoi)
1330 g_state $ g_at $ check_path
1331 <$> (g_canonfile $ g_ask_before $ fmap mk_path $
1332 (\d (PathFile p) -> PathFile $ d:p)
1333 <$> char '.' <*> g_pathfile)
1334 mk_path (PathFile fp) P.SourcePos{P.sourceName=fp_old} =
1336 FilePath.normalise $
1337 FilePath.takeDirectory fp_old </> fp
1338 check_path (fp, lr_cf) at
1340 { context_read_journals = Journals js
1341 , context_read_canonfiles = cfs
1342 , context_read_warnings = warns
1343 }::Context_Read j) =
1345 Left e -> (ctx, S.Left $ Error_Journal_Read fp e)
1347 if cf `Map.member` js
1350 then (ctx, S.Left $ Error_Journal_Include_loop cf)
1353 if isJust $ (`List.find` warns) $ \case
1354 At{atItem=Warning_Journal_Include_multiple cf'} -> cf' `elem` cf<|cfs
1357 { context_read_warnings =
1358 at (Warning_Journal_Include_multiple cf) : warns }
1359 else (ctx, S.Right fp)
1363 -- | Return the 'Integer' obtained by multiplying the given digits
1364 -- with the power of the given base respective to their rank.
1366 :: Integer -- ^ Base.
1367 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1369 integer_of_digits base =
1370 foldl' (\x d -> base*x + toInteger (Char.digitToInt d)) 0
1372 -- | Return the 'Int' obtained by multiplying the given digits
1373 -- with the power of the given base respective to their rank.
1376 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1378 int_of_digits base =
1379 foldl' (\x d -> base*x + Char.digitToInt d) 0
1382 char_account_sep :: Char
1383 char_account_sep = '/'
1384 char_account_tag_prefix :: Char
1385 char_account_tag_prefix = '~'
1386 char_ymd_sep :: Char
1388 char_tod_sep :: Char
1390 char_comment_prefix :: Char
1391 char_comment_prefix = ';'
1392 char_tag_prefix :: Char
1393 char_tag_prefix = '#'
1394 char_tag_sep :: Char
1396 char_tag_data_prefix :: Char
1397 char_tag_data_prefix = '='
1398 char_transaction_date_sep :: Char
1399 char_transaction_date_sep = '='
1402 :: forall is j cs e m a.
1404 , Gram_File (P.ParsecT P.Dec Text m)
1405 , Sym.Tokenize Meta is
1406 , m ~ S.StateT (Context_Read j) (S.StateT (Context_Sym cs is) IO)
1407 , e ~ P.ParseError Char P.Dec
1408 , cs ~ Sym.TyConsts_of_Ifaces is
1410 => CF (P.ParsecT P.Dec Text m) a
1413 -> IO ((Either e a, Context_Read j), Context_Sym cs is)
1415 S.runState context_sym $
1416 S.runState context_read $
1417 P.runParserT (unCF $ g <* eoi) (FilePath.normalise fp) inp
1419 read_file :: FilePath -> (FilePath -> Text -> IO a) -> IO a
1421 content <- Enc.decodeUtf8 <$> BS.readFile fp
1427 :: (Consable c j, Monoid j)
1430 -> ExceptT [R.Error Error_Read] IO (Journal j)
1431 read_file ctx path =
1434 (Right <$> Text.IO.readFile path) $
1435 \ko -> return $ Left $
1436 [R.Error_Custom (R.initialPos path) $
1437 Error_Read_reading_file path ko])
1438 >>= liftIO . R.runParserTWithError
1439 (read_journal path) ctx path
1441 Left ko -> throwE $ ko
1442 Right ok -> ExceptT $ return $ Right ok