1 module Hcompta.LCC.Write.Compta where
3 import Control.Monad (Monad(..))
5 import Data.Char (Char)
7 import Data.Eq (Eq(..))
8 import Data.Foldable (Foldable(..))
9 import Data.Function (($), (.), flip, id)
10 import Data.Functor ((<$>))
11 import Data.Functor.Compose (Compose(..))
12 import Data.Maybe (Maybe(..), fromMaybe)
13 import Data.Monoid (Monoid(..))
14 import Data.Ord (Ord(..))
15 import Data.Semigroup (Semigroup(..))
16 import Data.Tuple (fst, uncurry)
17 import GHC.Exts (Int(..))
18 import Prelude (Integer, Num(..), RealFrac(..), Show(..), Integral(..), fromIntegral, toInteger)
20 import qualified Data.ByteString as BS
21 import qualified Data.Char as Char
22 import qualified Data.List as List
23 import qualified Data.Map.Strict as Map
24 import qualified Data.MonoTraversable as MT
25 import qualified Data.NonNull as NonNull
26 import qualified Data.Strict as S
27 import qualified Data.Text as T
28 import qualified Data.Text.Encoding as Enc
29 import qualified Data.TreeMap.Strict as TreeMap
31 import qualified Language.Symantic.Document.Term.Dimension as Dim
32 import qualified Language.Symantic.Document as D
33 import qualified Language.Symantic.Grammar as G
34 import qualified Language.Symantic as Sym
36 import qualified Hcompta as H
38 import Hcompta.LCC.Account
39 import Hcompta.LCC.Amount
40 import Hcompta.LCC.Chart
42 import Hcompta.LCC.Journal
43 import Hcompta.LCC.Name
44 import Hcompta.LCC.Posting
45 import Hcompta.LCC.Tag
46 import Hcompta.LCC.Transaction
47 import Hcompta.LCC.Source
48 import Hcompta.LCC.Compta
49 import qualified Hcompta.LCC.Read.Compta as G
51 -- * Class 'Writeable'
52 class Writeable d a where
55 widthWrite :: Writeable Dim.Dimension a => a -> Int
57 case Dim.dim_width $ Dim.dim $ write a of
58 D.Nat i -> fromInteger i
60 -- import Debug.Trace (trace)
61 -- dbg msg x = trace (msg <> " = " <> show x) x
66 { reader_account_ref :: Bool
67 , reader_amounts :: Style_Amounts
68 , reader_width_acct_amt :: Int
73 { reader_account_ref = True
74 , reader_amounts = Style_Amounts Map.empty
75 , reader_width_acct_amt = 0
78 instance (D.Textable d, D.Colorable d, D.Indentable d) =>
79 Writeable d Date where
81 let (y, mo, d) = H.gregorianOf dat in
82 (if y == 0 then D.empty else D.integer y <> sep G.char_ymd_sep) <>
84 sep G.char_ymd_sep <> int2 d <>
93 (if s < 10 then D.charH '0' else D.empty) <>
94 D.integer ((truncate s::Integer))))
96 int2 i = (if i < 10 then D.charH '0' else D.empty) <> D.int i
97 sep = D.blacker . D.charH
98 instance (D.Textable d, D.Colorable d, D.Indentable d, Monoid d) =>
99 Writeable d Account where
101 (`MT.ofoldMap` acct) $ \a ->
102 D.blacker (D.charH G.char_account_sep) <>
104 instance D.Textable d =>
105 Writeable d NameAccount where
106 write = D.textH . unName
107 instance (D.Textable d, D.Colorable d, D.Indentable d) =>
108 Writeable d Tag_Path where
109 write (Tag_Path path) =
111 (:) (D.yellower $ D.charH G.char_account_tag_prefix) $
113 (D.yellower $ D.charH G.char_tag_sep)
114 (D.textH . unName <$> NonNull.toNullable path)
115 instance (D.Textable d, D.Colorable d, D.Indentable d) =>
116 Writeable d Account_Tag where
117 write (Account_Tag (Tag (Tag_Path path) (Tag_Data value))) =
119 (:) (D.yellower $ D.charH G.char_account_tag_prefix) $
121 (D.yellower $ D.charH G.char_tag_sep)
122 (D.textH . unName <$> NonNull.toNullable path) ) <>
126 D.yellower (D.charH G.char_tag_data_prefix) <>
128 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d) =>
129 Writeable d (Styled_Amount Amount) where
132 { style_amount_unit_side = uside
133 , style_amount_unit_spaced = uspaced
141 S.Just True | not (H.null u) -> D.space
148 S.Just True | not (H.null u) -> D.space
153 S.Just True | not (H.null u) -> D.space
157 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d) =>
158 Writeable (Reader -> d) Amount where
160 write (styled_amount (reader_amounts ro) amt)
161 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
162 Writeable (Reader -> d) Amounts where
163 write (Amounts amts) ro =
165 List.intersperse " + " $
167 uncurry Amount <$> Map.toList amts
168 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d) =>
169 Writeable d Unit where
173 (\c -> case Char.generalCategory c of
174 Char.CurrencySymbol -> True
175 Char.LowercaseLetter -> True
176 Char.ModifierLetter -> True
177 Char.OtherLetter -> True
178 Char.TitlecaseLetter -> True
179 Char.UppercaseLetter -> True
183 else D.between (D.charH '"') (D.charH '"') $ D.textH t
184 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d) =>
185 Writeable d (Styled_Amount Quantity) where
188 { style_amount_fractioning
189 , style_amount_grouping_integral
190 , style_amount_grouping_fractional
193 let Decimal e n = qty
194 let num = show $ abs n
195 let sign = D.bold $ D.yellower $ D.textH (if n < 0 then "-" else "")
197 then sign <> D.bold (D.blue $ D.stringH num)
199 let num_len = List.length num
202 [ List.replicate (fromIntegral e + 1 - num_len) '0'
204 -- , replicate (fromIntegral precision - fromIntegral e) '0'
206 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded
207 let default_fractioning =
209 del_grouping_sep style_amount_grouping_integral $
210 del_grouping_sep style_amount_grouping_fractional $
214 D.stringH (S.maybe id
215 (\g -> List.reverse . group g . List.reverse)
216 style_amount_grouping_integral $ int) <>
217 D.yellower (D.charH (S.fromMaybe default_fractioning style_amount_fractioning)) <>
218 D.stringH (S.maybe id group style_amount_grouping_fractional frac))
220 group :: Style_Amount_Grouping -> [Char] -> [Char]
221 group (Style_Amount_Grouping sep sizes_) =
222 List.concat . List.reverse .
223 List.map List.reverse . fst .
225 (flip (\digit x -> case x of
226 ([], sizes) -> ([[digit]], sizes)
227 (digits:groups, []) -> ((digit:digits):groups, [])
228 (digits:groups, curr_sizes@(size:sizes)) ->
229 if List.length digits < size
230 then ( (digit:digits):groups, curr_sizes)
231 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
234 del_grouping_sep grouping =
236 S.Just (Style_Amount_Grouping sep _) -> List.delete sep
238 instance (D.Textable d, D.Colorable d, D.Indentable d) =>
239 Writeable d Comment where
240 write (Comment com) =
242 D.charH G.char_comment_prefix
243 <> (case T.uncons com of
244 Just (c, _) | not $ Char.isSpace c -> D.space
247 instance (D.Textable d, D.Colorable d, D.Indentable d) =>
248 Writeable d (d, [Comment]) where
249 write (prefix, com) =
251 List.intersperse D.newline $
252 (\c -> prefix <> write c) <$> com
253 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
254 Writeable d (Reader, Posting src) where
257 , posting_account_ref
263 let d_indent = D.spaces 2 in
265 let (d_acct, w_acct) =
266 case posting_account_ref of
267 S.Just (a S.:!: sa) | reader_account_ref ro ->
268 ( write a <> S.maybe D.empty write sa
269 , widthWrite a + S.maybe 0 widthWrite sa )
270 _ -> (write posting_account, widthWrite posting_account) in
271 (case posting_amounts of
272 Amounts amts | Map.null amts -> d_acct
276 (\mdoc unit qty -> Just $
277 let amt = styled_amount (reader_amounts ro) $ Amount unit qty in
278 let pad = max 0 $ reader_width_acct_amt ro - (w_acct + widthWrite amt) in
281 Just doc -> doc <> D.newline <> d_indent) <>
282 d_acct <> D.stringH ( List.replicate (pad) '_') <> D.space <> write amt
283 <> D.space <> D.stringH (show (reader_width_acct_amt ro, w_acct, widthWrite amt))
285 (case posting_comments of
287 [c] -> D.space <> write c
288 _ -> D.newline <> write (d_indent <> D.space :: d, posting_comments))
289 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
290 Writeable d (Reader, Transaction src) where
293 { transaction_comments
295 , transaction_wording = Wording transaction_wording
296 , transaction_postings = Postings transaction_postings
297 , transaction_tags = Transaction_Tags (Tags tags)
301 (D.charH G.char_transaction_date_sep)
302 (write <$> NonNull.toNullable transaction_dates)) <>
303 (case transaction_wording of
305 _ -> D.space <> D.magenta (D.textH transaction_wording)) <>
307 (case transaction_comments of
309 _ -> write (D.space :: d, transaction_comments) <> D.newline) <>
310 TreeMap.foldrWithPath
312 foldr (\value -> (<>) (D.spaces 2 <>
313 write (Transaction_Tag (Tag (Tag_Path path) value)) <> D.newline)))
315 D.catV (write . (ro',) <$> Compose transaction_postings)
318 { reader_width_acct_amt =
319 case reader_width_acct_amt ro of
320 0 -> w_Transaction ro txn
323 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
324 Writeable d (Reader, Transactions src) where
325 write (ro, Transactions txns) =
326 let ro' = ro{reader_width_acct_amt =
327 foldr (max . w_Transaction ro) 0 $ Compose txns} in
329 foldl (\mdoc txn -> Just $
333 Just doc -> D.newline <> D.newline <> doc
334 ) Nothing (Compose txns)
335 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
336 Writeable d Transaction_Tag where
337 write (Transaction_Tag (Tag (Tag_Path path) (Tag_Data value))) =
339 (:) (D.yellower $ D.charH G.char_tag_prefix) $
341 (D.yellower $ D.charH G.char_tag_sep)
342 (D.bold . D.textH . unName <$> NonNull.toNullable path)) <>
345 else D.yellower (D.charH G.char_tag_data_prefix) <> D.textH value
346 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d, Writeable d (ro, j)) =>
347 Writeable d (ro, Journal src j) where
353 (if null journal_terms then D.empty else (write journal_terms <> D.newline)) <>
354 (if H.null journal_chart then D.empty else (write journal_chart <> D.newline)) <>
355 write (ro, journal_content)
356 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d, Writeable d (ro, j)) =>
357 Writeable d (ro, Journals src j) where
358 write (ro, Journals js) =
360 (\doc j@Journal{journal_file=PathFile jf} ->
362 write (Comment $ T.pack jf) <> D.newline <>
363 D.newline <> write (ro, j)
365 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
366 Writeable d Chart where
368 TreeMap.foldlWithPath
369 (\doc acct (Account_Tags (Tags ca)) ->
371 write (H.to acct :: Account) <> D.newline <>
372 TreeMap.foldlWithPath
377 doc'' <> D.spaces 2 <>
378 write (Account_Tag (Tag (Tag_Path tp) tv)) <>
386 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
387 Writeable d (Terms src) where
388 write = Map.foldlWithKey (\doc n (G.At _src t) -> doc <> write n <> D.space <> D.text t <> D.newline) D.empty
389 instance (D.Textable d, D.Colorable d, D.Indentable d) =>
390 Writeable d (Sym.Mod Sym.NameTe) where
391 write (ms `Sym.Mod` Sym.NameTe n) =
393 List.intersperse (D.charH '.') $
394 ((\(Sym.NameMod m) -> D.textH m) <$> ms) <>
395 [(if isOp n then id else D.yellower) $ D.text n]
397 isOp = T.all $ \case '_' -> True; '\'' -> True; c -> Char.isAlphaNum c
398 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
399 Writeable d (Reader, LCC src) where
404 write (ro{reader_amounts = reader_amounts ro <> amts}, js)
405 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d) =>
406 Writeable (Reader -> d) (LCC src) where
411 write (ro{reader_amounts = reader_amounts ro <> amts}, js)
413 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d, Monoid d, Writeable d (Reader, j)) =>
414 Writeable d (Reader, Compta src ss j) where
416 { compta_journals = js
417 , compta_chart = c@Chart{chart_accounts=ca}
418 , compta_style_amounts = amts
419 , compta_terms = terms
421 (if null terms then D.empty else (write terms <> D.newline)) <>
422 (if TreeMap.null ca then D.empty else (write c <> D.newline)) <>
423 write (ro{reader_amounts = reader_amounts ro <> amts}, js)
425 instance (D.Textable d, D.Colorable d, D.Indentable d, D.Decorable d) =>
426 Writeable (IO d) SourcePos where
427 write (SourcePos p (PosFile l) (PosFile c)) = do
428 content <- Enc.decodeUtf8 <$> BS.readFile p
429 let ls = T.lines content
430 let ll = max 1 $ l - size_ctx
432 List.take (intFrom $ (l - ll) + 1 + size_ctx) $
433 List.drop (intFrom $ ll-1) ls
434 let ns = show <$> List.take (List.length qs) [ll..]
435 let max_len_n = maximum $ 0 : (List.length <$> ns)
436 let ns' = (<$> ns) $ \n ->
437 List.replicate (max_len_n - List.length n) ' ' <> n
440 List.zipWith (\(n, sn) q ->
441 D.spaces 2 <> D.blacker (D.stringH sn) <>
442 D.spaces 2 <> (if n == l then mark q else D.textH q)
443 ) (List.zip [ll..] ns') qs
444 return $ quote <> D.newline
447 intFrom = fromInteger . toInteger
449 let (b, a) = T.splitAt (intFrom c - 1) q in
452 Nothing -> D.red D.space
453 Just (a0, a') -> D.red (D.charH a0) <> D.textH a'
455 -- | Return the width of given 'Postings',
456 -- considering only 'Account's and 'Amount's (not indentation, 'Comment's, nor 'Posting_Tag's).
457 w_Transaction :: Reader -> Transaction src -> Int
458 -- w_Postings ro = MT.ofoldr (max . widthWrite ro) 0
462 , posting_account_ref
466 case posting_account_ref of
467 S.Just (a S.:!: sa) | reader_account_ref ro ->
468 widthWrite a + S.maybe 0 widthWrite sa
469 _ -> widthWrite posting_account in
471 case posting_amounts of
472 Amounts amts | Map.null amts -> 0
476 let amt = styled_amount (reader_amounts ro) $ Amount unit qty in