csv: merge lucamolteni's cassava/custom separators (squashed) (#829)
commit 5ba464de761b298e50d57a8b7d14bc28adb30d5d
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Sep 7 17:54:12 2018 +0200
Fix CI 2
commit f060ae9449f4b61a915b0ed4629fc1ba9b66fb4a
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Sep 7 17:30:08 2018 +0200
Fix CI build
commit af0719a33b9b72ad244ae80198d881a1f7145e9d
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Sep 7 17:19:01 2018 +0200
Fix rebase
commit 1a24ddfa54dfb4ff1326e1a51005ffa82d3dc3c8
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Aug 10 16:25:24 2018 +0200
Fixed some GHC warnings
commit 1ac43398a359b5925ef71f53347698f1c6c510ef
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Aug 10 16:14:49 2018 +0200
Fix .cabal
commit 422456b925d8aa4ab3e869f51e98c2b1c3dcde0a
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jul 1 22:56:20 2018 +0200
Removed to-do list
commit 1118b762e4fd15c4fe7ba48ba86676706ea3a5a5
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jul 1 22:53:28 2018 +0200
Better test
commit 1146ed0941655668bf7684f18aa15c5f4b9b20c2
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jul 1 15:32:28 2018 +0200
Fix parsing
commit 4fc2374b2b81802990da30c96756aab54d77399c
Author: Luca Molteni <volothamp@gmail.com>
Date: Thu Jun 21 22:11:11 2018 +0200
Parsing of separator
commit f7a61737f1ad4460ba20ca9b2e86eb21468abb33
Author: Luca Molteni <volothamp@gmail.com>
Date: Thu Jun 21 14:29:23 2018 +0200
Almost separator in options
commit ac8841cf3b9c80914bc3271ad9b9ff4ae9ba48a7
Author: Luca Molteni <volothamp@gmail.com>
Date: Thu Jun 21 14:16:59 2018 +0200
Separator in parseCSV
commit 92a8b9f6ba77ea4237f769641e03029ac88542ea
Author: Luca Molteni <volothamp@gmail.com>
Date: Thu Jun 21 13:30:41 2018 +0200
separator option
commit ec417a81ae625647cf35e61776cdf02bdb2c6aea
Author: Luca Molteni <volothamp@gmail.com>
Date: Thu Jun 21 10:45:26 2018 +0200
Removed one qualified import
commit 8b2f386c2f780adcd34cff3de7edceacc1d325a7
Author: Luca Molteni <volothamp@gmail.com>
Date: Wed Jun 20 14:01:12 2018 +0200
Removed string conversions
commit a14d0e099e28a286bb81770cfc9cb8f5c7e5cf1f
Author: Luca Molteni <volothamp@gmail.com>
Date: Wed Jun 20 10:23:20 2018 +0200
custom delimiter in cassava
commit 694d48e2bc1ada0037b90367c017f3082f68ed45
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jun 10 17:51:54 2018 +0200
Use Text.getContents - remove UTF-8 compatibility library
commit a7ada2cc60033ebdd796ca34cc2ec69a4f387843
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jun 10 17:49:34 2018 +0200
todo list
commit 58ec47d3987909f6bace50e3e647e30dadd5bf03
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jun 10 17:45:22 2018 +0200
CSV test now has unicode characters
commit b7851e94c3f1683b63ec7250a12bcde3b7bed691
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jun 10 16:59:39 2018 +0200
Use decode from Text
commit 79f59fd28ccaca08fcd718fcd8d00b1c1d65d7e1
Author: Luca Molteni <volothamp@gmail.com>
Date: Sun Jun 10 13:28:57 2018 +0200
Use Text and Lazy Bytestring
commit 470c9bcb8dc00669beb4ef0303a1e7d9f7aecc89
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 15:30:22 2018 +0200
Use megaparsec error
commit f978848ba249ef4f67b855bea5d4e549290c205c
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 15:22:07 2018 +0200
Renamed qualify and remove Parsec
commit 152587fde204c43a55798d212e43f37cd3038c2e
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 15:12:36 2018 +0200
Use cassava mega parsec
commit cf281577a3d3a071196484a6fc8485f2ea1f7d67
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 14:01:47 2018 +0200
Removed Data.Vector
commit 1272e8e758369d8cc5778029a705b277355a5029
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 12:16:18 2018 +0200
Removed Parsec ParseError
commit ae07f043135a19307fd65b281ade37a74c76acb2
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 12:06:14 2018 +0200
Type sinonim for ParsecError
commit 8e15b253c11bd1c0c35a7641aeb18aa54e0ba9b0
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 11:16:08 2018 +0200
Replaced with typeclasses
commit 1ed46f9c175603611325f3d377004e4b85f29377
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 11:01:33 2018 +0200
Replaced Text/CSV with Cassava
commit 362f4111b5854145703174b976fc7acbd71b8783
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 10:34:37 2018 +0200
Use cassava parsin instead of Text/CSV
commit 83e678e371618687cf7c15a4e2cfa67f570b6b64
Author: Luca Molteni <volothamp@gmail.com>
Date: Sat Jun 9 08:22:51 2018 +0200
Text CSV error messages
commit f922df71d274beeacab9fb2530b16c97f005cc08
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Jun 8 21:45:20 2018 +0200
Better types
commit edd130781c84790a53bff2283e6041eb8232e7cf
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Jun 8 21:34:59 2018 +0200
Conversion to Text CSV type
commit 0799383214483018ad2d977a3c8022414959c2b2
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Jun 8 16:06:21 2018 +0200
First function with cassava
commit e92aeb151ff527b383ff3d0ced7764e81b71af82
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Jun 8 13:47:34 2018 +0200
Added cassava as dependency
commit 5ea005c558a3939af7e5f0cd735a9b4da931228e
Author: Luca Molteni <volothamp@gmail.com>
Date: Fri Jun 8 13:18:47 2018 +0200
Better .gitignore for multi idea modules
This commit is contained in:
parent
758c1fbc25
commit
23bdac41d9
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,7 +6,7 @@ _*
|
|||||||
# dev stuff
|
# dev stuff
|
||||||
.build
|
.build
|
||||||
.idea
|
.idea
|
||||||
/*.iml
|
*.iml
|
||||||
.shake
|
.shake
|
||||||
.tmp
|
.tmp
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@ -17,7 +17,8 @@ module Hledger.Data.RawOptions (
|
|||||||
maybestringopt,
|
maybestringopt,
|
||||||
listofstringopt,
|
listofstringopt,
|
||||||
intopt,
|
intopt,
|
||||||
maybeintopt
|
maybeintopt,
|
||||||
|
maybecharopt
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -50,6 +51,9 @@ maybestringopt name = maybe Nothing (Just . T.unpack . stripquotes . T.pack) . l
|
|||||||
stringopt :: String -> RawOpts -> String
|
stringopt :: String -> RawOpts -> String
|
||||||
stringopt name = fromMaybe "" . maybestringopt name
|
stringopt name = fromMaybe "" . maybestringopt name
|
||||||
|
|
||||||
|
maybecharopt :: String -> RawOpts -> Maybe Char
|
||||||
|
maybecharopt name rawopts = lookup name rawopts >>= headMay
|
||||||
|
|
||||||
listofstringopt :: String -> RawOpts -> [String]
|
listofstringopt :: String -> RawOpts -> [String]
|
||||||
listofstringopt name rawopts = [v | (k,v) <- rawopts, k==name]
|
listofstringopt name rawopts = [v | (k,v) <- rawopts, k==name]
|
||||||
|
|
||||||
|
|||||||
@ -158,6 +158,7 @@ data InputOpts = InputOpts {
|
|||||||
mformat_ :: Maybe StorageFormat -- ^ a file/storage format to try, unless overridden
|
mformat_ :: Maybe StorageFormat -- ^ a file/storage format to try, unless overridden
|
||||||
-- by a filename prefix. Nothing means try all.
|
-- by a filename prefix. Nothing means try all.
|
||||||
,mrules_file_ :: Maybe FilePath -- ^ a conversion rules file to use (when reading CSV)
|
,mrules_file_ :: Maybe FilePath -- ^ a conversion rules file to use (when reading CSV)
|
||||||
|
,separator_ :: Char -- ^ the separator to use (when reading CSV)
|
||||||
,aliases_ :: [String] -- ^ account name aliases to apply
|
,aliases_ :: [String] -- ^ account name aliases to apply
|
||||||
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
|
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
|
||||||
,ignore_assertions_ :: Bool -- ^ don't check balance assertions
|
,ignore_assertions_ :: Bool -- ^ don't check balance assertions
|
||||||
@ -170,13 +171,14 @@ data InputOpts = InputOpts {
|
|||||||
instance Default InputOpts where def = definputopts
|
instance Default InputOpts where def = definputopts
|
||||||
|
|
||||||
definputopts :: InputOpts
|
definputopts :: InputOpts
|
||||||
definputopts = InputOpts def def def def def def True def def
|
definputopts = InputOpts def def ',' def def def def True def def
|
||||||
|
|
||||||
rawOptsToInputOpts :: RawOpts -> InputOpts
|
rawOptsToInputOpts :: RawOpts -> InputOpts
|
||||||
rawOptsToInputOpts rawopts = InputOpts{
|
rawOptsToInputOpts rawopts = InputOpts{
|
||||||
-- files_ = map (T.unpack . stripquotes . T.pack) $ listofstringopt "file" rawopts
|
-- files_ = map (T.unpack . stripquotes . T.pack) $ listofstringopt "file" rawopts
|
||||||
mformat_ = Nothing
|
mformat_ = Nothing
|
||||||
,mrules_file_ = maybestringopt "rules-file" rawopts
|
,mrules_file_ = maybestringopt "rules-file" rawopts
|
||||||
|
,separator_ = fromMaybe ',' (maybecharopt "separator" rawopts)
|
||||||
,aliases_ = map (T.unpack . stripquotes . T.pack) $ listofstringopt "alias" rawopts
|
,aliases_ = map (T.unpack . stripquotes . T.pack) $ listofstringopt "alias" rawopts
|
||||||
,anon_ = boolopt "anon" rawopts
|
,anon_ = boolopt "anon" rawopts
|
||||||
,ignore_assertions_ = boolopt "ignore-assertions" rawopts
|
,ignore_assertions_ = boolopt "ignore-assertions" rawopts
|
||||||
|
|||||||
@ -18,23 +18,25 @@ module Hledger.Read.CsvReader (
|
|||||||
reader,
|
reader,
|
||||||
-- * Misc.
|
-- * Misc.
|
||||||
CsvRecord,
|
CsvRecord,
|
||||||
|
CSV, Record, Field,
|
||||||
-- rules,
|
-- rules,
|
||||||
rulesFileFor,
|
rulesFileFor,
|
||||||
parseRulesFile,
|
parseRulesFile,
|
||||||
parseAndValidateCsvRules,
|
parseAndValidateCsvRules,
|
||||||
expandIncludes,
|
expandIncludes,
|
||||||
transactionFromCsvRecord,
|
transactionFromCsvRecord,
|
||||||
|
printCSV,
|
||||||
-- * Tests
|
-- * Tests
|
||||||
tests_CsvReader,
|
tests_CsvReader,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
import Prelude ()
|
import Prelude ()
|
||||||
import "base-compat-batteries" Prelude.Compat hiding (getContents)
|
import "base-compat-batteries" Prelude.Compat
|
||||||
import Control.Exception hiding (try)
|
import Control.Exception hiding (try)
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
|
import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
|
||||||
import Data.Char (toLower, isDigit, isSpace)
|
import Data.Char (toLower, isDigit, isSpace, ord)
|
||||||
import "base-compat-batteries" Data.List.Compat
|
import "base-compat-batteries" Data.List.Compat
|
||||||
import Data.List.NonEmpty (fromList)
|
import Data.List.NonEmpty (fromList)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
@ -42,6 +44,7 @@ import Data.Ord
|
|||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Data.Text.IO as T
|
import qualified Data.Text.IO 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)
|
||||||
@ -53,17 +56,28 @@ import System.Locale (defaultTimeLocale)
|
|||||||
import Safe
|
import Safe
|
||||||
import System.Directory (doesFileExist)
|
import System.Directory (doesFileExist)
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
import Text.CSV (parseCSV, CSV)
|
import qualified Data.Csv as Cassava
|
||||||
|
import qualified Data.Csv.Parser.Megaparsec as CassavaMP
|
||||||
|
import qualified Data.ByteString as B
|
||||||
|
import Data.ByteString.Lazy (fromStrict)
|
||||||
|
import Data.Foldable
|
||||||
import Text.Megaparsec hiding (parse)
|
import Text.Megaparsec hiding (parse)
|
||||||
import Text.Megaparsec.Char
|
import Text.Megaparsec.Char
|
||||||
import qualified Text.Parsec as Parsec
|
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
|
import Data.Word
|
||||||
|
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
import Hledger.Utils.UTF8IOCompat (getContents)
|
|
||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos)
|
import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos)
|
||||||
|
|
||||||
|
type CSV = [Record]
|
||||||
|
|
||||||
|
type Record = [Field]
|
||||||
|
|
||||||
|
type Field = String
|
||||||
|
|
||||||
|
data CSVError = CSVError (ParseError Word8 CassavaMP.ConversionError)
|
||||||
|
deriving Show
|
||||||
|
|
||||||
reader :: Reader
|
reader :: Reader
|
||||||
reader = Reader
|
reader = Reader
|
||||||
@ -78,7 +92,8 @@ reader = Reader
|
|||||||
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
|
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
|
||||||
parse iopts f t = do
|
parse iopts f t = do
|
||||||
let rulesfile = mrules_file_ iopts
|
let rulesfile = mrules_file_ iopts
|
||||||
r <- liftIO $ readJournalFromCsv rulesfile f t
|
let separator = separator_ iopts
|
||||||
|
r <- liftIO $ readJournalFromCsv separator rulesfile f t
|
||||||
case r of Left e -> throwError e
|
case r of Left e -> throwError e
|
||||||
Right j -> return $ journalNumberAndTieTransactions j
|
Right j -> return $ journalNumberAndTieTransactions j
|
||||||
-- XXX does not use parseAndFinaliseJournal like the other readers
|
-- XXX does not use parseAndFinaliseJournal like the other readers
|
||||||
@ -92,11 +107,11 @@ parse iopts f t = do
|
|||||||
-- 2. parse the CSV data, or throw a parse error
|
-- 2. parse the CSV data, or throw a parse error
|
||||||
-- 3. convert the CSV records to transactions using the rules
|
-- 3. convert the CSV records to transactions using the rules
|
||||||
-- 4. if the rules file didn't exist, create it with the default rules and filename
|
-- 4. if the rules file didn't exist, create it with the default rules and filename
|
||||||
-- 5. return the transactions as a Journal
|
-- 5. return the transactions as a Journal
|
||||||
-- @
|
-- @
|
||||||
readJournalFromCsv :: Maybe FilePath -> FilePath -> Text -> IO (Either String Journal)
|
readJournalFromCsv :: Char -> Maybe FilePath -> FilePath -> Text -> IO (Either String Journal)
|
||||||
readJournalFromCsv Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin"
|
readJournalFromCsv _ Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin"
|
||||||
readJournalFromCsv mrulesfile csvfile csvdata =
|
readJournalFromCsv separator mrulesfile csvfile csvdata =
|
||||||
handle (\e -> return $ Left $ show (e :: IOException)) $ do
|
handle (\e -> return $ Left $ show (e :: IOException)) $ do
|
||||||
let throwerr = throw.userError
|
let throwerr = throw.userError
|
||||||
|
|
||||||
@ -109,7 +124,7 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
dbg1IO "using conversion rules file" rulesfile
|
dbg1IO "using conversion rules file" rulesfile
|
||||||
liftIO $ (readFilePortably rulesfile >>= expandIncludes (takeDirectory rulesfile))
|
liftIO $ (readFilePortably rulesfile >>= expandIncludes (takeDirectory rulesfile))
|
||||||
else return $ defaultRulesText rulesfile
|
else return $ defaultRulesText rulesfile
|
||||||
rules <- liftIO (runExceptT $ parseAndValidateCsvRules rulesfile rulestext) >>= either throwerr return
|
rules <- liftIO (runExceptT $ parseAndValidateCsvRules rulesfile rulestext) >>= either throwerr return
|
||||||
dbg2IO "rules" rules
|
dbg2IO "rules" rules
|
||||||
|
|
||||||
-- apply skip directive
|
-- apply skip directive
|
||||||
@ -124,17 +139,17 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
records <- (either throwerr id .
|
records <- (either throwerr id .
|
||||||
dbg2 "validateCsv" . validateCsv skip .
|
dbg2 "validateCsv" . validateCsv skip .
|
||||||
dbg2 "parseCsv")
|
dbg2 "parseCsv")
|
||||||
`fmap` parseCsv parsecfilename (T.unpack csvdata)
|
`fmap` parseCsv separator parsecfilename csvdata
|
||||||
dbg1IO "first 3 csv records" $ take 3 records
|
dbg1IO "first 3 csv records" $ take 3 records
|
||||||
|
|
||||||
-- identify header lines
|
-- identify header lines
|
||||||
-- let (headerlines, datalines) = identifyHeaderLines records
|
-- let (headerlines, datalines) = identifyHeaderLines records
|
||||||
-- mfieldnames = lastMay headerlines
|
-- mfieldnames = lastMay headerlines
|
||||||
|
|
||||||
let
|
let
|
||||||
-- convert CSV records to transactions
|
-- convert CSV records to transactions
|
||||||
txns = snd $ mapAccumL
|
txns = snd $ mapAccumL
|
||||||
(\pos r ->
|
(\pos r ->
|
||||||
let
|
let
|
||||||
SourcePos name line col = pos
|
SourcePos name line col = pos
|
||||||
line' = (mkPos . (+1) . unPos) line
|
line' = (mkPos . (+1) . unPos) line
|
||||||
@ -146,16 +161,16 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
|
|
||||||
-- Ensure transactions are ordered chronologically.
|
-- Ensure transactions are ordered chronologically.
|
||||||
-- First, reverse them to get same-date transactions ordered chronologically,
|
-- First, reverse them to get same-date transactions ordered chronologically,
|
||||||
-- if the CSV records seem to be most-recent-first, ie if there's an explicit
|
-- if the CSV records seem to be most-recent-first, ie if there's an explicit
|
||||||
-- "newest-first" directive, or if there's more than one date and the first date
|
-- "newest-first" directive, or if there's more than one date and the first date
|
||||||
-- is more recent than the last.
|
-- is more recent than the last.
|
||||||
txns' =
|
txns' =
|
||||||
(if newestfirst || mseemsnewestfirst == Just True then reverse else id) txns
|
(if newestfirst || mseemsnewestfirst == Just True then reverse else id) txns
|
||||||
where
|
where
|
||||||
newestfirst = dbg3 "newestfirst" $ isJust $ getDirective "newest-first" rules
|
newestfirst = dbg3 "newestfirst" $ isJust $ getDirective "newest-first" rules
|
||||||
mseemsnewestfirst = dbg3 "mseemsnewestfirst" $
|
mseemsnewestfirst = dbg3 "mseemsnewestfirst" $
|
||||||
case nub $ map tdate txns of
|
case nub $ map tdate txns of
|
||||||
ds | length ds > 1 -> Just $ head ds > last ds
|
ds | length ds > 1 -> Just $ head ds > last ds
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
-- Second, sort by date.
|
-- Second, sort by date.
|
||||||
txns'' = sortBy (comparing tdate) txns'
|
txns'' = sortBy (comparing tdate) txns'
|
||||||
@ -166,14 +181,41 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
|
|
||||||
return $ Right nulljournal{jtxns=txns''}
|
return $ Right nulljournal{jtxns=txns''}
|
||||||
|
|
||||||
parseCsv :: FilePath -> String -> IO (Either Parsec.ParseError CSV)
|
parseCsv :: Char -> FilePath -> Text -> IO (Either CSVError CSV)
|
||||||
parseCsv path csvdata =
|
parseCsv separator filePath csvdata =
|
||||||
case path of
|
case filePath of
|
||||||
"-" -> liftM (parseCSV "(stdin)") getContents
|
"-" -> liftM (parseCassava separator "(stdin)") T.getContents
|
||||||
_ -> return $ parseCSV path csvdata
|
_ -> return $ parseCassava separator filePath csvdata
|
||||||
|
|
||||||
|
parseCassava :: Char -> FilePath -> Text -> Either CSVError CSV
|
||||||
|
parseCassava separator path content =
|
||||||
|
case parseResult of
|
||||||
|
Left msg -> Left $ CSVError msg
|
||||||
|
Right a -> Right a
|
||||||
|
where parseResult = fmap parseResultToCsv $ CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path lazyContent
|
||||||
|
lazyContent = fromStrict $ T.encodeUtf8 content
|
||||||
|
|
||||||
|
decodeOptions :: Char -> Cassava.DecodeOptions
|
||||||
|
decodeOptions separator = Cassava.defaultDecodeOptions {
|
||||||
|
Cassava.decDelimiter = fromIntegral (ord separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResultToCsv :: (Foldable t, Functor t) => t (t B.ByteString) -> CSV
|
||||||
|
parseResultToCsv = toListList . unpackFields
|
||||||
|
where
|
||||||
|
toListList = toList . fmap toList
|
||||||
|
unpackFields = (fmap . fmap) (T.unpack . T.decodeUtf8)
|
||||||
|
|
||||||
|
printCSV :: CSV -> String
|
||||||
|
printCSV records = unlined (printRecord `map` records)
|
||||||
|
where printRecord = concat . intersperse "," . map printField
|
||||||
|
printField f = "\"" ++ concatMap escape f ++ "\""
|
||||||
|
escape '"' = "\"\""
|
||||||
|
escape x = [x]
|
||||||
|
unlined = concat . intersperse "\n"
|
||||||
|
|
||||||
-- | Return the cleaned up and validated CSV data (can be empty), or an error.
|
-- | Return the cleaned up and validated CSV data (can be empty), or an error.
|
||||||
validateCsv :: Int -> Either Parsec.ParseError CSV -> Either String [CsvRecord]
|
validateCsv :: Int -> Either CSVError CSV -> Either String [CsvRecord]
|
||||||
validateCsv _ (Left e) = Left $ show e
|
validateCsv _ (Left e) = Left $ show e
|
||||||
validateCsv numhdrlines (Right rs) = validate $ drop numhdrlines $ filternulls rs
|
validateCsv numhdrlines (Right rs) = validate $ drop numhdrlines $ filternulls rs
|
||||||
where
|
where
|
||||||
@ -363,11 +405,11 @@ getDirective directivename = lookup directivename . rdirectives
|
|||||||
instance ShowErrorComponent String where
|
instance ShowErrorComponent String where
|
||||||
showErrorComponent = id
|
showErrorComponent = id
|
||||||
|
|
||||||
-- | An error-throwing action that parses this file's content
|
-- | An error-throwing action that parses this file's content
|
||||||
-- as CSV conversion rules, interpolating any included files first,
|
-- as CSV conversion rules, interpolating any included files first,
|
||||||
-- and runs some extra validation checks.
|
-- and runs some extra validation checks.
|
||||||
parseRulesFile :: FilePath -> ExceptT String IO CsvRules
|
parseRulesFile :: FilePath -> ExceptT String IO CsvRules
|
||||||
parseRulesFile f =
|
parseRulesFile f =
|
||||||
liftIO (readFilePortably f >>= expandIncludes (takeDirectory f)) >>= parseAndValidateCsvRules f
|
liftIO (readFilePortably f >>= expandIncludes (takeDirectory f)) >>= parseAndValidateCsvRules f
|
||||||
|
|
||||||
-- | Inline all files referenced by include directives in this hledger CSV rules text, recursively.
|
-- | Inline all files referenced by include directives in this hledger CSV rules text, recursively.
|
||||||
@ -381,9 +423,9 @@ expandIncludes dir content = mapM (expandLine dir) (T.lines content) >>= return
|
|||||||
where
|
where
|
||||||
f' = dir </> dropWhile isSpace (T.unpack f)
|
f' = dir </> dropWhile isSpace (T.unpack f)
|
||||||
dir' = takeDirectory f'
|
dir' = takeDirectory f'
|
||||||
_ -> return line
|
_ -> return line
|
||||||
|
|
||||||
-- | An error-throwing action that parses this text as CSV conversion rules
|
-- | An error-throwing action that parses this text as CSV conversion rules
|
||||||
-- and runs some extra validation checks. The file path is for error messages.
|
-- and runs some extra validation checks. The file path is for error messages.
|
||||||
parseAndValidateCsvRules :: FilePath -> T.Text -> ExceptT String IO CsvRules
|
parseAndValidateCsvRules :: FilePath -> T.Text -> ExceptT String IO CsvRules
|
||||||
parseAndValidateCsvRules rulesfile s = do
|
parseAndValidateCsvRules rulesfile s = do
|
||||||
@ -513,8 +555,8 @@ journalfieldnamep = do
|
|||||||
lift (dbgparse 2 "trying journalfieldnamep")
|
lift (dbgparse 2 "trying journalfieldnamep")
|
||||||
T.unpack <$> choiceInState (map (lift . string . T.pack) journalfieldnames)
|
T.unpack <$> choiceInState (map (lift . string . T.pack) journalfieldnames)
|
||||||
|
|
||||||
-- Transaction fields and pseudo fields for CSV conversion.
|
-- Transaction fields and pseudo fields for CSV conversion.
|
||||||
-- Names must precede any other name they contain, for the parser
|
-- Names must precede any other name they contain, for the parser
|
||||||
-- (amount-in before amount; date2 before date). TODO: fix
|
-- (amount-in before amount; date2 before date). TODO: fix
|
||||||
journalfieldnames = [
|
journalfieldnames = [
|
||||||
"account1"
|
"account1"
|
||||||
@ -684,7 +726,7 @@ transactionFromCsvRecord sourcepos rules record = t
|
|||||||
account1 = T.pack $ maybe "" render (mfieldtemplate "account1") `or` defaccount1
|
account1 = T.pack $ maybe "" render (mfieldtemplate "account1") `or` defaccount1
|
||||||
account2 = T.pack $ maybe "" render (mfieldtemplate "account2") `or` defaccount2
|
account2 = T.pack $ maybe "" render (mfieldtemplate "account2") `or` defaccount2
|
||||||
balance = maybe Nothing (parsebalance.render) $ mfieldtemplate "balance"
|
balance = maybe Nothing (parsebalance.render) $ mfieldtemplate "balance"
|
||||||
parsebalance str
|
parsebalance str
|
||||||
| all isSpace str = Nothing
|
| all isSpace str = Nothing
|
||||||
| otherwise = Just $ (either (balanceerror str) id $ runParser (evalStateT (amountp <* eof) mempty) "" $ T.pack $ (currency++) $ simplifySign str, nullsourcepos)
|
| otherwise = Just $ (either (balanceerror str) id $ runParser (evalStateT (amountp <* eof) mempty) "" $ T.pack $ (currency++) $ simplifySign str, nullsourcepos)
|
||||||
balanceerror str err = error' $ unlines
|
balanceerror str err = error' $ unlines
|
||||||
@ -738,7 +780,7 @@ getAmountStr rules record =
|
|||||||
type CsvAmountString = String
|
type CsvAmountString = String
|
||||||
|
|
||||||
-- | Canonicalise the sign in a CSV amount string.
|
-- | Canonicalise the sign in a CSV amount string.
|
||||||
-- Such strings can have a minus sign, negating parentheses,
|
-- Such strings can have a minus sign, negating parentheses,
|
||||||
-- or any two of these (which cancels out).
|
-- or any two of these (which cancels out).
|
||||||
--
|
--
|
||||||
-- >>> simplifySign "1"
|
-- >>> simplifySign "1"
|
||||||
@ -840,15 +882,15 @@ tests_CsvReader = tests "CsvReader" [
|
|||||||
,tests "rulesp" [
|
,tests "rulesp" [
|
||||||
test "trailing comments" $
|
test "trailing comments" $
|
||||||
parseWithState' rules rulesp "skip\n# \n#\n" `is` Right rules{rdirectives = [("skip","")]}
|
parseWithState' rules rulesp "skip\n# \n#\n" `is` Right rules{rdirectives = [("skip","")]}
|
||||||
|
|
||||||
,test "trailing blank lines" $
|
,test "trailing blank lines" $
|
||||||
parseWithState' rules rulesp "skip\n\n \n" `is` (Right rules{rdirectives = [("skip","")]})
|
parseWithState' rules rulesp "skip\n\n \n" `is` (Right rules{rdirectives = [("skip","")]})
|
||||||
|
|
||||||
,test "no final newline" $
|
,test "no final newline" $
|
||||||
parseWithState' rules rulesp "skip" `is` (Right rules{rdirectives=[("skip","")]})
|
parseWithState' rules rulesp "skip" `is` (Right rules{rdirectives=[("skip","")]})
|
||||||
|
|
||||||
,test "assignment with empty value" $
|
,test "assignment with empty value" $
|
||||||
parseWithState' rules rulesp "account1 \nif foo\n account2 foo\n" `is`
|
parseWithState' rules rulesp "account1 \nif foo\n account2 foo\n" `is`
|
||||||
(Right rules{rassignments = [("account1","")], rconditionalblocks = [([["foo"]],[("account2","foo")])]})
|
(Right rules{rassignments = [("account1","")], rconditionalblocks = [([["foo"]],[("account2","foo")])]})
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
--
|
--
|
||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
--
|
--
|
||||||
-- hash: 642c6b4607959713188c82341f1050872ec6111a64f8e4b4cc1c1630da585baf
|
-- hash: 7d48cc897fb582a2600c3f3405a5463b853316f4a9fae370f0a74c46576a6198
|
||||||
|
|
||||||
name: hledger-lib
|
name: hledger-lib
|
||||||
version: 1.10.99
|
version: 1.10.99
|
||||||
@ -111,9 +111,10 @@ library
|
|||||||
, blaze-markup >=0.5.1
|
, blaze-markup >=0.5.1
|
||||||
, bytestring
|
, bytestring
|
||||||
, call-stack
|
, call-stack
|
||||||
|
, cassava
|
||||||
|
, cassava-megaparsec
|
||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, deepseq
|
, deepseq
|
||||||
, directory
|
, directory
|
||||||
@ -209,9 +210,10 @@ test-suite doctests
|
|||||||
, blaze-markup >=0.5.1
|
, blaze-markup >=0.5.1
|
||||||
, bytestring
|
, bytestring
|
||||||
, call-stack
|
, call-stack
|
||||||
|
, cassava
|
||||||
|
, cassava-megaparsec
|
||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, deepseq
|
, deepseq
|
||||||
, directory
|
, directory
|
||||||
@ -308,9 +310,10 @@ test-suite easytests
|
|||||||
, blaze-markup >=0.5.1
|
, blaze-markup >=0.5.1
|
||||||
, bytestring
|
, bytestring
|
||||||
, call-stack
|
, call-stack
|
||||||
|
, cassava
|
||||||
|
, cassava-megaparsec
|
||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, deepseq
|
, deepseq
|
||||||
, directory
|
, directory
|
||||||
|
|||||||
@ -48,7 +48,8 @@ dependencies:
|
|||||||
- call-stack
|
- call-stack
|
||||||
- cmdargs >=0.10
|
- cmdargs >=0.10
|
||||||
- containers
|
- containers
|
||||||
- csv
|
- cassava
|
||||||
|
- cassava-megaparsec
|
||||||
- data-default >=0.5
|
- data-default >=0.5
|
||||||
- Decimal
|
- Decimal
|
||||||
- deepseq
|
- deepseq
|
||||||
|
|||||||
@ -120,6 +120,7 @@ inputflags :: [Flag RawOpts]
|
|||||||
inputflags = [
|
inputflags = [
|
||||||
flagReq ["file","f"] (\s opts -> Right $ setopt "file" s opts) "FILE" "use a different input file. For stdin, use - (default: $LEDGER_FILE or $HOME/.hledger.journal)"
|
flagReq ["file","f"] (\s opts -> Right $ setopt "file" s opts) "FILE" "use a different input file. For stdin, use - (default: $LEDGER_FILE or $HOME/.hledger.journal)"
|
||||||
,flagReq ["rules-file"] (\s opts -> Right $ setopt "rules-file" s opts) "RFILE" "CSV conversion rules file (default: FILE.rules)"
|
,flagReq ["rules-file"] (\s opts -> Right $ setopt "rules-file" s opts) "RFILE" "CSV conversion rules file (default: FILE.rules)"
|
||||||
|
,flagReq ["separator"] (\s opts -> Right $ setopt "separator" s opts) "SEPARATOR" "CSV separator (default: ,)"
|
||||||
,flagReq ["alias"] (\s opts -> Right $ setopt "alias" s opts) "OLD=NEW" "rename accounts named OLD to NEW"
|
,flagReq ["alias"] (\s opts -> Right $ setopt "alias" s opts) "OLD=NEW" "rename accounts named OLD to NEW"
|
||||||
,flagNone ["anon"] (setboolopt "anon") "anonymize accounts and payees"
|
,flagNone ["anon"] (setboolopt "anon") "anonymize accounts and payees"
|
||||||
,flagReq ["pivot"] (\s opts -> Right $ setopt "pivot" s opts) "TAGNAME" "use some other field/tag for account names"
|
,flagReq ["pivot"] (\s opts -> Right $ setopt "pivot" s opts) "TAGNAME" "use some other field/tag for account names"
|
||||||
|
|||||||
@ -258,14 +258,15 @@ import qualified Data.Text as T
|
|||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
import System.Console.CmdArgs.Explicit as C
|
import System.Console.CmdArgs.Explicit as C
|
||||||
import Lucid as L
|
import Lucid as L
|
||||||
import Text.CSV
|
import Test.HUnit()
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
import Text.Tabular as T
|
import Text.Tabular as T
|
||||||
--import Text.Tabular.AsciiWide
|
--import Text.Tabular.AsciiWide
|
||||||
|
|
||||||
import Hledger
|
import Hledger
|
||||||
import Hledger.Cli.CliOptions
|
import Hledger.Cli.CliOptions
|
||||||
import Hledger.Cli.Utils
|
import Hledger.Cli.Utils
|
||||||
|
import Hledger.Read.CsvReader (CSV, printCSV)
|
||||||
|
|
||||||
|
|
||||||
-- | Command line options for this command.
|
-- | Command line options for this command.
|
||||||
|
|||||||
@ -17,7 +17,8 @@ where
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import System.Console.CmdArgs.Explicit
|
import System.Console.CmdArgs.Explicit
|
||||||
import Text.CSV
|
import Test.HUnit()
|
||||||
|
import Hledger.Read.CsvReader (CSV, printCSV)
|
||||||
|
|
||||||
import Hledger
|
import Hledger
|
||||||
import Hledger.Cli.CliOptions
|
import Hledger.Cli.CliOptions
|
||||||
|
|||||||
@ -20,13 +20,13 @@ import Data.Maybe
|
|||||||
-- import Data.Text (Text)
|
-- import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import System.Console.CmdArgs.Explicit
|
import System.Console.CmdArgs.Explicit
|
||||||
import Text.CSV
|
import Hledger.Read.CsvReader (CSV, Record, printCSV)
|
||||||
|
import Test.HUnit()
|
||||||
|
|
||||||
import Hledger
|
import Hledger
|
||||||
import Hledger.Cli.CliOptions
|
import Hledger.Cli.CliOptions
|
||||||
import Hledger.Cli.Utils
|
import Hledger.Cli.Utils
|
||||||
|
|
||||||
|
|
||||||
registermode = (defCommandMode $ ["register"] ++ aliases) {
|
registermode = (defCommandMode $ ["register"] ++ aliases) {
|
||||||
modeHelp = "show postings and running total. With --date2, show and sort by secondary date instead." `withAliases` aliases
|
modeHelp = "show postings and running total. With --date2, show and sort by secondary date instead." `withAliases` aliases
|
||||||
,modeGroupFlags = Group {
|
,modeGroupFlags = Group {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import Data.Maybe (fromMaybe)
|
|||||||
import qualified Data.Text as TS
|
import qualified Data.Text as TS
|
||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
import System.Console.CmdArgs.Explicit as C
|
import System.Console.CmdArgs.Explicit as C
|
||||||
import Text.CSV
|
import Hledger.Read.CsvReader (CSV, printCSV)
|
||||||
import Lucid as L
|
import Lucid as L
|
||||||
import Text.Tabular as T
|
import Text.Tabular as T
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
--
|
--
|
||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
--
|
--
|
||||||
-- hash: 3be7e8745a826dbfc9d0007b9b37c3962486573614267365e6dafb8f7079ece6
|
-- hash: 670748bbdefdd5950fbc676e79a7c3924edbe21ac333141915b5509e799fa071
|
||||||
|
|
||||||
name: hledger
|
name: hledger
|
||||||
version: 1.10.99
|
version: 1.10.99
|
||||||
@ -120,7 +120,6 @@ library
|
|||||||
, bytestring
|
, bytestring
|
||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, directory
|
, directory
|
||||||
, easytest
|
, easytest
|
||||||
@ -173,7 +172,6 @@ executable hledger
|
|||||||
, bytestring
|
, bytestring
|
||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, directory
|
, directory
|
||||||
, easytest
|
, easytest
|
||||||
@ -228,7 +226,6 @@ test-suite test
|
|||||||
, bytestring
|
, bytestring
|
||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, directory
|
, directory
|
||||||
, easytest
|
, easytest
|
||||||
@ -283,7 +280,6 @@ benchmark bench
|
|||||||
, cmdargs >=0.10
|
, cmdargs >=0.10
|
||||||
, containers
|
, containers
|
||||||
, criterion
|
, criterion
|
||||||
, csv
|
|
||||||
, data-default >=0.5
|
, data-default >=0.5
|
||||||
, directory
|
, directory
|
||||||
, easytest
|
, easytest
|
||||||
|
|||||||
@ -85,7 +85,6 @@ dependencies:
|
|||||||
- bytestring
|
- bytestring
|
||||||
- cmdargs >=0.10
|
- cmdargs >=0.10
|
||||||
- containers
|
- containers
|
||||||
- csv
|
|
||||||
- data-default >=0.5
|
- data-default >=0.5
|
||||||
- Decimal
|
- Decimal
|
||||||
- directory
|
- directory
|
||||||
|
|||||||
@ -9,7 +9,8 @@ packages:
|
|||||||
- hledger-web
|
- hledger-web
|
||||||
- hledger-api
|
- hledger-api
|
||||||
|
|
||||||
#extra-deps:
|
extra-deps:
|
||||||
|
- cassava-megaparsec-1.0.0
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
pure: false
|
pure: false
|
||||||
|
|||||||
@ -17,20 +17,21 @@
|
|||||||
# 2. reading CSV with in-field and out-field
|
# 2. reading CSV with in-field and out-field
|
||||||
printf 'account1 Assets:MyAccount\ndate %%1\ndate-format %%d/%%Y/%%m\ndescription %%2\namount-in %%3\namount-out %%4\ncurrency $\n' >t.$$.csv.rules ; hledger -f csv:- --rules-file t.$$.csv.rules print && rm -rf t.$$.csv.rules
|
printf 'account1 Assets:MyAccount\ndate %%1\ndate-format %%d/%%Y/%%m\ndescription %%2\namount-in %%3\namount-out %%4\ncurrency $\n' >t.$$.csv.rules ; hledger -f csv:- --rules-file t.$$.csv.rules print && rm -rf t.$$.csv.rules
|
||||||
<<<
|
<<<
|
||||||
10/2009/09,Flubber Co,50,
|
10/2009/09,Flubber Co🎅,50,
|
||||||
11/2009/09,Flubber Co,,50
|
11/2009/09,Flubber Co🎅,,50
|
||||||
>>>
|
>>>
|
||||||
2009/09/10 Flubber Co
|
2009/09/10 Flubber Co🎅
|
||||||
Assets:MyAccount $50
|
Assets:MyAccount $50
|
||||||
income:unknown $-50
|
income:unknown $-50
|
||||||
|
|
||||||
2009/09/11 Flubber Co
|
2009/09/11 Flubber Co🎅
|
||||||
Assets:MyAccount $-50
|
Assets:MyAccount $-50
|
||||||
expenses:unknown $50
|
expenses:unknown $50
|
||||||
|
|
||||||
>>>2
|
>>>2
|
||||||
>>>=0
|
>>>=0
|
||||||
|
|
||||||
|
|
||||||
# 3. handle conditions assigning multiple fields
|
# 3. handle conditions assigning multiple fields
|
||||||
printf 'fields date, description, amount\ndate-format %%d/%%Y/%%m\ncurrency $\naccount1 assets:myacct\nif Flubber\n account2 acct\n comment cmt' >t.$$.csv.rules; printf '10/2009/09,Flubber Co,50\n' | hledger -f csv:- --rules-file t.$$.csv.rules print && rm -rf t.$$.csv.rules
|
printf 'fields date, description, amount\ndate-format %%d/%%Y/%%m\ncurrency $\naccount1 assets:myacct\nif Flubber\n account2 acct\n comment cmt' >t.$$.csv.rules; printf '10/2009/09,Flubber Co,50\n' | hledger -f csv:- --rules-file t.$$.csv.rules print && rm -rf t.$$.csv.rules
|
||||||
>>>
|
>>>
|
||||||
@ -92,3 +93,20 @@
|
|||||||
|
|
||||||
>>>2
|
>>>2
|
||||||
>>>=0
|
>>>=0
|
||||||
|
|
||||||
|
# 8. reading CSV with custom separator
|
||||||
|
printf 'account1 Assets:MyAccount\ndate %%1\ndate-format %%d/%%Y/%%m\ndescription %%2\namount-in %%3\namount-out %%4\ncurrency $\n' >t.$$.csv.rules ; hledger --separator ';' -f csv:- --rules-file t.$$.csv.rules print && rm -rf t.$$.csv.rules
|
||||||
|
<<<
|
||||||
|
10/2009/09;Flubber Co🎅;50;
|
||||||
|
11/2009/09;Flubber Co🎅;;50
|
||||||
|
>>>
|
||||||
|
2009/09/10 Flubber Co🎅
|
||||||
|
Assets:MyAccount $50
|
||||||
|
income:unknown $-50
|
||||||
|
|
||||||
|
2009/09/11 Flubber Co🎅
|
||||||
|
Assets:MyAccount $-50
|
||||||
|
expenses:unknown $50
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
Loading…
Reference in New Issue
Block a user