1 {-# LANGUAGE AllowAmbiguousTypes #-}
2 {-# LANGUAGE ExistentialQuantification #-}
3 {-# LANGUAGE RecordWildCards #-}
4 {-# LANGUAGE UndecidableSuperClasses #-}
5 module Hcompta.LCC.Read.Compta where
7 import Control.Applicative (Applicative(..), liftA2)
8 import Control.Monad (Monad(..), void)
10 import Data.Char (Char)
12 import Data.Either (Either(..))
13 import Data.Eq (Eq(..))
15 import Data.Function (($), (.), const, id, flip)
16 import Data.Functor (Functor(..), (<$>), (<$))
17 import Data.Functor.Compose (Compose(..))
18 import Data.List.NonEmpty (NonEmpty(..), (<|))
19 import Data.Map.Strict (Map)
20 import Data.Maybe (Maybe(..), maybe, isJust)
21 import Data.Monoid (Monoid(..))
22 import Data.NonNull (NonNull)
23 import Data.Ord (Ord(..))
24 import Data.Semigroup (Semigroup(..))
25 import Data.String (String)
26 import Data.Text (Text)
27 import Data.Time.LocalTime (TimeZone(..))
28 import Data.Traversable (sequenceA)
29 import Data.Tuple (fst,curry)
30 import Data.Typeable (Typeable)
31 import Prelude (Int, Integer, Enum(..), Num(..), Integral(..), fromIntegral)
32 import System.FilePath ((</>))
33 import Text.Show (Show(..))
34 import qualified Control.Exception.Safe as Exn
35 import qualified Control.Monad.Classes as MC
36 import qualified Control.Monad.Trans.State.Strict as SS
37 import qualified Data.Char as Char
38 import qualified Data.List as L
39 import qualified Data.List.NonEmpty as NonEmpty
40 import qualified Data.Map.Strict as Map
41 import qualified Data.NonNull as NonNull
42 import qualified Data.Strict as S
43 import qualified Data.Text as Text
44 import qualified Data.Time.Calendar as Time
45 import qualified Data.Time.LocalTime as Time
46 import qualified Data.TreeMap.Strict as TreeMap
47 import qualified Hcompta as H
48 import qualified System.FilePath as FilePath
50 import qualified Language.Symantic.Grammar as G
51 import Language.Symantic.Grammar (CF, At(..), Gram_Rule(..), Gram_Terminal(..), Gram_Alt(..), Gram_AltApp(..), Gram_Try(..), Gram_CF(..))
52 import Language.Symantic.Lib ()
53 import qualified Language.Symantic as Sym
54 -- import qualified Language.Symantic.Grammar as Sym
56 import Hcompta.LCC.Account
57 import Hcompta.LCC.Amount
58 import Hcompta.LCC.Balance ()
59 import Hcompta.LCC.Chart
60 -- import Hcompta.LCC.Compta (State_Sym(..))
62 import Hcompta.LCC.Journal
63 import Hcompta.LCC.Name
64 import Hcompta.LCC.Posting
65 import Hcompta.LCC.Tag
66 import Hcompta.LCC.Transaction
67 import Hcompta.LCC.Source
69 import qualified Hcompta.LCC.Lib.Strict as S
72 import Debug.Trace (trace)
73 dbg :: Show a => String -> a -> a
74 dbg msg x = trace (msg <> " = " <> show x) x
77 -- * Type 'Context_Read'
78 data Context_Read src =
79 forall j. (Typeable j, H.Zeroable j) =>
81 { context_read_year :: !Year
82 , context_read_unit :: !(S.Maybe Unit)
83 , context_read_canonfiles :: !(NonEmpty CanonFile)
84 , context_read_warnings :: ![At src Warning_Compta]
85 , context_read_section :: !Section
87 , context_read_style_amounts :: !Style_Amounts
88 , context_read_chart :: !Chart
89 , context_read_journals :: !(Journals src j)
90 , context_read_journal :: !(NonEmpty (Journal src j))
91 , context_read_consTxn :: !(Transaction src -> j -> j)
92 } -- deriving (Eq, Show)
94 -- deriving instance Show src => Show (Context_Read src)
100 -- NonEmpty CanonFile
101 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffReader (NonEmpty CanonFile)) = 'True
102 instance Monad m => MC.MonadReaderN 'MC.Zero (NonEmpty CanonFile) (S.StateT (Context_Read src) m) where
103 askN _n = MC.gets $ \(x::Context_Read src) -> context_read_canonfiles x
106 -- States handled by a nested Monad
108 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState (Sym.Imports ns, Sym.Modules src ss)) = 'False
109 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState (Sym.Imports ns, Sym.ModulesTy src)) = 'False
114 (Transaction src -> j -> j) ->
116 context_read consTxn =
118 { context_read_year = Year $ H.yearOf (H.epoch::Date)
119 , context_read_style_amounts = mempty
120 , context_read_chart = mempty
121 , context_read_unit = S.Nothing
122 , context_read_journals = Journals Map.empty
123 , context_read_journal = journal H.zero :| []
124 , context_read_canonfiles = CanonFile "" :| []
125 , context_read_warnings = []
126 , context_read_section = Section_Terms
127 , context_read_consTxn = consTxn
131 -- * Type 'Context_Sym'
132 data Context_Sym src ss
134 { context_sym_imports :: !(Sym.Imports Sym.NameTe)
135 , context_sym_importsTy :: !(Sym.Imports Sym.NameTy)
136 , context_sym_modules :: !(Sym.Modules src ss)
137 , context_sym_modulesTy :: !(Sym.ModulesTy src)
138 , context_sym_env :: !(Env src ss)
139 , context_sym_terms :: !Terms
140 } deriving (Eq, Show)
145 Sym.ImportTypes ss =>
146 Sym.ModulesInj src ss =>
147 Sym.ModulesTyInj ss =>
150 let mods = either (error . show) id Sym.modulesInj in
152 { context_sym_imports = Sym.importModules [] mods
153 , context_sym_importsTy = Sym.importTypes @ss []
154 , context_sym_modules = mods
155 , context_sym_modulesTy = Sym.modulesTyInj @ss
156 , context_sym_env = Map.empty
157 , context_sym_terms = Map.empty
168 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState Terms) = 'True
169 instance Monad m => MC.MonadStateN 'MC.Zero Terms (S.StateT (Context_Sym src ss) m) where
170 stateN _px f = S.StateT $ SS.state $ \ctx ->
171 (\a -> ctx{context_sym_terms = a})
172 <$> f (context_sym_terms ctx)
175 type Env src ss = Map Sym.NameTe (Sym.TermVT src ss '[])
178 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Env src ss)) = 'True
179 instance Monad m => MC.MonadStateN 'MC.Zero (Env src ss) (S.StateT (Context_Sym src ss) m) where
180 stateN _px f = S.StateT $ SS.state $ \ctx ->
181 (\a -> ctx{context_sym_env = a})
182 <$> f (context_sym_env ctx)
186 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState (Context_Read src)) = 'True
187 instance Monad m => MC.MonadStateN 'MC.Zero (Context_Read src) (S.StateT (Context_Read src) m) where
188 stateN _px = S.StateT . SS.state
191 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState (S.Maybe Unit)) = 'True
192 instance Monad m => MC.MonadStateN 'MC.Zero (S.Maybe Unit) (S.StateT (Context_Read src) m) where
193 stateN _px f = S.StateT $ SS.state $ \ctx ->
194 (\a -> ctx{context_read_unit = a})
195 <$> f (context_read_unit ctx)
198 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState Chart) = 'True
199 instance Monad m => MC.MonadStateN 'MC.Zero Chart (S.StateT (Context_Read src) m) where
200 stateN _px f = S.StateT $ SS.state $ \ctx ->
201 (\a -> ctx{context_read_chart = a})
202 <$> f (context_read_chart ctx)
205 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState (Terms src)) = 'True
206 instance Monad m => MC.MonadStateN 'MC.Zero (Terms src) (S.StateT (Context_Read src) m) where
207 stateN _px f = S.StateT $ SS.state $ \Context_Read{context_read_journal = j :| js, ..} ->
208 (\a -> Context_Read{context_read_journal = j{journal_terms = a} :| js, ..})
209 <$> f (journal_terms j)
212 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState Year) = 'True
213 instance Monad m => MC.MonadStateN 'MC.Zero Year (S.StateT (Context_Read src) m) where
214 stateN _px f = S.StateT $ SS.state $ \ctx ->
215 (\a -> ctx{context_read_year = a})
216 <$> f (context_read_year ctx)
219 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState Section) = 'True
220 instance Monad m => MC.MonadStateN 'MC.Zero Section (S.StateT (Context_Read src) m) where
221 stateN _px f = S.StateT $ SS.state $ \ctx ->
222 (\a -> ctx{context_read_section = a})
223 <$> f (context_read_section ctx)
226 type instance MC.CanDo (S.StateT (Context_Read src) m) (MC.EffState Style_Amounts) = 'True
227 instance Monad m => MC.MonadStateN 'MC.Zero Style_Amounts (S.StateT (Context_Read src) m) where
228 stateN _px f = S.StateT $ SS.state $ \ctx ->
229 (\s -> ctx{context_read_style_amounts = s})
230 <$> f (context_read_style_amounts ctx)
232 -- * Class 'Gram_Path'
233 class Gram_Path g where
236 g (PathFile, Either Exn.IOException CanonFile)
237 deriving instance Gram_Path g => Gram_Path (CF g)
240 class {-G.Gram_Source src g =>-} Gram_IO src g where
242 g (S.Either (Error_Compta src) PathFile) ->
243 g (S.Either [At src (Error_Compta src)] a) ->
244 g (S.Either [At src (Error_Compta src)] a)
245 deriving instance Gram_IO src g => Gram_IO src (CF g)
247 -- * Class 'Gram_Count'
252 ) => Gram_Count g where
253 count :: Int -> CF g a -> CF g [a]
256 | otherwise = sequenceA $ L.replicate n p
257 count' :: Int -> Int -> CF g a -> CF g [a]
259 | n <= 0 || m > n = pure []
260 | m > 0 = (:) <$> p <*> count' (pred m) (pred n) p
262 let f t ts = maybe [] (:ts) t
263 in f <$> G.optional p <*> count' 0 (pred n) p
265 -- * Class 'Gram_Char'
275 ) => Gram_Char g where
278 (Text.singleton <$> (char '\n')) <+>
279 (Text.pack <$> G.string "\r\n")
281 g_tab = rule "Tab" $ void $ char '\t'
283 g_space = rule "Space" $ char ' '
284 g_spaces :: CF g Text
285 g_spaces = Text.pack <$> many g_space
286 g_spaces1 :: CF g Text
287 g_spaces1 = Text.pack <$> some g_space
289 g_char = g_char_passive <+> g_char_active
290 g_char_passive :: CF g Char
291 g_char_passive = choice $ G.unicat <$> [G.Unicat_Letter, G.Unicat_Number, G.Unicat_Mark]
292 g_char_active :: CF g Char
293 g_char_active = choice $ G.unicat <$> [G.Unicat_Punctuation, G.Unicat_Symbol]
294 g_char_attribute :: G.Reg lr g Char
295 g_char_attribute = choice $ char <$> "#/:;@~="
297 g_word = rule "Word" $ Text.pack <$> some g_char
299 g_words = Text.concat <$> many (try $ (<>) <$> g_spaces <*> g_word)
301 g_09 = range ('0', '9')
303 g_19 = range ('1', '9')
304 g_sign :: Num int => CF g (int -> int)
306 (negate <$ char '-') <+>
309 -- * Class 'Gram_Date'
311 ( G.Gram_State Year g
320 ) => Gram_Date g where
322 G.Gram_Source src g =>
323 CF g (S.Either (At src Error_Date) Date)
324 g_date = rule "Date" $
325 liftA2 (\day (tod, tz) ->
326 Time.localTimeToUTC tz $
327 Time.LocalTime day tod)
330 (S.Right (Time.midnight, Time.utc))
334 <*> option (S.Right Time.utc) g_timezone)
336 G.Gram_Source src g =>
337 CF g (S.Either (At src Error_Date) Time.Day)
348 <$> G.getAfter (pure $ \(Year y) -> y)
354 case Time.fromGregorianValid y m d of
355 Nothing -> S.Left $ At src $ Error_Date_Day_invalid (y, m, d)
356 Just day -> S.Right day
358 G.Gram_Source src g =>
359 CF g (S.Either (At src Error_Date) Time.TimeOfDay)
360 g_tod = rule "TimeOfDay" $
363 case Time.makeTimeOfDayValid hr mn (fromInteger $ toInteger sc) of
364 Nothing -> S.Left $ At src $ Error_Date_TimeOfDay_invalid (hr, mn, sc)
365 Just tod -> S.Right $ tod)
369 <$> (char char_tod_sep *> g_minute)
370 <*> option 0 (char char_tod_sep *> g_second))
371 g_year :: CF g Integer
372 g_year = rule "Year" $
373 (\sg y -> sg $ integer_of_digits 10 y)
374 <$> option id (negate <$ char '-')
377 g_month = rule "Month" $ int_of_digits 10 <$> count' 1 2 g_09
379 g_dom = rule "DayOfMonth" $ int_of_digits 10 <$> count' 1 2 g_09
381 g_hour = rule "Hour" $ int_of_digits 10 <$> count' 1 2 g_09
383 g_minute = rule "Minute" $ int_of_digits 10 <$> count' 1 2 g_09
385 g_second = rule "Second" $ int_of_digits 10 <$> count' 1 2 g_09
388 G.Gram_Source src g =>
389 CF g (S.Either (At src Error_Date) TimeZone)
390 g_timezone = rule "TimeZone" $
391 -- DOC: http://www.timeanddate.com/time/zones/
392 -- TODO: only a few time zones are suported below.
393 -- TODO: check the timeZoneSummerOnly values
394 (S.Right <$> g_timezone_digits) <+>
395 (G.source $ read_tz <$ char '_' <*> some (range ('A', 'Z')))
397 read_tz n src = case n of
398 "AST" -> S.Right $ TimeZone (- 4 * 60) False n
399 "ADT" -> S.Right $ TimeZone (- 3 * 60) True n
400 "A" -> S.Right $ TimeZone (- 1 * 60) False n
401 "BST" -> S.Right $ TimeZone (-11 * 60) False n
402 "BDT" -> S.Right $ TimeZone (-10 * 60) True n
403 "CET" -> S.Right $ TimeZone ( 1 * 60) True n
404 "CEST" -> S.Right $ TimeZone ( 2 * 60) False n
405 "CST" -> S.Right $ TimeZone (- 6 * 60) False n
406 "CDT" -> S.Right $ TimeZone (- 5 * 60) True n
407 "EST" -> S.Right $ TimeZone (- 5 * 60) False n
408 "EDT" -> S.Right $ TimeZone (- 4 * 60) True n
409 "GMT" -> S.Right $ TimeZone 0 False n
410 "HST" -> S.Right $ TimeZone (-10 * 60) False n
411 "HDT" -> S.Right $ TimeZone (- 9 * 60) True n
412 "MST" -> S.Right $ TimeZone (- 7 * 60) False n
413 "MDT" -> S.Right $ TimeZone (- 6 * 60) True n
414 "M" -> S.Right $ TimeZone (-12 * 60) False n
415 "NST" -> S.Right $ TimeZone (- 3 * 60 - 30) False n
416 "N" -> S.Right $ TimeZone ( 1 * 60) False n
417 "PST" -> S.Right $ TimeZone (- 8 * 60) False n
418 "PDT" -> S.Right $ TimeZone (- 7 * 60) True n
419 "YST" -> S.Right $ TimeZone (- 9 * 60) False n
420 "YDT" -> S.Right $ TimeZone (- 8 * 60) True n
421 "Y" -> S.Right $ TimeZone ( 12 * 60) False n
422 "Z" -> S.Right $ TimeZone 0 False n
423 _ -> S.Left $ At src $ Error_Date_TimeZone_unknown (Text.pack n)
424 g_timezone_digits :: CF g TimeZone
429 { timeZoneMinutes = sg $ hr * 60 + mn
430 , timeZoneSummerOnly = False
431 , timeZoneName = Time.timeZoneOffsetString tz
436 <*> option 0 (optional (char char_tod_sep) *> g_minute)
438 -- * Class 'Gram_Tag'
444 ) => Gram_Tag g where
447 <$ char char_tag_prefix
449 <*> option (Tag_Data "")
451 *> char char_tag_data_prefix
454 g_tag_path :: CF g Tag_Path
456 (\x xs -> Tag_Path $ NonNull.ncons x xs)
458 <*> many (try $ char char_tag_sep *> g_tag_section)
459 g_tag_section :: CF g Tag_Path_Section
462 <$> some (g_char `minus` g_char_attribute)
463 g_tag_value :: CF g Tag_Data
464 g_tag_value = Tag_Data <$> g_words
466 -- * Class 'Gram_Comment'
470 ) => Gram_Comment g where
471 g_comment :: CF g Comment
472 g_comment = rule "Comment" $
473 Comment <$ char ';' <* g_spaces <*> g_words
475 -- * Class 'Gram_Account'
481 ) => Gram_Account g where
482 g_account_section :: CF g NameAccount
485 <$> some (g_char `minus` g_char_attribute)
486 g_account :: CF g Account
487 g_account = rule "Account" $
488 Account . NonNull.impureNonNull
489 <$> some (try $ char '/' *> g_account_section)
490 g_account_tag :: CF g Account_Tag
494 <$ char char_account_tag_prefix
496 <*> option (Tag_Data "")
498 *> char char_tag_data_prefix
501 g_account_tag_path :: CF g Tag_Path
502 g_account_tag_path = rule "Tag_Path" $
503 char char_account_tag_prefix
506 g_anchor_section :: CF g Anchor_Section
507 g_anchor_section = rule "Anchor_Section" $
509 <$> some (g_char `minus` g_char_attribute)
512 -- * Class 'Gram_Amount'
517 ) => Gram_Amount g where
519 g_unit = rule "Unit" $
520 Unit . Text.singleton
521 <$> G.unicat (G.Unicat Char.CurrencySymbol)
522 g_quantity :: CF g (Quantity, Style_Amount)
523 g_quantity = rule "Quantity" $
524 (\(i, f, fr, gi, gf) ->
525 let int = concat i in
526 let frac = concat f in
527 let precision = length frac in
528 -- guard (precision <= 255)
529 let mantissa = integer_of_digits 10 $ int <> frac in
531 (fromIntegral precision)
534 { style_amount_fractioning=fr
535 , style_amount_grouping_integral=gi
536 , style_amount_grouping_fractional=gf
540 [ g_qty '_' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
541 , g_qty '_' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
542 , g_qty ',' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
543 , g_qty '.' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
546 :: Char -- ^ Integral grouping separator.
547 -> Char -- ^ Fractioning separator.
548 -> Char -- ^ Fractional grouping separator.
550 ( [String] -- integral
551 , [String] -- fractional
552 , S.Maybe Style_Amount_Fractioning -- fractioning
553 , S.Maybe Style_Amount_Grouping -- grouping_integral
554 , S.Maybe Style_Amount_Grouping -- grouping_fractional
556 g_qty int_group_sep frac_sep frac_group_sep =
563 , grouping_of_digits int_group_sep int
566 Just (fractioning, frac) ->
570 , grouping_of_digits int_group_sep int
571 , grouping_of_digits frac_group_sep $ L.reverse frac
575 <*> option [] (many $ try $ char int_group_sep *> some g_09))
576 <*> option Nothing (Just <$> ((,)
580 <*> option [] (many $ try $ char frac_group_sep *> some g_09))))
582 grouping_of_digits :: Char -> [String] -> S.Maybe Style_Amount_Grouping
583 grouping_of_digits group_sep digits =
588 Style_Amount_Grouping group_sep $
589 canonicalize_grouping $
591 canonicalize_grouping :: [Int] -> [Int]
592 canonicalize_grouping groups =
593 foldl' -- NOTE: remove duplicates at beginning and reverse.
594 (\acc l0 -> case acc of
595 l1:_ -> if l0 == l1 then acc else l0:acc
597 case groups of -- NOTE: keep only longer at beginning.
598 l0:l1:t -> if l0 > l1 then groups else l1:t
601 g_amount :: CF g (Styled_Amount Amount)
602 g_amount = rule "Amount" $ g_amount_minus <+> g_amount_plus
603 g_amount_minus :: CF g (Styled_Amount Amount)
607 <$> ((,) <$> g_unit <*> g_spaces)
613 (try $ flip (,) <$> g_spaces <*> g_unit) )
616 <$> ((,) <$> g_unit <*> g_spaces)
620 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
621 mk_amount side (unit, sp) (qty, sty) =
625 { style_amount_unit_side = S.Just side
626 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
629 { amount_quantity = negate qty
633 g_amount_plus :: CF g (Styled_Amount Amount)
637 <$> ((,) <$> g_unit <*> g_spaces)
643 (try $ flip (,) <$> g_spaces <*> g_unit) )
646 <$> ((,) <$> g_unit <*> g_spaces)
647 <* optional (char '+')
653 (try $ flip (,) <$> g_spaces <*> g_unit)
655 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
656 mk_amount side (unit, sp) (qty, sty) =
660 { style_amount_unit_side = S.Just side
661 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
664 { amount_quantity = qty
669 -- * Class 'Gram_Posting'
675 , G.Gram_Reader SourcePos g
676 , G.Gram_State (S.Maybe Unit) g
677 , G.Gram_State Chart g
678 , G.Gram_State Style_Amounts g
680 ) => Gram_Posting g where
682 G.Gram_Source src g =>
683 CF g (S.Either (At src (Error_Posting src)) [Posting src])
687 many (try $ g_spaces *> g_eol) *>
688 g_spaces1 *> g_posting
690 G.Gram_Source src g =>
691 CF g (S.Either (At src (Error_Posting src)) (Posting src))
692 g_posting = rule "Posting" $
693 G.stateAfter $ G.getAfter $ G.source $
696 posting_sourcepos ctx_unit
697 (sty_amts :: Style_Amounts) -> do
698 let (posting_tags, posting_comments) = attrs
699 let (stys, posting_amounts) =
701 Nothing -> (sty_amts, mempty)
703 (sty_amts H.+= (unit, sty),) $
704 Amounts $ Map.singleton unit $ amount_quantity amt
707 case amount_unit amt of
708 u | u == "" -> S.fromMaybe u ctx_unit
711 (posting_account, posting_account_ref) <- lr_acct
715 , posting_account_ref
722 <$> g_posting_account
723 <*> optional (try $ g_spaces1 *> g_amount)
726 G.Gram_Source src g =>
727 CF g (S.Either (At src (Error_Posting src))
728 (Account, S.Maybe (S.Pair Tag_Path (S.Maybe Account))))
729 g_posting_account = rule "Posting_Account" $
730 (S.Right . (, S.Nothing) <$> g_account) <+>
732 <$> (G.source $ G.getAfter $ expand_tag_path <$> g_account_tag_path)
733 <*> option S.Nothing (S.Just <$> g_account))
735 mk_posting_account path acct =
737 ( S.maybe a (a <>) acct
738 , S.Just (p S.:!: acct) ))
740 expand_tag_path tag chart src =
741 case Map.lookup tag $ chart_tags chart of
742 Just accts | Map.size accts > 0 ->
743 if Map.size accts == 1
745 let acct = fst $ Map.elemAt 0 accts in
747 else S.Left $ At src $ Error_Posting_Account_Ref_multiple tag accts
748 _ -> S.Left $ At src $ Error_Posting_Account_Ref_unknown tag
749 g_posting_tag :: CF g Posting_Tag
750 g_posting_tag = rule "Posting_Tag" $ Posting_Tag <$> g_tag
751 g_posting_attrs :: CF g (Posting_Tags, [Comment])
753 foldr ($) mempty . Compose
755 many (try $ g_spaces *> g_eol *> g_spaces1) *>
759 [ add_tag <$> g_posting_tag
760 , add_comment <$> g_comment
763 add_tag (Posting_Tag (Tag (Tag_Path p) v)) =
764 \(Posting_Tags (Tags tags), cmts) ->
765 ( Posting_Tags (Tags (TreeMap.insert (<>) p [v] tags))
771 -- * Class 'Gram_Transaction'
780 , G.Gram_State Section g
781 ) => Gram_Transaction g where
783 G.Gram_Source src g =>
784 CF g (S.Either (At src (Error_Transaction src)) (Transaction src))
785 g_transaction = rule "Transaction" $
786 G.stateAfter $ (update_year <$>) $
787 G.source $ G.source $
791 , transaction_comments )
793 transaction_sourcepos src -> do
794 date <- fmap Error_Transaction_Date `S.left` lr_date
795 posts <- fmap Error_Transaction_Posting `S.left` lr_posts
796 let postsByAcct = postings_by_account posts
800 , transaction_comments
801 , transaction_dates = NonNull.ncons date []
802 , transaction_wording
803 , transaction_postings = Postings postsByAcct
804 , transaction_sourcepos
806 case H.equilibrium postsByAcct of
807 (_, Left ko) -> S.Left $ At src $ Error_Transaction_not_equilibrated txn ko
808 (_bal, Right ok) -> S.Right txn{transaction_postings = Postings ok}
813 <*> g_transaction_attrs
816 update_year lr_txn y =
819 S.Right txn -> Year $ H.yearOf $ NonNull.head $ transaction_dates txn
821 g_wording :: CF g Wording
822 g_wording = rule "Wording" $
823 Wording . Text.concat
828 <$> some (g_char `minus` char char_tag_prefix)))
829 g_transaction_tag :: CF g Transaction_Tag
830 g_transaction_tag = rule "Transaction_Tag" $ Transaction_Tag <$> g_tag
831 g_transaction_attrs :: CF g (Transaction_Tags, [Comment])
832 g_transaction_attrs =
836 [ add_tag <$ many (try $ g_spaces *> g_eol *> g_spaces1) <*> g_transaction_tag
837 , add_comment <$ many (try $ g_spaces *> g_eol *> g_spaces) <*> g_comment
840 add_tag (Transaction_Tag (Tag (Tag_Path p) v)) =
841 \(Transaction_Tags (Tags tags), cmts) ->
842 ( Transaction_Tags (Tags (TreeMap.insert (<>) p [v] tags))
848 -- * Class 'Gram_File'
855 ) => Gram_File g where
856 g_pathfile :: CF g PathFile
857 g_pathfile = rule "PathFile" $
859 <$> some (try $ (:) <$> char '/' <*> some (g_char `minus` char '/'))
861 -- * Class 'Gram_Chart'
866 ) => Gram_Chart g where
868 G.Gram_Source src g =>
869 CF g (S.Either (At src (Error_Compta src)) Chart)
870 g_chart_entry = rule "Chart" $
872 let (tags, tags2, _comments) = attrs in
875 { chart_accounts = TreeMap.singleton (H.to acct) tags
876 , chart_tags = Map.singleton acct () <$ tags2
881 g_chart_attrs :: CF g (Account_Tags, Map Tag_Path (), [Comment])
885 many (try $ g_spaces *> g_eol) *>
887 [ add_tag <$ g_spaces1 <*> g_account_tag
888 , add_comment <$ g_spaces <*> g_comment
891 add_tag (Account_Tag (Tag (Tag_Path p) v)) =
892 \(Account_Tags (Tags tags), tags2, cmts) ->
893 ( Account_Tags (Tags (TreeMap.insert (<>) p [v] tags))
894 , Map.insert (Tag_Path p) () tags2
897 \(tags, tags2, cmts) ->
898 (tags, tags2, c:cmts)
900 -- * Class 'Gram_Input'
901 class Gram_Input g where
902 g_input :: g (Text -> a) -> g a
903 deriving instance Gram_Input g => Gram_Input (CF g)
905 -- * Class 'Gram_Term_Def'
908 ( G.Gram_Source src g
909 , Sym.Gram_Term src ss g
910 , G.SourceInj (Sym.TypeVT src) src
911 , G.SourceInj (Sym.KindK src) src
912 , G.SourceInj (Sym.AST_Type src) src
913 ) => Gram_Term_Def src ss g where
914 g_term_def :: CF g (S.Either (At src (Sym.NameTe, Sym.Error_Term src)) (Sym.NameTe, Sym.TermVT src ss '[]))
915 g_term_def = rule "TermDef" $
919 Sym.readTerm Sym.CtxTyZ $
920 foldr (\(x, ty_x) -> G.BinTree0 . Sym.Token_Term_Abst src x ty_x) v args in
922 Right t -> S.Right (n, t)
923 Left err -> S.Left $ At src (n, err)
926 <*> many Sym.g_term_abst_decl
934 , Sym.Gram_Term_Name g
935 ) => Gram_Term_Def src {-ss-} g where
936 g_term_def :: CF g (S.Either (At src (Error_Compta src))
938 g_term_def = rule "TermDef" $
941 <*> (Text.concat <$> many
942 ((Text.pack <$> some (G.any `minus` (G.char '\n' <+> G.char '\r'))) <+>
943 (try $ (<>) <$> g_eol <*> g_spaces1)))
946 -- * Class 'Gram_Compta'
948 ( G.Gram_Source src g
949 -- , G.Gram_Reader SourcePath g
950 -- , G.SourceInj SourcePath src
959 , Gram_Term_Def src {-ss-} g
960 , G.Gram_Reader (S.Either Exn.IOException CanonFile) g
961 , G.Gram_State (Context_Read src) g
962 , G.Gram_State (Terms src) g
963 -- , G.Gram_State (State_Sym src ss) g
964 -- , G.Gram_State (Sym.Imports Sym.NameTe, Sym.Modules src ss) g
965 -- , G.Gram_State (Journal j) g
966 -- , G.Gram_State (Journals j) g
970 ) => Gram_Compta {-ss-} src g where
971 g_compta :: CF g (S.Either [At src (Error_Compta src)] CanonFile)
972 g_compta = rule "Journal" $
973 G.stateAfter $ G.askBefore $
975 <$> (G.stateAfter $ G.source $ G.askBefore $ G.askBefore $ pure init_journal)
977 [ G.stateAfter $ mk_include <$> g_include {-@ss-}
978 -- NOTE: g_include must be the first choice
979 -- in order to have Megaparsec reporting the errors
980 -- of the included journal.
981 , G.stateAfter $ mk_transaction
982 <$> g_compta_section Section_Transactions g_transaction
983 , G.stateAfter $ mk_chart
984 <$> g_compta_section Section_Chart g_chart_entry
985 , {-G.stateBefore $ g_input $-} G.stateBefore $ G.source $ mk_term
986 <$> g_compta_section Section_Terms g_term_def
987 , ([], []) <$ try (g_spaces <* g_eol)
991 (SourcePos jf _ _) lr_cf src
993 { context_read_journals = Journals js
994 , context_read_journal = jnls
995 , context_read_canonfiles = cfs
997 }::Context_Read src) =
999 S.Left e -> (ctx, S.Left $ At src $ Error_Compta_Read (PathFile jf) e)
1003 { context_read_journals = Journals $ Map.insert cf jnl js
1004 , context_read_journal = jnl <| jnls
1005 , context_read_canonfiles = cf <| cfs
1008 where jnl = (journal H.zero){journal_file=PathFile jf}
1009 mk_journal err errs_warns
1012 { context_read_journals = Journals js
1013 , context_read_journal = jnl :| jnls
1014 , context_read_canonfiles = cf :| cfs
1015 , context_read_warnings = warnings
1017 }::Context_Read src) =
1018 let (errs, warns) = L.unzip errs_warns in
1019 case S.either pure (const []) err <> L.concat errs of
1021 let jnl' = jnl{journal_file=PathFile jf} in -- STUDYME: not necessary?
1024 { context_read_journals = Journals $ Map.insert cf jnl' js
1025 , context_read_journal = NonEmpty.fromList jnls
1026 , context_read_canonfiles = NonEmpty.fromList cfs
1027 , context_read_warnings = warnings <> L.concat warns
1030 es -> (ctx, S.Left es)
1031 mk_transaction lr_txn
1033 { context_read_journal = j :| js
1034 , context_read_consTxn
1036 }::Context_Read src) =
1038 S.Left err -> (ctx, ([err], []))
1039 S.Right txn -> (, ([], [])) Context_Read
1040 { context_read_journal = j{journal_content = txn `context_read_consTxn` journal_content j} :| js
1045 { context_read_journal = j :| js
1046 , context_read_consTxn
1048 }::Context_Read src) =
1050 S.Left errs -> (ctx, (errs, []))
1051 S.Right cf -> (, ([], [])) Context_Read
1052 { context_read_journal = j{journal_includes = journal_includes j <> [cf]} :| js
1057 { context_read_journal = j :| js
1058 , context_read_chart = ch
1060 }::Context_Read src) =
1062 S.Left err -> (ctx, ([err], []))
1063 S.Right chart -> (, ([], [])) Context_Read
1064 { context_read_journal = j{journal_chart = journal_chart j <> chart} :| js
1065 , context_read_chart = ch <> chart
1068 mk_term lr_nt src ts =
1070 S.Left err -> (ts, ([err], []))
1071 S.Right (n,t) -> (ins_body n (At src t) ts, ([], warn_redef n))
1073 ins_body :: Sym.NameTe -> At src Text -> Terms src -> Terms src
1074 ins_body n = Map.insert ([] `Sym.Mod` n)
1075 warn_redef :: Sym.NameTe -> [At src Warning_Compta]
1077 case Map.lookup ([] `Sym.Mod` n) ts of
1078 Just{} -> [At src $ Warning_Compta_Term_redefined n]
1081 mk_term lr_te src body (imps::Sym.Imports Sym.NameTe, mods) =
1083 S.Left err -> ((imps, mods), (, ([err], [])))
1084 S.Right (n, te) -> ((imps, ins_term n te mods), \ts -> (ins_body n body ts, ([], warn_redef n ts)))
1086 ins_term :: Sym.NameTe -> Sym.TermVT src ss '[] -> Sym.Modules src ss -> Sym.Modules src ss
1087 ins_term n t = Sym.insertTermVT ([] `Sym.Mod` t) n (Sym.Fixity2 Sym.infixN5)
1088 ins_body :: Sym.NameTe -> Text -> Terms -> Terms
1089 ins_body n = Map.insert ([] `Sym.Mod` n)
1090 warn_redef :: Sym.NameTe -> Terms -> [At src Warning_Compta]
1092 case Map.lookup ([] `Sym.Mod` n) ts of
1093 Just{} -> [At src $ Warning_Compta_Term_redefined n]
1096 g_include :: CF g (S.Either [At src (Error_Compta src)] CanonFile)
1097 g_include = rule "Include" $
1098 g_read g_path (g_compta {-@ss-} <* G.eoi)
1101 G.stateAfter $ G.source $ check_path
1102 <$> (g_canonfile $ G.askBefore $ (mk_path <$>) $
1103 (\d (PathFile p) -> PathFile $ d:p)
1104 <$> char '.' <*> g_pathfile)
1105 mk_path (PathFile fp) (SourcePos fp_old _ _) =
1107 FilePath.normalise $
1108 FilePath.takeDirectory fp_old </> fp
1109 check_path (fp, lr_cf) src
1111 { context_read_journals = Journals js
1112 , context_read_canonfiles = cfs
1113 , context_read_warnings = warns
1114 }::Context_Read src) =
1116 Left e -> (ctx, S.Left $ Error_Compta_Read fp e)
1118 if cf `Map.member` js
1121 then (ctx, S.Left $ Error_Compta_Include_loop cf)
1124 if isJust $ (`L.find` warns) $ \case
1125 At{unAt=Warning_Compta_Include_multiple cf'} -> cf' `elem` cf<|cfs
1129 { context_read_warnings =
1130 At src (Warning_Compta_Include_multiple cf) : warns }
1131 else (ctx, S.Right fp)
1135 -- | Return the 'Integer' obtained by multiplying the given digits
1136 -- with the power of the given base respective to their rank.
1138 :: Integer -- ^ Base.
1139 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1141 integer_of_digits base =
1142 foldl' (\x d -> base*x + toInteger (Char.digitToInt d)) 0
1144 -- | Return the 'Int' obtained by multiplying the given digits
1145 -- with the power of the given base respective to their rank.
1148 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1150 int_of_digits base =
1151 foldl' (\x d -> base*x + Char.digitToInt d) 0
1154 char_account_sep :: Char
1155 char_account_sep = '/'
1156 char_account_tag_prefix :: Char
1157 char_account_tag_prefix = '~'
1158 char_ymd_sep :: Char
1160 char_tod_sep :: Char
1162 char_comment_prefix :: Char
1163 char_comment_prefix = ';'
1164 char_tag_prefix :: Char
1165 char_tag_prefix = '#'
1166 char_tag_sep :: Char
1168 char_tag_data_prefix :: Char
1169 char_tag_data_prefix = '='
1170 char_transaction_date_sep :: Char
1171 char_transaction_date_sep = '='
1177 | Section_Transactions
1178 deriving (Eq, Ord, Show)
1182 Sym.ErrorInj err (Error_Compta src) =>
1183 G.Gram_State Section g =>
1184 G.Gram_Source src g =>
1187 g (S.Either (At src err) a) ->
1188 g (S.Either (At src (Error_Compta src)) a)
1189 g_compta_section sec g =
1190 G.stateBefore $ G.source $
1191 (<$> g) $ \a src sec_curr ->
1194 then (Sym.errorInj <$>) `S.left` a
1195 else S.Left $ At src $ Error_Compta_Section sec_curr sec
1198 newtype Year = Year (H.Date_Year Date)
1202 -- * Type 'Error_Date'
1204 = Error_Date_Day_invalid (Integer, Int, Int)
1205 | Error_Date_TimeOfDay_invalid (Int, Int, Int)
1206 | Error_Date_TimeZone_unknown Text
1209 -- * Type 'Error_Posting'
1210 data Error_Posting src
1211 = Error_Posting_Account_Ref_unknown Tag_Path
1212 | Error_Posting_Account_Ref_multiple Tag_Path (Map Account ())
1213 | Error_Postings_not_equilibrated (Postings src)
1216 -- * Type 'Error_Transaction'
1217 data Error_Transaction src
1218 = Error_Transaction_Date Error_Date
1219 | Error_Transaction_Posting (Error_Posting src)
1220 | Error_Transaction_not_equilibrated
1223 , H.SumByUnit (NonNull [NameAccount]) (H.Polarized Quantity)
1227 -- * Type 'Error_Chart'
1232 -- * Type 'Error_Compta'
1233 data Error_Compta src
1234 = Error_Compta_Transaction (Error_Transaction src)
1235 | Error_Compta_Read PathFile Exn.IOException
1236 | Error_Compta_Include_loop CanonFile
1237 | Error_Compta_Chart Error_Chart
1238 | Error_Compta_Section Section Section
1239 {- | Error_Compta_Term Sym.NameTe (Sym.Error_Term src) -}
1243 instance Sym.ErrorInj (Sym.NameTe,Sym.Error_Term src) Error_Compta where
1244 errorInj (n,t) = Error_Compta_Term n t
1246 instance Sym.ErrorInj (Error_Transaction src) (Error_Compta src) where
1247 errorInj = Error_Compta_Transaction
1248 instance Sym.ErrorInj (Error_Compta src) (Error_Compta src) where
1251 -- * Type 'Warning_Compta'
1253 = Warning_Compta_Include_multiple CanonFile
1254 | Warning_Compta_Term_redefined Sym.NameTe
1258 nonEmpty :: NonNull [a] -> NonEmpty a
1259 nonEmpty n = x :| xs where (x, xs) = NonNull.splitFirst n
1260 nonNull :: NonEmpty a -> NonNull [a]
1261 nonNull n = NonNull.ncons x xs where x :| xs = n