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