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 Language.Symantic.Grammar hiding (Side(..), Gram_Comment(..))
49 import Language.Symantic.Lib ()
50 import qualified Language.Symantic as Sym
51 import qualified Language.Symantic.Grammar as Sym
53 import Hcompta.LCC.Account
54 import Hcompta.LCC.Name
55 import Hcompta.LCC.Tag
56 import Hcompta.LCC.Amount
57 import Hcompta.LCC.Chart
58 import Hcompta.LCC.Posting
59 import Hcompta.LCC.Transaction
60 import Hcompta.LCC.Journal
62 import qualified Hcompta.LCC.Lib.Strict as S
65 import Debug.Trace (trace)
66 dbg :: Show a => String -> a -> a
67 dbg msg x = trace (msg <> " = " <> show x) x
71 type Terms = Map (Sym.Mod Sym.NameTe) Text
73 -- * Type 'Context_Read'
74 data Context_Read src j
76 { context_read_year :: !Year
77 , context_read_style_amounts :: !Style_Amounts
78 , context_read_chart :: !Chart
79 , context_read_unit :: !(S.Maybe Unit)
80 , context_read_journals :: !(Journals j)
81 , context_read_journal :: !(NonEmpty (Journal j))
82 , context_read_canonfiles :: !(NonEmpty CanonFile)
83 , context_read_warnings :: ![At src Warning_Compta]
84 , context_read_section :: !Section
92 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffReader (NonEmpty CanonFile)) = 'True
93 instance Monad m => MC.MonadReaderN 'MC.Zero (NonEmpty CanonFile) (S.StateT (Context_Read src j) m) where
94 askN _n = MC.gets $ \(x::Context_Read src j) -> context_read_canonfiles x
97 -- States handled by a nested Monad
99 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Modules src ss)) = 'False
100 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Sym.Imports) = 'False
101 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Imports, Sym.Modules src ss)) = 'False
102 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Name2Type src)) = 'False
103 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Env src ss)) = 'False
104 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Terms) = 'False
106 context_read :: Monoid j => Context_Read src j
109 { context_read_year = Year $ H.date_year (H.date_epoch::Date)
110 , context_read_style_amounts = mempty
111 , context_read_chart = mempty
112 , context_read_unit = S.Nothing
113 , context_read_journals = Journals Map.empty
114 , context_read_journal = journal :| []
115 , context_read_canonfiles = CanonFile "" :| []
116 , context_read_warnings = []
117 , context_read_section = Section_Terms
120 -- * Type 'Context_Sym'
121 data Context_Sym src ss
123 { context_sym_imports :: !Sym.Imports
124 , context_sym_modules :: !(Sym.Modules src ss)
125 , context_sym_name2type :: !(Sym.Name2Type src)
126 , context_sym_env :: !(Env src ss)
127 , context_sym_terms :: !Terms
128 } deriving (Eq, Show)
133 Sym.Inj_Modules src ss =>
134 Sym.Inj_Name2Type ss =>
137 let mods = either (error . show) id Sym.inj_Modules in
139 { context_sym_imports = Sym.importQualifiedAs [] mods
140 , context_sym_modules = mods
141 , context_sym_name2type = Sym.inj_Name2Type @ss
142 , context_sym_env = Map.empty
143 , context_sym_terms = Map.empty
150 -- Sym.Modules src ss
151 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Modules src ss)) = 'True
152 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Modules src ss) (S.StateT (Context_Sym src ss) m) where
153 stateN _px f = S.StateT $ SS.state $ \ctx ->
154 (\a -> ctx{context_sym_modules = a})
155 <$> f (context_sym_modules ctx)
158 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState Sym.Imports) = 'True
159 instance Monad m => MC.MonadStateN 'MC.Zero Sym.Imports (S.StateT (Context_Sym src ss) m) where
160 stateN _px f = S.StateT $ SS.state $ \ctx ->
161 (\a -> ctx{context_sym_imports = a})
162 <$> f (context_sym_imports ctx)
164 -- (Sym.Imports, Sym.Modules src ss)
165 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Imports, Sym.Modules src ss)) = 'True
166 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Imports, Sym.Modules src ss) (S.StateT (Context_Sym src ss) m) where
167 stateN _px f = S.StateT $ SS.state $ \ctx ->
168 (\(imps, mods) -> ctx{context_sym_imports=imps, context_sym_modules=mods})
169 <$> f (context_sym_imports ctx, context_sym_modules ctx)
172 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState Terms) = 'True
173 instance Monad m => MC.MonadStateN 'MC.Zero Terms (S.StateT (Context_Sym src ss) m) where
174 stateN _px f = S.StateT $ SS.state $ \ctx ->
175 (\a -> ctx{context_sym_terms = a})
176 <$> f (context_sym_terms ctx)
179 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Name2Type src)) = 'True
180 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Name2Type src) (S.StateT (Context_Sym src ss) m) where
181 stateN _px f = S.StateT $ SS.state $ \ctx ->
182 (\a -> ctx{context_sym_name2type = a})
183 <$> f (context_sym_name2type ctx)
186 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Env src ss)) = 'True
187 instance Monad m => MC.MonadStateN 'MC.Zero (Env src ss) (S.StateT (Context_Sym src ss) m) where
188 stateN _px f = S.StateT $ SS.state $ \ctx ->
189 (\a -> ctx{context_sym_env = a})
190 <$> f (context_sym_env ctx)
192 -- Context_Read src j
193 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Context_Read src j)) = 'True
194 instance Monad m => MC.MonadStateN 'MC.Zero (Context_Read src j) (S.StateT (Context_Read src j) m) where
195 stateN _px = S.StateT . SS.state
198 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (S.Maybe Unit)) = 'True
199 instance Monad m => MC.MonadStateN 'MC.Zero (S.Maybe Unit) (S.StateT (Context_Read src j) m) where
200 stateN _px f = S.StateT $ SS.state $ \ctx ->
201 (\a -> ctx{context_read_unit = a})
202 <$> f (context_read_unit ctx)
205 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Chart) = 'True
206 instance Monad m => MC.MonadStateN 'MC.Zero Chart (S.StateT (Context_Read src j) m) where
207 stateN _px f = S.StateT $ SS.state $ \ctx ->
208 (\a -> ctx{context_read_chart = a})
209 <$> f (context_read_chart ctx)
212 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Year) = 'True
213 instance Monad m => MC.MonadStateN 'MC.Zero Year (S.StateT (Context_Read src j) 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 j) m) (MC.EffState Section) = 'True
220 instance Monad m => MC.MonadStateN 'MC.Zero Section (S.StateT (Context_Read src j) 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 j) m) (MC.EffState (Journal j)) = 'True
227 instance Monad m => MC.MonadStateN 'MC.Zero (Journal j) (S.StateT (Context_Read src j) m) where
228 stateN _px f = S.StateT $ SS.state $ \ctx@Context_Read{context_read_journal = j:|js} ->
229 (\a -> ctx{context_read_journal = a:|js}) <$> f j
232 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Journals j)) = 'True
233 instance Monad m => MC.MonadStateN 'MC.Zero (Journals j) (S.StateT (Context_Read src j) m) where
234 stateN _px f = S.StateT $ SS.state $ \ctx ->
235 (\a -> ctx{context_read_journals = a})
236 <$> f (context_read_journals ctx)
239 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Style_Amounts) = 'True
240 instance Monad m => MC.MonadStateN 'MC.Zero Style_Amounts (S.StateT (Context_Read src j) m) where
241 stateN _px f = S.StateT $ SS.state $ \ctx ->
242 (\s -> ctx{context_read_style_amounts = s})
243 <$> f (context_read_style_amounts ctx)
245 -- * Class 'Gram_Path'
246 class Gram_Path g where
249 -> g (PathFile, Either Exn.IOException CanonFile)
250 deriving instance Gram_Path g => Gram_Path (CF g)
253 class Gram_Source src g => Gram_IO src g where
255 :: g (S.Either (Error_Compta src) PathFile)
256 -> g (S.Either [At src (Error_Compta src)] (CanonFile, a))
257 -> g (S.Either [At src (Error_Compta src)] (CanonFile, a))
258 deriving instance Gram_IO src g => Gram_IO src (CF g)
260 -- * Class 'Gram_Count'
265 ) => Gram_Count g where
266 count :: Int -> CF g a -> CF g [a]
269 | otherwise = sequenceA $ L.replicate n p
270 count' :: Int -> Int -> CF g a -> CF g [a]
272 | n <= 0 || m > n = pure []
273 | m > 0 = (:) <$> p <*> count' (pred m) (pred n) p
275 let f t ts = maybe [] (:ts) t
276 in f <$> optional p <*> count' 0 (pred n) p
278 -- * Class 'Gram_Char'
288 ) => Gram_Char g where
290 g_eol = rule "EOL" $ void (char '\n') <+> void (string "\r\n")
292 g_tab = rule "Tab" $ void $ char '\t'
294 g_space = rule "Space" $ char ' '
295 g_spaces :: CF g Text
296 g_spaces = Text.pack <$> many g_space
298 g_spaces1 = void $ some g_space
300 g_char = g_char_passive <+> g_char_active
301 g_char_passive :: CF g Char
302 g_char_passive = choice $ unicat <$> [Unicat_Letter, Unicat_Number, Unicat_Mark]
303 g_char_active :: CF g Char
304 g_char_active = choice $ unicat <$> [Unicat_Punctuation, Unicat_Symbol]
305 g_char_attribute :: Reg lr g Char
306 g_char_attribute = choice $ char <$> "#/:;@~="
308 g_word = rule "Word" $ Text.pack <$> some g_char
310 g_words = Text.concat <$> many (try $ (<>) <$> g_spaces <*> g_word)
312 g_09 = range ('0', '9')
314 g_19 = range ('1', '9')
315 g_sign :: Num int => CF g (int -> int)
317 (negate <$ char '-') <+>
320 -- * Class 'Gram_Date'
331 ) => Gram_Date g where
334 CF g (S.Either (At src Error_Date) Date)
335 g_date = rule "Date" $
336 (liftA2 $ \day (tod, tz) ->
337 Time.localTimeToUTC tz $
338 Time.LocalTime day tod)
341 (S.Right (Time.midnight, Time.utc))
345 <*> option (S.Right Time.utc) g_timezone)
348 CF g (S.Either (At src Error_Date) Time.Day)
359 <$> g_get_after (pure $ \(Year y) -> y)
365 case Time.fromGregorianValid y m d of
366 Nothing -> S.Left $ At src $ Error_Date_Day_invalid (y, m, d)
367 Just day -> S.Right day
370 CF g (S.Either (At src Error_Date) Time.TimeOfDay)
371 g_tod = rule "TimeOfDay" $
374 case Time.makeTimeOfDayValid hr mn (fromInteger $ toInteger sc) of
375 Nothing -> S.Left $ At src $ Error_Date_TimeOfDay_invalid (hr, mn, sc)
376 Just tod -> S.Right $ tod)
380 <$> (char char_tod_sep *> g_minute)
381 <*> option 0 (char char_tod_sep *> g_second))
382 g_year :: CF g Integer
383 g_year = rule "Year" $
384 (\sg y -> sg $ integer_of_digits 10 y)
385 <$> option id (negate <$ char '-')
388 g_month = rule "Month" $ int_of_digits 10 <$> count' 1 2 g_09
390 g_dom = rule "DayOfMonth" $ int_of_digits 10 <$> count' 1 2 g_09
392 g_hour = rule "Hour" $ int_of_digits 10 <$> count' 1 2 g_09
394 g_minute = rule "Minute" $ int_of_digits 10 <$> count' 1 2 g_09
396 g_second = rule "Second" $ int_of_digits 10 <$> count' 1 2 g_09
400 CF g (S.Either (At src Error_Date) TimeZone)
401 g_timezone = rule "TimeZone" $
402 -- DOC: http://www.timeanddate.com/time/zones/
403 -- TODO: only a few time zones are suported below.
404 -- TODO: check the timeZoneSummerOnly values
405 (S.Right <$> g_timezone_digits) <+>
406 (g_source $ read_tz <$ char '_' <*> some (range ('A', 'Z')))
408 read_tz n src = case n of
409 "AST" -> S.Right $ TimeZone (- 4 * 60) False n
410 "ADT" -> S.Right $ TimeZone (- 3 * 60) True n
411 "A" -> S.Right $ TimeZone (- 1 * 60) False n
412 "BST" -> S.Right $ TimeZone (-11 * 60) False n
413 "BDT" -> S.Right $ TimeZone (-10 * 60) True n
414 "CET" -> S.Right $ TimeZone ( 1 * 60) True n
415 "CEST" -> S.Right $ TimeZone ( 2 * 60) False n
416 "CST" -> S.Right $ TimeZone (- 6 * 60) False n
417 "CDT" -> S.Right $ TimeZone (- 5 * 60) True n
418 "EST" -> S.Right $ TimeZone (- 5 * 60) False n
419 "EDT" -> S.Right $ TimeZone (- 4 * 60) True n
420 "GMT" -> S.Right $ TimeZone 0 False n
421 "HST" -> S.Right $ TimeZone (-10 * 60) False n
422 "HDT" -> S.Right $ TimeZone (- 9 * 60) True n
423 "MST" -> S.Right $ TimeZone (- 7 * 60) False n
424 "MDT" -> S.Right $ TimeZone (- 6 * 60) True n
425 "M" -> S.Right $ TimeZone (-12 * 60) False n
426 "NST" -> S.Right $ TimeZone (- 3 * 60 - 30) False n
427 "N" -> S.Right $ TimeZone ( 1 * 60) False n
428 "PST" -> S.Right $ TimeZone (- 8 * 60) False n
429 "PDT" -> S.Right $ TimeZone (- 7 * 60) True n
430 "YST" -> S.Right $ TimeZone (- 9 * 60) False n
431 "YDT" -> S.Right $ TimeZone (- 8 * 60) True n
432 "Y" -> S.Right $ TimeZone ( 12 * 60) False n
433 "Z" -> S.Right $ TimeZone 0 False n
434 _ -> S.Left $ At src $ Error_Date_TimeZone_unknown (Text.pack n)
435 g_timezone_digits :: CF g TimeZone
436 g_timezone_digits = do
440 { timeZoneMinutes = sg $ hr * 60 + mn
441 , timeZoneSummerOnly = False
442 , timeZoneName = Time.timeZoneOffsetString tz
447 <*> option 0 (optional (char char_tod_sep) *> g_minute)
449 -- * Class 'Gram_Tag'
455 ) => Gram_Tag g where
458 <$ char char_tag_prefix
460 <*> option (Tag_Data "")
462 *> char char_tag_data_prefix
465 g_tag_path :: CF g Tag_Path
467 (\x xs -> Tag_Path $ NonNull.ncons x xs)
469 <*> many (try $ char char_tag_sep *> g_tag_section)
470 g_tag_section :: CF g Tag_Path_Section
473 <$> some (g_char `minus` g_char_attribute)
474 g_tag_value :: CF g Tag_Data
475 g_tag_value = Tag_Data <$> g_words
477 -- * Class 'Gram_Comment'
481 ) => Gram_Comment g where
482 g_comment :: CF g Comment
483 g_comment = rule "Comment" $
484 Comment <$ char ';' <* g_spaces <*> g_words
486 -- * Class 'Gram_Account'
492 ) => Gram_Account g where
493 g_account_section :: CF g Account_Section
496 <$> some (g_char `minus` g_char_attribute)
497 g_account :: CF g Account
498 g_account = rule "Account" $
499 Account . NonNull.impureNonNull
500 <$> some (try $ char '/' *> g_account_section)
501 g_account_tag :: CF g Account_Tag
505 <$ char char_account_tag_prefix
507 <*> option (Tag_Data "")
509 *> char char_tag_data_prefix
512 g_account_tag_path :: CF g Tag_Path
513 g_account_tag_path = rule "Tag_Path" $
514 char char_account_tag_prefix
517 g_anchor_section :: CF g Anchor_Section
518 g_anchor_section = rule "Anchor_Section" $
520 <$> some (g_char `minus` g_char_attribute)
523 -- * Class 'Gram_Amount'
528 ) => Gram_Amount g where
530 g_unit = rule "Unit" $
531 Unit . Text.singleton
532 <$> unicat (Unicat Char.CurrencySymbol)
533 g_quantity :: CF g (Quantity, Style_Amount)
534 g_quantity = rule "Quantity" $
535 (\(i, f, fr, gi, gf) ->
536 let int = concat i in
537 let frac = concat f in
538 let precision = length frac in
539 -- guard (precision <= 255)
540 let mantissa = integer_of_digits 10 $ int <> frac in
542 (fromIntegral precision)
545 { style_amount_fractioning=fr
546 , style_amount_grouping_integral=gi
547 , style_amount_grouping_fractional=gf
551 [ g_qty '_' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
552 , g_qty '_' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
553 , g_qty ',' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
554 , g_qty '.' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
557 :: Char -- ^ Integral grouping separator.
558 -> Char -- ^ Fractioning separator.
559 -> Char -- ^ Fractional grouping separator.
561 ( [String] -- integral
562 , [String] -- fractional
563 , S.Maybe Style_Amount_Fractioning -- fractioning
564 , S.Maybe Style_Amount_Grouping -- grouping_integral
565 , S.Maybe Style_Amount_Grouping -- grouping_fractional
567 g_qty int_group_sep frac_sep frac_group_sep = do
574 , grouping_of_digits int_group_sep int
577 Just (fractioning, frac) ->
581 , grouping_of_digits int_group_sep int
582 , grouping_of_digits frac_group_sep $ L.reverse frac
586 <*> option [] (many $ try $ char int_group_sep *> some g_09))
587 <*> option Nothing (Just <$> ((,)
591 <*> option [] (many $ try $ char frac_group_sep *> some g_09))))
593 grouping_of_digits :: Char -> [String] -> S.Maybe Style_Amount_Grouping
594 grouping_of_digits group_sep digits =
599 Style_Amount_Grouping group_sep $
600 canonicalize_grouping $
602 canonicalize_grouping :: [Int] -> [Int]
603 canonicalize_grouping groups =
604 foldl' -- NOTE: remove duplicates at beginning and reverse.
605 (\acc l0 -> case acc of
606 l1:_ -> if l0 == l1 then acc else l0:acc
608 case groups of -- NOTE: keep only longer at beginning.
609 l0:l1:t -> if l0 > l1 then groups else l1:t
612 g_amount :: CF g (Styled_Amount Amount)
613 g_amount = rule "Amount" $ g_amount_minus <+> g_amount_plus
614 g_amount_minus :: CF g (Styled_Amount Amount)
618 <$> ((,) <$> g_unit <*> g_spaces)
623 <*> option ("", H.unit_empty)
624 (try $ flip (,) <$> g_spaces <*> g_unit) )
627 <$> ((,) <$> g_unit <*> g_spaces)
631 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
632 mk_amount side (unit, sp) (qty, sty) =
636 { style_amount_unit_side = S.Just side
637 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
640 { amount_quantity = negate qty
644 g_amount_plus :: CF g (Styled_Amount Amount)
648 <$> ((,) <$> g_unit <*> g_spaces)
653 <*> option ("", H.unit_empty)
654 (try $ flip (,) <$> g_spaces <*> g_unit) )
657 <$> ((,) <$> g_unit <*> g_spaces)
658 <* optional (char '+')
663 <*> option ("", H.unit_empty)
664 (try $ flip (,) <$> g_spaces <*> g_unit)
666 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
667 mk_amount side (unit, sp) (qty, sty) =
671 { style_amount_unit_side = S.Just side
672 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
675 { amount_quantity = qty
680 -- * Class 'Gram_Posting'
686 , Gram_Reader SourcePos g
687 , Gram_State (S.Maybe Unit) g
689 , Gram_State Style_Amounts g
691 ) => Gram_Posting g where
694 CF g (S.Either (At src Error_Posting) [Posting])
698 many (try $ g_spaces *> g_eol) *>
699 g_spaces1 *> g_posting
702 CF g (S.Either (At src Error_Posting) Posting)
703 g_posting = rule "Posting" $
704 g_state_after $ g_get_after $ g_ask_before $
707 posting_sourcepos ctx_unit
708 (Style_Amounts ctx_stys) -> do
709 let (posting_tags, posting_comments) = attrs
710 let (stys, posting_amounts) =
712 Nothing -> (Style_Amounts ctx_stys, mempty)
716 Map.insertWith (flip (<>))
720 case amount_unit amt of
721 u | u == H.unit_empty -> S.fromMaybe u ctx_unit
728 (posting_account, posting_account_ref) <- lr_acct
732 , posting_account_ref
739 <$> g_posting_account
740 <*> optional (try $ g_spaces1 *> g_amount)
744 CF g (S.Either (At src Error_Posting)
745 (Account, S.Maybe (S.Pair Tag_Path (S.Maybe Account))))
746 g_posting_account = rule "Posting_Account" $
747 (S.Right . (, S.Nothing) <$> g_account) <+>
749 <$> (g_source $ g_get_after $ expand_tag_path <$> g_account_tag_path)
750 <*> option S.Nothing (S.Just <$> g_account))
752 mk_posting_account path acct =
755 (S.maybe a (a <>) acct)
756 (S.Just (p S.:!: acct)) )
758 expand_tag_path tag chart src =
759 case Map.lookup tag $ chart_tags chart of
760 Just accts | Map.size accts > 0 ->
761 if Map.size accts == 1
763 let acct = fst $ Map.elemAt 0 accts in
765 else S.Left $ At src $ Error_Posting_Account_Ref_multiple tag accts
766 _ -> S.Left $ At src $ Error_Posting_Account_Ref_unknown tag
767 g_posting_tag :: CF g Posting_Tag
768 g_posting_tag = rule "Posting_Tag" $ Posting_Tag <$> g_tag
769 g_posting_attrs :: CF g (Posting_Tags, [Comment])
771 foldr ($) mempty . Compose
773 many (try $ g_spaces *> g_eol *> g_spaces1) *>
777 [ add_tag <$> g_posting_tag
778 , add_comment <$> g_comment
781 add_tag (Posting_Tag (Tag (Tag_Path p) v)) =
782 \(Posting_Tags (Tags tags), cmts) ->
783 ( Posting_Tags (Tags (TreeMap.insert (<>) p [v] tags))
789 -- * Class 'Gram_Transaction'
798 , Gram_State Section g
799 ) => Gram_Transaction g where
802 CF g (S.Either (At src Error_Transaction) Transaction)
803 g_transaction = rule "Transaction" $
804 g_state_after $ (update_year <$>) $
805 g_source $ g_ask_before $
809 , transaction_comments )
811 transaction_sourcepos src -> do
812 date <- fmap Error_Transaction_Date `S.left` lr_date
813 posts <- fmap Error_Transaction_Posting `S.left` lr_posts
814 let postsByAcct = postings_by_account posts
818 , transaction_comments
819 , transaction_dates = NonNull.ncons date []
820 , transaction_wording
821 , transaction_postings = Postings postsByAcct
822 , transaction_sourcepos
824 case H.equilibrium postsByAcct of
825 (_, Left ko) -> S.Left $ At src $ Error_Transaction_not_equilibrated txn ko
826 (_bal, Right ok) -> S.Right txn{transaction_postings = Postings ok}
831 <*> g_transaction_attrs
834 update_year lr_txn y =
837 S.Right txn -> Year $ H.date_year $ NonNull.head $ transaction_dates txn
839 g_wording :: CF g Wording
840 g_wording = rule "Wording" $
841 Wording . Text.concat
846 <$> some (g_char `minus` char char_tag_prefix)))
847 g_transaction_tag :: CF g Transaction_Tag
848 g_transaction_tag = rule "Transaction_Tag" $ Transaction_Tag <$> g_tag
849 g_transaction_attrs :: CF g (Transaction_Tags, [Comment])
850 g_transaction_attrs =
854 [ add_tag <$ many (try $ g_spaces *> g_eol *> g_spaces1) <*> g_transaction_tag
855 , add_comment <$ many (try $ g_spaces *> g_eol *> g_spaces) <*> g_comment
858 add_tag (Transaction_Tag (Tag (Tag_Path p) v)) =
859 \(Transaction_Tags (Tags tags), cmts) ->
860 ( Transaction_Tags (Tags (TreeMap.insert (<>) p [v] tags))
866 -- * Class 'Gram_File'
873 ) => Gram_File g where
874 g_pathfile :: CF g PathFile
875 g_pathfile = rule "PathFile" $
877 <$> some (try $ (:) <$> char '/' <*> some (g_char `minus` char '/'))
879 -- * Class 'Gram_Chart'
885 , Gram_State Section g
887 ) => Gram_Chart g where
890 CF g (S.Either (At src (Error_Compta src)) Chart)
891 g_chart_entry = rule "Chart" $
893 let (tags, tags2, _comments) = attrs in
896 { chart_accounts = TreeMap.singleton (H.get acct) tags
897 , chart_tags = Map.singleton acct () <$ tags2
902 g_chart_attrs :: CF g (Account_Tags, Map Tag_Path (), [Comment])
906 many (try $ g_spaces *> g_eol) *>
908 [ add_tag <$ g_spaces1 <*> g_account_tag
909 , add_comment <$ g_spaces <*> g_comment
912 add_tag (Account_Tag (Tag (Tag_Path p) v)) =
913 \(Account_Tags (Tags tags), tags2, cmts) ->
914 ( Account_Tags (Tags (TreeMap.insert (<>) p [v] tags))
915 , Map.insert (Tag_Path p) () tags2
918 \(tags, tags2, cmts) ->
919 (tags, tags2, c:cmts)
921 class Gram_Input g where
922 g_input :: g (Text -> a) -> g a
923 deriving instance Gram_Input g => Gram_Input (CF g)
925 -- * Class 'Gram_Term_Def'
928 , Sym.Gram_Term src ss g
929 , Gram_State (Sym.Name2Type src) g
930 , Inj_Source (Sym.TypeVT src) src
931 , Inj_Source (Sym.KindK src) src
932 , Inj_Source (Sym.AST_Type src) src
933 ) => Gram_Term_Def src ss g where
934 g_term_def :: CF g (S.Either (At src (Sym.NameTe, Sym.Error_Term src)) (Sym.NameTe, Sym.TermVT src ss '[]))
935 g_term_def = rule "TermDef" $
936 g_source $ g_get_after $
937 (\n args v n2t src ->
939 Sym.readTerm n2t Sym.CtxTyZ $
940 foldr (\(x, ty_x) -> BinTree0 . Sym.Token_Term_Abst src x ty_x) v args in
942 Right t -> S.Right (n, t)
943 Left err -> S.Left $ At src (n, err)
946 <*> many Sym.g_term_abst_decl
950 -- * Class 'Gram_Compta'
961 , Gram_Term_Def src ss g
962 , Gram_Reader (S.Either Exn.IOException CanonFile) g
963 , Gram_State (Context_Read src j) g
964 , Gram_State (Sym.Modules src ss) g
965 , Gram_State (Journal j) g
966 , Gram_State (Journals j) g
971 ) => Gram_Compta ss src j g where
973 :: (Transaction -> j -> j)
974 -> CF g (S.Either [At src (Error_Compta src)]
975 (CanonFile, Journal j))
976 g_compta consTxn = rule "Journal" $
977 g_state_after $ g_ask_before $
979 <$> (g_state_after $ g_source $ g_ask_before $ g_ask_before $ pure init_journal)
981 [ g_state_after $ mk_include <$> g_include @ss consTxn
982 -- NOTE: g_include must be the first choice
983 -- in order to have Megaparsec reporting the errors
984 -- of the included journal.
985 , g_state_after $ mk_transaction
986 <$> g_compta_section Section_Transactions g_transaction
987 , g_state_after $ mk_chart
988 <$> g_compta_section Section_Chart g_chart_entry
989 , g_state_before $ g_state_before $ g_input $ g_source $ mk_term
990 <$> g_compta_section Section_Terms g_term_def
991 , ([], []) <$ try (g_spaces <* g_eol)
995 (SourcePos jf _ _) lr_cf src
997 { context_read_journals = Journals js
998 , context_read_journal = jnls
999 , context_read_canonfiles = cfs
1000 }::Context_Read src j) =
1002 S.Left e -> (ctx, S.Left $ At src $ Error_Compta_Read (PathFile jf) e)
1004 let jnl = journal{journal_file=PathFile jf} in
1007 { context_read_journals = Journals $ Map.insert cf jnl js
1008 , context_read_journal = jnl <| jnls
1009 , context_read_canonfiles = cf <| cfs
1011 mk_journal err errs_warns
1014 { context_read_journals = Journals js
1015 , context_read_journal = jnl :| jnls
1016 , context_read_canonfiles = cf :| cfs
1017 , context_read_warnings = warnings
1018 }::Context_Read src j) =
1019 let (errs, warns) = L.unzip errs_warns in
1020 case S.either pure (const []) err <> L.concat errs of
1022 let jnl' = jnl{journal_file=PathFile jf} in
1023 (,S.Right (cf, jnl'))
1025 { context_read_journals = Journals $ Map.insert cf jnl' js
1026 , context_read_journal = NonEmpty.fromList jnls
1027 , context_read_canonfiles = NonEmpty.fromList cfs
1028 , context_read_warnings = warnings <> L.concat warns
1030 es -> (ctx, S.Left es)
1031 mk_transaction lr_txn jnl@Journal{journal_content=j} =
1033 S.Left err -> (jnl, ([err], []))
1034 S.Right txn -> (jnl{ journal_content = txn `consTxn` j }, ([], []))
1035 mk_include lr_inc (jnl::Journal j) =
1037 S.Left errs -> (jnl, (errs, []))
1038 S.Right (cf, _inc) -> (jnl{ journal_includes = journal_includes jnl <> [cf] }, ([], []))
1039 mk_chart lr_ch chart =
1041 S.Left err -> (chart, ([err], []))
1042 S.Right ch -> (chart <> ch, ([], []))
1043 mk_term lr_te src body mods =
1045 S.Left err -> (mods, (, ([err], [])))
1046 S.Right (n, te) -> (ins_term n te mods, \ts -> (ins_body n body ts, ([], warn_redef n ts)))
1048 ins_term :: Sym.NameTe -> Sym.TermVT src ss '[] -> Sym.Modules src ss -> Sym.Modules src ss
1049 ins_term n t = Sym.insertTermVT ([] `Sym.Mod` t) n (Sym.Fixity2 Sym.infixN5)
1050 ins_body :: Sym.NameTe -> Text -> Terms -> Terms
1051 ins_body n t = Map.insert ([] `Sym.Mod` n) t
1052 warn_redef :: Sym.NameTe -> Terms -> [At src Warning_Compta]
1054 case Map.lookup ([] `Sym.Mod` n) ts of
1055 Just{} -> [At src $ Warning_Compta_Term_redefined n]
1058 :: (Transaction -> j -> j)
1059 -> CF g (S.Either [At src (Error_Compta src)]
1060 (CanonFile, Journal j))
1061 g_include consTxn = rule "Include" $
1062 g_read g_path (g_compta @ss consTxn <* eoi)
1065 g_state_after $ g_source $ check_path
1066 <$> (g_canonfile $ g_ask_before $ fmap mk_path $
1067 (\d (PathFile p) -> PathFile $ d:p)
1068 <$> char '.' <*> g_pathfile)
1069 mk_path (PathFile fp) (SourcePos fp_old _ _) =
1071 FilePath.normalise $
1072 FilePath.takeDirectory fp_old </> fp
1073 check_path (fp, lr_cf) src
1075 { context_read_journals = Journals js
1076 , context_read_canonfiles = cfs
1077 , context_read_warnings = warns
1078 }::Context_Read src j) =
1080 Left e -> (ctx, S.Left $ Error_Compta_Read fp e)
1082 if cf `Map.member` js
1085 then (ctx, S.Left $ Error_Compta_Include_loop cf)
1088 if isJust $ (`L.find` warns) $ \case
1089 At{unAt=Warning_Compta_Include_multiple cf'} -> cf' `elem` cf<|cfs
1093 { context_read_warnings =
1094 At src (Warning_Compta_Include_multiple cf) : warns }
1095 else (ctx, S.Right fp)
1099 -- | Return the 'Integer' obtained by multiplying the given digits
1100 -- with the power of the given base respective to their rank.
1102 :: Integer -- ^ Base.
1103 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1105 integer_of_digits base =
1106 foldl' (\x d -> base*x + toInteger (Char.digitToInt d)) 0
1108 -- | Return the 'Int' obtained by multiplying the given digits
1109 -- with the power of the given base respective to their rank.
1112 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1114 int_of_digits base =
1115 foldl' (\x d -> base*x + Char.digitToInt d) 0
1118 char_account_sep :: Char
1119 char_account_sep = '/'
1120 char_account_tag_prefix :: Char
1121 char_account_tag_prefix = '~'
1122 char_ymd_sep :: Char
1124 char_tod_sep :: Char
1126 char_comment_prefix :: Char
1127 char_comment_prefix = ';'
1128 char_tag_prefix :: Char
1129 char_tag_prefix = '#'
1130 char_tag_sep :: Char
1132 char_tag_data_prefix :: Char
1133 char_tag_data_prefix = '='
1134 char_transaction_date_sep :: Char
1135 char_transaction_date_sep = '='
1138 type Env src ss = Map Sym.NameTe (Sym.TermVT src ss '[])
1144 | Section_Transactions
1145 deriving (Eq, Ord, Show)
1149 Sym.Inj_Error err (Error_Compta src) =>
1150 Gram_State Section g =>
1151 Gram_Source src g =>
1154 g (S.Either (At src err) a) ->
1155 g (S.Either (At src (Error_Compta src)) a)
1156 g_compta_section sec g =
1157 g_state_before $ g_source $
1161 then fmap Sym.inj_Error `S.left` a
1162 else S.Left $ At src $ Error_Compta_Section sec_curr sec
1166 newtype Year = Year (H.Date_Year Date)
1169 -- * Type 'Error_Date'
1171 = Error_Date_Day_invalid (Integer, Int, Int)
1172 | Error_Date_TimeOfDay_invalid (Int, Int, Int)
1173 | Error_Date_TimeZone_unknown Text
1176 -- * Type 'Error_Posting'
1178 = Error_Posting_Account_Ref_unknown Tag_Path
1179 | Error_Posting_Account_Ref_multiple Tag_Path (Map Account ())
1180 | Error_Postings_not_equilibrated Postings
1183 -- * Type 'Error_Transaction'
1184 data Error_Transaction
1185 = Error_Transaction_Date Error_Date
1186 | Error_Transaction_Posting Error_Posting
1187 | Error_Transaction_not_equilibrated
1190 , H.SumByUnit (NonNull [Account_Section]) (H.Polarized Quantity)
1194 -- * Type 'Error_Chart'
1199 -- * Type 'Error_Compta'
1200 data Error_Compta src
1201 = Error_Compta_Transaction Error_Transaction
1202 | Error_Compta_Read PathFile Exn.IOException
1203 | Error_Compta_Include_loop CanonFile
1204 | Error_Compta_Chart Error_Chart
1205 | Error_Compta_Section Section Section
1206 | Error_Compta_Term Sym.NameTe (Sym.Error_Term src)
1209 instance Sym.Inj_Error (Sym.NameTe, Sym.Error_Term src) (Error_Compta src) where
1210 inj_Error (n, t) = Error_Compta_Term n t
1211 instance Sym.Inj_Error Error_Transaction (Error_Compta src) where
1212 inj_Error = Error_Compta_Transaction
1213 instance Sym.Inj_Error (Error_Compta src) (Error_Compta src) where
1216 -- * Type 'Warning_Compta'
1218 = Warning_Compta_Include_multiple CanonFile
1219 | Warning_Compta_Term_redefined Sym.NameTe
1223 nonEmpty :: NonNull [a] -> NonEmpty a
1224 nonEmpty n = x :| xs where (x, xs) = NonNull.splitFirst n
1225 nonNull :: NonEmpty a -> NonNull [a]
1226 nonNull n = NonNull.ncons x xs where x :| xs = n