1 {-# LANGUAGE AllowAmbiguousTypes #-}
2 {-# LANGUAGE UndecidableSuperClasses #-}
3 module Hcompta.LCC.Grammar where
5 import Control.Applicative (Applicative(..), liftA2)
6 import Control.Monad (Monad(..), void)
8 import Data.Char (Char)
10 import Data.Either (Either(..), either)
11 import Data.Eq (Eq(..))
13 import Data.Function (($), (.), const, id, flip)
14 import Data.Functor (Functor(..), (<$>), (<$))
15 import Data.Functor.Compose (Compose(..))
16 import Data.List.NonEmpty (NonEmpty(..), (<|))
17 import Data.Map.Strict (Map)
18 import Data.Maybe (Maybe(..), maybe, isJust)
19 import Data.Monoid (Monoid(..))
20 import Data.NonNull (NonNull)
21 import Data.Ord (Ord(..))
22 import Data.Semigroup (Semigroup(..))
23 import Data.String (String)
24 import Data.Text (Text)
25 import Data.Time.LocalTime (TimeZone(..))
26 import Data.Traversable (sequenceA)
27 import Data.Tuple (fst)
28 import Data.Typeable ()
29 import Prelude (Int, Integer, Enum(..), Num(..), Integral(..), fromIntegral, error)
30 import System.FilePath ((</>))
31 import Text.Show (Show(..))
32 import qualified Control.Exception.Safe as Exn
33 import qualified Control.Monad.Classes as MC
34 import qualified Control.Monad.Trans.State.Strict as SS
35 import qualified Data.Char as Char
36 import qualified Data.List as L
37 import qualified Data.List.NonEmpty as NonEmpty
38 import qualified Data.Map.Strict as Map
39 import qualified Data.NonNull as NonNull
40 import qualified Data.Strict as S
41 import qualified Data.Text as Text
42 import qualified Data.Time.Calendar as Time
43 import qualified Data.Time.LocalTime as Time
44 import qualified Data.TreeMap.Strict as TreeMap
45 import qualified Hcompta as H
46 import qualified System.FilePath as FilePath
48 import qualified Language.Symantic.Grammar as G
49 import Language.Symantic.Grammar (CF, At(..), Gram_Rule(..), Gram_Terminal(..), Gram_Alt(..), Gram_AltApp(..), Gram_Try(..), Gram_CF(..))
50 import Language.Symantic.Lib ()
51 import qualified Language.Symantic as Sym
52 import qualified Language.Symantic.Grammar as Sym
54 import Hcompta.LCC.Account
55 import Hcompta.LCC.Name
56 import Hcompta.LCC.Tag
57 import Hcompta.LCC.Amount
58 import Hcompta.LCC.Chart
59 import Hcompta.LCC.Posting
60 import Hcompta.LCC.Transaction
61 import Hcompta.LCC.Journal
62 import Hcompta.LCC.Compta
64 import qualified Hcompta.LCC.Lib.Strict as S
67 import Debug.Trace (trace)
68 dbg :: Show a => String -> a -> a
69 dbg msg x = trace (msg <> " = " <> show x) x
72 -- * Type 'Context_Read'
73 data Context_Read src j
75 { context_read_year :: !Year
76 , context_read_style_amounts :: !Style_Amounts
77 , context_read_chart :: !Chart
78 , context_read_unit :: !(S.Maybe Unit)
79 , context_read_journals :: !(Journals j)
80 , context_read_journal :: !(NonEmpty (Journal j))
81 , context_read_canonfiles :: !(NonEmpty CanonFile)
82 , context_read_warnings :: ![At src Warning_Compta]
83 , context_read_section :: !Section
91 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffReader (NonEmpty CanonFile)) = 'True
92 instance Monad m => MC.MonadReaderN 'MC.Zero (NonEmpty CanonFile) (S.StateT (Context_Read src j) m) where
93 askN _n = MC.gets $ \(x::Context_Read src j) -> context_read_canonfiles x
96 -- States handled by a nested Monad
98 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Imports ns, Sym.Modules src ss)) = 'False
99 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Imports ns, Sym.ModulesTy src)) = 'False
100 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Env src ss)) = 'False
101 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Terms) = 'False
103 context_read :: Monoid j => Context_Read src j
106 { context_read_year = Year $ H.date_year (H.date_epoch::Date)
107 , context_read_style_amounts = mempty
108 , context_read_chart = mempty
109 , context_read_unit = S.Nothing
110 , context_read_journals = Journals Map.empty
111 , context_read_journal = journal :| []
112 , context_read_canonfiles = CanonFile "" :| []
113 , context_read_warnings = []
114 , context_read_section = Section_Terms
117 -- * Type 'Context_Sym'
118 data Context_Sym src ss
120 { context_sym_imports :: !(Sym.Imports Sym.NameTe)
121 , context_sym_importsTy :: !(Sym.Imports Sym.NameTy)
122 , context_sym_modules :: !(Sym.Modules src ss)
123 , context_sym_modulesTy :: !(Sym.ModulesTy src)
124 , context_sym_env :: !(Env src ss)
125 , context_sym_terms :: !Terms
126 } deriving (Eq, Show)
131 Sym.ImportTypes ss =>
132 Sym.ModulesInj src ss =>
133 Sym.ModulesTyInj ss =>
136 let mods = either (error . show) id Sym.modulesInj in
138 { context_sym_imports = Sym.importModules [] mods
139 , context_sym_importsTy = Sym.importTypes @ss []
140 , context_sym_modules = mods
141 , context_sym_modulesTy = Sym.modulesTyInj @ss
142 , context_sym_env = Map.empty
143 , context_sym_terms = Map.empty
150 -- (Sym.Imports Sym.NameTe, Sym.Modules src ss)
151 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Imports Sym.NameTe, Sym.Modules src ss)) = 'True
152 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Imports Sym.NameTe, Sym.Modules src ss) (S.StateT (Context_Sym src ss) m) where
153 stateN _px f = S.StateT $ SS.state $ \ctx ->
154 (\(imps, mods) -> ctx{context_sym_imports=imps, context_sym_modules=mods})
155 <$> f (context_sym_imports ctx, context_sym_modules ctx)
157 -- (Sym.Imports Sym.NameTy, Sym.ModulesTy src)
158 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Imports Sym.NameTy, Sym.ModulesTy src)) = 'True
159 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Imports Sym.NameTy, Sym.ModulesTy src) (S.StateT (Context_Sym src ss) m) where
160 stateN _px f = S.StateT $ SS.state $ \ctx ->
161 (\(imps, mods) -> ctx{context_sym_importsTy=imps, context_sym_modulesTy=mods})
162 <$> f (context_sym_importsTy ctx, context_sym_modulesTy ctx)
165 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState Terms) = 'True
166 instance Monad m => MC.MonadStateN 'MC.Zero Terms (S.StateT (Context_Sym src ss) m) where
167 stateN _px f = S.StateT $ SS.state $ \ctx ->
168 (\a -> ctx{context_sym_terms = a})
169 <$> f (context_sym_terms ctx)
172 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Env src ss)) = 'True
173 instance Monad m => MC.MonadStateN 'MC.Zero (Env src ss) (S.StateT (Context_Sym src ss) m) where
174 stateN _px f = S.StateT $ SS.state $ \ctx ->
175 (\a -> ctx{context_sym_env = a})
176 <$> f (context_sym_env ctx)
178 -- Context_Read src j
179 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Context_Read src j)) = 'True
180 instance Monad m => MC.MonadStateN 'MC.Zero (Context_Read src j) (S.StateT (Context_Read src j) m) where
181 stateN _px = S.StateT . SS.state
184 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (S.Maybe Unit)) = 'True
185 instance Monad m => MC.MonadStateN 'MC.Zero (S.Maybe Unit) (S.StateT (Context_Read src j) m) where
186 stateN _px f = S.StateT $ SS.state $ \ctx ->
187 (\a -> ctx{context_read_unit = a})
188 <$> f (context_read_unit ctx)
191 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Chart) = 'True
192 instance Monad m => MC.MonadStateN 'MC.Zero Chart (S.StateT (Context_Read src j) m) where
193 stateN _px f = S.StateT $ SS.state $ \ctx ->
194 (\a -> ctx{context_read_chart = a})
195 <$> f (context_read_chart ctx)
198 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Year) = 'True
199 instance Monad m => MC.MonadStateN 'MC.Zero Year (S.StateT (Context_Read src j) m) where
200 stateN _px f = S.StateT $ SS.state $ \ctx ->
201 (\a -> ctx{context_read_year = a})
202 <$> f (context_read_year ctx)
205 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Section) = 'True
206 instance Monad m => MC.MonadStateN 'MC.Zero Section (S.StateT (Context_Read src j) m) where
207 stateN _px f = S.StateT $ SS.state $ \ctx ->
208 (\a -> ctx{context_read_section = a})
209 <$> f (context_read_section ctx)
212 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Journal j)) = 'True
213 instance Monad m => MC.MonadStateN 'MC.Zero (Journal j) (S.StateT (Context_Read src j) m) where
214 stateN _px f = S.StateT $ SS.state $ \ctx@Context_Read{context_read_journal = j:|js} ->
215 (\a -> ctx{context_read_journal = a:|js}) <$> f j
218 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Journals j)) = 'True
219 instance Monad m => MC.MonadStateN 'MC.Zero (Journals j) (S.StateT (Context_Read src j) m) where
220 stateN _px f = S.StateT $ SS.state $ \ctx ->
221 (\a -> ctx{context_read_journals = a})
222 <$> f (context_read_journals ctx)
225 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Style_Amounts) = 'True
226 instance Monad m => MC.MonadStateN 'MC.Zero Style_Amounts (S.StateT (Context_Read src j) m) where
227 stateN _px f = S.StateT $ SS.state $ \ctx ->
228 (\s -> ctx{context_read_style_amounts = s})
229 <$> f (context_read_style_amounts ctx)
231 -- * Class 'Gram_Path'
232 class Gram_Path g where
235 -> g (PathFile, Either Exn.IOException CanonFile)
236 deriving instance Gram_Path g => Gram_Path (CF g)
239 class G.Gram_Source src g => Gram_IO src g where
241 :: g (S.Either (Error_Compta src) PathFile)
242 -> g (S.Either [At src (Error_Compta src)] (CanonFile, a))
243 -> g (S.Either [At src (Error_Compta src)] (CanonFile, a))
244 deriving instance Gram_IO src g => Gram_IO src (CF g)
246 -- * Class 'Gram_Count'
251 ) => Gram_Count g where
252 count :: Int -> CF g a -> CF g [a]
255 | otherwise = sequenceA $ L.replicate n p
256 count' :: Int -> Int -> CF g a -> CF g [a]
258 | n <= 0 || m > n = pure []
259 | m > 0 = (:) <$> p <*> count' (pred m) (pred n) p
261 let f t ts = maybe [] (:ts) t
262 in f <$> G.optional p <*> count' 0 (pred n) p
264 -- * Class 'Gram_Char'
274 ) => Gram_Char g where
276 g_eol = rule "EOL" $ void (char '\n') <+> void (G.string "\r\n")
278 g_tab = rule "Tab" $ void $ char '\t'
280 g_space = rule "Space" $ char ' '
281 g_spaces :: CF g Text
282 g_spaces = Text.pack <$> many g_space
284 g_spaces1 = void $ some g_space
286 g_char = g_char_passive <+> g_char_active
287 g_char_passive :: CF g Char
288 g_char_passive = choice $ G.unicat <$> [G.Unicat_Letter, G.Unicat_Number, G.Unicat_Mark]
289 g_char_active :: CF g Char
290 g_char_active = choice $ G.unicat <$> [G.Unicat_Punctuation, G.Unicat_Symbol]
291 g_char_attribute :: G.Reg lr g Char
292 g_char_attribute = choice $ char <$> "#/:;@~="
294 g_word = rule "Word" $ Text.pack <$> some g_char
296 g_words = Text.concat <$> many (try $ (<>) <$> g_spaces <*> g_word)
298 g_09 = range ('0', '9')
300 g_19 = range ('1', '9')
301 g_sign :: Num int => CF g (int -> int)
303 (negate <$ char '-') <+>
306 -- * Class 'Gram_Date'
308 ( G.Gram_State Year g
317 ) => Gram_Date g where
319 G.Gram_Source src g =>
320 CF g (S.Either (At src Error_Date) Date)
321 g_date = rule "Date" $
322 liftA2 (\day (tod, tz) ->
323 Time.localTimeToUTC tz $
324 Time.LocalTime day tod)
327 (S.Right (Time.midnight, Time.utc))
331 <*> option (S.Right Time.utc) g_timezone)
333 G.Gram_Source src g =>
334 CF g (S.Either (At src Error_Date) Time.Day)
345 <$> G.getAfter (pure $ \(Year y) -> y)
351 case Time.fromGregorianValid y m d of
352 Nothing -> S.Left $ At src $ Error_Date_Day_invalid (y, m, d)
353 Just day -> S.Right day
355 G.Gram_Source src g =>
356 CF g (S.Either (At src Error_Date) Time.TimeOfDay)
357 g_tod = rule "TimeOfDay" $
360 case Time.makeTimeOfDayValid hr mn (fromInteger $ toInteger sc) of
361 Nothing -> S.Left $ At src $ Error_Date_TimeOfDay_invalid (hr, mn, sc)
362 Just tod -> S.Right $ tod)
366 <$> (char char_tod_sep *> g_minute)
367 <*> option 0 (char char_tod_sep *> g_second))
368 g_year :: CF g Integer
369 g_year = rule "Year" $
370 (\sg y -> sg $ integer_of_digits 10 y)
371 <$> option id (negate <$ char '-')
374 g_month = rule "Month" $ int_of_digits 10 <$> count' 1 2 g_09
376 g_dom = rule "DayOfMonth" $ int_of_digits 10 <$> count' 1 2 g_09
378 g_hour = rule "Hour" $ int_of_digits 10 <$> count' 1 2 g_09
380 g_minute = rule "Minute" $ int_of_digits 10 <$> count' 1 2 g_09
382 g_second = rule "Second" $ int_of_digits 10 <$> count' 1 2 g_09
385 G.Gram_Source src g =>
386 CF g (S.Either (At src Error_Date) TimeZone)
387 g_timezone = rule "TimeZone" $
388 -- DOC: http://www.timeanddate.com/time/zones/
389 -- TODO: only a few time zones are suported below.
390 -- TODO: check the timeZoneSummerOnly values
391 (S.Right <$> g_timezone_digits) <+>
392 (G.source $ read_tz <$ char '_' <*> some (range ('A', 'Z')))
394 read_tz n src = case n of
395 "AST" -> S.Right $ TimeZone (- 4 * 60) False n
396 "ADT" -> S.Right $ TimeZone (- 3 * 60) True n
397 "A" -> S.Right $ TimeZone (- 1 * 60) False n
398 "BST" -> S.Right $ TimeZone (-11 * 60) False n
399 "BDT" -> S.Right $ TimeZone (-10 * 60) True n
400 "CET" -> S.Right $ TimeZone ( 1 * 60) True n
401 "CEST" -> S.Right $ TimeZone ( 2 * 60) False n
402 "CST" -> S.Right $ TimeZone (- 6 * 60) False n
403 "CDT" -> S.Right $ TimeZone (- 5 * 60) True n
404 "EST" -> S.Right $ TimeZone (- 5 * 60) False n
405 "EDT" -> S.Right $ TimeZone (- 4 * 60) True n
406 "GMT" -> S.Right $ TimeZone 0 False n
407 "HST" -> S.Right $ TimeZone (-10 * 60) False n
408 "HDT" -> S.Right $ TimeZone (- 9 * 60) True n
409 "MST" -> S.Right $ TimeZone (- 7 * 60) False n
410 "MDT" -> S.Right $ TimeZone (- 6 * 60) True n
411 "M" -> S.Right $ TimeZone (-12 * 60) False n
412 "NST" -> S.Right $ TimeZone (- 3 * 60 - 30) False n
413 "N" -> S.Right $ TimeZone ( 1 * 60) False n
414 "PST" -> S.Right $ TimeZone (- 8 * 60) False n
415 "PDT" -> S.Right $ TimeZone (- 7 * 60) True n
416 "YST" -> S.Right $ TimeZone (- 9 * 60) False n
417 "YDT" -> S.Right $ TimeZone (- 8 * 60) True n
418 "Y" -> S.Right $ TimeZone ( 12 * 60) False n
419 "Z" -> S.Right $ TimeZone 0 False n
420 _ -> S.Left $ At src $ Error_Date_TimeZone_unknown (Text.pack n)
421 g_timezone_digits :: CF g TimeZone
426 { timeZoneMinutes = sg $ hr * 60 + mn
427 , timeZoneSummerOnly = False
428 , timeZoneName = Time.timeZoneOffsetString tz
433 <*> option 0 (optional (char char_tod_sep) *> g_minute)
435 -- * Class 'Gram_Tag'
441 ) => Gram_Tag g where
444 <$ char char_tag_prefix
446 <*> option (Tag_Data "")
448 *> char char_tag_data_prefix
451 g_tag_path :: CF g Tag_Path
453 (\x xs -> Tag_Path $ NonNull.ncons x xs)
455 <*> many (try $ char char_tag_sep *> g_tag_section)
456 g_tag_section :: CF g Tag_Path_Section
459 <$> some (g_char `minus` g_char_attribute)
460 g_tag_value :: CF g Tag_Data
461 g_tag_value = Tag_Data <$> g_words
463 -- * Class 'Gram_Comment'
467 ) => Gram_Comment g where
468 g_comment :: CF g Comment
469 g_comment = rule "Comment" $
470 Comment <$ char ';' <* g_spaces <*> g_words
472 -- * Class 'Gram_Account'
478 ) => Gram_Account g where
479 g_account_section :: CF g Account_Section
482 <$> some (g_char `minus` g_char_attribute)
483 g_account :: CF g Account
484 g_account = rule "Account" $
485 Account . NonNull.impureNonNull
486 <$> some (try $ char '/' *> g_account_section)
487 g_account_tag :: CF g Account_Tag
491 <$ char char_account_tag_prefix
493 <*> option (Tag_Data "")
495 *> char char_tag_data_prefix
498 g_account_tag_path :: CF g Tag_Path
499 g_account_tag_path = rule "Tag_Path" $
500 char char_account_tag_prefix
503 g_anchor_section :: CF g Anchor_Section
504 g_anchor_section = rule "Anchor_Section" $
506 <$> some (g_char `minus` g_char_attribute)
509 -- * Class 'Gram_Amount'
514 ) => Gram_Amount g where
516 g_unit = rule "Unit" $
517 Unit . Text.singleton
518 <$> G.unicat (G.Unicat Char.CurrencySymbol)
519 g_quantity :: CF g (Quantity, Style_Amount)
520 g_quantity = rule "Quantity" $
521 (\(i, f, fr, gi, gf) ->
522 let int = concat i in
523 let frac = concat f in
524 let precision = length frac in
525 -- guard (precision <= 255)
526 let mantissa = integer_of_digits 10 $ int <> frac in
528 (fromIntegral precision)
531 { style_amount_fractioning=fr
532 , style_amount_grouping_integral=gi
533 , style_amount_grouping_fractional=gf
537 [ g_qty '_' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
538 , g_qty '_' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
539 , g_qty ',' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
540 , g_qty '.' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
543 :: Char -- ^ Integral grouping separator.
544 -> Char -- ^ Fractioning separator.
545 -> Char -- ^ Fractional grouping separator.
547 ( [String] -- integral
548 , [String] -- fractional
549 , S.Maybe Style_Amount_Fractioning -- fractioning
550 , S.Maybe Style_Amount_Grouping -- grouping_integral
551 , S.Maybe Style_Amount_Grouping -- grouping_fractional
553 g_qty int_group_sep frac_sep frac_group_sep =
560 , grouping_of_digits int_group_sep int
563 Just (fractioning, frac) ->
567 , grouping_of_digits int_group_sep int
568 , grouping_of_digits frac_group_sep $ L.reverse frac
572 <*> option [] (many $ try $ char int_group_sep *> some g_09))
573 <*> option Nothing (Just <$> ((,)
577 <*> option [] (many $ try $ char frac_group_sep *> some g_09))))
579 grouping_of_digits :: Char -> [String] -> S.Maybe Style_Amount_Grouping
580 grouping_of_digits group_sep digits =
585 Style_Amount_Grouping group_sep $
586 canonicalize_grouping $
588 canonicalize_grouping :: [Int] -> [Int]
589 canonicalize_grouping groups =
590 foldl' -- NOTE: remove duplicates at beginning and reverse.
591 (\acc l0 -> case acc of
592 l1:_ -> if l0 == l1 then acc else l0:acc
594 case groups of -- NOTE: keep only longer at beginning.
595 l0:l1:t -> if l0 > l1 then groups else l1:t
598 g_amount :: CF g (Styled_Amount Amount)
599 g_amount = rule "Amount" $ g_amount_minus <+> g_amount_plus
600 g_amount_minus :: CF g (Styled_Amount Amount)
604 <$> ((,) <$> g_unit <*> g_spaces)
609 <*> option ("", H.unit_empty)
610 (try $ flip (,) <$> g_spaces <*> g_unit) )
613 <$> ((,) <$> g_unit <*> g_spaces)
617 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
618 mk_amount side (unit, sp) (qty, sty) =
622 { style_amount_unit_side = S.Just side
623 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
626 { amount_quantity = negate qty
630 g_amount_plus :: CF g (Styled_Amount Amount)
634 <$> ((,) <$> g_unit <*> g_spaces)
639 <*> option ("", H.unit_empty)
640 (try $ flip (,) <$> g_spaces <*> g_unit) )
643 <$> ((,) <$> g_unit <*> g_spaces)
644 <* optional (char '+')
649 <*> option ("", H.unit_empty)
650 (try $ flip (,) <$> g_spaces <*> g_unit)
652 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
653 mk_amount side (unit, sp) (qty, sty) =
657 { style_amount_unit_side = S.Just side
658 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
661 { amount_quantity = qty
666 -- * Class 'Gram_Posting'
672 , G.Gram_Reader SourcePos g
673 , G.Gram_State (S.Maybe Unit) g
674 , G.Gram_State Chart g
675 , G.Gram_State Style_Amounts g
677 ) => Gram_Posting g where
679 G.Gram_Source src g =>
680 CF g (S.Either (At src Error_Posting) [Posting])
684 many (try $ g_spaces *> g_eol) *>
685 g_spaces1 *> g_posting
687 G.Gram_Source src g =>
688 CF g (S.Either (At src Error_Posting) Posting)
689 g_posting = rule "Posting" $
690 G.stateAfter $ G.getAfter $ G.askBefore $
693 posting_sourcepos ctx_unit
694 (Style_Amounts ctx_stys) -> do
695 let (posting_tags, posting_comments) = attrs
696 let (stys, posting_amounts) =
698 Nothing -> (Style_Amounts ctx_stys, mempty)
702 Map.insertWith (flip (<>))
706 case amount_unit amt of
707 u | u == H.unit_empty -> S.fromMaybe u ctx_unit
714 (posting_account, posting_account_ref) <- lr_acct
718 , posting_account_ref
725 <$> g_posting_account
726 <*> optional (try $ g_spaces1 *> g_amount)
729 G.Gram_Source src g =>
730 CF g (S.Either (At src Error_Posting)
731 (Account, S.Maybe (S.Pair Tag_Path (S.Maybe Account))))
732 g_posting_account = rule "Posting_Account" $
733 (S.Right . (, S.Nothing) <$> g_account) <+>
735 <$> (G.source $ G.getAfter $ expand_tag_path <$> g_account_tag_path)
736 <*> option S.Nothing (S.Just <$> g_account))
738 mk_posting_account path acct =
741 (S.maybe a (a <>) acct)
742 (S.Just (p S.:!: acct)) )
744 expand_tag_path tag chart src =
745 case Map.lookup tag $ chart_tags chart of
746 Just accts | Map.size accts > 0 ->
747 if Map.size accts == 1
749 let acct = fst $ Map.elemAt 0 accts in
751 else S.Left $ At src $ Error_Posting_Account_Ref_multiple tag accts
752 _ -> S.Left $ At src $ Error_Posting_Account_Ref_unknown tag
753 g_posting_tag :: CF g Posting_Tag
754 g_posting_tag = rule "Posting_Tag" $ Posting_Tag <$> g_tag
755 g_posting_attrs :: CF g (Posting_Tags, [Comment])
757 foldr ($) mempty . Compose
759 many (try $ g_spaces *> g_eol *> g_spaces1) *>
763 [ add_tag <$> g_posting_tag
764 , add_comment <$> g_comment
767 add_tag (Posting_Tag (Tag (Tag_Path p) v)) =
768 \(Posting_Tags (Tags tags), cmts) ->
769 ( Posting_Tags (Tags (TreeMap.insert (<>) p [v] tags))
775 -- * Class 'Gram_Transaction'
784 , G.Gram_State Section g
785 ) => Gram_Transaction g where
787 G.Gram_Source src g =>
788 CF g (S.Either (At src Error_Transaction) Transaction)
789 g_transaction = rule "Transaction" $
790 G.stateAfter $ (update_year <$>) $
791 G.source $ G.askBefore $
795 , transaction_comments )
797 transaction_sourcepos src -> do
798 date <- fmap Error_Transaction_Date `S.left` lr_date
799 posts <- fmap Error_Transaction_Posting `S.left` lr_posts
800 let postsByAcct = postings_by_account posts
804 , transaction_comments
805 , transaction_dates = NonNull.ncons date []
806 , transaction_wording
807 , transaction_postings = Postings postsByAcct
808 , transaction_sourcepos
810 case H.equilibrium postsByAcct of
811 (_, Left ko) -> S.Left $ At src $ Error_Transaction_not_equilibrated txn ko
812 (_bal, Right ok) -> S.Right txn{transaction_postings = Postings ok}
817 <*> g_transaction_attrs
820 update_year lr_txn y =
823 S.Right txn -> Year $ H.date_year $ NonNull.head $ transaction_dates txn
825 g_wording :: CF g Wording
826 g_wording = rule "Wording" $
827 Wording . Text.concat
832 <$> some (g_char `minus` char char_tag_prefix)))
833 g_transaction_tag :: CF g Transaction_Tag
834 g_transaction_tag = rule "Transaction_Tag" $ Transaction_Tag <$> g_tag
835 g_transaction_attrs :: CF g (Transaction_Tags, [Comment])
836 g_transaction_attrs =
840 [ add_tag <$ many (try $ g_spaces *> g_eol *> g_spaces1) <*> g_transaction_tag
841 , add_comment <$ many (try $ g_spaces *> g_eol *> g_spaces) <*> g_comment
844 add_tag (Transaction_Tag (Tag (Tag_Path p) v)) =
845 \(Transaction_Tags (Tags tags), cmts) ->
846 ( Transaction_Tags (Tags (TreeMap.insert (<>) p [v] tags))
852 -- * Class 'Gram_File'
859 ) => Gram_File g where
860 g_pathfile :: CF g PathFile
861 g_pathfile = rule "PathFile" $
863 <$> some (try $ (:) <$> char '/' <*> some (g_char `minus` char '/'))
865 -- * Class 'Gram_Chart'
870 , G.Gram_State Chart g
871 , G.Gram_State Section g
873 ) => Gram_Chart g where
875 G.Gram_Source src g =>
876 CF g (S.Either (At src (Error_Compta src)) Chart)
877 g_chart_entry = rule "Chart" $
879 let (tags, tags2, _comments) = attrs in
882 { chart_accounts = TreeMap.singleton (H.get acct) tags
883 , chart_tags = Map.singleton acct () <$ tags2
888 g_chart_attrs :: CF g (Account_Tags, Map Tag_Path (), [Comment])
892 many (try $ g_spaces *> g_eol) *>
894 [ add_tag <$ g_spaces1 <*> g_account_tag
895 , add_comment <$ g_spaces <*> g_comment
898 add_tag (Account_Tag (Tag (Tag_Path p) v)) =
899 \(Account_Tags (Tags tags), tags2, cmts) ->
900 ( Account_Tags (Tags (TreeMap.insert (<>) p [v] tags))
901 , Map.insert (Tag_Path p) () tags2
904 \(tags, tags2, cmts) ->
905 (tags, tags2, c:cmts)
907 class Gram_Input g where
908 g_input :: g (Text -> a) -> g a
909 deriving instance Gram_Input g => Gram_Input (CF g)
911 -- * Class 'Gram_Term_Def'
913 ( G.Gram_Source src g
914 , Sym.Gram_Term src ss g
915 , G.SourceInj (Sym.TypeVT src) src
916 , G.SourceInj (Sym.KindK src) src
917 , G.SourceInj (Sym.AST_Type src) src
918 ) => Gram_Term_Def src ss g where
919 g_term_def :: CF g (S.Either (At src (Sym.NameTe, Sym.Error_Term src)) (Sym.NameTe, Sym.TermVT src ss '[]))
920 g_term_def = rule "TermDef" $
924 Sym.readTerm Sym.CtxTyZ $
925 foldr (\(x, ty_x) -> G.BinTree0 . Sym.Token_Term_Abst src x ty_x) v args in
927 Right t -> S.Right (n, t)
928 Left err -> S.Left $ At src (n, err)
931 <*> many Sym.g_term_abst_decl
935 -- * Class 'Gram_Compta'
937 ( G.Gram_Source src g
946 , Gram_Term_Def src ss g
947 , G.Gram_Reader (S.Either Exn.IOException CanonFile) g
948 , G.Gram_State (Context_Read src j) g
949 , G.Gram_State (Sym.Imports Sym.NameTe, Sym.Modules src ss) g
950 , G.Gram_State (Journal j) g
951 , G.Gram_State (Journals j) g
952 , G.Gram_State Terms g
956 ) => Gram_Compta ss src j g where
958 :: (Transaction -> j -> j)
959 -> CF g (S.Either [At src (Error_Compta src)]
960 (CanonFile, Journal j))
961 g_compta consTxn = rule "Journal" $
962 G.stateAfter $ G.askBefore $
964 <$> (G.stateAfter $ G.source $ G.askBefore $ G.askBefore $ pure init_journal)
966 [ G.stateAfter $ mk_include <$> g_include @ss consTxn
967 -- NOTE: g_include must be the first choice
968 -- in order to have Megaparsec reporting the errors
969 -- of the included journal.
970 , G.stateAfter $ mk_transaction
971 <$> g_compta_section Section_Transactions g_transaction
972 , G.stateAfter $ mk_chart
973 <$> g_compta_section Section_Chart g_chart_entry
974 , G.stateBefore $ G.stateBefore $ g_input $ G.source $ mk_term
975 <$> g_compta_section Section_Terms g_term_def
976 , ([], []) <$ try (g_spaces <* g_eol)
980 (SourcePos jf _ _) lr_cf src
982 { context_read_journals = Journals js
983 , context_read_journal = jnls
984 , context_read_canonfiles = cfs
985 }::Context_Read src j) =
987 S.Left e -> (ctx, S.Left $ At src $ Error_Compta_Read (PathFile jf) e)
989 let jnl = journal{journal_file=PathFile jf} in
992 { context_read_journals = Journals $ Map.insert cf jnl js
993 , context_read_journal = jnl <| jnls
994 , context_read_canonfiles = cf <| cfs
996 mk_journal err errs_warns
999 { context_read_journals = Journals js
1000 , context_read_journal = jnl :| jnls
1001 , context_read_canonfiles = cf :| cfs
1002 , context_read_warnings = warnings
1003 }::Context_Read src j) =
1004 let (errs, warns) = L.unzip errs_warns in
1005 case S.either pure (const []) err <> L.concat errs of
1007 let jnl' = jnl{journal_file=PathFile jf} in
1008 (,S.Right (cf, jnl'))
1010 { context_read_journals = Journals $ Map.insert cf jnl' js
1011 , context_read_journal = NonEmpty.fromList jnls
1012 , context_read_canonfiles = NonEmpty.fromList cfs
1013 , context_read_warnings = warnings <> L.concat warns
1015 es -> (ctx, S.Left es)
1016 mk_transaction lr_txn jnl@Journal{journal_content=j} =
1018 S.Left err -> (jnl, ([err], []))
1019 S.Right txn -> (jnl{ journal_content = txn `consTxn` j }, ([], []))
1020 mk_include lr_inc (jnl::Journal j) =
1022 S.Left errs -> (jnl, (errs, []))
1023 S.Right (cf, _inc) -> (jnl{ journal_includes = journal_includes jnl <> [cf] }, ([], []))
1024 mk_chart lr_ch chart =
1026 S.Left err -> (chart, ([err], []))
1027 S.Right ch -> (chart <> ch, ([], []))
1028 mk_term lr_te src body (imps::Sym.Imports Sym.NameTe, mods) =
1030 S.Left err -> ((imps, mods), (, ([err], [])))
1031 S.Right (n, te) -> ((imps, ins_term n te mods), \ts -> (ins_body n body ts, ([], warn_redef n ts)))
1033 ins_term :: Sym.NameTe -> Sym.TermVT src ss '[] -> Sym.Modules src ss -> Sym.Modules src ss
1034 ins_term n t = Sym.insertTermVT ([] `Sym.Mod` t) n (Sym.Fixity2 Sym.infixN5)
1035 ins_body :: Sym.NameTe -> Text -> Terms -> Terms
1036 ins_body n = Map.insert ([] `Sym.Mod` n)
1037 warn_redef :: Sym.NameTe -> Terms -> [At src Warning_Compta]
1039 case Map.lookup ([] `Sym.Mod` n) ts of
1040 Just{} -> [At src $ Warning_Compta_Term_redefined n]
1043 :: (Transaction -> j -> j)
1044 -> CF g (S.Either [At src (Error_Compta src)]
1045 (CanonFile, Journal j))
1046 g_include consTxn = rule "Include" $
1047 g_read g_path (g_compta @ss consTxn <* G.eoi)
1050 G.stateAfter $ G.source $ check_path
1051 <$> (g_canonfile $ G.askBefore $ fmap mk_path $
1052 (\d (PathFile p) -> PathFile $ d:p)
1053 <$> char '.' <*> g_pathfile)
1054 mk_path (PathFile fp) (SourcePos fp_old _ _) =
1056 FilePath.normalise $
1057 FilePath.takeDirectory fp_old </> fp
1058 check_path (fp, lr_cf) src
1060 { context_read_journals = Journals js
1061 , context_read_canonfiles = cfs
1062 , context_read_warnings = warns
1063 }::Context_Read src j) =
1065 Left e -> (ctx, S.Left $ Error_Compta_Read fp e)
1067 if cf `Map.member` js
1070 then (ctx, S.Left $ Error_Compta_Include_loop cf)
1073 if isJust $ (`L.find` warns) $ \case
1074 At{unAt=Warning_Compta_Include_multiple cf'} -> cf' `elem` cf<|cfs
1078 { context_read_warnings =
1079 At src (Warning_Compta_Include_multiple cf) : warns }
1080 else (ctx, S.Right fp)
1084 -- | Return the 'Integer' obtained by multiplying the given digits
1085 -- with the power of the given base respective to their rank.
1087 :: Integer -- ^ Base.
1088 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1090 integer_of_digits base =
1091 foldl' (\x d -> base*x + toInteger (Char.digitToInt d)) 0
1093 -- | Return the 'Int' obtained by multiplying the given digits
1094 -- with the power of the given base respective to their rank.
1097 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1099 int_of_digits base =
1100 foldl' (\x d -> base*x + Char.digitToInt d) 0
1103 char_account_sep :: Char
1104 char_account_sep = '/'
1105 char_account_tag_prefix :: Char
1106 char_account_tag_prefix = '~'
1107 char_ymd_sep :: Char
1109 char_tod_sep :: Char
1111 char_comment_prefix :: Char
1112 char_comment_prefix = ';'
1113 char_tag_prefix :: Char
1114 char_tag_prefix = '#'
1115 char_tag_sep :: Char
1117 char_tag_data_prefix :: Char
1118 char_tag_data_prefix = '='
1119 char_transaction_date_sep :: Char
1120 char_transaction_date_sep = '='
1123 type Env src ss = Map Sym.NameTe (Sym.TermVT src ss '[])
1129 | Section_Transactions
1130 deriving (Eq, Ord, Show)
1134 Sym.ErrorInj err (Error_Compta src) =>
1135 G.Gram_State Section g =>
1136 G.Gram_Source src g =>
1139 g (S.Either (At src err) a) ->
1140 g (S.Either (At src (Error_Compta src)) a)
1141 g_compta_section sec g =
1142 G.stateBefore $ G.source $
1146 then fmap Sym.errorInj `S.left` a
1147 else S.Left $ At src $ Error_Compta_Section sec_curr sec
1151 newtype Year = Year (H.Date_Year Date)
1155 -- * Type 'Error_Date'
1157 = Error_Date_Day_invalid (Integer, Int, Int)
1158 | Error_Date_TimeOfDay_invalid (Int, Int, Int)
1159 | Error_Date_TimeZone_unknown Text
1162 -- * Type 'Error_Posting'
1164 = Error_Posting_Account_Ref_unknown Tag_Path
1165 | Error_Posting_Account_Ref_multiple Tag_Path (Map Account ())
1166 | Error_Postings_not_equilibrated Postings
1169 -- * Type 'Error_Transaction'
1170 data Error_Transaction
1171 = Error_Transaction_Date Error_Date
1172 | Error_Transaction_Posting Error_Posting
1173 | Error_Transaction_not_equilibrated
1176 , H.SumByUnit (NonNull [Account_Section]) (H.Polarized Quantity)
1180 -- * Type 'Error_Chart'
1185 -- * Type 'Error_Compta'
1186 data Error_Compta src
1187 = Error_Compta_Transaction Error_Transaction
1188 | Error_Compta_Read PathFile Exn.IOException
1189 | Error_Compta_Include_loop CanonFile
1190 | Error_Compta_Chart Error_Chart
1191 | Error_Compta_Section Section Section
1192 | Error_Compta_Term Sym.NameTe (Sym.Error_Term src)
1195 instance Sym.ErrorInj (Sym.NameTe, Sym.Error_Term src) (Error_Compta src) where
1196 errorInj (n, t) = Error_Compta_Term n t
1197 instance Sym.ErrorInj Error_Transaction (Error_Compta src) where
1198 errorInj = Error_Compta_Transaction
1199 instance Sym.ErrorInj (Error_Compta src) (Error_Compta src) where
1202 -- * Type 'Warning_Compta'
1204 = Warning_Compta_Include_multiple CanonFile
1205 | Warning_Compta_Term_redefined Sym.NameTe
1209 nonEmpty :: NonNull [a] -> NonEmpty a
1210 nonEmpty n = x :| xs where (x, xs) = NonNull.splitFirst n
1211 nonNull :: NonEmpty a -> NonNull [a]
1212 nonNull n = NonNull.ncons x xs where x :| xs = n