module Hcompta.LCC.Write.Compta where import Control.Monad (Monad(..)) import Data.Bool import Data.Char (Char) import Data.Decimal import Data.Eq (Eq(..)) import Data.Foldable (Foldable(..)) import Data.Function (($), (.), flip, id) import Data.Functor ((<$>)) import Data.Functor.Compose (Compose(..)) import Data.Maybe (Maybe(..), fromMaybe) import Data.Monoid (Monoid(..)) import Data.Ord (Ord(..)) import Data.Semigroup (Semigroup(..)) import Data.Tuple (fst, uncurry) import GHC.Exts (Int(..)) import Prelude (Integer, Num(..), RealFrac(..), Show(..), Integral(..), fromIntegral) import System.IO (IO) import qualified Data.ByteString as BS import qualified Data.Char as Char import qualified Data.List as L import qualified Data.Map.Strict as Map import qualified Data.MonoTraversable as MT import qualified Data.NonNull as NonNull import qualified Data.Strict as S import qualified Data.Text as T import qualified Data.Text.Encoding as Enc import qualified Data.TreeMap.Strict as TreeMap import qualified Language.Symantic.Document as D import qualified Language.Symantic.Grammar as G import qualified Language.Symantic as Sym import qualified Hcompta as H import Hcompta.LCC.Account import Hcompta.LCC.Amount import Hcompta.LCC.Chart import Hcompta.LCC.IO import Hcompta.LCC.Journal import Hcompta.LCC.Name import Hcompta.LCC.Posting import Hcompta.LCC.Tag import Hcompta.LCC.Transaction import Hcompta.LCC.Source import Hcompta.LCC.Compta import qualified Hcompta.LCC.Read.Compta as G -- * Class 'Writeable' class Writeable d a where write :: a -> d widthWrite :: Writeable D.Dim a => a -> Int widthWrite = D.width . D.dim . write -- import Debug.Trace (trace) -- dbg msg x = trace (msg <> " = " <> show x) x -- * Type 'Context_Write' data Context_Write = Context_Write { context_write_account_ref :: Bool , context_write_amounts :: Style_Amounts , context_write_width_acct_amt :: Int } context_write :: Context_Write context_write = Context_Write { context_write_account_ref = True , context_write_amounts = Style_Amounts Map.empty , context_write_width_acct_amt = 0 } instance (D.Doc_Text d, D.Doc_Color d) => Writeable d Date where write dat = let (y, mo, d) = H.gregorianOf dat in (if y == 0 then D.empty else D.integer y <> sep G.char_ymd_sep) <> int2 mo <> sep G.char_ymd_sep <> int2 d <> (case H.todOf dat of (0, 0, 0) -> D.empty (h, m, s) -> sep '_' <> int2 h <> sep ':' <> int2 m <> (case s of 0 -> D.empty _ -> sep ':' <> (if s < 10 then D.charH '0' else D.empty) <> D.integer ((truncate s::Integer)))) where int2 i = (if i < 10 then D.charH '0' else D.empty) <> D.int i sep = D.blacker . D.charH instance (D.Doc_Text d, D.Doc_Color d, Monoid d) => Writeable d Account where write acct = (`MT.ofoldMap` acct) $ \a -> D.blacker (D.charH G.char_account_sep) <> write a instance D.Doc_Text d => Writeable d NameAccount where write = D.textH . unName instance (D.Doc_Text d, D.Doc_Color d) => Writeable d Tag_Path where write (Tag_Path path) = D.catH $ (:) (D.yellower $ D.charH G.char_account_tag_prefix) $ L.intersperse (D.yellower $ D.charH G.char_tag_sep) (D.textH . unName <$> NonNull.toNullable path) instance (D.Doc_Text d, D.Doc_Color d) => Writeable d Account_Tag where write (Account_Tag (Tag (Tag_Path path) (Tag_Data value))) = D.catH ( (:) (D.yellower $ D.charH G.char_account_tag_prefix) $ L.intersperse (D.yellower $ D.charH G.char_tag_sep) (D.textH . unName <$> NonNull.toNullable path) ) <> if T.null value then D.empty else D.yellower (D.charH G.char_tag_data_prefix) <> D.textH value instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d) => Writeable d (Styled_Amount Amount) where write ( sty@Style_Amount { style_amount_unit_side = uside , style_amount_unit_spaced = uspaced } , Amount u q ) = case uside of S.Just L -> write u <> case uspaced of S.Just True | not (H.null u) -> D.space _ -> D.empty _ -> D.empty <> write (sty, q) <> case uside of S.Just R -> (case uspaced of S.Just True | not (H.null u) -> D.space _ -> D.empty) <> write u S.Nothing -> (case uspaced of S.Just True | not (H.null u) -> D.space _ -> D.empty) <> write u _ -> D.empty instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d) => Writeable (Context_Write -> d) Amount where write amt ctx = write (styled_amount (context_write_amounts ctx) amt) instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable (Context_Write -> d) Amounts where write (Amounts amts) ctx = mconcat $ L.intersperse " + " $ ((`write` ctx) <$>) $ uncurry Amount <$> Map.toList amts instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d) => Writeable d Unit where write (Unit t) = D.yellower $ if T.all (\c -> case Char.generalCategory c of Char.CurrencySymbol -> True Char.LowercaseLetter -> True Char.ModifierLetter -> True Char.OtherLetter -> True Char.TitlecaseLetter -> True Char.UppercaseLetter -> True _ -> False ) t then D.textH t else D.dquote $ D.textH t instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d) => Writeable d (Styled_Amount Quantity) where write ( Style_Amount { style_amount_fractioning , style_amount_grouping_integral , style_amount_grouping_fractional } , qty ) = do let Decimal e n = qty let num = show $ abs n let sign = D.bold $ D.yellower $ D.textH (if n < 0 then "-" else "") if e == 0 then sign <> D.bold (D.blue $ D.stringH num) else do let num_len = L.length num let padded = L.concat [ L.replicate (fromIntegral e + 1 - num_len) '0' , num -- , replicate (fromIntegral precision - fromIntegral e) '0' ] let (int, frac) = L.splitAt (max 1 (num_len - fromIntegral e)) padded let default_fractioning = L.head $ del_grouping_sep style_amount_grouping_integral $ del_grouping_sep style_amount_grouping_fractional $ ['.', ','] sign <> D.bold (D.blue $ D.stringH (S.maybe id (\g -> L.reverse . group g . L.reverse) style_amount_grouping_integral $ int) <> D.yellower (D.charH (S.fromMaybe default_fractioning style_amount_fractioning)) <> D.stringH (S.maybe id group style_amount_grouping_fractional frac)) where group :: Style_Amount_Grouping -> [Char] -> [Char] group (Style_Amount_Grouping sep sizes_) = L.concat . L.reverse . L.map L.reverse . fst . L.foldl' (flip (\digit x -> case x of ([], sizes) -> ([[digit]], sizes) (digits:groups, []) -> ((digit:digits):groups, []) (digits:groups, curr_sizes@(size:sizes)) -> if L.length digits < size then ( (digit:digits):groups, curr_sizes) else ([digit]:[sep]:digits:groups, if L.null sizes then curr_sizes else sizes) )) ([], sizes_) del_grouping_sep grouping = case grouping of S.Just (Style_Amount_Grouping sep _) -> L.delete sep _ -> id instance (D.Doc_Text d, D.Doc_Color d) => Writeable d Comment where write (Comment com) = D.cyan $ D.charH G.char_comment_prefix <> (case T.uncons com of Just (c, _) | not $ Char.isSpace c -> D.space _ -> D.empty) <> D.textH com instance (D.Doc_Text d, D.Doc_Color d) => Writeable d (d, [Comment]) where write (prefix, com) = D.catH $ L.intersperse D.eol $ (\c -> prefix <> write c) <$> com instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d (Context_Write, Posting src) where write (ctx, Posting { posting_account , posting_account_ref , posting_amounts , posting_comments -- , posting_dates -- , posting_tags }) = let d_indent = D.spaces 2 in d_indent <> let (d_acct, w_acct) = case posting_account_ref of S.Just (a S.:!: sa) | context_write_account_ref ctx -> ( write a <> S.maybe D.empty write sa , widthWrite a + S.maybe 0 widthWrite sa ) _ -> (write posting_account, widthWrite posting_account) in (case posting_amounts of Amounts amts | Map.null amts -> d_acct Amounts amts -> fromMaybe D.empty $ Map.foldlWithKey (\mdoc unit qty -> Just $ let amt = styled_amount (context_write_amounts ctx) $ Amount unit qty in let pad = max 0 $ context_write_width_acct_amt ctx - (w_acct + widthWrite amt) in (case mdoc of Nothing -> D.empty Just doc -> doc <> D.eol <> d_indent) <> d_acct <> D.spaces pad <> D.space <> write amt ) Nothing amts) <> (case posting_comments of [] -> D.empty [c] -> D.space <> write c _ -> D.eol <> write (d_indent <> D.space :: d, posting_comments)) instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d (Context_Write, Transaction src) where write (ctx, txn@Transaction { transaction_comments , transaction_dates , transaction_wording = Wording transaction_wording , transaction_postings = Postings transaction_postings , transaction_tags = Transaction_Tags (Tags tags) }) = let ctx' = ctx { context_write_width_acct_amt = let w = context_write_width_acct_amt ctx in if w == 0 then w_Transaction ctx txn else w } in D.catH ( L.intersperse (D.charH G.char_transaction_date_sep) (write <$> NonNull.toNullable transaction_dates)) <> (case transaction_wording of "" -> D.empty _ -> D.space <> D.magenta (D.textH transaction_wording)) <> D.eol <> (case transaction_comments of [] -> D.empty _ -> write (D.space :: d, transaction_comments) <> D.eol) <> TreeMap.foldrWithPath (\path -> flip $ foldr (\value -> (<>) (D.spaces 2 <> write (Transaction_Tag (Tag (Tag_Path path) value)) <> D.eol))) D.empty tags <> D.catV (write . (ctx',) <$> Compose transaction_postings) instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d (Context_Write, Transactions src) where write (ctx, Transactions txns) = let ctx' = ctx{context_write_width_acct_amt = foldr (max . w_Transaction ctx) 0 $ Compose txns} in fromMaybe D.empty $ foldl (\mdoc txn -> Just $ write (ctx', txn) <> case mdoc of Nothing -> D.eol Just doc -> D.eol <> D.eol <> doc ) Nothing (Compose txns) instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d Transaction_Tag where write (Transaction_Tag (Tag (Tag_Path path) (Tag_Data value))) = D.catH ( (:) (D.yellower $ D.charH G.char_tag_prefix) $ L.intersperse (D.yellower $ D.charH G.char_tag_sep) (D.bold . D.textH . unName <$> NonNull.toNullable path)) <> if T.null value then D.empty else D.yellower (D.charH G.char_tag_data_prefix) <> D.textH value instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d, Writeable d (ctx, j)) => Writeable d (ctx, Journal src j) where write (ctx, Journal { journal_content , journal_terms , journal_chart }) = (if null journal_terms then D.empty else (write journal_terms <> D.eol)) <> (if H.null journal_chart then D.empty else (write journal_chart <> D.eol)) <> write (ctx, journal_content) instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d, Writeable d (ctx, j)) => Writeable d (ctx, Journals src j) where write (ctx, Journals js) = Map.foldl (\doc j@Journal{journal_file=PathFile jf} -> doc <> write (Comment $ T.pack jf) <> D.eol <> D.eol <> write (ctx, j) ) D.empty js instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d Chart where write = TreeMap.foldlWithPath (\doc acct (Account_Tags (Tags ca)) -> doc <> write (H.to acct :: Account) <> D.eol <> TreeMap.foldlWithPath (\doc' tp tvs -> doc' <> foldl' (\doc'' tv -> doc'' <> D.spaces 2 <> write (Account_Tag (Tag (Tag_Path tp) tv)) <> D.eol) D.empty tvs) D.empty ca ) D.empty . chart_accounts instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d (Terms src) where write = Map.foldlWithKey (\doc n (G.At _src t) -> doc <> write n <> D.space <> D.text t <> D.eol) D.empty instance (D.Doc_Text d, D.Doc_Color d) => Writeable d (Sym.Mod Sym.NameTe) where write (ms `Sym.Mod` Sym.NameTe n) = D.catH $ L.intersperse (D.charH '.') $ ((\(Sym.NameMod m) -> D.textH m) <$> ms) <> [(if isOp n then id else D.yellower) $ D.text n] where isOp = T.all $ \case '_' -> True; '\'' -> True; c -> Char.isAlphaNum c instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable d (Context_Write, LCC src) where write (ctx, LCC { lcc_journals = js , lcc_style = amts }) = write (ctx{context_write_amounts = context_write_amounts ctx <> amts}, js) instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d) => Writeable (Context_Write -> d) (LCC src) where write LCC { lcc_journals = js , lcc_style = amts } ctx = write (ctx{context_write_amounts = context_write_amounts ctx <> amts}, js) {- instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d, Monoid d, Writeable d (Context_Write, j)) => Writeable d (Context_Write, Compta src ss j) where write (ctx, Compta { compta_journals = js , compta_chart = c@Chart{chart_accounts=ca} , compta_style_amounts = amts , compta_terms = terms }) = (if null terms then D.empty else (write terms <> D.eol)) <> (if TreeMap.null ca then D.empty else (write c <> D.eol)) <> write (ctx{context_write_amounts = context_write_amounts ctx <> amts}, js) -} instance (D.Doc_Text d, D.Doc_Color d, D.Doc_Decoration d) => Writeable (IO d) SourcePos where write (SourcePos p (PosFile l) (PosFile c)) = do content <- Enc.decodeUtf8 <$> BS.readFile p let ls = T.lines content let ll = max 1 $ l - size_ctx let qs = L.take (intFrom $ (l - ll) + 1 + size_ctx) $ L.drop (intFrom $ ll-1) ls let ns = show <$> L.take (L.length qs) [ll..] let max_len_n = maximum $ 0 : (L.length <$> ns) let ns' = (<$> ns) $ \n -> L.replicate (max_len_n - L.length n) ' ' <> n let quote = D.catV $ L.zipWith (\(n, sn) q -> D.spaces 2 <> D.blacker (D.stringH sn) <> D.spaces 2 <> (if n == l then mark q else D.textH q) ) (L.zip [ll..] ns') qs return $ quote <> D.eol where size_ctx = 2 intFrom = fromInteger . toInteger mark q = let (b, a) = T.splitAt (intFrom c - 1) q in D.textH b <> case T.uncons a of Nothing -> D.red D.space Just (a0, a') -> D.red (D.charH a0) <> D.textH a' -- | Return the width of given 'Postings', -- considering only 'Account's and 'Amount's (not indentation, 'Comment's, nor 'Posting_Tag's). w_Transaction :: Context_Write -> Transaction src -> Int -- w_Postings ctx = MT.ofoldr (max . widthWrite ctx) 0 w_Transaction ctx = MT.ofoldr (\Posting { posting_account , posting_account_ref , posting_amounts } -> max $ let w_Acct = case posting_account_ref of S.Just (a S.:!: sa) | context_write_account_ref ctx -> widthWrite a + S.maybe 0 widthWrite sa _ -> widthWrite posting_account in let w_Amt = case posting_amounts of Amounts amts | Map.null amts -> 0 Amounts amts -> Map.foldrWithKey (\unit qty -> max $ let amt = styled_amount (context_write_amounts ctx) $ Amount unit qty in widthWrite amt) 1 amts in w_Acct + w_Amt ) 0 . transaction_postings