1 {-# LANGUAGE AllowAmbiguousTypes #-}
2 {-# LANGUAGE UndecidableSuperClasses #-}
3 module Hcompta.LCC.Grammar where
5 import Control.Applicative (Applicative(..), liftA2)
6 import Control.Arrow (left)
7 import Control.Monad (Monad(..), void)
9 import Data.Char (Char)
11 import Data.Either (Either(..), either)
12 import Data.Eq (Eq(..))
14 import Data.Function (($), (.), const, id, flip)
15 import Data.Functor (Functor(..), (<$>), (<$))
16 import Data.Functor.Compose (Compose(..))
17 import Data.List.NonEmpty (NonEmpty(..), (<|))
18 import Data.Map.Strict (Map)
19 import Data.Maybe (Maybe(..), maybe, isJust)
20 import Data.Monoid (Monoid(..))
21 import Data.NonNull (NonNull)
22 import Data.Ord (Ord(..))
23 import Data.Proxy (Proxy(..))
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)
30 import Data.Typeable ()
31 import Prelude (Int, Integer, Enum(..), Num(..), Integral(..), fromIntegral, error)
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 List
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 Language.Symantic.Grammar hiding (Side(..), Gram_Comment(..))
51 import Language.Symantic.Lib ()
52 import qualified Language.Symantic as Sym
53 import qualified Language.Symantic.Grammar as Sym
55 import Hcompta.LCC.Account
56 import Hcompta.LCC.Name
57 import Hcompta.LCC.Tag
58 import Hcompta.LCC.Amount
59 import Hcompta.LCC.Chart
60 import Hcompta.LCC.Posting
61 import Hcompta.LCC.Transaction
62 import Hcompta.LCC.Journal
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
73 type Terms = Map (Sym.Mod Sym.NameTe) Text
75 -- * Type 'Context_Read'
76 data Context_Read src j
78 { context_read_year :: !Year
79 , context_read_style_amounts :: !Style_Amounts
80 , context_read_chart :: !Chart
81 , context_read_unit :: !(S.Maybe Unit)
82 , context_read_journals :: !(Journals j)
83 , context_read_journal :: !(NonEmpty (Journal j))
84 , context_read_canonfiles :: !(NonEmpty CanonFile)
85 , context_read_warnings :: ![At src Warning_Journal]
86 , context_read_section :: !Section
94 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffReader (NonEmpty CanonFile)) = 'True
95 instance Monad m => MC.MonadReaderN 'MC.Zero (NonEmpty CanonFile) (S.StateT (Context_Read src j) m) where
96 askN _n = MC.gets $ \(x::Context_Read src j) -> context_read_canonfiles x
99 -- States handled by a nested Monad
101 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Modules src ss)) = 'False
102 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Sym.Imports) = 'False
103 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Imports, Sym.Modules src ss)) = 'False
104 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Sym.Name2Type src)) = 'False
105 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Env src ss)) = 'False
106 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Terms) = 'False
108 context_read :: Monoid j => Context_Read src j
111 { context_read_year = Year $ H.date_year (H.date_epoch::Date)
112 , context_read_style_amounts = mempty
113 , context_read_chart = mempty
114 , context_read_unit = S.Nothing
115 , context_read_journals = Journals Map.empty
116 , context_read_journal = journal :| []
117 , context_read_canonfiles = CanonFile "" :| []
118 , context_read_warnings = []
119 , context_read_section = Section_Chart
122 -- * Type 'Context_Sym'
123 data Context_Sym src ss
125 { context_sym_imports :: !Sym.Imports
126 , context_sym_modules :: !(Sym.Modules src ss)
127 , context_sym_name2type :: !(Sym.Name2Type src)
128 , context_sym_env :: !(Env src ss)
129 , context_sym_terms :: !Terms
130 } deriving (Eq, Show)
135 Sym.Inj_Modules src ss =>
136 Sym.Inj_Name2Type ss =>
139 let mods = either (error . show) id Sym.inj_Modules in
141 { context_sym_imports = Sym.importQualifiedAs [] mods
142 , context_sym_modules = mods
143 , context_sym_name2type = Sym.inj_Name2Type (Proxy @ss)
144 , context_sym_env = Map.empty
145 , context_sym_terms = Map.empty
152 -- Sym.Modules src ss
153 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Modules src ss)) = 'True
154 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Modules src ss) (S.StateT (Context_Sym src ss) m) where
155 stateN _px f = S.StateT $ SS.state $ \ctx ->
156 (\a -> ctx{context_sym_modules = a})
157 <$> f (context_sym_modules ctx)
160 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState Sym.Imports) = 'True
161 instance Monad m => MC.MonadStateN 'MC.Zero Sym.Imports (S.StateT (Context_Sym src ss) m) where
162 stateN _px f = S.StateT $ SS.state $ \ctx ->
163 (\a -> ctx{context_sym_imports = a})
164 <$> f (context_sym_imports ctx)
166 -- (Sym.Imports, Sym.Modules src ss)
167 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Imports, Sym.Modules src ss)) = 'True
168 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Imports, Sym.Modules src ss) (S.StateT (Context_Sym src ss) m) where
169 stateN _px f = S.StateT $ SS.state $ \ctx ->
170 (\(imps, mods) -> ctx{context_sym_imports=imps, context_sym_modules=mods})
171 <$> f (context_sym_imports ctx, context_sym_modules ctx)
174 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState Terms) = 'True
175 instance Monad m => MC.MonadStateN 'MC.Zero Terms (S.StateT (Context_Sym src ss) m) where
176 stateN _px f = S.StateT $ SS.state $ \ctx ->
177 (\a -> ctx{context_sym_terms = a})
178 <$> f (context_sym_terms ctx)
181 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Sym.Name2Type src)) = 'True
182 instance Monad m => MC.MonadStateN 'MC.Zero (Sym.Name2Type src) (S.StateT (Context_Sym src ss) m) where
183 stateN _px f = S.StateT $ SS.state $ \ctx ->
184 (\a -> ctx{context_sym_name2type = a})
185 <$> f (context_sym_name2type ctx)
188 type instance MC.CanDo (S.StateT (Context_Sym src ss) m) (MC.EffState (Env src ss)) = 'True
189 instance Monad m => MC.MonadStateN 'MC.Zero (Env src ss) (S.StateT (Context_Sym src ss) m) where
190 stateN _px f = S.StateT $ SS.state $ \ctx ->
191 (\a -> ctx{context_sym_env = a})
192 <$> f (context_sym_env ctx)
194 -- Context_Read src j
195 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Context_Read src j)) = 'True
196 instance Monad m => MC.MonadStateN 'MC.Zero (Context_Read src j) (S.StateT (Context_Read src j) m) where
197 stateN _px = S.StateT . SS.state
200 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (S.Maybe Unit)) = 'True
201 instance Monad m => MC.MonadStateN 'MC.Zero (S.Maybe Unit) (S.StateT (Context_Read src j) m) where
202 stateN _px f = S.StateT $ SS.state $ \ctx ->
203 (\a -> ctx{context_read_unit = a})
204 <$> f (context_read_unit ctx)
207 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Chart) = 'True
208 instance Monad m => MC.MonadStateN 'MC.Zero Chart (S.StateT (Context_Read src j) m) where
209 stateN _px f = S.StateT $ SS.state $ \ctx ->
210 (\a -> ctx{context_read_chart = a})
211 <$> f (context_read_chart ctx)
214 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Year) = 'True
215 instance Monad m => MC.MonadStateN 'MC.Zero Year (S.StateT (Context_Read src j) m) where
216 stateN _px f = S.StateT $ SS.state $ \ctx ->
217 (\a -> ctx{context_read_year = a})
218 <$> f (context_read_year ctx)
221 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Section) = 'True
222 instance Monad m => MC.MonadStateN 'MC.Zero Section (S.StateT (Context_Read src j) m) where
223 stateN _px f = S.StateT $ SS.state $ \ctx ->
224 (\a -> ctx{context_read_section = a})
225 <$> f (context_read_section ctx)
228 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Journal j)) = 'True
229 instance Monad m => MC.MonadStateN 'MC.Zero (Journal j) (S.StateT (Context_Read src j) m) where
230 stateN _px f = S.StateT $ SS.state $ \ctx@Context_Read{context_read_journal = j:|js} ->
231 (\a -> ctx{context_read_journal = a:|js}) <$> f j
234 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState (Journals j)) = 'True
235 instance Monad m => MC.MonadStateN 'MC.Zero (Journals j) (S.StateT (Context_Read src j) m) where
236 stateN _px f = S.StateT $ SS.state $ \ctx ->
237 (\a -> ctx{context_read_journals = a})
238 <$> f (context_read_journals ctx)
241 type instance MC.CanDo (S.StateT (Context_Read src j) m) (MC.EffState Style_Amounts) = 'True
242 instance Monad m => MC.MonadStateN 'MC.Zero Style_Amounts (S.StateT (Context_Read src j) m) where
243 stateN _px f = S.StateT $ SS.state $ \ctx ->
244 (\s -> ctx{context_read_style_amounts = s})
245 <$> f (context_read_style_amounts ctx)
247 -- * Class 'Gram_Path'
248 class Gram_Path g where
251 -> g (PathFile, Either Exn.IOException CanonFile)
252 deriving instance Gram_Path g => Gram_Path (CF g)
255 class Gram_Source src g => Gram_IO src g where
257 :: g (S.Either (Error_Journal src) PathFile)
258 -> g (S.Either [At src (Error_Journal src)] (CanonFile, a))
259 -> g (S.Either [At src (Error_Journal src)] (CanonFile, a))
260 deriving instance Gram_IO src g => Gram_IO src (CF g)
262 -- * Class 'Gram_Count'
267 ) => Gram_Count g where
268 count :: Int -> CF g a -> CF g [a]
271 | otherwise = sequenceA $ List.replicate n p
272 count' :: Int -> Int -> CF g a -> CF g [a]
274 | n <= 0 || m > n = pure []
275 | m > 0 = (:) <$> p <*> count' (pred m) (pred n) p
277 let f t ts = maybe [] (:ts) t
278 in f <$> optional p <*> count' 0 (pred n) p
280 -- * Class 'Gram_Char'
290 ) => Gram_Char g where
292 g_eol = rule "EOL" $ void (char '\n') <+> void (string "\r\n")
294 g_tab = rule "Tab" $ void $ char '\t'
296 g_space = rule "Space" $ char ' '
297 g_spaces :: CF g Text
298 g_spaces = Text.pack <$> many g_space
300 g_spaces1 = void $ some g_space
302 g_char = g_char_passive <+> g_char_active
303 g_char_passive :: CF g Char
304 g_char_passive = choice $ unicat <$> [Unicat_Letter, Unicat_Number, Unicat_Mark]
305 g_char_active :: CF g Char
306 g_char_active = choice $ unicat <$> [Unicat_Punctuation, Unicat_Symbol]
307 g_char_attribute :: Reg lr g Char
308 g_char_attribute = choice $ char <$> "#/:;@~="
310 g_word = rule "Word" $ Text.pack <$> some g_char
312 g_words = Text.concat <$> many (try $ (<>) <$> g_spaces <*> g_word)
314 g_09 = range ('0', '9')
316 g_19 = range ('1', '9')
317 g_sign :: Num int => CF g (int -> int)
319 (negate <$ char '-') <+>
322 -- * Class 'Gram_Date'
333 ) => Gram_Date g where
336 CF g (S.Either (At src Error_Date) Date)
337 g_date = rule "Date" $
338 (liftA2 $ \day (tod, tz) ->
339 Time.localTimeToUTC tz $
340 Time.LocalTime day tod)
343 (S.Right (Time.midnight, Time.utc))
347 <*> option (S.Right Time.utc) g_timezone)
350 CF g (S.Either (At src Error_Date) Time.Day)
361 <$> g_get_after (pure $ \(Year y) -> y)
367 case Time.fromGregorianValid y m d of
368 Nothing -> S.Left $ At src $ Error_Date_Day_invalid (y, m, d)
369 Just day -> S.Right day
372 CF g (S.Either (At src Error_Date) Time.TimeOfDay)
373 g_tod = rule "TimeOfDay" $
376 case Time.makeTimeOfDayValid hr mn (fromInteger $ toInteger sc) of
377 Nothing -> S.Left $ At src $ Error_Date_TimeOfDay_invalid (hr, mn, sc)
378 Just tod -> S.Right $ tod)
382 <$> (char char_tod_sep *> g_minute)
383 <*> option 0 (char char_tod_sep *> g_second))
384 g_year :: CF g Integer
385 g_year = rule "Year" $
386 (\sg y -> sg $ integer_of_digits 10 y)
387 <$> option id (negate <$ char '-')
390 g_month = rule "Month" $ int_of_digits 10 <$> count' 1 2 g_09
392 g_dom = rule "DayOfMonth" $ int_of_digits 10 <$> count' 1 2 g_09
394 g_hour = rule "Hour" $ int_of_digits 10 <$> count' 1 2 g_09
396 g_minute = rule "Minute" $ int_of_digits 10 <$> count' 1 2 g_09
398 g_second = rule "Second" $ int_of_digits 10 <$> count' 1 2 g_09
402 CF g (S.Either (At src Error_Date) TimeZone)
403 g_timezone = rule "TimeZone" $
404 -- DOC: http://www.timeanddate.com/time/zones/
405 -- TODO: only a few time zones are suported below.
406 -- TODO: check the timeZoneSummerOnly values
407 (S.Right <$> g_timezone_digits) <+>
408 (g_source $ read_tz <$ char '_' <*> some (range ('A', 'Z')))
410 read_tz n src = case n of
411 "AST" -> S.Right $ TimeZone (- 4 * 60) False n
412 "ADT" -> S.Right $ TimeZone (- 3 * 60) True n
413 "A" -> S.Right $ TimeZone (- 1 * 60) False n
414 "BST" -> S.Right $ TimeZone (-11 * 60) False n
415 "BDT" -> S.Right $ TimeZone (-10 * 60) True n
416 "CET" -> S.Right $ TimeZone ( 1 * 60) True n
417 "CEST" -> S.Right $ TimeZone ( 2 * 60) False n
418 "CST" -> S.Right $ TimeZone (- 6 * 60) False n
419 "CDT" -> S.Right $ TimeZone (- 5 * 60) True n
420 "EST" -> S.Right $ TimeZone (- 5 * 60) False n
421 "EDT" -> S.Right $ TimeZone (- 4 * 60) True n
422 "GMT" -> S.Right $ TimeZone 0 False n
423 "HST" -> S.Right $ TimeZone (-10 * 60) False n
424 "HDT" -> S.Right $ TimeZone (- 9 * 60) True n
425 "MST" -> S.Right $ TimeZone (- 7 * 60) False n
426 "MDT" -> S.Right $ TimeZone (- 6 * 60) True n
427 "M" -> S.Right $ TimeZone (-12 * 60) False n
428 "NST" -> S.Right $ TimeZone (- 3 * 60 - 30) False n
429 "N" -> S.Right $ TimeZone ( 1 * 60) False n
430 "PST" -> S.Right $ TimeZone (- 8 * 60) False n
431 "PDT" -> S.Right $ TimeZone (- 7 * 60) True n
432 "YST" -> S.Right $ TimeZone (- 9 * 60) False n
433 "YDT" -> S.Right $ TimeZone (- 8 * 60) True n
434 "Y" -> S.Right $ TimeZone ( 12 * 60) False n
435 "Z" -> S.Right $ TimeZone 0 False n
436 _ -> S.Left $ At src $ Error_Date_TimeZone_unknown (Text.pack n)
437 g_timezone_digits :: CF g TimeZone
438 g_timezone_digits = do
442 { timeZoneMinutes = sg $ hr * 60 + mn
443 , timeZoneSummerOnly = False
444 , timeZoneName = Time.timeZoneOffsetString tz
449 <*> option 0 (optional (char char_tod_sep) *> g_minute)
451 -- * Class 'Gram_Tag'
457 ) => Gram_Tag g where
460 <$ char char_tag_prefix
462 <*> option (Tag_Data "")
464 *> char char_tag_data_prefix
467 g_tag_path :: CF g Tag_Path
469 (\x xs -> Tag_Path $ NonNull.ncons x xs)
471 <*> many (try $ char char_tag_sep *> g_tag_section)
472 g_tag_section :: CF g Tag_Path_Section
475 <$> some (g_char `minus` g_char_attribute)
476 g_tag_value :: CF g Tag_Data
477 g_tag_value = Tag_Data <$> g_words
479 -- * Class 'Gram_Comment'
483 ) => Gram_Comment g where
484 g_comment :: CF g Comment
485 g_comment = rule "Comment" $
486 Comment <$ char ';' <* g_spaces <*> g_words
488 -- * Class 'Gram_Account'
494 ) => Gram_Account g where
495 g_account_section :: CF g Account_Section
498 <$> some (g_char `minus` g_char_attribute)
499 g_account :: CF g Account
500 g_account = rule "Account" $
501 Account . NonNull.impureNonNull
502 <$> some (try $ char '/' *> g_account_section)
503 g_account_tag :: CF g Account_Tag
507 <$ char char_account_tag_prefix
509 <*> option (Tag_Data "")
511 *> char char_tag_data_prefix
514 g_account_tag_path :: CF g Tag_Path
515 g_account_tag_path = rule "Tag_Path" $
516 char char_account_tag_prefix
519 g_anchor_section :: CF g Anchor_Section
520 g_anchor_section = rule "Anchor_Section" $
522 <$> some (g_char `minus` g_char_attribute)
525 -- * Class 'Gram_Amount'
530 ) => Gram_Amount g where
532 g_unit = rule "Unit" $
533 Unit . Text.singleton
534 <$> unicat (Unicat Char.CurrencySymbol)
535 g_quantity :: CF g (Quantity, Style_Amount)
536 g_quantity = rule "Quantity" $
537 (\(i, f, fr, gi, gf) ->
538 let int = concat i in
539 let frac = concat f in
540 let precision = length frac in
541 -- guard (precision <= 255)
542 let mantissa = integer_of_digits 10 $ int <> frac in
544 (fromIntegral precision)
547 { style_amount_fractioning=fr
548 , style_amount_grouping_integral=gi
549 , style_amount_grouping_fractional=gf
553 [ g_qty '_' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
554 , g_qty '_' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
555 , g_qty ',' '.' '_' <* (pure () `minus` choice (char <$> ",._"))
556 , g_qty '.' ',' '_' <* (pure () `minus` choice (char <$> ",._"))
559 :: Char -- ^ Integral grouping separator.
560 -> Char -- ^ Fractioning separator.
561 -> Char -- ^ Fractional grouping separator.
563 ( [String] -- integral
564 , [String] -- fractional
565 , S.Maybe Style_Amount_Fractioning -- fractioning
566 , S.Maybe Style_Amount_Grouping -- grouping_integral
567 , S.Maybe Style_Amount_Grouping -- grouping_fractional
569 g_qty int_group_sep frac_sep frac_group_sep = do
576 , grouping_of_digits int_group_sep int
579 Just (fractioning, frac) ->
583 , grouping_of_digits int_group_sep int
584 , grouping_of_digits frac_group_sep $ List.reverse frac
588 <*> option [] (many $ try $ char int_group_sep *> some g_09))
589 <*> option Nothing (Just <$> ((,)
593 <*> option [] (many $ try $ char frac_group_sep *> some g_09))))
595 grouping_of_digits :: Char -> [String] -> S.Maybe Style_Amount_Grouping
596 grouping_of_digits group_sep digits =
601 Style_Amount_Grouping group_sep $
602 canonicalize_grouping $
604 canonicalize_grouping :: [Int] -> [Int]
605 canonicalize_grouping groups =
606 foldl' -- NOTE: remove duplicates at beginning and reverse.
607 (\acc l0 -> case acc of
608 l1:_ -> if l0 == l1 then acc else l0:acc
610 case groups of -- NOTE: keep only longer at beginning.
611 l0:l1:t -> if l0 > l1 then groups else l1:t
614 g_amount :: CF g (Styled_Amount Amount)
615 g_amount = rule "Amount" $ g_amount_minus <+> g_amount_plus
616 g_amount_minus :: CF g (Styled_Amount Amount)
620 <$> ((,) <$> g_unit <*> g_spaces)
625 <*> option ("", H.unit_empty)
626 (try $ flip (,) <$> g_spaces <*> g_unit) )
629 <$> ((,) <$> g_unit <*> g_spaces)
633 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
634 mk_amount side (unit, sp) (qty, sty) =
638 { style_amount_unit_side = S.Just side
639 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
642 { amount_quantity = negate qty
646 g_amount_plus :: CF g (Styled_Amount Amount)
650 <$> ((,) <$> g_unit <*> g_spaces)
655 <*> option ("", H.unit_empty)
656 (try $ flip (,) <$> g_spaces <*> g_unit) )
659 <$> ((,) <$> g_unit <*> g_spaces)
660 <* optional (char '+')
665 <*> option ("", H.unit_empty)
666 (try $ flip (,) <$> g_spaces <*> g_unit)
668 mk_amount :: LR -> (Unit, Text) -> (Quantity, Style_Amount) -> (Style_Amount, Amount)
669 mk_amount side (unit, sp) (qty, sty) =
673 { style_amount_unit_side = S.Just side
674 , style_amount_unit_spaced = S.Just $ not $ Text.null sp
677 { amount_quantity = qty
682 -- * Class 'Gram_Posting'
688 , Gram_Reader SourcePos g
689 , Gram_State (S.Maybe Unit) g
691 , Gram_State Style_Amounts g
693 ) => Gram_Posting g where
696 CF g (S.Either (At src Error_Posting) [Posting])
700 many (try $ g_spaces *> g_eol) *>
701 g_spaces1 *> g_posting
704 CF g (S.Either (At src Error_Posting) Posting)
705 g_posting = rule "Posting" $
706 g_state_after $ g_get_after $ g_ask_before $
709 posting_sourcepos ctx_unit
710 (Style_Amounts ctx_stys) -> do
711 let (posting_tags, posting_comments) = attrs
712 let (stys, posting_amounts) =
714 Nothing -> (Style_Amounts ctx_stys, mempty)
718 Map.insertWith (flip (<>))
722 case amount_unit amt of
723 u | u == H.unit_empty -> S.fromMaybe u ctx_unit
730 (posting_account, posting_account_ref) <- lr_acct
734 , posting_account_ref
741 <$> g_posting_account
742 <*> optional (try $ g_spaces1 *> g_amount)
746 CF g (S.Either (At src Error_Posting)
747 (Account, S.Maybe (S.Pair Tag_Path (S.Maybe Account))))
748 g_posting_account = rule "Posting_Account" $
749 (S.Right . (, S.Nothing) <$> g_account) <+>
751 <$> (g_source $ g_get_after $ expand_tag_path <$> g_account_tag_path)
752 <*> option S.Nothing (S.Just <$> g_account))
754 mk_posting_account path acct =
757 (S.maybe a (a <>) acct)
758 (S.Just (p S.:!: acct)) )
760 expand_tag_path tag chart src =
761 case Map.lookup tag $ chart_tags chart of
762 Just accts | Map.size accts > 0 ->
763 if Map.size accts == 1
765 let acct = fst $ Map.elemAt 0 accts in
767 else S.Left $ At src $ Error_Posting_Account_Ref_multiple tag accts
768 _ -> S.Left $ At src $ Error_Posting_Account_Ref_unknown tag
769 g_posting_tag :: CF g Posting_Tag
770 g_posting_tag = rule "Posting_Tag" $ Posting_Tag <$> g_tag
771 g_posting_attrs :: CF g (Posting_Tags, [Comment])
773 foldr ($) mempty . Compose
775 many (try $ g_spaces *> g_eol *> g_spaces1) *>
779 [ add_tag <$> g_posting_tag
780 , add_comment <$> g_comment
783 add_tag (Posting_Tag (Tag (Tag_Path p) v)) =
784 \(Posting_Tags (Tags tags), cmts) ->
785 ( Posting_Tags (Tags (TreeMap.insert (<>) p [v] tags))
791 -- * Class 'Gram_Transaction'
800 , Gram_State Section g
801 ) => Gram_Transaction g where
804 CF g (S.Either (At src Error_Transaction) Transaction)
805 g_transaction = rule "Transaction" $
806 g_put $ ((Section_Transaction,) <$>) $
807 g_state_after $ (update_year <$>) $
808 g_source $ g_ask_before $
812 , transaction_comments )
814 transaction_sourcepos src -> do
815 date <- fmap Error_Transaction_Date `S.left` lr_date
816 posts <- fmap Error_Transaction_Posting `S.left` lr_posts
817 let postsByAcct = postings_by_account posts
821 , transaction_comments
822 , transaction_dates = NonNull.ncons date []
823 , transaction_wording
824 , transaction_postings = Postings postsByAcct
825 , transaction_sourcepos
827 case H.equilibrium postsByAcct of
828 (_, Left ko) -> S.Left $ At src $ Error_Transaction_not_equilibrated txn ko
829 (_bal, Right ok) -> S.Right txn{transaction_postings = Postings ok}
834 <*> g_transaction_attrs
837 update_year lr_txn y =
840 S.Right txn -> Year $ H.date_year $ NonNull.head $ transaction_dates txn
842 g_wording :: CF g Wording
843 g_wording = rule "Wording" $
844 Wording . Text.concat
849 <$> some (g_char `minus` char char_tag_prefix)))
850 g_transaction_tag :: CF g Transaction_Tag
851 g_transaction_tag = rule "Transaction_Tag" $ Transaction_Tag <$> g_tag
852 g_transaction_attrs :: CF g (Transaction_Tags, [Comment])
853 g_transaction_attrs =
857 [ add_tag <$ many (try $ g_spaces *> g_eol *> g_spaces1) <*> g_transaction_tag
858 , add_comment <$ many (try $ g_spaces *> g_eol *> g_spaces) <*> g_comment
861 add_tag (Transaction_Tag (Tag (Tag_Path p) v)) =
862 \(Transaction_Tags (Tags tags), cmts) ->
863 ( Transaction_Tags (Tags (TreeMap.insert (<>) p [v] tags))
869 -- * Class 'Gram_File'
876 ) => Gram_File g where
877 g_pathfile :: CF g PathFile
878 g_pathfile = rule "PathFile" $
880 <$> some (try $ (:) <$> char '/' <*> some (g_char `minus` char '/'))
882 -- * Class 'Gram_Chart'
888 , Gram_State Section g
890 ) => Gram_Chart g where
893 CF g (S.Either (At src (Error_Journal src)) Chart)
894 g_chart_entry = rule "Chart" $
895 g_get_after $ g_source $
896 (\acct attrs src section ->
897 let (tags, tags2, _comments) = attrs in
899 Section_Transaction -> False
900 Section_Chart -> True
903 { chart_accounts = TreeMap.singleton (H.get acct) tags
904 , chart_tags = Map.singleton acct () <$ tags2
906 else S.Left $ At src $ Error_Journal_Section section Section_Chart
910 g_chart_attrs :: CF g (Account_Tags, Map Tag_Path (), [Comment])
914 many (try $ g_spaces *> g_eol) *>
916 [ add_tag <$ g_spaces1 <*> g_account_tag
917 , add_comment <$ g_spaces <*> g_comment
920 add_tag (Account_Tag (Tag (Tag_Path p) v)) =
921 \(Account_Tags (Tags tags), tags2, cmts) ->
922 ( Account_Tags (Tags (TreeMap.insert (<>) p [v] tags))
923 , Map.insert (Tag_Path p) () tags2
926 \(tags, tags2, cmts) ->
927 (tags, tags2, c:cmts)
929 class Gram_Input g where
930 g_input :: g (Text -> a) -> g a
931 deriving instance Gram_Input g => Gram_Input (CF g)
933 -- * Class 'Gram_Term_Def'
936 , Sym.Gram_Term src ss g
937 , Gram_State (Sym.Name2Type src) g
938 , Inj_Source (Sym.TypeVT src) src
939 , Inj_Source (Sym.KindK src) src
940 , Inj_Source (Sym.AST_Type src) src
941 ) => Gram_Term_Def src ss g where
942 g_term_def :: CF g ( Sym.NameTe
943 , Either (At src (Sym.Error_Term src))
944 (Sym.TermVT src ss '[]) )
946 g_source $ g_get_after $
947 (\n args v n2t src ->
950 Sym.readTerm n2t Sym.CtxTyZ $
951 foldr (\(x, ty_x) -> BinTree0 . Sym.Token_Term_Abst src x ty_x) v args)
953 <*> many Sym.g_term_abst_decl
957 -- * Class 'Gram_Journal'
968 , Gram_Term_Def src ss g
969 , Gram_Reader (S.Either Exn.IOException CanonFile) g
970 , Gram_State (Context_Read src j) g
971 , Gram_State (Sym.Modules src ss) g
972 , Gram_State (Journal j) g
973 , Gram_State (Journals j) g
978 ) => Gram_Journal ss src j g where
980 :: (Transaction -> j -> j)
981 -> CF g (S.Either [At src (Error_Journal src)]
982 (CanonFile, Journal j))
983 g_journal consTxn = rule "Journal" $
984 g_state_after $ g_ask_before $
986 <$> (g_state_after $ g_source $ g_ask_before $ g_ask_before $ pure init_journal)
988 [ g_state_after $ mk_include <$> g_include @ss consTxn
989 -- NOTE: g_include must be the first choice
990 -- in order to have Megaparsec reporting the errors
991 -- of the included journal.
992 , g_state_after $ mk_transaction <$> g_transaction
993 , g_state_after $ mk_chart <$> g_chart_entry
994 , g_state_after $ g_state_after $ g_input $ mk_term <$> g_term_def
995 , [] <$ try (g_spaces <* g_eol)
999 (SourcePos jf _ _) lr_cf src
1001 { context_read_journals = Journals js
1002 , context_read_journal = jnls
1003 , context_read_canonfiles = cfs
1004 }::Context_Read src j) =
1006 S.Left e -> (ctx, S.Left $ At src $ Error_Journal_Read (PathFile jf) e)
1008 let jnl = journal{journal_file=PathFile jf} in
1011 { context_read_journals = Journals $ Map.insert cf jnl js
1012 , context_read_journal = jnl <| jnls
1013 , context_read_canonfiles = cf <| cfs
1018 { context_read_journals = Journals js
1019 , context_read_journal = jnl :| jnls
1020 , context_read_canonfiles = cf :| cfs
1021 }::Context_Read src j) =
1022 case concat $ S.either (pure . pure) (const []) err <> errs of
1024 let jnl' = jnl{journal_file=PathFile jf} in
1025 (,S.Right (cf, jnl'))
1027 { context_read_journals = Journals $ Map.insert cf jnl' js
1028 , context_read_journal = NonEmpty.fromList jnls
1029 , context_read_canonfiles = NonEmpty.fromList cfs
1031 es -> (ctx, S.Left es)
1032 mk_transaction lr_txn jnl@Journal{journal_content=j} =
1034 S.Left err -> (jnl, [Error_Journal_Transaction <$> err])
1035 S.Right txn -> (jnl{ journal_content = txn `consTxn` j }, [])
1036 mk_include lr_inc (jnl::Journal j) =
1038 S.Left errs -> (jnl, errs)
1039 S.Right (cf, _inc) -> (jnl{ journal_includes = journal_includes jnl <> [cf] }, [])
1040 mk_chart lr_ch chart =
1042 S.Left err -> (chart, [err])
1043 S.Right ch -> (chart <> ch, [])
1044 mk_term (n, lr_te) txt mods =
1046 Left err -> (mods, \(terms::Terms) -> (terms, [Error_Journal_Term <$> err]))
1047 Right te -> (ins_term te mods, \terms -> (Map.insert ([] `Sym.Mod` n) txt terms, []))
1049 ins_term :: Sym.TermVT src ss '[] -> Sym.Modules src ss -> Sym.Modules src ss
1050 ins_term t = Sym.insertTermVT ([] `Sym.Mod` t) n (Sym.Fixity2 Sym.infixN5)
1052 :: (Transaction -> j -> j)
1053 -> CF g (S.Either [At src (Error_Journal src)]
1054 (CanonFile, Journal j))
1055 g_include consTxn = rule "Include" $
1056 g_read g_path (g_journal @ss consTxn <* eoi)
1059 g_state_after $ g_source $ check_path
1060 <$> (g_canonfile $ g_ask_before $ fmap mk_path $
1061 (\d (PathFile p) -> PathFile $ d:p)
1062 <$> char '.' <*> g_pathfile)
1063 mk_path (PathFile fp) (SourcePos fp_old _ _) =
1065 FilePath.normalise $
1066 FilePath.takeDirectory fp_old </> fp
1067 check_path (fp, lr_cf) src
1069 { context_read_journals = Journals js
1070 , context_read_canonfiles = cfs
1071 , context_read_warnings = warns
1072 }::Context_Read src j) =
1074 Left e -> (ctx, S.Left $ Error_Journal_Read fp e)
1076 if cf `Map.member` js
1079 then (ctx, S.Left $ Error_Journal_Include_loop cf)
1082 if isJust $ (`List.find` warns) $ \case
1083 At{unAt=Warning_Journal_Include_multiple cf'} -> cf' `elem` cf<|cfs
1086 { context_read_warnings =
1087 At src (Warning_Journal_Include_multiple cf) : warns }
1088 else (ctx, S.Right fp)
1092 -- | Return the 'Integer' obtained by multiplying the given digits
1093 -- with the power of the given base respective to their rank.
1095 :: Integer -- ^ Base.
1096 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1098 integer_of_digits base =
1099 foldl' (\x d -> base*x + toInteger (Char.digitToInt d)) 0
1101 -- | Return the 'Int' obtained by multiplying the given digits
1102 -- with the power of the given base respective to their rank.
1105 -> String -- ^ Digits (MUST be recognised by 'Char.digitToInt').
1107 int_of_digits base =
1108 foldl' (\x d -> base*x + Char.digitToInt d) 0
1111 char_account_sep :: Char
1112 char_account_sep = '/'
1113 char_account_tag_prefix :: Char
1114 char_account_tag_prefix = '~'
1115 char_ymd_sep :: Char
1117 char_tod_sep :: Char
1119 char_comment_prefix :: Char
1120 char_comment_prefix = ';'
1121 char_tag_prefix :: Char
1122 char_tag_prefix = '#'
1123 char_tag_sep :: Char
1125 char_tag_data_prefix :: Char
1126 char_tag_data_prefix = '='
1127 char_transaction_date_sep :: Char
1128 char_transaction_date_sep = '='
1131 type Env src ss = Map Sym.NameTe (Sym.TermVT src ss '[])
1136 | Section_Transaction
1140 newtype Year = Year (H.Date_Year Date)
1143 -- * Type 'Error_Date'
1145 = Error_Date_Day_invalid (Integer, Int, Int)
1146 | Error_Date_TimeOfDay_invalid (Int, Int, Int)
1147 | Error_Date_TimeZone_unknown Text
1150 -- * Type 'Error_Posting'
1152 = Error_Posting_Account_Ref_unknown Tag_Path
1153 | Error_Posting_Account_Ref_multiple Tag_Path (Map Account ())
1154 | Error_Postings_not_equilibrated Postings
1157 -- * Type 'Error_Transaction'
1158 data Error_Transaction
1159 = Error_Transaction_Date Error_Date
1160 | Error_Transaction_Posting Error_Posting
1161 | Error_Transaction_not_equilibrated
1164 , H.SumByUnit (NonNull [Account_Section]) (H.Polarized Quantity)
1168 -- * Type 'Error_Chart'
1173 -- * Type 'Error_Journal'
1174 data Error_Journal src
1175 = Error_Journal_Transaction Error_Transaction
1176 | Error_Journal_Read PathFile Exn.IOException
1177 | Error_Journal_Include_loop CanonFile
1178 | Error_Journal_Chart Error_Chart
1179 | Error_Journal_Section Section Section
1180 | Error_Journal_Term (Sym.Error_Term src)
1183 -- * Type 'Warning_Journal'
1184 data Warning_Journal
1185 = Warning_Journal_Include_multiple CanonFile
1189 nonEmpty :: NonNull [a] -> NonEmpty a
1190 nonEmpty n = x :| xs where (x, xs) = NonNull.splitFirst n
1191 nonNull :: NonEmpty a -> NonNull [a]
1192 nonNull n = NonNull.ncons x xs where x :| xs = n