push csv rule and format string types down
This commit is contained in:
parent
f4602cc803
commit
e396c0dc8d
@ -1325,6 +1325,7 @@ http://ajaxcssblog.com/jquery/url-read-request-variables/
|
||||
*** inspiration
|
||||
http://community.haskell.org/~ndm/downloads/paper-hoogle_overview-19_nov_2008.pdf -> Design Guidelines
|
||||
** features/wishlist
|
||||
*** don't moan about ~
|
||||
*** support apostrophe digit group separator
|
||||
*** detect .hs plugins
|
||||
*** Clint's ofx support
|
||||
|
||||
@ -183,9 +183,65 @@ data Reader = Reader {
|
||||
rFormat :: Format
|
||||
-- quickly check if this reader can probably handle the given file path and file content
|
||||
,rDetector :: FilePath -> String -> Bool
|
||||
-- really parse the given file path and file content, returning a journal or error
|
||||
,rParser :: FilePath -> String -> ErrorT String IO Journal
|
||||
}
|
||||
-- parse the given string, using the given parsing rules if any, returning a journal or error aware of the given file path
|
||||
,rParser :: Maybe ParseRules -> FilePath -> String -> ErrorT String IO Journal
|
||||
}
|
||||
|
||||
-- data format parse/conversion rules
|
||||
|
||||
-- currently the only parse (conversion) rules are those for the CSV format
|
||||
type ParseRules = CsvRules
|
||||
|
||||
-- XXX copied from Convert.hs
|
||||
{- |
|
||||
A set of data definitions and account-matching patterns sufficient to
|
||||
convert a particular CSV data file into meaningful journal transactions. See above.
|
||||
-}
|
||||
data CsvRules = CsvRules {
|
||||
dateField :: Maybe FieldPosition,
|
||||
dateFormat :: Maybe String,
|
||||
statusField :: Maybe FieldPosition,
|
||||
codeField :: Maybe FieldPosition,
|
||||
descriptionField :: [FormatString],
|
||||
amountField :: Maybe FieldPosition,
|
||||
amountInField :: Maybe FieldPosition,
|
||||
amountOutField :: Maybe FieldPosition,
|
||||
currencyField :: Maybe FieldPosition,
|
||||
baseCurrency :: Maybe String,
|
||||
accountField :: Maybe FieldPosition,
|
||||
account2Field :: Maybe FieldPosition,
|
||||
effectiveDateField :: Maybe FieldPosition,
|
||||
baseAccount :: AccountName,
|
||||
accountRules :: [AccountRule]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
type FieldPosition = Int
|
||||
|
||||
type AccountRule = (
|
||||
[(String, Maybe String)] -- list of regex match patterns with optional replacements
|
||||
,AccountName -- account name to use for a transaction matching this rule
|
||||
)
|
||||
|
||||
-- format strings
|
||||
|
||||
data HledgerFormatField =
|
||||
AccountField
|
||||
| DefaultDateField
|
||||
| DescriptionField
|
||||
| TotalField
|
||||
| DepthSpacerField
|
||||
| FieldNo Int
|
||||
deriving (Show, Eq)
|
||||
|
||||
data FormatString =
|
||||
FormatLiteral String
|
||||
| FormatField Bool -- Left justified ?
|
||||
(Maybe Int) -- Min width
|
||||
(Maybe Int) -- Max width
|
||||
HledgerFormatField -- Field
|
||||
deriving (Show, Eq)
|
||||
|
||||
|
||||
data Ledger = Ledger {
|
||||
journal :: Journal,
|
||||
accountnametree :: Tree AccountName,
|
||||
|
||||
@ -34,7 +34,7 @@ import Test.HUnit
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Data.Dates (getCurrentDay)
|
||||
import Hledger.Data.Types (Journal(..), Reader(..), Format)
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Journal (nullctx)
|
||||
import Hledger.Read.JournalReader as JournalReader
|
||||
import Hledger.Read.TimelogReader as TimelogReader
|
||||
@ -93,8 +93,8 @@ readerForFormat s | null rs = Nothing
|
||||
-- the specified data format or trying all known formats. CSV
|
||||
-- conversion rules may be provided for better conversion of that
|
||||
-- format, and/or a file path for better error messages.
|
||||
readJournal :: Maybe Format -> Maybe CsvReader.CsvRules -> Maybe FilePath -> String -> IO (Either String Journal)
|
||||
readJournal format _ path s =
|
||||
readJournal :: Maybe Format -> Maybe ParseRules -> Maybe FilePath -> String -> IO (Either String Journal)
|
||||
readJournal format rules path s =
|
||||
let readerstotry = case format of Nothing -> readers
|
||||
Just f -> case readerForFormat f of Just r -> [r]
|
||||
Nothing -> []
|
||||
@ -103,7 +103,7 @@ readJournal format _ path s =
|
||||
path' = fromMaybe "(string)" path
|
||||
tryReader :: Reader -> IO (Either String Journal)
|
||||
tryReader r = do -- printf "trying %s reader\n" (rFormat r)
|
||||
(runErrorT . (rParser r) path') s
|
||||
(runErrorT . (rParser r) rules path') s
|
||||
|
||||
-- if no reader succeeds, we return the error of the first;
|
||||
-- ideally it would be the error of the most likely intended
|
||||
|
||||
@ -53,9 +53,9 @@ detect f _ = fileSuffix f == format
|
||||
|
||||
-- | Parse and post-process a "Journal" from CSV data, or give an error.
|
||||
-- XXX currently ignores the string and reads from the file path
|
||||
parse_ :: FilePath -> String -> ErrorT String IO Journal
|
||||
parse_ f s = do
|
||||
r <- liftIO $ journalFromCsv f s
|
||||
parse_ :: Maybe ParseRules -> FilePath -> String -> ErrorT String IO Journal
|
||||
parse_ rules f s = do
|
||||
r <- liftIO $ journalFromCsv rules f s
|
||||
case r of Left e -> throwError e
|
||||
Right j -> return j
|
||||
|
||||
@ -67,30 +67,6 @@ parse_ f s = do
|
||||
|
||||
|
||||
|
||||
-- XXX copied from Convert.hs
|
||||
|
||||
{- |
|
||||
A set of data definitions and account-matching patterns sufficient to
|
||||
convert a particular CSV data file into meaningful journal transactions. See above.
|
||||
-}
|
||||
data CsvRules = CsvRules {
|
||||
dateField :: Maybe FieldPosition,
|
||||
dateFormat :: Maybe String,
|
||||
statusField :: Maybe FieldPosition,
|
||||
codeField :: Maybe FieldPosition,
|
||||
descriptionField :: [FormatString],
|
||||
amountField :: Maybe FieldPosition,
|
||||
amountInField :: Maybe FieldPosition,
|
||||
amountOutField :: Maybe FieldPosition,
|
||||
currencyField :: Maybe FieldPosition,
|
||||
baseCurrency :: Maybe String,
|
||||
accountField :: Maybe FieldPosition,
|
||||
account2Field :: Maybe FieldPosition,
|
||||
effectiveDateField :: Maybe FieldPosition,
|
||||
baseAccount :: AccountName,
|
||||
accountRules :: [AccountRule]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
nullrules = CsvRules {
|
||||
dateField=Nothing,
|
||||
dateFormat=Nothing,
|
||||
@ -109,35 +85,32 @@ nullrules = CsvRules {
|
||||
accountRules=[]
|
||||
}
|
||||
|
||||
type FieldPosition = Int
|
||||
|
||||
type AccountRule = (
|
||||
[(String, Maybe String)] -- list of regex match patterns with optional replacements
|
||||
,AccountName -- account name to use for a transaction matching this rule
|
||||
)
|
||||
|
||||
type CsvRecord = [String]
|
||||
|
||||
|
||||
-- | Read the CSV file named as an argument and print equivalent journal transactions,
|
||||
-- using/creating a .rules file.
|
||||
journalFromCsv :: FilePath -> String -> IO (Either String Journal)
|
||||
journalFromCsv csvfile content = do
|
||||
journalFromCsv :: Maybe CsvRules -> FilePath -> String -> IO (Either String Journal)
|
||||
journalFromCsv csvrules csvfile content = do
|
||||
let usingStdin = csvfile == "-"
|
||||
-- rulesFileSpecified = isJust $ rules_file_ opts
|
||||
rulesfile = rulesFileFor csvfile
|
||||
-- when (usingStdin && (not rulesFileSpecified)) $ error' "please use --rules-file to specify a rules file when converting stdin"
|
||||
csvparse <- parseCsv csvfile content
|
||||
let records = case csvparse of
|
||||
Left e -> error' $ show e
|
||||
Right rs -> filter (/= [""]) rs
|
||||
exists <- doesFileExist rulesfile
|
||||
if (not exists) then do
|
||||
hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile
|
||||
writeFile rulesfile initialRulesFileContent
|
||||
else
|
||||
hPrintf stderr "using conversion rules file %s\n" rulesfile
|
||||
rules <- liftM (either (error'.show) id) $ parseCsvRulesFile rulesfile
|
||||
rules <- case csvrules of
|
||||
Nothing -> do
|
||||
let rulesfile = rulesFileFor csvfile
|
||||
exists <- doesFileExist rulesfile
|
||||
if (not exists)
|
||||
then do
|
||||
hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile
|
||||
writeFile rulesfile initialRulesFileContent
|
||||
else
|
||||
hPrintf stderr "using conversion rules file %s\n" rulesfile
|
||||
liftM (either (error'.show) id) $ parseCsvRulesFile rulesfile
|
||||
Just r -> return r
|
||||
let invalid = validateRules rules
|
||||
-- when (debug_ opts) $ hPrintf stderr "rules: %s\n" (show rules)
|
||||
when (isJust invalid) $ error (fromJust invalid)
|
||||
@ -384,15 +357,15 @@ matchreplacepattern = do
|
||||
return (matchpat,replpat)
|
||||
|
||||
-- csv record conversion
|
||||
formatD :: CsvRecord -> Bool -> Maybe Int -> Maybe Int -> Field -> String
|
||||
formatD :: CsvRecord -> Bool -> Maybe Int -> Maybe Int -> HledgerFormatField -> String
|
||||
formatD record leftJustified min max f = case f of
|
||||
FieldNo n -> maybe "" show $ atMay record n
|
||||
-- Some of these might in theory in read from fields
|
||||
FormatStrings.Account -> ""
|
||||
DepthSpacer -> ""
|
||||
Total -> ""
|
||||
DefaultDate -> ""
|
||||
Description -> ""
|
||||
AccountField -> ""
|
||||
DepthSpacerField -> ""
|
||||
TotalField -> ""
|
||||
DefaultDateField -> ""
|
||||
DescriptionField -> ""
|
||||
where
|
||||
show = formatValue leftJustified min max
|
||||
|
||||
|
||||
@ -153,8 +153,8 @@ detect f _ = fileSuffix f == format
|
||||
|
||||
-- | Parse and post-process a "Journal" from hledger's journal file
|
||||
-- format, or give an error.
|
||||
parse :: FilePath -> String -> ErrorT String IO Journal
|
||||
parse = parseJournalWith journalFile
|
||||
parse :: Maybe ParseRules -> FilePath -> String -> ErrorT String IO Journal
|
||||
parse _ = parseJournalWith journalFile
|
||||
|
||||
-- | Top-level journal parser. Returns a single composite, I/O performing,
|
||||
-- error-raising "JournalUpdate" (and final "JournalContext") which can be
|
||||
|
||||
@ -71,8 +71,8 @@ detect f _ = fileSuffix f == format
|
||||
-- | Parse and post-process a "Journal" from timeclock.el's timelog
|
||||
-- format, saving the provided file path and the current time, or give an
|
||||
-- error.
|
||||
parse :: FilePath -> String -> ErrorT String IO Journal
|
||||
parse = parseJournalWith timelogFile
|
||||
parse :: Maybe ParseRules -> FilePath -> String -> ErrorT String IO Journal
|
||||
parse _ = parseJournalWith timelogFile
|
||||
|
||||
timelogFile :: GenParser Char JournalContext (JournalUpdate,JournalContext)
|
||||
timelogFile = do items <- many timelogItem
|
||||
|
||||
@ -170,13 +170,13 @@ formatAccountsReportItem opts accountName depth amount (fmt:fmts) =
|
||||
FormatLiteral l -> l
|
||||
FormatField ljust min max field -> formatField opts accountName depth amount ljust min max field
|
||||
|
||||
formatField :: ReportOpts -> Maybe AccountName -> Int -> Amount -> Bool -> Maybe Int -> Maybe Int -> Field -> String
|
||||
formatField :: ReportOpts -> Maybe AccountName -> Int -> Amount -> Bool -> Maybe Int -> Maybe Int -> HledgerFormatField -> String
|
||||
formatField opts accountName depth total ljust min max field = case field of
|
||||
Format.Account -> formatValue ljust min max $ maybe "" (accountNameDrop (drop_ opts)) accountName
|
||||
Format.DepthSpacer -> case min of
|
||||
AccountField -> formatValue ljust min max $ maybe "" (accountNameDrop (drop_ opts)) accountName
|
||||
DepthSpacerField -> case min of
|
||||
Just m -> formatValue ljust Nothing max $ replicate (depth * m) ' '
|
||||
Nothing -> formatValue ljust Nothing max $ replicate depth ' '
|
||||
Format.Total -> formatValue ljust min max $ showAmountWithoutPrice total
|
||||
TotalField -> formatValue ljust min max $ showAmountWithoutPrice total
|
||||
_ -> ""
|
||||
|
||||
tests_Hledger_Cli_Balance = TestList
|
||||
|
||||
@ -28,28 +28,6 @@ import qualified Hledger.Cli.Format as Format
|
||||
import Hledger.Cli.Options
|
||||
import Hledger.Cli.Version
|
||||
|
||||
{- |
|
||||
A set of data definitions and account-matching patterns sufficient to
|
||||
convert a particular CSV data file into meaningful journal transactions. See above.
|
||||
-}
|
||||
data CsvRules = CsvRules {
|
||||
dateField :: Maybe FieldPosition,
|
||||
dateFormat :: Maybe String,
|
||||
statusField :: Maybe FieldPosition,
|
||||
codeField :: Maybe FieldPosition,
|
||||
descriptionField :: [FormatString],
|
||||
amountField :: Maybe FieldPosition,
|
||||
amountInField :: Maybe FieldPosition,
|
||||
amountOutField :: Maybe FieldPosition,
|
||||
currencyField :: Maybe FieldPosition,
|
||||
baseCurrency :: Maybe String,
|
||||
accountField :: Maybe FieldPosition,
|
||||
account2Field :: Maybe FieldPosition,
|
||||
effectiveDateField :: Maybe FieldPosition,
|
||||
baseAccount :: AccountName,
|
||||
accountRules :: [AccountRule]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
nullrules = CsvRules {
|
||||
dateField=Nothing,
|
||||
dateFormat=Nothing,
|
||||
@ -68,13 +46,6 @@ nullrules = CsvRules {
|
||||
accountRules=[]
|
||||
}
|
||||
|
||||
type FieldPosition = Int
|
||||
|
||||
type AccountRule = (
|
||||
[(String, Maybe String)] -- list of regex match patterns with optional replacements
|
||||
,AccountName -- account name to use for a transaction matching this rule
|
||||
)
|
||||
|
||||
type CsvRecord = [String]
|
||||
|
||||
|
||||
@ -344,15 +315,15 @@ matchreplacepattern = do
|
||||
return (matchpat,replpat)
|
||||
|
||||
-- csv record conversion
|
||||
formatD :: CsvRecord -> Bool -> Maybe Int -> Maybe Int -> Field -> String
|
||||
formatD :: CsvRecord -> Bool -> Maybe Int -> Maybe Int -> HledgerFormatField -> String
|
||||
formatD record leftJustified min max f = case f of
|
||||
FieldNo n -> maybe "" show $ atMay record n
|
||||
-- Some of these might in theory in read from fields
|
||||
Format.Account -> ""
|
||||
DepthSpacer -> ""
|
||||
Total -> ""
|
||||
DefaultDate -> ""
|
||||
Description -> ""
|
||||
AccountField -> ""
|
||||
DepthSpacerField -> ""
|
||||
TotalField -> ""
|
||||
DefaultDateField -> ""
|
||||
DescriptionField -> ""
|
||||
where
|
||||
show = formatValue leftJustified min max
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ module Hledger.Cli.Format (
|
||||
, formatStrings
|
||||
, formatValue
|
||||
, FormatString(..)
|
||||
, Field(..)
|
||||
, HledgerFormatField(..)
|
||||
, tests
|
||||
) where
|
||||
|
||||
@ -14,23 +14,8 @@ import Test.HUnit
|
||||
import Text.ParserCombinators.Parsec
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Data.Types
|
||||
|
||||
data Field =
|
||||
Account
|
||||
| DefaultDate
|
||||
| Description
|
||||
| Total
|
||||
| DepthSpacer
|
||||
| FieldNo Int
|
||||
deriving (Show, Eq)
|
||||
|
||||
data FormatString =
|
||||
FormatLiteral String
|
||||
| FormatField Bool -- Left justified ?
|
||||
(Maybe Int) -- Min width
|
||||
(Maybe Int) -- Max width
|
||||
Field -- Field
|
||||
deriving (Show, Eq)
|
||||
|
||||
formatValue :: Bool -> Maybe Int -> Maybe Int -> String -> String
|
||||
formatValue leftJustified min max value = printf formatS value
|
||||
@ -49,13 +34,13 @@ parseFormatString input = case (runParser formatStrings () "(unknown)") input of
|
||||
Parsers
|
||||
-}
|
||||
|
||||
field :: GenParser Char st Field
|
||||
field :: GenParser Char st HledgerFormatField
|
||||
field = do
|
||||
try (string "account" >> return Account)
|
||||
<|> try (string "depth_spacer" >> return DepthSpacer)
|
||||
<|> try (string "date" >> return Description)
|
||||
<|> try (string "description" >> return Description)
|
||||
<|> try (string "total" >> return Total)
|
||||
try (string "account" >> return AccountField)
|
||||
<|> try (string "depth_spacer" >> return DepthSpacerField)
|
||||
<|> try (string "date" >> return DescriptionField)
|
||||
<|> try (string "description" >> return DescriptionField)
|
||||
<|> try (string "total" >> return TotalField)
|
||||
<|> try (many1 digit >>= (\s -> return $ FieldNo $ read s))
|
||||
|
||||
formatField :: GenParser Char st FormatString
|
||||
@ -106,28 +91,28 @@ tests = test [ formattingTests ++ parserTests ]
|
||||
|
||||
formattingTests = [
|
||||
testFormat (FormatLiteral " ") "" " "
|
||||
, testFormat (FormatField False Nothing Nothing Description) "description" "description"
|
||||
, testFormat (FormatField False (Just 20) Nothing Description) "description" " description"
|
||||
, testFormat (FormatField False Nothing (Just 20) Description) "description" "description"
|
||||
, testFormat (FormatField True Nothing (Just 20) Description) "description" "description"
|
||||
, testFormat (FormatField True (Just 20) Nothing Description) "description" "description "
|
||||
, testFormat (FormatField True (Just 20) (Just 20) Description) "description" "description "
|
||||
, testFormat (FormatField True Nothing (Just 3) Description) "description" "des"
|
||||
, testFormat (FormatField False Nothing Nothing DescriptionField) "description" "description"
|
||||
, testFormat (FormatField False (Just 20) Nothing DescriptionField) "description" " description"
|
||||
, testFormat (FormatField False Nothing (Just 20) DescriptionField) "description" "description"
|
||||
, testFormat (FormatField True Nothing (Just 20) DescriptionField) "description" "description"
|
||||
, testFormat (FormatField True (Just 20) Nothing DescriptionField) "description" "description "
|
||||
, testFormat (FormatField True (Just 20) (Just 20) DescriptionField) "description" "description "
|
||||
, testFormat (FormatField True Nothing (Just 3) DescriptionField) "description" "des"
|
||||
]
|
||||
|
||||
parserTests = [
|
||||
testParser "" []
|
||||
, testParser "D" [FormatLiteral "D"]
|
||||
, testParser "%(date)" [FormatField False Nothing Nothing Description]
|
||||
, testParser "%(total)" [FormatField False Nothing Nothing Total]
|
||||
, testParser "Hello %(date)!" [FormatLiteral "Hello ", FormatField False Nothing Nothing Description, FormatLiteral "!"]
|
||||
, testParser "%-(date)" [FormatField True Nothing Nothing Description]
|
||||
, testParser "%20(date)" [FormatField False (Just 20) Nothing Description]
|
||||
, testParser "%.10(date)" [FormatField False Nothing (Just 10) Description]
|
||||
, testParser "%20.10(date)" [FormatField False (Just 20) (Just 10) Description]
|
||||
, testParser "%20(account) %.10(total)\n" [ FormatField False (Just 20) Nothing Account
|
||||
, testParser "%(date)" [FormatField False Nothing Nothing DescriptionField]
|
||||
, testParser "%(total)" [FormatField False Nothing Nothing TotalField]
|
||||
, testParser "Hello %(date)!" [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"]
|
||||
, testParser "%-(date)" [FormatField True Nothing Nothing DescriptionField]
|
||||
, testParser "%20(date)" [FormatField False (Just 20) Nothing DescriptionField]
|
||||
, testParser "%.10(date)" [FormatField False Nothing (Just 10) DescriptionField]
|
||||
, testParser "%20.10(date)" [FormatField False (Just 20) (Just 10) DescriptionField]
|
||||
, testParser "%20(account) %.10(total)\n" [ FormatField False (Just 20) Nothing AccountField
|
||||
, FormatLiteral " "
|
||||
, FormatField False Nothing (Just 10) Total
|
||||
, FormatField False Nothing (Just 10) TotalField
|
||||
, FormatLiteral "\n"
|
||||
]
|
||||
]
|
||||
|
||||
@ -415,10 +415,10 @@ formatFromOpts = maybe (Right defaultBalanceFormatString) parseFormatString . fo
|
||||
-- | Default line format for balance report: "%20(total) %2(depth_spacer)%-(account)"
|
||||
defaultBalanceFormatString :: [FormatString]
|
||||
defaultBalanceFormatString = [
|
||||
FormatField False (Just 20) Nothing Total
|
||||
FormatField False (Just 20) Nothing TotalField
|
||||
, FormatLiteral " "
|
||||
, FormatField True (Just 2) Nothing DepthSpacer
|
||||
, FormatField True Nothing Nothing Format.Account
|
||||
, FormatField True (Just 2) Nothing DepthSpacerField
|
||||
, FormatField True Nothing Nothing AccountField
|
||||
]
|
||||
|
||||
-- | Get the journal file path from options, an environment variable, or a default.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user