lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully reducing space and time usage. This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1: hledger -f data/100x100x10.journal stats string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>> text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>> hledger -f data/1000x100x10.journal stats string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>> text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>> hledger -f data/10000x100x10.journal stats string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>> text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>> hledger -f data/100000x100x10.journal stats string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>> text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
This commit is contained in:
parent
097c9e09b6
commit
2538d14ea7
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE RecordWildCards, StandaloneDeriving #-}
|
{-# LANGUAGE RecordWildCards, StandaloneDeriving, OverloadedStrings #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE NoMonomorphismRestriction#-}
|
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
'AccountName's are strings like @assets:cash:petty@, with multiple
|
'AccountName's are strings like @assets:cash:petty@, with multiple
|
||||||
@ -10,7 +10,9 @@ hierarchy.
|
|||||||
module Hledger.Data.AccountName
|
module Hledger.Data.AccountName
|
||||||
where
|
where
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.List.Split (splitOn)
|
import Data.Monoid
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
@ -19,24 +21,29 @@ import Hledger.Data.Types
|
|||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
|
|
||||||
|
|
||||||
|
acctsepchar :: Char
|
||||||
-- change to use a different separator for nested accounts
|
|
||||||
acctsepchar = ':'
|
acctsepchar = ':'
|
||||||
|
|
||||||
accountNameComponents :: AccountName -> [String]
|
acctsep :: Text
|
||||||
accountNameComponents = splitAtElement acctsepchar
|
acctsep = T.pack [acctsepchar]
|
||||||
|
|
||||||
accountNameFromComponents :: [String] -> AccountName
|
-- accountNameComponents :: AccountName -> [String]
|
||||||
accountNameFromComponents = concat . intersperse [acctsepchar]
|
-- accountNameComponents = splitAtElement acctsepchar
|
||||||
|
|
||||||
accountLeafName :: AccountName -> String
|
accountNameComponents :: AccountName -> [Text]
|
||||||
|
accountNameComponents = T.splitOn acctsep
|
||||||
|
|
||||||
|
accountNameFromComponents :: [Text] -> AccountName
|
||||||
|
accountNameFromComponents = T.intercalate acctsep
|
||||||
|
|
||||||
|
accountLeafName :: AccountName -> Text
|
||||||
accountLeafName = last . accountNameComponents
|
accountLeafName = last . accountNameComponents
|
||||||
|
|
||||||
-- | Truncate all account name components but the last to two characters.
|
-- | Truncate all account name components but the last to two characters.
|
||||||
accountSummarisedName :: AccountName -> String
|
accountSummarisedName :: AccountName -> Text
|
||||||
accountSummarisedName a
|
accountSummarisedName a
|
||||||
-- length cs > 1 = take 2 (head cs) ++ ":" ++ a'
|
-- length cs > 1 = take 2 (head cs) ++ ":" ++ a'
|
||||||
| length cs > 1 = intercalate ":" (map (take 2) $ init cs) ++ ":" ++ a'
|
| length cs > 1 = (T.intercalate ":" (map (T.take 2) $ init cs)) <> ":" <> a'
|
||||||
| otherwise = a'
|
| otherwise = a'
|
||||||
where
|
where
|
||||||
cs = accountNameComponents a
|
cs = accountNameComponents a
|
||||||
@ -44,7 +51,7 @@ accountSummarisedName a
|
|||||||
|
|
||||||
accountNameLevel :: AccountName -> Int
|
accountNameLevel :: AccountName -> Int
|
||||||
accountNameLevel "" = 0
|
accountNameLevel "" = 0
|
||||||
accountNameLevel a = length (filter (==acctsepchar) a) + 1
|
accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1
|
||||||
|
|
||||||
accountNameDrop :: Int -> AccountName -> AccountName
|
accountNameDrop :: Int -> AccountName -> AccountName
|
||||||
accountNameDrop n = accountNameFromComponents . drop n . accountNameComponents
|
accountNameDrop n = accountNameFromComponents . drop n . accountNameComponents
|
||||||
@ -72,7 +79,7 @@ parentAccountNames a = parentAccountNames' $ parentAccountName a
|
|||||||
|
|
||||||
-- | Is the first account a parent or other ancestor of (and not the same as) the second ?
|
-- | Is the first account a parent or other ancestor of (and not the same as) the second ?
|
||||||
isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
|
isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
|
||||||
isAccountNamePrefixOf = isPrefixOf . (++ [acctsepchar])
|
isAccountNamePrefixOf = T.isPrefixOf . (<> acctsep)
|
||||||
|
|
||||||
isSubAccountNameOf :: AccountName -> AccountName -> Bool
|
isSubAccountNameOf :: AccountName -> AccountName -> Bool
|
||||||
s `isSubAccountNameOf` p =
|
s `isSubAccountNameOf` p =
|
||||||
@ -113,22 +120,22 @@ nullaccountnametree = Node "root" []
|
|||||||
elideAccountName :: Int -> AccountName -> AccountName
|
elideAccountName :: Int -> AccountName -> AccountName
|
||||||
elideAccountName width s
|
elideAccountName width s
|
||||||
-- XXX special case for transactions register's multi-account pseudo-names
|
-- XXX special case for transactions register's multi-account pseudo-names
|
||||||
| " (split)" `isSuffixOf` s =
|
| " (split)" `T.isSuffixOf` s =
|
||||||
let
|
let
|
||||||
names = splitOn ", " $ take (length s - 8) s
|
names = T.splitOn ", " $ T.take (T.length s - 8) s
|
||||||
widthpername = (max 0 (width - 8 - 2 * (max 1 (length names) - 1))) `div` length names
|
widthpername = (max 0 (width - 8 - 2 * (max 1 (length names) - 1))) `div` length names
|
||||||
in
|
in
|
||||||
fitString Nothing (Just width) True False $
|
fitText Nothing (Just width) True False $
|
||||||
(++" (split)") $
|
(<>" (split)") $
|
||||||
intercalate ", " $
|
T.intercalate ", " $
|
||||||
[accountNameFromComponents $ elideparts widthpername [] $ accountNameComponents s' | s' <- names]
|
[accountNameFromComponents $ elideparts widthpername [] $ accountNameComponents s' | s' <- names]
|
||||||
| otherwise =
|
| otherwise =
|
||||||
fitString Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s
|
fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s
|
||||||
where
|
where
|
||||||
elideparts :: Int -> [String] -> [String] -> [String]
|
elideparts :: Int -> [Text] -> [Text] -> [Text]
|
||||||
elideparts width done ss
|
elideparts width done ss
|
||||||
| strWidth (accountNameFromComponents $ done++ss) <= width = done++ss
|
| textWidth (accountNameFromComponents $ done++ss) <= width = done++ss
|
||||||
| length ss > 1 = elideparts width (done++[takeWidth 2 $ head ss]) (tail ss)
|
| length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss)
|
||||||
| otherwise = done++ss
|
| otherwise = done++ss
|
||||||
|
|
||||||
-- | Keep only the first n components of an account name, where n
|
-- | Keep only the first n components of an account name, where n
|
||||||
@ -143,18 +150,18 @@ clipOrEllipsifyAccountName 0 = const "..."
|
|||||||
clipOrEllipsifyAccountName n = accountNameFromComponents . take n . accountNameComponents
|
clipOrEllipsifyAccountName n = accountNameFromComponents . take n . accountNameComponents
|
||||||
|
|
||||||
-- | Convert an account name to a regular expression matching it and its subaccounts.
|
-- | Convert an account name to a regular expression matching it and its subaccounts.
|
||||||
accountNameToAccountRegex :: String -> String
|
accountNameToAccountRegex :: AccountName -> Regexp
|
||||||
accountNameToAccountRegex "" = ""
|
accountNameToAccountRegex "" = ""
|
||||||
accountNameToAccountRegex a = printf "^%s(:|$)" a
|
accountNameToAccountRegex a = printf "^%s(:|$)" (T.unpack a)
|
||||||
|
|
||||||
-- | Convert an account name to a regular expression matching it but not its subaccounts.
|
-- | Convert an account name to a regular expression matching it but not its subaccounts.
|
||||||
accountNameToAccountOnlyRegex :: String -> String
|
accountNameToAccountOnlyRegex :: AccountName -> Regexp
|
||||||
accountNameToAccountOnlyRegex "" = ""
|
accountNameToAccountOnlyRegex "" = ""
|
||||||
accountNameToAccountOnlyRegex a = printf "^%s$" a
|
accountNameToAccountOnlyRegex a = printf "^%s$" $ T.unpack a -- XXX pack
|
||||||
|
|
||||||
-- | Convert an exact account-matching regular expression to a plain account name.
|
-- | Convert an exact account-matching regular expression to a plain account name.
|
||||||
accountRegexToAccountName :: String -> String
|
accountRegexToAccountName :: Regexp -> AccountName
|
||||||
accountRegexToAccountName = regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1"
|
accountRegexToAccountName = T.pack . regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1" -- XXX pack
|
||||||
|
|
||||||
-- | Does this string look like an exact account-matching regular expression ?
|
-- | Does this string look like an exact account-matching regular expression ?
|
||||||
isAccountRegex :: String -> Bool
|
isAccountRegex :: String -> Bool
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE StandaloneDeriving #-}
|
{-# LANGUAGE StandaloneDeriving, OverloadedStrings #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
A 'Journal' is a set of transactions, plus optional related data. This is
|
A 'Journal' is a set of transactions, plus optional related data. This is
|
||||||
@ -63,6 +63,8 @@ import Data.List
|
|||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Data.Ord
|
import Data.Ord
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Safe (headMay, headDef)
|
import Safe (headMay, headDef)
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
@ -520,7 +522,7 @@ checkBalanceAssertion (errs,startbal) ps
|
|||||||
"%s"
|
"%s"
|
||||||
])
|
])
|
||||||
(showDate $ postingDate p)
|
(showDate $ postingDate p)
|
||||||
(paccount p)
|
(T.unpack $ paccount p) -- XXX pack
|
||||||
assertedcomm
|
assertedcomm
|
||||||
(showMixedAmount assertedbal)
|
(showMixedAmount assertedbal)
|
||||||
(showMixedAmount finalsinglebal)
|
(showMixedAmount finalsinglebal)
|
||||||
@ -528,7 +530,7 @@ checkBalanceAssertion (errs,startbal) ps
|
|||||||
(showPostingLine p)
|
(showPostingLine p)
|
||||||
(case ptransaction p of
|
(case ptransaction p of
|
||||||
Nothing -> ""
|
Nothing -> ""
|
||||||
Just t -> printf "in transaction at %s line %d:\n%s" f l (show t)
|
Just t -> printf "in transaction at %s line %d:\n%s" f l (show t) :: String
|
||||||
where GenericSourcePos f l _ = tsourcepos t
|
where GenericSourcePos f l _ = tsourcepos t
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ balances, and postings in each account.
|
|||||||
module Hledger.Data.Ledger
|
module Hledger.Data.Ledger
|
||||||
where
|
where
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Safe (headDef)
|
import Safe (headDef)
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
@ -72,7 +74,7 @@ ledgerLeafAccounts = filter (null.asubs) . laccounts
|
|||||||
|
|
||||||
-- | Accounts in ledger whose name matches the pattern, in tree order.
|
-- | Accounts in ledger whose name matches the pattern, in tree order.
|
||||||
ledgerAccountsMatching :: [String] -> Ledger -> [Account]
|
ledgerAccountsMatching :: [String] -> Ledger -> [Account]
|
||||||
ledgerAccountsMatching pats = filter (matchpats pats . aname) . laccounts
|
ledgerAccountsMatching pats = filter (matchpats pats . T.unpack . aname) . laccounts -- XXX pack
|
||||||
|
|
||||||
-- | List a ledger's postings, in the order parsed.
|
-- | List a ledger's postings, in the order parsed.
|
||||||
ledgerPostings :: Ledger -> [Posting]
|
ledgerPostings :: Ledger -> [Posting]
|
||||||
|
|||||||
@ -7,6 +7,8 @@ look up the date or description there.
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Data.Posting (
|
module Hledger.Data.Posting (
|
||||||
-- * Posting
|
-- * Posting
|
||||||
nullposting,
|
nullposting,
|
||||||
@ -50,7 +52,10 @@ where
|
|||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.MemoUgly (memo)
|
import Data.MemoUgly (memo)
|
||||||
|
import Data.Monoid
|
||||||
import Data.Ord
|
import Data.Ord
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Safe
|
import Safe
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
@ -89,7 +94,7 @@ showPosting p@Posting{paccount=a,pamount=amt,ptype=t} =
|
|||||||
where
|
where
|
||||||
ledger3ishlayout = False
|
ledger3ishlayout = False
|
||||||
acctnamewidth = if ledger3ishlayout then 25 else 22
|
acctnamewidth = if ledger3ishlayout then 25 else 22
|
||||||
showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . elideAccountName width
|
showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . T.unpack . elideAccountName width
|
||||||
(bracket,width) = case t of
|
(bracket,width) = case t of
|
||||||
BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)
|
BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)
|
||||||
VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)
|
VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)
|
||||||
@ -192,32 +197,32 @@ postingsDateSpan' wd ps = DateSpan (Just $ postingdate $ head ps') (Just $ addDa
|
|||||||
|
|
||||||
accountNamePostingType :: AccountName -> PostingType
|
accountNamePostingType :: AccountName -> PostingType
|
||||||
accountNamePostingType a
|
accountNamePostingType a
|
||||||
| null a = RegularPosting
|
| T.null a = RegularPosting
|
||||||
| head a == '[' && last a == ']' = BalancedVirtualPosting
|
| T.head a == '[' && T.last a == ']' = BalancedVirtualPosting
|
||||||
| head a == '(' && last a == ')' = VirtualPosting
|
| T.head a == '(' && T.last a == ')' = VirtualPosting
|
||||||
| otherwise = RegularPosting
|
| otherwise = RegularPosting
|
||||||
|
|
||||||
accountNameWithoutPostingType :: AccountName -> AccountName
|
accountNameWithoutPostingType :: AccountName -> AccountName
|
||||||
accountNameWithoutPostingType a = case accountNamePostingType a of
|
accountNameWithoutPostingType a = case accountNamePostingType a of
|
||||||
BalancedVirtualPosting -> init $ tail a
|
BalancedVirtualPosting -> T.init $ T.tail a
|
||||||
VirtualPosting -> init $ tail a
|
VirtualPosting -> T.init $ T.tail a
|
||||||
RegularPosting -> a
|
RegularPosting -> a
|
||||||
|
|
||||||
accountNameWithPostingType :: PostingType -> AccountName -> AccountName
|
accountNameWithPostingType :: PostingType -> AccountName -> AccountName
|
||||||
accountNameWithPostingType BalancedVirtualPosting a = "["++accountNameWithoutPostingType a++"]"
|
accountNameWithPostingType BalancedVirtualPosting a = "["<>accountNameWithoutPostingType a<>"]"
|
||||||
accountNameWithPostingType VirtualPosting a = "("++accountNameWithoutPostingType a++")"
|
accountNameWithPostingType VirtualPosting a = "("<>accountNameWithoutPostingType a<>")"
|
||||||
accountNameWithPostingType RegularPosting a = accountNameWithoutPostingType a
|
accountNameWithPostingType RegularPosting a = accountNameWithoutPostingType a
|
||||||
|
|
||||||
-- | Prefix one account name to another, preserving posting type
|
-- | Prefix one account name to another, preserving posting type
|
||||||
-- indicators like concatAccountNames.
|
-- indicators like concatAccountNames.
|
||||||
joinAccountNames :: AccountName -> AccountName -> AccountName
|
joinAccountNames :: AccountName -> AccountName -> AccountName
|
||||||
joinAccountNames a b = concatAccountNames $ filter (not . null) [a,b]
|
joinAccountNames a b = concatAccountNames $ filter (not . T.null) [a,b]
|
||||||
|
|
||||||
-- | Join account names into one. If any of them has () or [] posting type
|
-- | Join account names into one. If any of them has () or [] posting type
|
||||||
-- indicators, these (the first type encountered) will also be applied to
|
-- indicators, these (the first type encountered) will also be applied to
|
||||||
-- the resulting account name.
|
-- the resulting account name.
|
||||||
concatAccountNames :: [AccountName] -> AccountName
|
concatAccountNames :: [AccountName] -> AccountName
|
||||||
concatAccountNames as = accountNameWithPostingType t $ intercalate ":" $ map accountNameWithoutPostingType as
|
concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map accountNameWithoutPostingType as
|
||||||
where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as
|
where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as
|
||||||
|
|
||||||
-- | Rewrite an account name using all matching aliases from the given list, in sequence.
|
-- | Rewrite an account name using all matching aliases from the given list, in sequence.
|
||||||
@ -241,9 +246,9 @@ accountNameApplyAliasesMemo aliases = memo (accountNameApplyAliases aliases)
|
|||||||
|
|
||||||
aliasReplace :: AccountAlias -> AccountName -> AccountName
|
aliasReplace :: AccountAlias -> AccountName -> AccountName
|
||||||
aliasReplace (BasicAlias old new) a
|
aliasReplace (BasicAlias old new) a
|
||||||
| old `isAccountNamePrefixOf` a || old == a = new ++ drop (length old) a
|
| old `isAccountNamePrefixOf` a || old == a = new <> T.drop (T.length old) a
|
||||||
| otherwise = a
|
| otherwise = a
|
||||||
aliasReplace (RegexAlias re repl) a = regexReplaceCIMemo re repl a
|
aliasReplace (RegexAlias re repl) a = T.pack $ regexReplaceCIMemo re repl $ T.unpack a -- XXX
|
||||||
|
|
||||||
|
|
||||||
tests_Hledger_Data_Posting = TestList [
|
tests_Hledger_Data_Posting = TestList [
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
A 'TimeclockEntry' is a clock-in, clock-out, or other directive in a timeclock
|
A 'TimeclockEntry' is a clock-in, clock-out, or other directive in a timeclock
|
||||||
@ -7,9 +6,13 @@ converted to 'Transactions' and queried like a ledger.
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE CPP, OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Data.Timeclock
|
module Hledger.Data.Timeclock
|
||||||
where
|
where
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
-- import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Data.Time.Clock
|
import Data.Time.Clock
|
||||||
import Data.Time.Format
|
import Data.Time.Format
|
||||||
|
|||||||
@ -7,11 +7,15 @@ tags.
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Data.Transaction (
|
module Hledger.Data.Transaction (
|
||||||
-- * Transaction
|
-- * Transaction
|
||||||
nullsourcepos,
|
nullsourcepos,
|
||||||
nulltransaction,
|
nulltransaction,
|
||||||
txnTieKnot,
|
txnTieKnot,
|
||||||
|
txnUntieKnot,
|
||||||
|
journalUntieKnots,
|
||||||
-- settxn,
|
-- settxn,
|
||||||
-- * operations
|
-- * operations
|
||||||
showAccountName,
|
showAccountName,
|
||||||
@ -38,6 +42,8 @@ module Hledger.Data.Transaction (
|
|||||||
where
|
where
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
@ -188,7 +194,7 @@ postingAsLines elideamount onelineamounts ps p =
|
|||||||
showstatus p ++ fitString (Just acctwidth) Nothing False True (showAccountName Nothing (ptype p) (paccount p))
|
showstatus p ++ fitString (Just acctwidth) Nothing False True (showAccountName Nothing (ptype p) (paccount p))
|
||||||
where
|
where
|
||||||
showstatus p = if pstatus p == Cleared then "* " else ""
|
showstatus p = if pstatus p == Cleared then "* " else ""
|
||||||
acctwidth = maximum $ map (strWidth . paccount) ps
|
acctwidth = maximum $ map (textWidth . paccount) ps
|
||||||
|
|
||||||
-- currently prices are considered part of the amount string when right-aligning amounts
|
-- currently prices are considered part of the amount string when right-aligning amounts
|
||||||
amount
|
amount
|
||||||
@ -239,12 +245,16 @@ indent = (" "++)
|
|||||||
showAccountName :: Maybe Int -> PostingType -> AccountName -> String
|
showAccountName :: Maybe Int -> PostingType -> AccountName -> String
|
||||||
showAccountName w = fmt
|
showAccountName w = fmt
|
||||||
where
|
where
|
||||||
fmt RegularPosting = take w'
|
fmt RegularPosting = take w' . T.unpack
|
||||||
fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse
|
fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse . T.unpack
|
||||||
fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse
|
fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse . T.unpack
|
||||||
w' = fromMaybe 999999 w
|
w' = fromMaybe 999999 w
|
||||||
parenthesise s = "("++s++")"
|
|
||||||
bracket s = "["++s++"]"
|
parenthesise :: String -> String
|
||||||
|
parenthesise s = "("++s++")"
|
||||||
|
|
||||||
|
bracket :: String -> String
|
||||||
|
bracket s = "["++s++"]"
|
||||||
|
|
||||||
hasRealPostings :: Transaction -> Bool
|
hasRealPostings :: Transaction -> Bool
|
||||||
hasRealPostings = not . null . realPostings
|
hasRealPostings = not . null . realPostings
|
||||||
@ -414,6 +424,16 @@ transactionDate2 t = fromMaybe (tdate t) $ tdate2 t
|
|||||||
txnTieKnot :: Transaction -> Transaction
|
txnTieKnot :: Transaction -> Transaction
|
||||||
txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (settxn t) ps}
|
txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (settxn t) ps}
|
||||||
|
|
||||||
|
-- | Ensure a transaction's postings do not refer back to it, so that eg
|
||||||
|
-- recursiveSize and GHCI's :sprint work right.
|
||||||
|
txnUntieKnot :: Transaction -> Transaction
|
||||||
|
txnUntieKnot t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}
|
||||||
|
|
||||||
|
-- | Untie all transaction-posting knots in this journal, so that eg
|
||||||
|
-- recursiveSize and GHCI's :sprint can work on it.
|
||||||
|
journalUntieKnots :: Transaction -> Transaction
|
||||||
|
journalUntieKnots t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}
|
||||||
|
|
||||||
-- | Set a posting's parent transaction.
|
-- | Set a posting's parent transaction.
|
||||||
settxn :: Transaction -> Posting -> Posting
|
settxn :: Transaction -> Posting -> Posting
|
||||||
settxn t p = p{ptransaction=Just t}
|
settxn t p = p{ptransaction=Just t}
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import Data.Data
|
|||||||
import Data.Decimal
|
import Data.Decimal
|
||||||
import Text.Blaze (ToMarkup(..))
|
import Text.Blaze (ToMarkup(..))
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
|
import Data.Text (Text)
|
||||||
|
-- import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Data.Time.LocalTime
|
import Data.Time.LocalTime
|
||||||
import System.Time (ClockTime(..))
|
import System.Time (ClockTime(..))
|
||||||
@ -50,7 +52,7 @@ data Interval = NoInterval
|
|||||||
|
|
||||||
instance NFData Interval
|
instance NFData Interval
|
||||||
|
|
||||||
type AccountName = String
|
type AccountName = Text
|
||||||
|
|
||||||
data AccountAlias = BasicAlias AccountName AccountName
|
data AccountAlias = BasicAlias AccountName AccountName
|
||||||
| RegexAlias Regexp Replacement
|
| RegexAlias Regexp Replacement
|
||||||
@ -206,7 +208,7 @@ data TimeclockEntry = TimeclockEntry {
|
|||||||
tlsourcepos :: GenericSourcePos,
|
tlsourcepos :: GenericSourcePos,
|
||||||
tlcode :: TimeclockCode,
|
tlcode :: TimeclockCode,
|
||||||
tldatetime :: LocalTime,
|
tldatetime :: LocalTime,
|
||||||
tlaccount :: String,
|
tlaccount :: AccountName,
|
||||||
tldescription :: String
|
tldescription :: String
|
||||||
} deriving (Eq,Ord,Typeable,Data,Generic)
|
} deriving (Eq,Ord,Typeable,Data,Generic)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
A general query system for matching things (accounts, postings,
|
A general query system for matching things (accounts, postings,
|
||||||
@ -6,6 +5,8 @@ transactions..) by various criteria, and a parser for query expressions.
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE DeriveDataTypeable, OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Query (
|
module Hledger.Query (
|
||||||
-- * Query and QueryOpt
|
-- * Query and QueryOpt
|
||||||
Query(..),
|
Query(..),
|
||||||
@ -45,6 +46,8 @@ import Data.Data
|
|||||||
import Data.Either
|
import Data.Either
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Safe (readDef, headDef)
|
import Safe (readDef, headDef)
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
@ -236,8 +239,8 @@ defaultprefix = "acct"
|
|||||||
-- | Parse a single query term as either a query or a query option,
|
-- | Parse a single query term as either a query or a query option,
|
||||||
-- or raise an error if it has invalid syntax.
|
-- or raise an error if it has invalid syntax.
|
||||||
parseQueryTerm :: Day -> String -> Either Query QueryOpt
|
parseQueryTerm :: Day -> String -> Either Query QueryOpt
|
||||||
parseQueryTerm _ ('i':'n':'a':'c':'c':'t':'o':'n':'l':'y':':':s) = Right $ QueryOptInAcctOnly s
|
parseQueryTerm _ ('i':'n':'a':'c':'c':'t':'o':'n':'l':'y':':':s) = Right $ QueryOptInAcctOnly $ T.pack s
|
||||||
parseQueryTerm _ ('i':'n':'a':'c':'c':'t':':':s) = Right $ QueryOptInAcct s
|
parseQueryTerm _ ('i':'n':'a':'c':'c':'t':':':s) = Right $ QueryOptInAcct $ T.pack s
|
||||||
parseQueryTerm d ('n':'o':'t':':':s) = case parseQueryTerm d s of
|
parseQueryTerm d ('n':'o':'t':':':s) = case parseQueryTerm d s of
|
||||||
Left m -> Left $ Not m
|
Left m -> Left $ Not m
|
||||||
Right _ -> Left Any -- not:somequeryoption will be ignored
|
Right _ -> Left Any -- not:somequeryoption will be ignored
|
||||||
@ -557,8 +560,8 @@ inAccount (QueryOptInAcct a:_) = Just (a,True)
|
|||||||
-- Just looks at the first query option.
|
-- Just looks at the first query option.
|
||||||
inAccountQuery :: [QueryOpt] -> Maybe Query
|
inAccountQuery :: [QueryOpt] -> Maybe Query
|
||||||
inAccountQuery [] = Nothing
|
inAccountQuery [] = Nothing
|
||||||
inAccountQuery (QueryOptInAcctOnly a:_) = Just $ Acct $ accountNameToAccountOnlyRegex a
|
inAccountQuery (QueryOptInAcctOnly a : _) = Just $ Acct $ accountNameToAccountOnlyRegex a
|
||||||
inAccountQuery (QueryOptInAcct a:_) = Just $ Acct $ accountNameToAccountRegex a
|
inAccountQuery (QueryOptInAcct a : _) = Just $ Acct $ accountNameToAccountRegex a
|
||||||
|
|
||||||
-- -- | Convert a query to its inverse.
|
-- -- | Convert a query to its inverse.
|
||||||
-- negateQuery :: Query -> Query
|
-- negateQuery :: Query -> Query
|
||||||
@ -573,7 +576,7 @@ matchesAccount (None) _ = False
|
|||||||
matchesAccount (Not m) a = not $ matchesAccount m a
|
matchesAccount (Not m) a = not $ matchesAccount m a
|
||||||
matchesAccount (Or ms) a = any (`matchesAccount` a) ms
|
matchesAccount (Or ms) a = any (`matchesAccount` a) ms
|
||||||
matchesAccount (And ms) a = all (`matchesAccount` a) ms
|
matchesAccount (And ms) a = all (`matchesAccount` a) ms
|
||||||
matchesAccount (Acct r) a = regexMatchesCI r a
|
matchesAccount (Acct r) a = regexMatchesCI r (T.unpack a) -- XXX pack
|
||||||
matchesAccount (Depth d) a = accountNameLevel a <= d
|
matchesAccount (Depth d) a = accountNameLevel a <= d
|
||||||
matchesAccount (Tag _ _) _ = False
|
matchesAccount (Tag _ _) _ = False
|
||||||
matchesAccount _ _ = True
|
matchesAccount _ _ = True
|
||||||
@ -634,7 +637,7 @@ matchesPosting (Or qs) p = any (`matchesPosting` p) qs
|
|||||||
matchesPosting (And qs) p = all (`matchesPosting` p) qs
|
matchesPosting (And qs) p = all (`matchesPosting` p) qs
|
||||||
matchesPosting (Code r) p = regexMatchesCI r $ maybe "" tcode $ ptransaction p
|
matchesPosting (Code r) p = regexMatchesCI r $ maybe "" tcode $ ptransaction p
|
||||||
matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" tdescription $ ptransaction p
|
matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" tdescription $ ptransaction p
|
||||||
matchesPosting (Acct r) p = regexMatchesCI r $ paccount p
|
matchesPosting (Acct r) p = regexMatchesCI r $ T.unpack $ paccount p -- XXX pack
|
||||||
matchesPosting (Date span) p = span `spanContainsDate` postingDate p
|
matchesPosting (Date span) p = span `spanContainsDate` postingDate p
|
||||||
matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p
|
matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p
|
||||||
matchesPosting (Status Uncleared) p = postingStatus p /= Cleared
|
matchesPosting (Status Uncleared) p = postingStatus p /= Cleared
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import Data.Functor.Identity
|
|||||||
import Data.List.Compat
|
import Data.List.Compat
|
||||||
import Data.List.Split (wordsBy)
|
import Data.List.Split (wordsBy)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Data.Time.LocalTime
|
import Data.Time.LocalTime
|
||||||
import Safe
|
import Safe
|
||||||
@ -104,7 +106,7 @@ popParentAccount = do
|
|||||||
[] -> unexpected "End of apply account block with no beginning"
|
[] -> unexpected "End of apply account block with no beginning"
|
||||||
(_:rest) -> setState j{jparseparentaccounts=rest}
|
(_:rest) -> setState j{jparseparentaccounts=rest}
|
||||||
|
|
||||||
getParentAccount :: Monad m => JournalParser m String
|
getParentAccount :: Monad m => JournalParser m AccountName
|
||||||
getParentAccount = fmap (concatAccountNames . reverse . jparseparentaccounts) getState
|
getParentAccount = fmap (concatAccountNames . reverse . jparseparentaccounts) getState
|
||||||
|
|
||||||
addAccountAlias :: Monad m => AccountAlias -> JournalParser m ()
|
addAccountAlias :: Monad m => AccountAlias -> JournalParser m ()
|
||||||
@ -271,12 +273,13 @@ modifiedaccountnamep = do
|
|||||||
-- (This parser will also consume one following space, if present.)
|
-- (This parser will also consume one following space, if present.)
|
||||||
accountnamep :: Monad m => StringParser u m AccountName
|
accountnamep :: Monad m => StringParser u m AccountName
|
||||||
accountnamep = do
|
accountnamep = do
|
||||||
a <- do
|
astr <- do
|
||||||
c <- nonspace
|
c <- nonspace
|
||||||
cs <- striptrailingspace <$> many (nonspace <|> singlespace)
|
cs <- striptrailingspace <$> many (nonspace <|> singlespace)
|
||||||
return $ c:cs
|
return $ c:cs
|
||||||
|
let a = T.pack astr
|
||||||
when (accountNameFromComponents (accountNameComponents a) /= a)
|
when (accountNameFromComponents (accountNameComponents a) /= a)
|
||||||
(fail $ "account name seems ill-formed: "++a)
|
(fail $ "account name seems ill-formed: "++astr)
|
||||||
return a
|
return a
|
||||||
where
|
where
|
||||||
singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})
|
singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import Data.Char (toLower, isDigit, isSpace)
|
|||||||
import Data.List.Compat
|
import Data.List.Compat
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Ord
|
import Data.Ord
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar (Day)
|
import Data.Time.Calendar (Day)
|
||||||
#if MIN_VERSION_time(1,5,0)
|
#if MIN_VERSION_time(1,5,0)
|
||||||
import Data.Time.Format (parseTimeM, defaultTimeLocale)
|
import Data.Time.Format (parseTimeM, defaultTimeLocale)
|
||||||
@ -638,8 +640,8 @@ transactionFromCsvRecord sourcepos rules record = t
|
|||||||
defaccount2 = case isNegativeMixedAmount amount2 of
|
defaccount2 = case isNegativeMixedAmount amount2 of
|
||||||
Just True -> "income:unknown"
|
Just True -> "income:unknown"
|
||||||
_ -> "expenses:unknown"
|
_ -> "expenses:unknown"
|
||||||
account1 = maybe "" render (mfieldtemplate "account1") `or` defaccount1
|
account1 = T.pack $ maybe "" render (mfieldtemplate "account1") `or` defaccount1
|
||||||
account2 = maybe "" render (mfieldtemplate "account2") `or` defaccount2
|
account2 = T.pack $ maybe "" render (mfieldtemplate "account2") `or` defaccount2
|
||||||
|
|
||||||
-- build the transaction
|
-- build the transaction
|
||||||
t = nulltransaction{
|
t = nulltransaction{
|
||||||
|
|||||||
@ -82,6 +82,8 @@ import Control.Monad
|
|||||||
import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError)
|
import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError)
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Data.Time.LocalTime
|
import Data.Time.LocalTime
|
||||||
import Safe
|
import Safe
|
||||||
@ -319,7 +321,7 @@ basicaliasp = do
|
|||||||
char '='
|
char '='
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
new <- rstrip <$> anyChar `manyTill` eolof -- don't require a final newline, good for cli options
|
new <- rstrip <$> anyChar `manyTill` eolof -- don't require a final newline, good for cli options
|
||||||
return $ BasicAlias old new
|
return $ BasicAlias (T.pack old) (T.pack new)
|
||||||
|
|
||||||
regexaliasp :: Monad m => StringParser u m AccountAlias
|
regexaliasp :: Monad m => StringParser u m AccountAlias
|
||||||
regexaliasp = do
|
regexaliasp = do
|
||||||
@ -550,7 +552,7 @@ postingp mtdate = do
|
|||||||
status <- statusp
|
status <- statusp
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
account <- modifiedaccountnamep
|
account <- modifiedaccountnamep
|
||||||
let (ptype, account') = (accountNamePostingType account, unbracket account)
|
let (ptype, account') = (accountNamePostingType account, textUnbracket account)
|
||||||
amount <- spaceandamountormissingp
|
amount <- spaceandamountormissingp
|
||||||
massertion <- partialbalanceassertionp
|
massertion <- partialbalanceassertionp
|
||||||
_ <- fixedlotpricep
|
_ <- fixedlotpricep
|
||||||
|
|||||||
@ -40,6 +40,8 @@ i, o or O. The meanings of the codes are:
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Read.TimeclockReader (
|
module Hledger.Read.TimeclockReader (
|
||||||
-- * Reader
|
-- * Reader
|
||||||
reader,
|
reader,
|
||||||
@ -55,6 +57,8 @@ import Control.Monad
|
|||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Monad.Except (ExceptT)
|
import Control.Monad.Except (ExceptT)
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
-- import qualified Data.Text as T
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
import Text.Parsec hiding (parse)
|
import Text.Parsec hiding (parse)
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, ScopedTypeVariables #-}
|
{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, ScopedTypeVariables, OverloadedStrings #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
Balance report, used by the balance command.
|
Balance report, used by the balance command.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections #-}
|
{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections, OverloadedStrings #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
Postings report, used by the register command.
|
Postings report, used by the register command.
|
||||||
|
|||||||
@ -30,6 +30,8 @@ where
|
|||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Ord
|
import Data.Ord
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
-- import Test.HUnit
|
-- import Test.HUnit
|
||||||
|
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
@ -204,7 +206,7 @@ accountTransactionsReportItems query thisacctquery bal signfn (torig:ts) =
|
|||||||
-- To reduce noise, if there are both real and virtual postings, show only the real ones.
|
-- To reduce noise, if there are both real and virtual postings, show only the real ones.
|
||||||
summarisePostingAccounts :: [Posting] -> String
|
summarisePostingAccounts :: [Posting] -> String
|
||||||
summarisePostingAccounts ps =
|
summarisePostingAccounts ps =
|
||||||
(intercalate ", " . map accountSummarisedName . nub . map paccount) displayps
|
(intercalate ", " . map (T.unpack . accountSummarisedName) . nub . map paccount) displayps -- XXX pack
|
||||||
where
|
where
|
||||||
realps = filter isReal ps
|
realps = filter isReal ps
|
||||||
displayps | null realps = ps
|
displayps | null realps = ps
|
||||||
|
|||||||
@ -22,6 +22,7 @@ module Hledger.Utils (---- provide these frequently used modules - or not, for c
|
|||||||
module Hledger.Utils.Parse,
|
module Hledger.Utils.Parse,
|
||||||
module Hledger.Utils.Regex,
|
module Hledger.Utils.Regex,
|
||||||
module Hledger.Utils.String,
|
module Hledger.Utils.String,
|
||||||
|
module Hledger.Utils.Text,
|
||||||
module Hledger.Utils.Test,
|
module Hledger.Utils.Test,
|
||||||
module Hledger.Utils.Tree,
|
module Hledger.Utils.Tree,
|
||||||
-- Debug.Trace.trace,
|
-- Debug.Trace.trace,
|
||||||
@ -38,6 +39,8 @@ import Control.Monad (liftM)
|
|||||||
-- import Data.PPrint
|
-- import Data.PPrint
|
||||||
import Data.Time.Clock
|
import Data.Time.Clock
|
||||||
import Data.Time.LocalTime
|
import Data.Time.LocalTime
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
-- import qualified Data.Text as T
|
||||||
import System.Directory (getHomeDirectory)
|
import System.Directory (getHomeDirectory)
|
||||||
import System.FilePath((</>), isRelative)
|
import System.FilePath((</>), isRelative)
|
||||||
import System.IO
|
import System.IO
|
||||||
@ -48,6 +51,7 @@ import Hledger.Utils.Debug
|
|||||||
import Hledger.Utils.Parse
|
import Hledger.Utils.Parse
|
||||||
import Hledger.Utils.Regex
|
import Hledger.Utils.Regex
|
||||||
import Hledger.Utils.String
|
import Hledger.Utils.String
|
||||||
|
import Hledger.Utils.Text
|
||||||
import Hledger.Utils.Test
|
import Hledger.Utils.Test
|
||||||
import Hledger.Utils.Tree
|
import Hledger.Utils.Tree
|
||||||
-- import Prelude hiding (readFile,writeFile,appendFile,getContents,putStr,putStrLn)
|
-- import Prelude hiding (readFile,writeFile,appendFile,getContents,putStr,putStrLn)
|
||||||
@ -91,6 +95,8 @@ splitAtElement x l =
|
|||||||
split es = let (first,rest) = break (x==) es
|
split es = let (first,rest) = break (x==) es
|
||||||
in first : splitAtElement x rest
|
in first : splitAtElement x rest
|
||||||
|
|
||||||
|
-- text
|
||||||
|
|
||||||
-- time
|
-- time
|
||||||
|
|
||||||
getCurrentLocalTime :: IO LocalTime
|
getCurrentLocalTime :: IO LocalTime
|
||||||
|
|||||||
@ -42,6 +42,7 @@ module Hledger.Utils.String (
|
|||||||
cliptopleft,
|
cliptopleft,
|
||||||
fitto,
|
fitto,
|
||||||
-- * wide-character-aware layout
|
-- * wide-character-aware layout
|
||||||
|
charWidth,
|
||||||
strWidth,
|
strWidth,
|
||||||
takeWidth,
|
takeWidth,
|
||||||
fitString,
|
fitString,
|
||||||
|
|||||||
404
hledger-lib/Hledger/Utils/Text.hs
Normal file
404
hledger-lib/Hledger/Utils/Text.hs
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
-- | Text formatting helpers, ported from String as needed.
|
||||||
|
-- There may be better alternatives out there.
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Hledger.Utils.Text
|
||||||
|
-- (
|
||||||
|
-- -- * misc
|
||||||
|
-- lowercase,
|
||||||
|
-- uppercase,
|
||||||
|
-- underline,
|
||||||
|
-- stripbrackets,
|
||||||
|
-- unbracket,
|
||||||
|
-- -- quoting
|
||||||
|
-- quoteIfSpaced,
|
||||||
|
-- quoteIfNeeded,
|
||||||
|
-- singleQuoteIfNeeded,
|
||||||
|
-- -- quotechars,
|
||||||
|
-- -- whitespacechars,
|
||||||
|
-- escapeDoubleQuotes,
|
||||||
|
-- escapeSingleQuotes,
|
||||||
|
-- escapeQuotes,
|
||||||
|
-- words',
|
||||||
|
-- unwords',
|
||||||
|
-- stripquotes,
|
||||||
|
-- isSingleQuoted,
|
||||||
|
-- isDoubleQuoted,
|
||||||
|
-- -- * single-line layout
|
||||||
|
-- strip,
|
||||||
|
-- lstrip,
|
||||||
|
-- rstrip,
|
||||||
|
-- chomp,
|
||||||
|
-- elideLeft,
|
||||||
|
-- elideRight,
|
||||||
|
-- formatString,
|
||||||
|
-- -- * multi-line layout
|
||||||
|
-- concatTopPadded,
|
||||||
|
-- concatBottomPadded,
|
||||||
|
-- concatOneLine,
|
||||||
|
-- vConcatLeftAligned,
|
||||||
|
-- vConcatRightAligned,
|
||||||
|
-- padtop,
|
||||||
|
-- padbottom,
|
||||||
|
-- padleft,
|
||||||
|
-- padright,
|
||||||
|
-- cliptopleft,
|
||||||
|
-- fitto,
|
||||||
|
-- -- * wide-character-aware layout
|
||||||
|
-- strWidth,
|
||||||
|
-- textTakeWidth,
|
||||||
|
-- fitString,
|
||||||
|
-- fitStringMulti,
|
||||||
|
-- padLeftWide,
|
||||||
|
-- padRightWide
|
||||||
|
-- )
|
||||||
|
where
|
||||||
|
|
||||||
|
-- import Data.Char
|
||||||
|
import Data.List
|
||||||
|
import Data.Monoid
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
-- import Text.Parsec
|
||||||
|
-- import Text.Printf (printf)
|
||||||
|
|
||||||
|
-- import Hledger.Utils.Parse
|
||||||
|
-- import Hledger.Utils.Regex
|
||||||
|
import Hledger.Utils.String (charWidth)
|
||||||
|
|
||||||
|
-- lowercase, uppercase :: String -> String
|
||||||
|
-- lowercase = map toLower
|
||||||
|
-- uppercase = map toUpper
|
||||||
|
|
||||||
|
-- -- | Remove leading and trailing whitespace.
|
||||||
|
-- strip :: String -> String
|
||||||
|
-- strip = lstrip . rstrip
|
||||||
|
|
||||||
|
-- -- | Remove leading whitespace.
|
||||||
|
-- lstrip :: String -> String
|
||||||
|
-- lstrip = dropWhile (`elem` " \t") :: String -> String -- XXX isSpace ?
|
||||||
|
|
||||||
|
-- -- | Remove trailing whitespace.
|
||||||
|
-- rstrip :: String -> String
|
||||||
|
-- rstrip = reverse . lstrip . reverse
|
||||||
|
|
||||||
|
-- -- | Remove trailing newlines/carriage returns.
|
||||||
|
-- chomp :: String -> String
|
||||||
|
-- chomp = reverse . dropWhile (`elem` "\r\n") . reverse
|
||||||
|
|
||||||
|
-- stripbrackets :: String -> String
|
||||||
|
-- stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse :: String -> String
|
||||||
|
|
||||||
|
-- elideLeft :: Int -> String -> String
|
||||||
|
-- elideLeft width s =
|
||||||
|
-- if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s
|
||||||
|
|
||||||
|
-- elideRight :: Int -> String -> String
|
||||||
|
-- elideRight width s =
|
||||||
|
-- if length s > width then take (width - 2) s ++ ".." else s
|
||||||
|
|
||||||
|
-- -- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it.
|
||||||
|
-- -- Works on multi-line strings too (but will rewrite non-unix line endings).
|
||||||
|
-- formatString :: Bool -> Maybe Int -> Maybe Int -> String -> String
|
||||||
|
-- formatString leftJustified minwidth maxwidth s = intercalate "\n" $ map (printf fmt) $ lines s
|
||||||
|
-- where
|
||||||
|
-- justify = if leftJustified then "-" else ""
|
||||||
|
-- minwidth' = maybe "" show minwidth
|
||||||
|
-- maxwidth' = maybe "" (("."++).show) maxwidth
|
||||||
|
-- fmt = "%" ++ justify ++ minwidth' ++ maxwidth' ++ "s"
|
||||||
|
|
||||||
|
-- underline :: String -> String
|
||||||
|
-- underline s = s' ++ replicate (length s) '-' ++ "\n"
|
||||||
|
-- where s'
|
||||||
|
-- | last s == '\n' = s
|
||||||
|
-- | otherwise = s ++ "\n"
|
||||||
|
|
||||||
|
-- -- | Wrap a string in double quotes, and \-prefix any embedded single
|
||||||
|
-- -- quotes, if it contains whitespace and is not already single- or
|
||||||
|
-- -- double-quoted.
|
||||||
|
-- quoteIfSpaced :: String -> String
|
||||||
|
-- quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s
|
||||||
|
-- | not $ any (`elem` s) whitespacechars = s
|
||||||
|
-- | otherwise = "'"++escapeSingleQuotes s++"'"
|
||||||
|
|
||||||
|
-- -- | Double-quote this string if it contains whitespace, single quotes
|
||||||
|
-- -- or double-quotes, escaping the quotes as needed.
|
||||||
|
-- quoteIfNeeded :: String -> String
|
||||||
|
-- quoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = "\"" ++ escapeDoubleQuotes s ++ "\""
|
||||||
|
-- | otherwise = s
|
||||||
|
|
||||||
|
-- -- | Single-quote this string if it contains whitespace or double-quotes.
|
||||||
|
-- -- No good for strings containing single quotes.
|
||||||
|
-- singleQuoteIfNeeded :: String -> String
|
||||||
|
-- singleQuoteIfNeeded s | any (`elem` s) whitespacechars = "'"++s++"'"
|
||||||
|
-- | otherwise = s
|
||||||
|
|
||||||
|
-- quotechars, whitespacechars :: [Char]
|
||||||
|
-- quotechars = "'\""
|
||||||
|
-- whitespacechars = " \t\n\r"
|
||||||
|
|
||||||
|
-- escapeDoubleQuotes :: String -> String
|
||||||
|
-- escapeDoubleQuotes = regexReplace "\"" "\""
|
||||||
|
|
||||||
|
-- escapeSingleQuotes :: String -> String
|
||||||
|
-- escapeSingleQuotes = regexReplace "'" "\'"
|
||||||
|
|
||||||
|
-- escapeQuotes :: String -> String
|
||||||
|
-- escapeQuotes = regexReplace "([\"'])" "\\1"
|
||||||
|
|
||||||
|
-- -- | Quote-aware version of words - don't split on spaces which are inside quotes.
|
||||||
|
-- -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.
|
||||||
|
-- words' :: String -> [String]
|
||||||
|
-- words' "" = []
|
||||||
|
-- words' s = map stripquotes $ fromparse $ parsewith p s
|
||||||
|
-- where
|
||||||
|
-- p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` many1 spacenonewline
|
||||||
|
-- -- eof
|
||||||
|
-- return ss
|
||||||
|
-- pattern = many (noneOf whitespacechars)
|
||||||
|
-- singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf "'")
|
||||||
|
-- doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf "\"")
|
||||||
|
|
||||||
|
-- -- | Quote-aware version of unwords - single-quote strings which contain whitespace
|
||||||
|
-- unwords' :: [String] -> String
|
||||||
|
-- unwords' = unwords . map quoteIfNeeded
|
||||||
|
|
||||||
|
-- -- | Strip one matching pair of single or double quotes on the ends of a string.
|
||||||
|
-- stripquotes :: String -> String
|
||||||
|
-- stripquotes s = if isSingleQuoted s || isDoubleQuoted s then init $ tail s else s
|
||||||
|
|
||||||
|
-- isSingleQuoted s@(_:_:_) = head s == '\'' && last s == '\''
|
||||||
|
-- isSingleQuoted _ = False
|
||||||
|
|
||||||
|
-- isDoubleQuoted s@(_:_:_) = head s == '"' && last s == '"'
|
||||||
|
-- isDoubleQuoted _ = False
|
||||||
|
|
||||||
|
textUnbracket :: Text -> Text
|
||||||
|
textUnbracket s
|
||||||
|
| (T.head s == '[' && T.last s == ']') || (T.head s == '(' && T.last s == ')') = T.init $ T.tail s
|
||||||
|
| otherwise = s
|
||||||
|
|
||||||
|
-- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded.
|
||||||
|
-- Treats wide characters as double width.
|
||||||
|
textConcatTopPadded :: [Text] -> Text
|
||||||
|
textConcatTopPadded ts = T.intercalate "\n" $ map T.concat $ transpose padded
|
||||||
|
where
|
||||||
|
lss = map T.lines ts :: [[Text]]
|
||||||
|
h = maximum $ map length lss
|
||||||
|
ypad ls = replicate (difforzero h (length ls)) "" ++ ls
|
||||||
|
xpad ls = map (textPadLeftWide w) ls
|
||||||
|
where w | null ls = 0
|
||||||
|
| otherwise = maximum $ map textWidth ls
|
||||||
|
padded = map (xpad . ypad) lss :: [[Text]]
|
||||||
|
|
||||||
|
-- -- | Join several multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.
|
||||||
|
-- -- Treats wide characters as double width.
|
||||||
|
-- concatBottomPadded :: [String] -> String
|
||||||
|
-- concatBottomPadded strs = intercalate "\n" $ map concat $ transpose padded
|
||||||
|
-- where
|
||||||
|
-- lss = map lines strs
|
||||||
|
-- h = maximum $ map length lss
|
||||||
|
-- ypad ls = ls ++ replicate (difforzero h (length ls)) ""
|
||||||
|
-- xpad ls = map (padRightWide w) ls where w | null ls = 0
|
||||||
|
-- | otherwise = maximum $ map strWidth ls
|
||||||
|
-- padded = map (xpad . ypad) lss
|
||||||
|
|
||||||
|
|
||||||
|
-- -- | Join multi-line strings horizontally, after compressing each of
|
||||||
|
-- -- them to a single line with a comma and space between each original line.
|
||||||
|
-- concatOneLine :: [String] -> String
|
||||||
|
-- concatOneLine strs = concat $ map ((intercalate ", ").lines) strs
|
||||||
|
|
||||||
|
-- -- | Join strings vertically, left-aligned and right-padded.
|
||||||
|
-- vConcatLeftAligned :: [String] -> String
|
||||||
|
-- vConcatLeftAligned ss = intercalate "\n" $ map showfixedwidth ss
|
||||||
|
-- where
|
||||||
|
-- showfixedwidth = printf (printf "%%-%ds" width)
|
||||||
|
-- width = maximum $ map length ss
|
||||||
|
|
||||||
|
-- -- | Join strings vertically, right-aligned and left-padded.
|
||||||
|
-- vConcatRightAligned :: [String] -> String
|
||||||
|
-- vConcatRightAligned ss = intercalate "\n" $ map showfixedwidth ss
|
||||||
|
-- where
|
||||||
|
-- showfixedwidth = printf (printf "%%%ds" width)
|
||||||
|
-- width = maximum $ map length ss
|
||||||
|
|
||||||
|
-- -- | Convert a multi-line string to a rectangular string top-padded to the specified height.
|
||||||
|
-- padtop :: Int -> String -> String
|
||||||
|
-- padtop h s = intercalate "\n" xpadded
|
||||||
|
-- where
|
||||||
|
-- ls = lines s
|
||||||
|
-- sh = length ls
|
||||||
|
-- sw | null ls = 0
|
||||||
|
-- | otherwise = maximum $ map length ls
|
||||||
|
-- ypadded = replicate (difforzero h sh) "" ++ ls
|
||||||
|
-- xpadded = map (padleft sw) ypadded
|
||||||
|
|
||||||
|
-- -- | Convert a multi-line string to a rectangular string bottom-padded to the specified height.
|
||||||
|
-- padbottom :: Int -> String -> String
|
||||||
|
-- padbottom h s = intercalate "\n" xpadded
|
||||||
|
-- where
|
||||||
|
-- ls = lines s
|
||||||
|
-- sh = length ls
|
||||||
|
-- sw | null ls = 0
|
||||||
|
-- | otherwise = maximum $ map length ls
|
||||||
|
-- ypadded = ls ++ replicate (difforzero h sh) ""
|
||||||
|
-- xpadded = map (padleft sw) ypadded
|
||||||
|
|
||||||
|
difforzero :: (Num a, Ord a) => a -> a -> a
|
||||||
|
difforzero a b = maximum [(a - b), 0]
|
||||||
|
|
||||||
|
-- -- | Convert a multi-line string to a rectangular string left-padded to the specified width.
|
||||||
|
-- -- Treats wide characters as double width.
|
||||||
|
-- padleft :: Int -> String -> String
|
||||||
|
-- padleft w "" = concat $ replicate w " "
|
||||||
|
-- padleft w s = intercalate "\n" $ map (printf (printf "%%%ds" w)) $ lines s
|
||||||
|
|
||||||
|
-- -- | Convert a multi-line string to a rectangular string right-padded to the specified width.
|
||||||
|
-- -- Treats wide characters as double width.
|
||||||
|
-- padright :: Int -> String -> String
|
||||||
|
-- padright w "" = concat $ replicate w " "
|
||||||
|
-- padright w s = intercalate "\n" $ map (printf (printf "%%-%ds" w)) $ lines s
|
||||||
|
|
||||||
|
-- -- | Clip a multi-line string to the specified width and height from the top left.
|
||||||
|
-- cliptopleft :: Int -> Int -> String -> String
|
||||||
|
-- cliptopleft w h = intercalate "\n" . take h . map (take w) . lines
|
||||||
|
|
||||||
|
-- -- | Clip and pad a multi-line string to fill the specified width and height.
|
||||||
|
-- fitto :: Int -> Int -> String -> String
|
||||||
|
-- fitto w h s = intercalate "\n" $ take h $ rows ++ repeat blankline
|
||||||
|
-- where
|
||||||
|
-- rows = map (fit w) $ lines s
|
||||||
|
-- fit w = take w . (++ repeat ' ')
|
||||||
|
-- blankline = replicate w ' '
|
||||||
|
|
||||||
|
-- -- Functions below treat wide (eg CJK) characters as double-width.
|
||||||
|
|
||||||
|
-- | General-purpose wide-char-aware single-line text layout function.
|
||||||
|
-- It can left- or right-pad a short string to a minimum width.
|
||||||
|
-- It can left- or right-clip a long string to a maximum width, optionally inserting an ellipsis (the third argument).
|
||||||
|
-- It clips and pads on the right when the fourth argument is true, otherwise on the left.
|
||||||
|
-- It treats wide characters as double width.
|
||||||
|
fitText :: Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text
|
||||||
|
fitText mminwidth mmaxwidth ellipsify rightside s = (clip . pad) s
|
||||||
|
where
|
||||||
|
clip :: Text -> Text
|
||||||
|
clip s =
|
||||||
|
case mmaxwidth of
|
||||||
|
Just w
|
||||||
|
| textWidth s > w ->
|
||||||
|
case rightside of
|
||||||
|
True -> textTakeWidth (w - T.length ellipsis) s <> ellipsis
|
||||||
|
False -> ellipsis <> T.reverse (textTakeWidth (w - T.length ellipsis) $ T.reverse s)
|
||||||
|
| otherwise -> s
|
||||||
|
where
|
||||||
|
ellipsis = if ellipsify then ".." else ""
|
||||||
|
Nothing -> s
|
||||||
|
pad :: Text -> Text
|
||||||
|
pad s =
|
||||||
|
case mminwidth of
|
||||||
|
Just w
|
||||||
|
| sw < w ->
|
||||||
|
case rightside of
|
||||||
|
True -> s <> T.replicate (w - sw) " "
|
||||||
|
False -> T.replicate (w - sw) " " <> s
|
||||||
|
| otherwise -> s
|
||||||
|
Nothing -> s
|
||||||
|
where sw = textWidth s
|
||||||
|
|
||||||
|
-- -- | A version of fitString that works on multi-line strings,
|
||||||
|
-- -- separate for now to avoid breakage.
|
||||||
|
-- -- This will rewrite any line endings to unix newlines.
|
||||||
|
-- fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String
|
||||||
|
-- fitStringMulti mminwidth mmaxwidth ellipsify rightside s =
|
||||||
|
-- (intercalate "\n" . map (fitString mminwidth mmaxwidth ellipsify rightside) . lines) s
|
||||||
|
|
||||||
|
-- | Left-pad a text to the specified width.
|
||||||
|
-- Treats wide characters as double width.
|
||||||
|
-- Works on multi-line texts too (but will rewrite non-unix line endings).
|
||||||
|
textPadLeftWide :: Int -> Text -> Text
|
||||||
|
textPadLeftWide w "" = T.replicate w " "
|
||||||
|
textPadLeftWide w s = T.intercalate "\n" $ map (fitText (Just w) Nothing False False) $ T.lines s
|
||||||
|
-- XXX not yet replaceable by
|
||||||
|
-- padLeftWide w = fitStringMulti (Just w) Nothing False False
|
||||||
|
|
||||||
|
-- | Right-pad a string to the specified width.
|
||||||
|
-- Treats wide characters as double width.
|
||||||
|
-- Works on multi-line strings too (but will rewrite non-unix line endings).
|
||||||
|
textPadRightWide :: Int -> Text -> Text
|
||||||
|
textPadRightWide w "" = T.replicate w " "
|
||||||
|
textPadRightWide w s = T.intercalate "\n" $ map (fitText (Just w) Nothing False True) $ T.lines s
|
||||||
|
-- XXX not yet replaceable by
|
||||||
|
-- padRightWide w = fitStringMulti (Just w) Nothing False True
|
||||||
|
|
||||||
|
-- | Double-width-character-aware string truncation. Take as many
|
||||||
|
-- characters as possible from a string without exceeding the
|
||||||
|
-- specified width. Eg textTakeWidth 3 "りんご" = "り".
|
||||||
|
textTakeWidth :: Int -> Text -> Text
|
||||||
|
textTakeWidth _ "" = ""
|
||||||
|
textTakeWidth 0 _ = ""
|
||||||
|
textTakeWidth w t | not (T.null t),
|
||||||
|
let c = T.head t,
|
||||||
|
let cw = charWidth c,
|
||||||
|
cw <= w
|
||||||
|
= T.cons c $ textTakeWidth (w-cw) (T.tail t)
|
||||||
|
| otherwise = ""
|
||||||
|
|
||||||
|
-- -- from Pandoc (copyright John MacFarlane, GPL)
|
||||||
|
-- -- see also http://unicode.org/reports/tr11/#Description
|
||||||
|
|
||||||
|
-- | Calculate the designated render width of a string, taking into
|
||||||
|
-- account wide characters and line breaks (the longest line within a
|
||||||
|
-- multi-line string determines the width ).
|
||||||
|
textWidth :: Text -> Int
|
||||||
|
textWidth "" = 0
|
||||||
|
textWidth s = maximum $ map (T.foldr (\a b -> charWidth a + b) 0) $ T.lines s
|
||||||
|
|
||||||
|
-- -- | Get the designated render width of a character: 0 for a combining
|
||||||
|
-- -- character, 1 for a regular character, 2 for a wide character.
|
||||||
|
-- -- (Wide characters are rendered as exactly double width in apps and
|
||||||
|
-- -- fonts that support it.) (From Pandoc.)
|
||||||
|
-- charWidth :: Char -> Int
|
||||||
|
-- charWidth c =
|
||||||
|
-- case c of
|
||||||
|
-- _ | c < '\x0300' -> 1
|
||||||
|
-- | c >= '\x0300' && c <= '\x036F' -> 0 -- combining
|
||||||
|
-- | c >= '\x0370' && c <= '\x10FC' -> 1
|
||||||
|
-- | c >= '\x1100' && c <= '\x115F' -> 2
|
||||||
|
-- | c >= '\x1160' && c <= '\x11A2' -> 1
|
||||||
|
-- | c >= '\x11A3' && c <= '\x11A7' -> 2
|
||||||
|
-- | c >= '\x11A8' && c <= '\x11F9' -> 1
|
||||||
|
-- | c >= '\x11FA' && c <= '\x11FF' -> 2
|
||||||
|
-- | c >= '\x1200' && c <= '\x2328' -> 1
|
||||||
|
-- | c >= '\x2329' && c <= '\x232A' -> 2
|
||||||
|
-- | c >= '\x232B' && c <= '\x2E31' -> 1
|
||||||
|
-- | c >= '\x2E80' && c <= '\x303E' -> 2
|
||||||
|
-- | c == '\x303F' -> 1
|
||||||
|
-- | c >= '\x3041' && c <= '\x3247' -> 2
|
||||||
|
-- | c >= '\x3248' && c <= '\x324F' -> 1 -- ambiguous
|
||||||
|
-- | c >= '\x3250' && c <= '\x4DBF' -> 2
|
||||||
|
-- | c >= '\x4DC0' && c <= '\x4DFF' -> 1
|
||||||
|
-- | c >= '\x4E00' && c <= '\xA4C6' -> 2
|
||||||
|
-- | c >= '\xA4D0' && c <= '\xA95F' -> 1
|
||||||
|
-- | c >= '\xA960' && c <= '\xA97C' -> 2
|
||||||
|
-- | c >= '\xA980' && c <= '\xABF9' -> 1
|
||||||
|
-- | c >= '\xAC00' && c <= '\xD7FB' -> 2
|
||||||
|
-- | c >= '\xD800' && c <= '\xDFFF' -> 1
|
||||||
|
-- | c >= '\xE000' && c <= '\xF8FF' -> 1 -- ambiguous
|
||||||
|
-- | c >= '\xF900' && c <= '\xFAFF' -> 2
|
||||||
|
-- | c >= '\xFB00' && c <= '\xFDFD' -> 1
|
||||||
|
-- | c >= '\xFE00' && c <= '\xFE0F' -> 1 -- ambiguous
|
||||||
|
-- | c >= '\xFE10' && c <= '\xFE19' -> 2
|
||||||
|
-- | c >= '\xFE20' && c <= '\xFE26' -> 1
|
||||||
|
-- | c >= '\xFE30' && c <= '\xFE6B' -> 2
|
||||||
|
-- | c >= '\xFE70' && c <= '\xFEFF' -> 1
|
||||||
|
-- | c >= '\xFF01' && c <= '\xFF60' -> 2
|
||||||
|
-- | c >= '\xFF61' && c <= '\x16A38' -> 1
|
||||||
|
-- | c >= '\x1B000' && c <= '\x1B001' -> 2
|
||||||
|
-- | c >= '\x1D000' && c <= '\x1F1FF' -> 1
|
||||||
|
-- | c >= '\x1F200' && c <= '\x1F251' -> 2
|
||||||
|
-- | c >= '\x1F300' && c <= '\x1F773' -> 1
|
||||||
|
-- | c >= '\x20000' && c <= '\x3FFFD' -> 2
|
||||||
|
-- | otherwise -> 1
|
||||||
|
|
||||||
@ -81,6 +81,7 @@ dependencies:
|
|||||||
- regex-tdfa
|
- regex-tdfa
|
||||||
- safe >= 0.2
|
- safe >= 0.2
|
||||||
- split >= 0.1 && < 0.3
|
- split >= 0.1 && < 0.3
|
||||||
|
- text >= 1.2 && < 1.3
|
||||||
- transformers >= 0.2 && < 0.6
|
- transformers >= 0.2 && < 0.6
|
||||||
- uglymemo
|
- uglymemo
|
||||||
- utf8-string >= 0.3.5 && < 1.1
|
- utf8-string >= 0.3.5 && < 1.1
|
||||||
|
|||||||
@ -82,6 +82,7 @@ library
|
|||||||
, regex-tdfa
|
, regex-tdfa
|
||||||
, safe >= 0.2
|
, safe >= 0.2
|
||||||
, split >= 0.1 && < 0.3
|
, split >= 0.1 && < 0.3
|
||||||
|
, text >= 1.2 && < 1.3
|
||||||
, transformers >= 0.2 && < 0.6
|
, transformers >= 0.2 && < 0.6
|
||||||
, uglymemo
|
, uglymemo
|
||||||
, utf8-string >= 0.3.5 && < 1.1
|
, utf8-string >= 0.3.5 && < 1.1
|
||||||
@ -132,6 +133,7 @@ library
|
|||||||
Hledger.Utils.Regex
|
Hledger.Utils.Regex
|
||||||
Hledger.Utils.String
|
Hledger.Utils.String
|
||||||
Hledger.Utils.Test
|
Hledger.Utils.Test
|
||||||
|
Hledger.Utils.Text
|
||||||
Hledger.Utils.Tree
|
Hledger.Utils.Tree
|
||||||
Hledger.Utils.UTF8IOCompat
|
Hledger.Utils.UTF8IOCompat
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
@ -161,6 +163,7 @@ test-suite hunittests
|
|||||||
, regex-tdfa
|
, regex-tdfa
|
||||||
, safe >= 0.2
|
, safe >= 0.2
|
||||||
, split >= 0.1 && < 0.3
|
, split >= 0.1 && < 0.3
|
||||||
|
, text >= 1.2 && < 1.3
|
||||||
, transformers >= 0.2 && < 0.6
|
, transformers >= 0.2 && < 0.6
|
||||||
, uglymemo
|
, uglymemo
|
||||||
, utf8-string >= 0.3.5 && < 1.1
|
, utf8-string >= 0.3.5 && < 1.1
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import Control.Monad.IO.Class (liftIO)
|
|||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar (Day)
|
import Data.Time.Calendar (Day)
|
||||||
import System.FilePath (takeFileName)
|
import System.FilePath (takeFileName)
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
@ -57,7 +59,7 @@ initAccountsScreen d st@AppState{
|
|||||||
l = list (Name "accounts") (V.fromList displayitems) 1
|
l = list (Name "accounts") (V.fromList displayitems) 1
|
||||||
|
|
||||||
-- keep the selection near the last known selected account if possible
|
-- keep the selection near the last known selected account if possible
|
||||||
l' | null selacct = l
|
l' | T.null selacct = l
|
||||||
| otherwise = maybe l (flip listMoveTo l) midx
|
| otherwise = maybe l (flip listMoveTo l) midx
|
||||||
where
|
where
|
||||||
midx = findIndex (\((a,_,_),_) -> a==selacctclipped) items
|
midx = findIndex (\((a,_,_),_) -> a==selacctclipped) items
|
||||||
@ -147,7 +149,7 @@ drawAccountsScreen _st@AppState{aopts=uopts, ajournal=j, aScreen=AccountsScreen{
|
|||||||
maxacctwidthseen =
|
maxacctwidthseen =
|
||||||
-- ltrace "maxacctwidthseen" $
|
-- ltrace "maxacctwidthseen" $
|
||||||
V.maximum $
|
V.maximum $
|
||||||
V.map (\(indent,_,displayacct,_) -> indent*2 + strWidth displayacct) $
|
V.map (\(indent,_,displayacct,_) -> indent*2 + textWidth displayacct) $
|
||||||
-- V.filter (\(indent,_,_,_) -> (indent-1) <= fromMaybe 99999 mdepth) $
|
-- V.filter (\(indent,_,_,_) -> (indent-1) <= fromMaybe 99999 mdepth) $
|
||||||
displayitems
|
displayitems
|
||||||
maxbalwidthseen =
|
maxbalwidthseen =
|
||||||
@ -175,14 +177,14 @@ drawAccountsScreen _st@AppState{aopts=uopts, ajournal=j, aScreen=AccountsScreen{
|
|||||||
|
|
||||||
drawAccountsScreen _ = error "draw function called with wrong screen type, should not happen"
|
drawAccountsScreen _ = error "draw function called with wrong screen type, should not happen"
|
||||||
|
|
||||||
drawAccountsItem :: (Int,Int) -> Bool -> (Int, String, String, [String]) -> Widget
|
drawAccountsItem :: (Int,Int) -> Bool -> (Int, AccountName, AccountName, [String]) -> Widget
|
||||||
drawAccountsItem (acctwidth, balwidth) selected (indent, _fullacct, displayacct, balamts) =
|
drawAccountsItem (acctwidth, balwidth) selected (indent, _fullacct, displayacct, balamts) =
|
||||||
Widget Greedy Fixed $ do
|
Widget Greedy Fixed $ do
|
||||||
-- c <- getContext
|
-- c <- getContext
|
||||||
-- let showitem = intercalate "\n" . balanceReportItemAsText defreportopts fmt
|
-- let showitem = intercalate "\n" . balanceReportItemAsText defreportopts fmt
|
||||||
render $
|
render $
|
||||||
addamts balamts $
|
addamts balamts $
|
||||||
str (fitString (Just acctwidth) (Just acctwidth) True True $ replicate (2*indent) ' ' ++ displayacct) <+>
|
str (T.unpack $ fitText (Just acctwidth) (Just acctwidth) True True $ T.replicate (2*indent) " " <> displayacct) <+>
|
||||||
str " " <+>
|
str " " <+>
|
||||||
str (balspace balamts)
|
str (balspace balamts)
|
||||||
where
|
where
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import Control.Monad
|
|||||||
-- import Data.Monoid --
|
-- import Data.Monoid --
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
-- import Data.Time.Calendar
|
-- import Data.Time.Calendar
|
||||||
import Safe
|
import Safe
|
||||||
import System.Exit
|
import System.Exit
|
||||||
@ -100,7 +102,7 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} j = do
|
|||||||
where
|
where
|
||||||
acct = headDef
|
acct = headDef
|
||||||
(error' $ "--register "++apat++" did not match any account")
|
(error' $ "--register "++apat++" did not match any account")
|
||||||
$ filter (regexMatches apat) $ journalAccountNames j
|
$ filter (regexMatches apat . T.unpack) $ journalAccountNames j
|
||||||
-- Initialising the accounts screen is awkward, requiring
|
-- Initialising the accounts screen is awkward, requiring
|
||||||
-- another temporary AppState value..
|
-- another temporary AppState value..
|
||||||
ascr' = aScreen $
|
ascr' = aScreen $
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import Data.List
|
|||||||
import Data.List.Split (splitOn)
|
import Data.List.Split (splitOn)
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
-- import Data.Maybe
|
-- import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar (Day)
|
import Data.Time.Calendar (Day)
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import Graphics.Vty as Vty
|
import Graphics.Vty as Vty
|
||||||
@ -86,7 +88,7 @@ drawRegisterScreen :: AppState -> [Widget]
|
|||||||
drawRegisterScreen AppState{aopts=uopts -- @UIOpts{cliopts_=_copts@CliOpts{reportopts_=_ropts@ReportOpts{query_=querystr}}
|
drawRegisterScreen AppState{aopts=uopts -- @UIOpts{cliopts_=_copts@CliOpts{reportopts_=_ropts@ReportOpts{query_=querystr}}
|
||||||
,aScreen=RegisterScreen{rsState=(l,acct)}} = [ui]
|
,aScreen=RegisterScreen{rsState=(l,acct)}} = [ui]
|
||||||
where
|
where
|
||||||
toplabel = withAttr ("border" <> "bold") (str acct)
|
toplabel = withAttr ("border" <> "bold") (str $ T.unpack acct)
|
||||||
<+> cleared
|
<+> cleared
|
||||||
<+> str " transactions"
|
<+> str " transactions"
|
||||||
-- <+> borderQueryStr querystr -- no, account transactions report shows all transactions in the acct ?
|
-- <+> borderQueryStr querystr -- no, account transactions report shows all transactions in the acct ?
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import Control.Monad.IO.Class (liftIO)
|
|||||||
-- import Data.Ord
|
-- import Data.Ord
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
-- import Data.Maybe
|
-- import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar (Day)
|
import Data.Time.Calendar (Day)
|
||||||
-- import qualified Data.Vector as V
|
-- import qualified Data.Vector as V
|
||||||
import Graphics.Vty as Vty
|
import Graphics.Vty as Vty
|
||||||
@ -56,7 +58,7 @@ drawTransactionScreen AppState{ -- aopts=_uopts@UIOpts{cliopts_=_copts@CliOpts{r
|
|||||||
<+> (str $ "#" ++ show (tindex t))
|
<+> (str $ "#" ++ show (tindex t))
|
||||||
<+> str " ("
|
<+> str " ("
|
||||||
<+> withAttr ("border" <> "bold") (str $ show i)
|
<+> withAttr ("border" <> "bold") (str $ show i)
|
||||||
<+> str (" of "++show (length nts)++" in "++acct++")")
|
<+> str (" of "++show (length nts)++" in "++T.unpack acct++")")
|
||||||
bottomlabel = borderKeysStr [
|
bottomlabel = borderKeysStr [
|
||||||
("left", "back")
|
("left", "back")
|
||||||
,("up/down", "prev/next")
|
,("up/down", "prev/next")
|
||||||
|
|||||||
@ -28,7 +28,7 @@ data AppState = AppState {
|
|||||||
-- This type causes partial functions, so take care.
|
-- This type causes partial functions, so take care.
|
||||||
data Screen =
|
data Screen =
|
||||||
AccountsScreen {
|
AccountsScreen {
|
||||||
asState :: (List (Int,String,String,[String]), AccountName) -- ^ list widget holding (indent level, full account name, full or short account name to display, rendered amounts);
|
asState :: (List (Int,AccountName,AccountName,[String]), AccountName) -- ^ list widget holding (indent level, full account name, full or short account name to display, rendered amounts);
|
||||||
-- the full name of the currently selected account (or "")
|
-- the full name of the currently selected account (or "")
|
||||||
,sInitFn :: Day -> AppState -> AppState -- ^ function to initialise the screen's state on entry
|
,sInitFn :: Day -> AppState -> AppState -- ^ function to initialise the screen's state on entry
|
||||||
,sHandleFn :: AppState -> V.Event -> EventM (Next AppState) -- ^ brick event handler to use for this screen
|
,sHandleFn :: AppState -> V.Event -> EventM (Next AppState) -- ^ brick event handler to use for this screen
|
||||||
|
|||||||
@ -83,6 +83,7 @@ executables:
|
|||||||
- microlens >= 0.3.5.1 && < 0.5
|
- microlens >= 0.3.5.1 && < 0.5
|
||||||
- safe >= 0.2
|
- safe >= 0.2
|
||||||
- split >= 0.1 && < 0.3
|
- split >= 0.1 && < 0.3
|
||||||
|
- text >= 1.2 && < 1.3
|
||||||
- transformers
|
- transformers
|
||||||
- vector
|
- vector
|
||||||
- vty >= 5.2 && < 5.5
|
- vty >= 5.2 && < 5.5
|
||||||
|
|||||||
@ -74,6 +74,7 @@ executable hledger-ui
|
|||||||
, microlens >= 0.3.5.1 && < 0.5
|
, microlens >= 0.3.5.1 && < 0.5
|
||||||
, safe >= 0.2
|
, safe >= 0.2
|
||||||
, split >= 0.1 && < 0.3
|
, split >= 0.1 && < 0.3
|
||||||
|
, text >= 1.2 && < 1.3
|
||||||
, transformers
|
, transformers
|
||||||
, vector
|
, vector
|
||||||
, vty >= 5.2 && < 5.6
|
, vty >= 5.2 && < 5.6
|
||||||
|
|||||||
@ -7,7 +7,8 @@ module Handler.Common where
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Text(pack)
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import System.FilePath (takeFileName)
|
import System.FilePath (takeFileName)
|
||||||
#if BLAZE_HTML_0_4
|
#if BLAZE_HTML_0_4
|
||||||
@ -221,17 +222,17 @@ balanceReportAsHtml _ vd@VD{..} (items',total) =
|
|||||||
Just m' -> if m' `matchesAccount` acct then "inacct" else "notinacct"
|
Just m' -> if m' `matchesAccount` acct then "inacct" else "notinacct"
|
||||||
Nothing -> "" :: String
|
Nothing -> "" :: String
|
||||||
indent = preEscapedString $ concat $ replicate (2 * (1+aindent)) " "
|
indent = preEscapedString $ concat $ replicate (2 * (1+aindent)) " "
|
||||||
acctquery = (RegisterR, [("q", pack $ accountQuery acct)])
|
acctquery = (RegisterR, [("q", T.pack $ accountQuery acct)])
|
||||||
acctonlyquery = (RegisterR, [("q", pack $ accountOnlyQuery acct)])
|
acctonlyquery = (RegisterR, [("q", T.pack $ accountOnlyQuery acct)])
|
||||||
|
|
||||||
accountQuery :: AccountName -> String
|
accountQuery :: AccountName -> String
|
||||||
accountQuery a = "inacct:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a)
|
accountQuery a = "inacct:" ++ quoteIfSpaced (T.unpack a) -- (accountNameToAccountRegex a)
|
||||||
|
|
||||||
accountOnlyQuery :: AccountName -> String
|
accountOnlyQuery :: AccountName -> String
|
||||||
accountOnlyQuery a = "inacctonly:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a)
|
accountOnlyQuery a = "inacctonly:" ++ quoteIfSpaced (T.unpack a) -- (accountNameToAccountRegex a)
|
||||||
|
|
||||||
accountUrl :: AppRoute -> AccountName -> (AppRoute, [(Text, Text)])
|
accountUrl :: AppRoute -> AccountName -> (AppRoute, [(Text, Text)])
|
||||||
accountUrl r a = (r, [("q", pack $ accountQuery a)])
|
accountUrl r a = (r, [("q", T.pack $ accountQuery a)])
|
||||||
|
|
||||||
-- stringIfLongerThan :: Int -> String -> String
|
-- stringIfLongerThan :: Int -> String -> String
|
||||||
-- stringIfLongerThan n s = if length s > n then s else ""
|
-- stringIfLongerThan n s = if length s > n then s else ""
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
module Handler.JournalR where
|
module Handler.JournalR where
|
||||||
|
|
||||||
import Data.Text (pack)
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Handler.AddForm
|
import Handler.AddForm
|
||||||
@ -27,7 +28,7 @@ getJournalR = do
|
|||||||
-- showlastcolumn = if injournal && not filtering then False else True
|
-- showlastcolumn = if injournal && not filtering then False else True
|
||||||
title = case inacct of
|
title = case inacct of
|
||||||
Nothing -> "General Journal"++s2
|
Nothing -> "General Journal"++s2
|
||||||
Just (a,inclsubs) -> "Transactions in "++a++s1++s2
|
Just (a,inclsubs) -> "Transactions in "++T.unpack a++s1++s2
|
||||||
where s1 = if inclsubs then "" else " (excluding subaccounts)"
|
where s1 = if inclsubs then "" else " (excluding subaccounts)"
|
||||||
where
|
where
|
||||||
s2 = if filtering then ", filtered" else ""
|
s2 = if filtering then ", filtered" else ""
|
||||||
@ -82,12 +83,12 @@ journalTransactionsReportAsHtml _ vd (_,items) = [hamlet|
|
|||||||
<td>
|
<td>
|
||||||
|]
|
|]
|
||||||
where
|
where
|
||||||
acctlink a = (RegisterR, [("q", pack $ accountQuery a)])
|
acctlink a = (RegisterR, [("q", T.pack $ accountQuery a)])
|
||||||
evenodd = if even n then "even" else "odd" :: String
|
evenodd = if even n then "even" else "odd" :: String
|
||||||
-- datetransition | newm = "newmonth"
|
-- datetransition | newm = "newmonth"
|
||||||
-- | newd = "newday"
|
-- | newd = "newday"
|
||||||
-- | otherwise = "" :: String
|
-- | otherwise = "" :: String
|
||||||
(firstposting, date, desc) = (False, show $ tdate torig, tdescription torig)
|
(firstposting, date, desc) = (False, show $ tdate torig, tdescription torig)
|
||||||
-- acctquery = (here, [("q", pack $ accountQuery acct)])
|
-- acctquery = (here, [("q", T.pack $ accountQuery acct)])
|
||||||
showamt = not split || not (isZeroMixedAmount amt)
|
showamt = not split || not (isZeroMixedAmount amt)
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import Import
|
|||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Safe
|
import Safe
|
||||||
|
|
||||||
import Handler.AddForm
|
import Handler.AddForm
|
||||||
@ -28,7 +30,7 @@ getRegisterR = do
|
|||||||
let -- injournal = isNothing inacct
|
let -- injournal = isNothing inacct
|
||||||
filtering = m /= Any
|
filtering = m /= Any
|
||||||
-- title = "Transactions in "++a++s1++s2
|
-- title = "Transactions in "++a++s1++s2
|
||||||
title = a++s1++s2
|
title = T.unpack a++s1++s2
|
||||||
where
|
where
|
||||||
(a,inclsubs) = fromMaybe ("all accounts",True) $ inAccount qopts
|
(a,inclsubs) = fromMaybe ("all accounts",True) $ inAccount qopts
|
||||||
s1 = if inclsubs then "" else " (excluding subaccounts)"
|
s1 = if inclsubs then "" else " (excluding subaccounts)"
|
||||||
|
|||||||
@ -100,6 +100,7 @@ dependencies:
|
|||||||
- shakespeare >= 2.0
|
- shakespeare >= 2.0
|
||||||
- template-haskell
|
- template-haskell
|
||||||
- text
|
- text
|
||||||
|
- text >= 1.2 && < 1.3
|
||||||
- transformers
|
- transformers
|
||||||
- wai
|
- wai
|
||||||
- wai-extra
|
- wai-extra
|
||||||
|
|||||||
@ -105,7 +105,7 @@ library
|
|||||||
, safe >= 0.2
|
, safe >= 0.2
|
||||||
, shakespeare >= 2.0
|
, shakespeare >= 2.0
|
||||||
, template-haskell
|
, template-haskell
|
||||||
, text
|
, text >= 1.2 && < 1.3
|
||||||
, transformers
|
, transformers
|
||||||
, wai
|
, wai
|
||||||
, wai-extra
|
, wai-extra
|
||||||
@ -178,7 +178,7 @@ executable hledger-web
|
|||||||
, safe >= 0.2
|
, safe >= 0.2
|
||||||
, shakespeare >= 2.0
|
, shakespeare >= 2.0
|
||||||
, template-haskell
|
, template-haskell
|
||||||
, text
|
, text >= 1.2 && < 1.3
|
||||||
, transformers
|
, transformers
|
||||||
, wai
|
, wai
|
||||||
, wai-extra
|
, wai-extra
|
||||||
@ -231,7 +231,7 @@ test-suite test
|
|||||||
, safe >= 0.2
|
, safe >= 0.2
|
||||||
, shakespeare >= 2.0
|
, shakespeare >= 2.0
|
||||||
, template-haskell
|
, template-haskell
|
||||||
, text
|
, text >= 1.2 && < 1.3
|
||||||
, transformers
|
, transformers
|
||||||
, wai
|
, wai
|
||||||
, wai-extra
|
, wai-extra
|
||||||
|
|||||||
@ -7,6 +7,8 @@ adds some more which are easier to define here.
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Cli (
|
module Hledger.Cli (
|
||||||
module Hledger.Cli.Accounts,
|
module Hledger.Cli.Accounts,
|
||||||
module Hledger.Cli.Add,
|
module Hledger.Cli.Add,
|
||||||
|
|||||||
@ -10,6 +10,8 @@ The @accounts@ command lists account names:
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Cli.Accounts (
|
module Hledger.Cli.Accounts (
|
||||||
accountsmode
|
accountsmode
|
||||||
,accounts
|
,accounts
|
||||||
@ -17,6 +19,9 @@ module Hledger.Cli.Accounts (
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import Data.Monoid
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import System.Console.CmdArgs.Explicit as C
|
import System.Console.CmdArgs.Explicit as C
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
|
|
||||||
@ -52,11 +57,11 @@ accounts CliOpts{reportopts_=ropts} j = do
|
|||||||
nodepthq = dbg1 "nodepthq" $ filterQuery (not . queryIsDepth) q
|
nodepthq = dbg1 "nodepthq" $ filterQuery (not . queryIsDepth) q
|
||||||
depth = dbg1 "depth" $ queryDepth $ filterQuery queryIsDepth q
|
depth = dbg1 "depth" $ queryDepth $ filterQuery queryIsDepth q
|
||||||
ps = dbg1 "ps" $ journalPostings $ filterJournalPostings nodepthq j
|
ps = dbg1 "ps" $ journalPostings $ filterJournalPostings nodepthq j
|
||||||
as = dbg1 "as" $ nub $ filter (not . null) $ map (clipAccountName depth) $ sort $ map paccount ps
|
as = dbg1 "as" $ nub $ filter (not . T.null) $ map (clipAccountName depth) $ sort $ map paccount ps
|
||||||
as' | tree_ ropts = expandAccountNames as
|
as' | tree_ ropts = expandAccountNames as
|
||||||
| otherwise = as
|
| otherwise = as
|
||||||
render a | tree_ ropts = replicate (2 * (accountNameLevel a - 1)) ' ' ++ accountLeafName a
|
render a | tree_ ropts = T.replicate (2 * (accountNameLevel a - 1)) " " <> accountLeafName a
|
||||||
| otherwise = maybeAccountNameDrop ropts a
|
| otherwise = maybeAccountNameDrop ropts a
|
||||||
mapM_ (putStrLn . render) as'
|
mapM_ (putStrLn . T.unpack . render) as'
|
||||||
|
|
||||||
tests_Hledger_Cli_Accounts = TestList []
|
tests_Hledger_Cli_Accounts = TestList []
|
||||||
|
|||||||
@ -3,7 +3,7 @@ A history-aware add command to help with data entry.
|
|||||||
|-}
|
|-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}
|
{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}
|
||||||
{-# LANGUAGE CPP, ScopedTypeVariables, DeriveDataTypeable, RecordWildCards, TypeOperators, FlexibleContexts #-}
|
{-# LANGUAGE CPP, ScopedTypeVariables, DeriveDataTypeable, RecordWildCards, TypeOperators, FlexibleContexts, OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Cli.Add
|
module Hledger.Cli.Add
|
||||||
where
|
where
|
||||||
@ -17,6 +17,8 @@ import Data.Char (toUpper, toLower)
|
|||||||
import Data.List.Compat
|
import Data.List.Compat
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar (Day)
|
import Data.Time.Calendar (Day)
|
||||||
import Data.Typeable (Typeable)
|
import Data.Typeable (Typeable)
|
||||||
import Safe (headDef, headMay)
|
import Safe (headDef, headMay)
|
||||||
@ -216,10 +218,10 @@ postingWizard es@EntryState{..} = do
|
|||||||
else do
|
else do
|
||||||
let es1 = es{esArgs=drop 1 esArgs}
|
let es1 = es{esArgs=drop 1 esArgs}
|
||||||
(amt,comment) <- amountAndCommentWizard es1
|
(amt,comment) <- amountAndCommentWizard es1
|
||||||
return $ Just nullposting{paccount=stripbrackets acct
|
return $ Just nullposting{paccount=T.pack $ stripbrackets acct
|
||||||
,pamount=Mixed [amt]
|
,pamount=Mixed [amt]
|
||||||
,pcomment=comment
|
,pcomment=comment
|
||||||
,ptype=accountNamePostingType acct
|
,ptype=accountNamePostingType $ T.pack acct
|
||||||
}
|
}
|
||||||
|
|
||||||
postingsBalanced :: [Posting] -> Bool
|
postingsBalanced :: [Posting] -> Bool
|
||||||
@ -245,7 +247,7 @@ accountWizard EntryState{..} = do
|
|||||||
parseAccountOrDotOrNull _ _ "." = dbg1 $ Just "." -- . always signals end of txn
|
parseAccountOrDotOrNull _ _ "." = dbg1 $ Just "." -- . always signals end of txn
|
||||||
parseAccountOrDotOrNull "" True "" = dbg1 $ Just "" -- when there's no default and txn is balanced, "" also signals end of txn
|
parseAccountOrDotOrNull "" True "" = dbg1 $ Just "" -- when there's no default and txn is balanced, "" also signals end of txn
|
||||||
parseAccountOrDotOrNull def@(_:_) _ "" = dbg1 $ Just def -- when there's a default, "" means use that
|
parseAccountOrDotOrNull def@(_:_) _ "" = dbg1 $ Just def -- when there's a default, "" means use that
|
||||||
parseAccountOrDotOrNull _ _ s = dbg1 $ either (const Nothing) validateAccount $ runParser (accountnamep <* eof) esJournal "" s -- otherwise, try to parse the input as an accountname
|
parseAccountOrDotOrNull _ _ s = dbg1 $ either (const Nothing) ((T.unpack <$>) . validateAccount) $ runParser (accountnamep <* eof) esJournal "" s -- otherwise, try to parse the input as an accountname
|
||||||
dbg1 = id -- strace
|
dbg1 = id -- strace
|
||||||
validateAccount s | no_new_accounts_ esOpts && not (s `elem` journalAccountNames esJournal) = Nothing
|
validateAccount s | no_new_accounts_ esOpts && not (s `elem` journalAccountNames esJournal) = Nothing
|
||||||
| otherwise = Just s
|
| otherwise = Just s
|
||||||
@ -315,7 +317,7 @@ descriptionCompleter :: Journal -> String -> CompletionFunc IO
|
|||||||
descriptionCompleter j = completer (journalDescriptions j)
|
descriptionCompleter j = completer (journalDescriptions j)
|
||||||
|
|
||||||
accountCompleter :: Journal -> String -> CompletionFunc IO
|
accountCompleter :: Journal -> String -> CompletionFunc IO
|
||||||
accountCompleter j = completer (journalAccountNamesUsed j)
|
accountCompleter j = completer (map T.unpack $ journalAccountNamesUsed j)
|
||||||
|
|
||||||
amountCompleter :: String -> CompletionFunc IO
|
amountCompleter :: String -> CompletionFunc IO
|
||||||
amountCompleter = completer []
|
amountCompleter = completer []
|
||||||
@ -407,7 +409,7 @@ compareDescriptions :: String -> String -> Double
|
|||||||
compareDescriptions s t = compareStrings s' t'
|
compareDescriptions s t = compareStrings s' t'
|
||||||
where s' = simplify s
|
where s' = simplify s
|
||||||
t' = simplify t
|
t' = simplify t
|
||||||
simplify = filter (not . (`elem` "0123456789"))
|
simplify = filter (not . (`elem` ("0123456789" :: String)))
|
||||||
|
|
||||||
-- | Return a similarity measure, from 0 to 1, for two strings. This
|
-- | Return a similarity measure, from 0 to 1, for two strings. This
|
||||||
-- was based on Simon White's string similarity algorithm
|
-- was based on Simon White's string similarity algorithm
|
||||||
|
|||||||
@ -232,6 +232,8 @@ Currently, empty cells show 0.
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Hledger.Cli.Balance (
|
module Hledger.Cli.Balance (
|
||||||
balancemode
|
balancemode
|
||||||
,balance
|
,balance
|
||||||
@ -245,6 +247,9 @@ module Hledger.Cli.Balance (
|
|||||||
|
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Data.Maybe (fromMaybe, isJust)
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
|
import Data.Monoid
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import System.Console.CmdArgs.Explicit as C
|
import System.Console.CmdArgs.Explicit as C
|
||||||
import Text.CSV
|
import Text.CSV
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
@ -327,7 +332,7 @@ balance opts@CliOpts{reportopts_=ropts} j = do
|
|||||||
balanceReportAsCsv :: ReportOpts -> BalanceReport -> CSV
|
balanceReportAsCsv :: ReportOpts -> BalanceReport -> CSV
|
||||||
balanceReportAsCsv opts (items, total) =
|
balanceReportAsCsv opts (items, total) =
|
||||||
["account","balance"] :
|
["account","balance"] :
|
||||||
[[a, showMixedAmountOneLineWithoutPrice b] | ((a, _, _), b) <- items]
|
[[T.unpack a, showMixedAmountOneLineWithoutPrice b] | ((a, _, _), b) <- items]
|
||||||
++
|
++
|
||||||
if no_total_ opts
|
if no_total_ opts
|
||||||
then []
|
then []
|
||||||
@ -348,8 +353,8 @@ balanceReportAsText opts ((items, total)) = unlines $ concat lines ++ t
|
|||||||
Right fmt ->
|
Right fmt ->
|
||||||
let
|
let
|
||||||
-- abuse renderBalanceReportItem to render the total with similar format
|
-- abuse renderBalanceReportItem to render the total with similar format
|
||||||
acctcolwidth = maximum' [length fullname | ((fullname, _, _), _) <- items]
|
acctcolwidth = maximum' [T.length fullname | ((fullname, _, _), _) <- items]
|
||||||
totallines = map rstrip $ renderBalanceReportItem fmt (replicate (acctcolwidth+1) ' ', 0, total)
|
totallines = map rstrip $ renderBalanceReportItem fmt (T.replicate (acctcolwidth+1) " ", 0, total)
|
||||||
-- with a custom format, extend the line to the full report width;
|
-- with a custom format, extend the line to the full report width;
|
||||||
-- otherwise show the usual 20-char line for compatibility
|
-- otherwise show the usual 20-char line for compatibility
|
||||||
overlinewidth | isJust (format_ opts) = maximum' $ map length $ concat lines
|
overlinewidth | isJust (format_ opts) = maximum' $ map length $ concat lines
|
||||||
@ -417,7 +422,7 @@ renderComponent (acctname, depth, total) (FormatField ljust min max field) = cas
|
|||||||
where d = case min of
|
where d = case min of
|
||||||
Just m -> depth * m
|
Just m -> depth * m
|
||||||
Nothing -> depth
|
Nothing -> depth
|
||||||
AccountField -> formatString ljust min max acctname
|
AccountField -> formatString ljust min max (T.unpack acctname)
|
||||||
TotalField -> fitStringMulti min max True False $ showMixedAmountWithoutPrice total
|
TotalField -> fitStringMulti min max True False $ showMixedAmountWithoutPrice total
|
||||||
_ -> ""
|
_ -> ""
|
||||||
|
|
||||||
@ -428,7 +433,7 @@ renderComponent (acctname, depth, total) (FormatField ljust min max field) = cas
|
|||||||
renderComponent1 :: (AccountName, Int, MixedAmount) -> StringFormatComponent -> String
|
renderComponent1 :: (AccountName, Int, MixedAmount) -> StringFormatComponent -> String
|
||||||
renderComponent1 _ (FormatLiteral s) = s
|
renderComponent1 _ (FormatLiteral s) = s
|
||||||
renderComponent1 (acctname, depth, total) (FormatField ljust min max field) = case field of
|
renderComponent1 (acctname, depth, total) (FormatField ljust min max field) = case field of
|
||||||
AccountField -> formatString ljust min max ((intercalate ", " . lines) (indented acctname))
|
AccountField -> formatString ljust min max ((intercalate ", " . lines) (indented (T.unpack acctname)))
|
||||||
where
|
where
|
||||||
-- better to indent the account name here rather than use a DepthField component
|
-- better to indent the account name here rather than use a DepthField component
|
||||||
-- so that it complies with width spec. Uses a fixed indent step size.
|
-- so that it complies with width spec. Uses a fixed indent step size.
|
||||||
@ -445,7 +450,7 @@ multiBalanceReportAsCsv opts (MultiBalanceReport (colspans, items, (coltotals,to
|
|||||||
++ (if row_total_ opts then ["total"] else [])
|
++ (if row_total_ opts then ["total"] else [])
|
||||||
++ (if average_ opts then ["average"] else [])
|
++ (if average_ opts then ["average"] else [])
|
||||||
) :
|
) :
|
||||||
[a : a' : show i :
|
[T.unpack a : T.unpack a' : show i :
|
||||||
map showMixedAmountOneLineWithoutPrice
|
map showMixedAmountOneLineWithoutPrice
|
||||||
(amts
|
(amts
|
||||||
++ (if row_total_ opts then [rowtot] else [])
|
++ (if row_total_ opts then [rowtot] else [])
|
||||||
@ -470,7 +475,7 @@ periodBalanceReportAsText opts r@(MultiBalanceReport (colspans, items, (coltotal
|
|||||||
render id (" "++) showMixedAmountOneLineWithoutPrice $
|
render id (" "++) showMixedAmountOneLineWithoutPrice $
|
||||||
addtotalrow $
|
addtotalrow $
|
||||||
Table
|
Table
|
||||||
(T.Group NoLine $ map (Header . padRightWide acctswidth) accts)
|
(T.Group NoLine $ map (Header . padRightWide acctswidth . T.unpack) accts)
|
||||||
(T.Group NoLine $ map Header colheadings)
|
(T.Group NoLine $ map Header colheadings)
|
||||||
(map rowvals items')
|
(map rowvals items')
|
||||||
where
|
where
|
||||||
@ -482,9 +487,9 @@ periodBalanceReportAsText opts r@(MultiBalanceReport (colspans, items, (coltotal
|
|||||||
| otherwise = items -- dbg1 "2" $ filter (any (not . isZeroMixedAmount) . snd) $ dbg1 "1" items
|
| otherwise = items -- dbg1 "2" $ filter (any (not . isZeroMixedAmount) . snd) $ dbg1 "1" items
|
||||||
accts = map renderacct items'
|
accts = map renderacct items'
|
||||||
renderacct ((a,a',i),_,_,_)
|
renderacct ((a,a',i),_,_,_)
|
||||||
| tree_ opts = replicate ((i-1)*2) ' ' ++ a'
|
| tree_ opts = T.replicate ((i-1)*2) " " <> a'
|
||||||
| otherwise = maybeAccountNameDrop opts a
|
| otherwise = maybeAccountNameDrop opts a
|
||||||
acctswidth = maximum' $ map strWidth accts
|
acctswidth = maximum' $ map textWidth accts
|
||||||
rowvals (_,as,rowtot,rowavg) = as
|
rowvals (_,as,rowtot,rowavg) = as
|
||||||
++ (if row_total_ opts then [rowtot] else [])
|
++ (if row_total_ opts then [rowtot] else [])
|
||||||
++ (if average_ opts then [rowavg] else [])
|
++ (if average_ opts then [rowavg] else [])
|
||||||
@ -514,8 +519,8 @@ cumulativeBalanceReportAsText opts r@(MultiBalanceReport (colspans, items, (colt
|
|||||||
++ (if average_ opts then ["Average"] else [])
|
++ (if average_ opts then ["Average"] else [])
|
||||||
accts = map renderacct items
|
accts = map renderacct items
|
||||||
renderacct ((a,a',i),_,_,_)
|
renderacct ((a,a',i),_,_,_)
|
||||||
| tree_ opts = replicate ((i-1)*2) ' ' ++ a'
|
| tree_ opts = replicate ((i-1)*2) ' ' ++ T.unpack a'
|
||||||
| otherwise = maybeAccountNameDrop opts a
|
| otherwise = T.unpack $ maybeAccountNameDrop opts a
|
||||||
acctswidth = maximum' $ map strWidth accts
|
acctswidth = maximum' $ map strWidth accts
|
||||||
rowvals (_,as,rowtot,rowavg) = as
|
rowvals (_,as,rowtot,rowavg) = as
|
||||||
++ (if row_total_ opts then [rowtot] else [])
|
++ (if row_total_ opts then [rowtot] else [])
|
||||||
@ -546,8 +551,8 @@ historicalBalanceReportAsText opts r@(MultiBalanceReport (colspans, items, (colt
|
|||||||
++ (if average_ opts then ["Average"] else [])
|
++ (if average_ opts then ["Average"] else [])
|
||||||
accts = map renderacct items
|
accts = map renderacct items
|
||||||
renderacct ((a,a',i),_,_,_)
|
renderacct ((a,a',i),_,_,_)
|
||||||
| tree_ opts = replicate ((i-1)*2) ' ' ++ a'
|
| tree_ opts = replicate ((i-1)*2) ' ' ++ T.unpack a'
|
||||||
| otherwise = maybeAccountNameDrop opts a
|
| otherwise = T.unpack $ maybeAccountNameDrop opts a
|
||||||
acctswidth = maximum' $ map strWidth accts
|
acctswidth = maximum' $ map strWidth accts
|
||||||
rowvals (_,as,rowtot,rowavg) = as
|
rowvals (_,as,rowtot,rowavg) = as
|
||||||
++ (if row_total_ opts then [rowtot] else [])
|
++ (if row_total_ opts then [rowtot] else [])
|
||||||
|
|||||||
@ -16,6 +16,8 @@ module Hledger.Cli.Register (
|
|||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import System.Console.CmdArgs.Explicit
|
import System.Console.CmdArgs.Explicit
|
||||||
import Text.CSV
|
import Text.CSV
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
@ -70,7 +72,7 @@ postingsReportItemAsCsvRecord (_, _, _, p, b) = [date,desc,acct,amt,bal]
|
|||||||
where
|
where
|
||||||
date = showDate $ postingDate p -- XXX csv should show date2 with --date2
|
date = showDate $ postingDate p -- XXX csv should show date2 with --date2
|
||||||
desc = maybe "" tdescription $ ptransaction p
|
desc = maybe "" tdescription $ ptransaction p
|
||||||
acct = bracket $ paccount p
|
acct = bracket $ T.unpack $ paccount p
|
||||||
where
|
where
|
||||||
bracket = case ptype p of
|
bracket = case ptype p of
|
||||||
BalancedVirtualPosting -> (\s -> "["++s++"]")
|
BalancedVirtualPosting -> (\s -> "["++s++"]")
|
||||||
@ -173,7 +175,7 @@ postingsReportItemAsText opts preferredamtwidth preferredbalwidth (mdate, mendda
|
|||||||
|
|
||||||
-- gather content
|
-- gather content
|
||||||
desc = fromMaybe "" mdesc
|
desc = fromMaybe "" mdesc
|
||||||
acct = parenthesise $ elideAccountName awidth $ paccount p
|
acct = parenthesise $ T.unpack $ elideAccountName awidth $ paccount p
|
||||||
where
|
where
|
||||||
(parenthesise, awidth) =
|
(parenthesise, awidth) =
|
||||||
case ptype p of
|
case ptype p of
|
||||||
|
|||||||
@ -13,6 +13,8 @@ module Hledger.Cli.Tests (
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ runTests = liftM (fst . flip (,) 0) . runTestTT . flatTests
|
|||||||
-- -- firstproblem = find (\counts -> )
|
-- -- firstproblem = find (\counts -> )
|
||||||
|
|
||||||
-- | All or pattern-matched tests, as a flat list to show simple names.
|
-- | All or pattern-matched tests, as a flat list to show simple names.
|
||||||
flatTests opts = TestList $ filter (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) $ flattenTests tests_Hledger_Cli
|
flatTests opts = TestList $ filter (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . T.pack . testName) $ flattenTests tests_Hledger_Cli
|
||||||
|
|
||||||
-- -- | All or pattern-matched tests, in the original suites to show hierarchical names.
|
-- -- | All or pattern-matched tests, in the original suites to show hierarchical names.
|
||||||
-- hierarchicalTests opts = filterTests (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) tests_Hledger_Cli
|
-- hierarchicalTests opts = filterTests (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) tests_Hledger_Cli
|
||||||
|
|||||||
@ -25,6 +25,8 @@ where
|
|||||||
import Control.Exception as C
|
import Control.Exception as C
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
-- import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Time (Day)
|
import Data.Time (Day)
|
||||||
import Safe (readMay)
|
import Safe (readMay)
|
||||||
import System.Console.CmdArgs
|
import System.Console.CmdArgs
|
||||||
@ -85,7 +87,7 @@ pivot tag j = j{jtxns = map pivotTrans . jtxns $ j}
|
|||||||
where
|
where
|
||||||
pivotTrans t = t{tpostings = map pivotPosting . tpostings $ t}
|
pivotTrans t = t{tpostings = map pivotPosting . tpostings $ t}
|
||||||
pivotPosting p
|
pivotPosting p
|
||||||
| Just (_ , value) <- tagTuple = p{paccount = joinAccountNames tag value}
|
| Just (_ , value) <- tagTuple = p{paccount = joinAccountNames (T.pack tag) (T.pack value)}
|
||||||
| _ <- tagTuple = p
|
| _ <- tagTuple = p
|
||||||
where tagTuple = find ((tag ==) . fst) . ptags $ p
|
where tagTuple = find ((tag ==) . fst) . ptags $ p
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user