lib: textification: parse stream
10% more allocation, but 35% lower maximum residency, and slightly quicker. hledger -f data/100x100x10.journal stats <<ghc: 39327768 bytes, 77 GCs, 196834/269496 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.010 elapsed), 0.020 MUT (0.092 elapsed), 0.014 GC (0.119 elapsed) :ghc>> <<ghc: 42842136 bytes, 84 GCs, 194010/270912 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.009 elapsed), 0.016 MUT (0.029 elapsed), 0.012 GC (0.120 elapsed) :ghc>> hledger -f data/1000x1000x10.journal stats <<ghc: 314291440 bytes, 612 GCs, 2070776/6628048 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.000 elapsed), 0.128 MUT (0.144 elapsed), 0.059 GC (0.070 elapsed) :ghc>> <<ghc: 349558872 bytes, 681 GCs, 1397597/4106384 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.004 elapsed), 0.124 MUT (0.133 elapsed), 0.047 GC (0.053 elapsed) :ghc>> hledger -f data/10000x1000x10.journal stats <<ghc: 3070026824 bytes, 5973 GCs, 12698030/62951784 avg/max bytes residency (10 samples), 124M in use, 0.000 INIT (0.002 elapsed), 1.268 MUT (1.354 elapsed), 0.514 GC (0.587 elapsed) :ghc>> <<ghc: 3424013128 bytes, 6658 GCs, 11405501/41071624 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.001 elapsed), 1.343 MUT (1.406 elapsed), 0.511 GC (0.573 elapsed) :ghc>> hledger -f data/100000x1000x10.journal stats <<ghc: 30753387392 bytes, 59811 GCs, 117615462/666703600 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.000 elapsed), 12.068 MUT (12.238 elapsed), 6.015 GC (7.190 elapsed) :ghc>> <<ghc: 34306530696 bytes, 66727 GCs, 76806196/414629312 avg/max bytes residency (14 samples), 1009M in use, 0.000 INIT (0.010 elapsed), 14.357 MUT (16.370 elapsed), 5.298 GC (6.534 elapsed) :ghc>>
This commit is contained in:
		
							parent
							
								
									58c3362908
								
							
						
					
					
						commit
						c89c33b36e
					
				| @ -579,14 +579,14 @@ and maybe some others: | |||||||
| Returns a SmartDate, to be converted to a full date later (see fixSmartDate). | Returns a SmartDate, to be converted to a full date later (see fixSmartDate). | ||||||
| Assumes any text in the parse stream has been lowercased. | Assumes any text in the parse stream has been lowercased. | ||||||
| -} | -} | ||||||
| smartdate :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | smartdate :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| smartdate = do | smartdate = do | ||||||
|   -- XXX maybe obscures date errors ? see ledgerdate |   -- XXX maybe obscures date errors ? see ledgerdate | ||||||
|   (y,m,d) <- choice' [yyyymmdd, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing] |   (y,m,d) <- choice' [yyyymmdd, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing] | ||||||
|   return (y,m,d) |   return (y,m,d) | ||||||
| 
 | 
 | ||||||
| -- | Like smartdate, but there must be nothing other than whitespace after the date. | -- | Like smartdate, but there must be nothing other than whitespace after the date. | ||||||
| smartdateonly :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | smartdateonly :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| smartdateonly = do | smartdateonly = do | ||||||
|   d <- smartdate |   d <- smartdate | ||||||
|   many spacenonewline |   many spacenonewline | ||||||
| @ -594,7 +594,7 @@ smartdateonly = do | |||||||
|   return d |   return d | ||||||
| 
 | 
 | ||||||
| datesepchars = "/-." | datesepchars = "/-." | ||||||
| datesepchar :: Stream [Char] m Char => ParsecT [Char] st m Char | datesepchar :: Stream s m Char => ParsecT s st m Char | ||||||
| datesepchar = oneOf datesepchars | datesepchar = oneOf datesepchars | ||||||
| 
 | 
 | ||||||
| validYear, validMonth, validDay :: String -> Bool | validYear, validMonth, validDay :: String -> Bool | ||||||
| @ -607,7 +607,7 @@ failIfInvalidYear s  = unless (validYear s)  $ fail $ "bad year number: " ++ s | |||||||
| failIfInvalidMonth s = unless (validMonth s) $ fail $ "bad month number: " ++ s | failIfInvalidMonth s = unless (validMonth s) $ fail $ "bad month number: " ++ s | ||||||
| failIfInvalidDay s   = unless (validDay s)   $ fail $ "bad day number: " ++ s | failIfInvalidDay s   = unless (validDay s)   $ fail $ "bad day number: " ++ s | ||||||
| 
 | 
 | ||||||
| yyyymmdd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | yyyymmdd :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| yyyymmdd = do | yyyymmdd = do | ||||||
|   y <- count 4 digit |   y <- count 4 digit | ||||||
|   m <- count 2 digit |   m <- count 2 digit | ||||||
| @ -616,7 +616,7 @@ yyyymmdd = do | |||||||
|   failIfInvalidDay d |   failIfInvalidDay d | ||||||
|   return (y,m,d) |   return (y,m,d) | ||||||
| 
 | 
 | ||||||
| ymd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | ymd :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| ymd = do | ymd = do | ||||||
|   y <- many1 digit |   y <- many1 digit | ||||||
|   failIfInvalidYear y |   failIfInvalidYear y | ||||||
| @ -628,7 +628,7 @@ ymd = do | |||||||
|   failIfInvalidDay d |   failIfInvalidDay d | ||||||
|   return $ (y,m,d) |   return $ (y,m,d) | ||||||
| 
 | 
 | ||||||
| ym :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | ym :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| ym = do | ym = do | ||||||
|   y <- many1 digit |   y <- many1 digit | ||||||
|   failIfInvalidYear y |   failIfInvalidYear y | ||||||
| @ -637,19 +637,19 @@ ym = do | |||||||
|   failIfInvalidMonth m |   failIfInvalidMonth m | ||||||
|   return (y,m,"") |   return (y,m,"") | ||||||
| 
 | 
 | ||||||
| y :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | y :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| y = do | y = do | ||||||
|   y <- many1 digit |   y <- many1 digit | ||||||
|   failIfInvalidYear y |   failIfInvalidYear y | ||||||
|   return (y,"","") |   return (y,"","") | ||||||
| 
 | 
 | ||||||
| d :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | d :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| d = do | d = do | ||||||
|   d <- many1 digit |   d <- many1 digit | ||||||
|   failIfInvalidDay d |   failIfInvalidDay d | ||||||
|   return ("","",d) |   return ("","",d) | ||||||
| 
 | 
 | ||||||
| md :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | md :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| md = do | md = do | ||||||
|   m <- many1 digit |   m <- many1 digit | ||||||
|   failIfInvalidMonth m |   failIfInvalidMonth m | ||||||
| @ -667,24 +667,24 @@ monthabbrevs   = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","n | |||||||
| monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months | monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months | ||||||
| monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs | monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs | ||||||
| 
 | 
 | ||||||
| month :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | month :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| month = do | month = do | ||||||
|   m <- choice $ map (try . string) months |   m <- choice $ map (try . string) months | ||||||
|   let i = monthIndex m |   let i = monthIndex m | ||||||
|   return ("",show i,"") |   return ("",show i,"") | ||||||
| 
 | 
 | ||||||
| mon :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | mon :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| mon = do | mon = do | ||||||
|   m <- choice $ map (try . string) monthabbrevs |   m <- choice $ map (try . string) monthabbrevs | ||||||
|   let i = monIndex m |   let i = monIndex m | ||||||
|   return ("",show i,"") |   return ("",show i,"") | ||||||
| 
 | 
 | ||||||
| today,yesterday,tomorrow :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | today,yesterday,tomorrow :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| today     = string "today"     >> return ("","","today") | today     = string "today"     >> return ("","","today") | ||||||
| yesterday = string "yesterday" >> return ("","","yesterday") | yesterday = string "yesterday" >> return ("","","yesterday") | ||||||
| tomorrow  = string "tomorrow"  >> return ("","","tomorrow") | tomorrow  = string "tomorrow"  >> return ("","","tomorrow") | ||||||
| 
 | 
 | ||||||
| lastthisnextthing :: Stream [Char] m Char => ParsecT [Char] st m SmartDate | lastthisnextthing :: Stream s m Char => ParsecT s st m SmartDate | ||||||
| lastthisnextthing = do | lastthisnextthing = do | ||||||
|   r <- choice [ |   r <- choice [ | ||||||
|         string "last" |         string "last" | ||||||
| @ -716,7 +716,7 @@ lastthisnextthing = do | |||||||
| -- Right (Days 1,DateSpan 2008/08/01-) | -- Right (Days 1,DateSpan 2008/08/01-) | ||||||
| -- >>> p "every week to 2009" | -- >>> p "every week to 2009" | ||||||
| -- Right (Weeks 1,DateSpan -2008/12/31) | -- Right (Weeks 1,DateSpan -2008/12/31) | ||||||
| periodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan) | periodexpr :: Stream s m Char => Day -> ParsecT s st m (Interval, DateSpan) | ||||||
| periodexpr rdate = choice $ map try [ | periodexpr rdate = choice $ map try [ | ||||||
|                     intervalanddateperiodexpr rdate, |                     intervalanddateperiodexpr rdate, | ||||||
|                     intervalperiodexpr, |                     intervalperiodexpr, | ||||||
| @ -724,7 +724,7 @@ periodexpr rdate = choice $ map try [ | |||||||
|                     (return (NoInterval,DateSpan Nothing Nothing)) |                     (return (NoInterval,DateSpan Nothing Nothing)) | ||||||
|                    ] |                    ] | ||||||
| 
 | 
 | ||||||
| intervalanddateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan) | intervalanddateperiodexpr :: Stream s m Char => Day -> ParsecT s st m (Interval, DateSpan) | ||||||
| intervalanddateperiodexpr rdate = do | intervalanddateperiodexpr rdate = do | ||||||
|   many spacenonewline |   many spacenonewline | ||||||
|   i <- reportinginterval |   i <- reportinginterval | ||||||
| @ -732,20 +732,20 @@ intervalanddateperiodexpr rdate = do | |||||||
|   s <- periodexprdatespan rdate |   s <- periodexprdatespan rdate | ||||||
|   return (i,s) |   return (i,s) | ||||||
| 
 | 
 | ||||||
| intervalperiodexpr :: Stream [Char] m Char => ParsecT [Char] st m (Interval, DateSpan) | intervalperiodexpr :: Stream s m Char => ParsecT s st m (Interval, DateSpan) | ||||||
| intervalperiodexpr = do | intervalperiodexpr = do | ||||||
|   many spacenonewline |   many spacenonewline | ||||||
|   i <- reportinginterval |   i <- reportinginterval | ||||||
|   return (i, DateSpan Nothing Nothing) |   return (i, DateSpan Nothing Nothing) | ||||||
| 
 | 
 | ||||||
| dateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan) | dateperiodexpr :: Stream s m Char => Day -> ParsecT s st m (Interval, DateSpan) | ||||||
| dateperiodexpr rdate = do | dateperiodexpr rdate = do | ||||||
|   many spacenonewline |   many spacenonewline | ||||||
|   s <- periodexprdatespan rdate |   s <- periodexprdatespan rdate | ||||||
|   return (NoInterval, s) |   return (NoInterval, s) | ||||||
| 
 | 
 | ||||||
| -- Parse a reporting interval. | -- Parse a reporting interval. | ||||||
| reportinginterval :: Stream [Char] m Char => ParsecT [Char] st m Interval | reportinginterval :: Stream s m Char => ParsecT s st m Interval | ||||||
| reportinginterval = choice' [ | reportinginterval = choice' [ | ||||||
|                        tryinterval "day"     "daily"     Days, |                        tryinterval "day"     "daily"     Days, | ||||||
|                        tryinterval "week"    "weekly"    Weeks, |                        tryinterval "week"    "weekly"    Weeks, | ||||||
| @ -785,7 +785,7 @@ reportinginterval = choice' [ | |||||||
|       thsuffix = choice' $ map string ["st","nd","rd","th"] |       thsuffix = choice' $ map string ["st","nd","rd","th"] | ||||||
| 
 | 
 | ||||||
|       -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days". |       -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days". | ||||||
|       tryinterval :: Stream [Char] m Char => String -> String -> (Int -> Interval) -> ParsecT [Char] st m Interval |       tryinterval :: Stream s m Char => String -> String -> (Int -> Interval) -> ParsecT s st m Interval | ||||||
|       tryinterval singular compact intcons = |       tryinterval singular compact intcons = | ||||||
|           choice' [ |           choice' [ | ||||||
|            do string compact |            do string compact | ||||||
| @ -803,7 +803,7 @@ reportinginterval = choice' [ | |||||||
|            ] |            ] | ||||||
|           where plural = singular ++ "s" |           where plural = singular ++ "s" | ||||||
| 
 | 
 | ||||||
| periodexprdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan | periodexprdatespan :: Stream s m Char => Day -> ParsecT s st m DateSpan | ||||||
| periodexprdatespan rdate = choice $ map try [ | periodexprdatespan rdate = choice $ map try [ | ||||||
|                             doubledatespan rdate, |                             doubledatespan rdate, | ||||||
|                             fromdatespan rdate, |                             fromdatespan rdate, | ||||||
| @ -811,7 +811,7 @@ periodexprdatespan rdate = choice $ map try [ | |||||||
|                             justdatespan rdate |                             justdatespan rdate | ||||||
|                            ] |                            ] | ||||||
| 
 | 
 | ||||||
| doubledatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan | doubledatespan :: Stream s m Char => Day -> ParsecT s st m DateSpan | ||||||
| doubledatespan rdate = do | doubledatespan rdate = do | ||||||
|   optional (string "from" >> many spacenonewline) |   optional (string "from" >> many spacenonewline) | ||||||
|   b <- smartdate |   b <- smartdate | ||||||
| @ -820,7 +820,7 @@ doubledatespan rdate = do | |||||||
|   e <- smartdate |   e <- smartdate | ||||||
|   return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e) |   return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e) | ||||||
| 
 | 
 | ||||||
| fromdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan | fromdatespan :: Stream s m Char => Day -> ParsecT s st m DateSpan | ||||||
| fromdatespan rdate = do | fromdatespan rdate = do | ||||||
|   b <- choice [ |   b <- choice [ | ||||||
|     do |     do | ||||||
| @ -834,13 +834,13 @@ fromdatespan rdate = do | |||||||
|     ] |     ] | ||||||
|   return $ DateSpan (Just $ fixSmartDate rdate b) Nothing |   return $ DateSpan (Just $ fixSmartDate rdate b) Nothing | ||||||
| 
 | 
 | ||||||
| todatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan | todatespan :: Stream s m Char => Day -> ParsecT s st m DateSpan | ||||||
| todatespan rdate = do | todatespan rdate = do | ||||||
|   choice [string "to", string "-"] >> many spacenonewline |   choice [string "to", string "-"] >> many spacenonewline | ||||||
|   e <- smartdate |   e <- smartdate | ||||||
|   return $ DateSpan Nothing (Just $ fixSmartDate rdate e) |   return $ DateSpan Nothing (Just $ fixSmartDate rdate e) | ||||||
| 
 | 
 | ||||||
| justdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan | justdatespan :: Stream s m Char => Day -> ParsecT s st m DateSpan | ||||||
| justdatespan rdate = do | justdatespan rdate = do | ||||||
|   optional (string "in" >> many spacenonewline) |   optional (string "in" >> many spacenonewline) | ||||||
|   d <- smartdate |   d <- smartdate | ||||||
|  | |||||||
| @ -277,9 +277,9 @@ data Reader = Reader { | |||||||
|      -- name of the format this reader handles |      -- name of the format this reader handles | ||||||
|      rFormat   :: StorageFormat |      rFormat   :: StorageFormat | ||||||
|      -- quickly check if this reader can probably handle the given file path and file content |      -- quickly check if this reader can probably handle the given file path and file content | ||||||
|     ,rDetector :: FilePath -> String -> Bool |     ,rDetector :: FilePath -> Text -> Bool | ||||||
|      -- parse the given string, using the given parse rules file if any, returning a journal or error aware of the given file path |      -- parse the given string, using the given parse rules file if any, returning a journal or error aware of the given file path | ||||||
|     ,rParser   :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal |     ,rParser   :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| instance Show Reader where show r = rFormat r ++ " reader" | instance Show Reader where show r = rFormat r ++ " reader" | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| {-# LANGUAGE ScopedTypeVariables #-} |  | ||||||
| {-| | {-| | ||||||
| 
 | 
 | ||||||
| This is the entry point to hledger's reading system, which can read | This is the entry point to hledger's reading system, which can read | ||||||
| @ -8,6 +7,8 @@ to import modules below this one. | |||||||
| 
 | 
 | ||||||
| -} | -} | ||||||
| 
 | 
 | ||||||
|  | {-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-} | ||||||
|  | 
 | ||||||
| module Hledger.Read | module Hledger.Read | ||||||
|   ( |   ( | ||||||
|        module Hledger.Read.Common, |        module Hledger.Read.Common, | ||||||
| @ -39,11 +40,13 @@ import qualified Control.Exception as C | |||||||
| import Control.Monad.Except | import Control.Monad.Except | ||||||
| import Data.List | import Data.List | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
|  | import Data.Text (Text) | ||||||
|  | import qualified Data.Text as T | ||||||
| import System.Directory (doesFileExist, getHomeDirectory) | import System.Directory (doesFileExist, getHomeDirectory) | ||||||
| import System.Environment (getEnv) | import System.Environment (getEnv) | ||||||
| import System.Exit (exitFailure) | import System.Exit (exitFailure) | ||||||
| import System.FilePath ((</>)) | import System.FilePath ((</>)) | ||||||
| import System.IO (IOMode(..), openFile, stdin, stderr, hSetNewlineMode, universalNewlineMode) | import System.IO (stderr) | ||||||
| import Test.HUnit | import Test.HUnit | ||||||
| import Text.Printf | import Text.Printf | ||||||
| 
 | 
 | ||||||
| @ -56,7 +59,7 @@ import Hledger.Read.TimeclockReader as TimeclockReader | |||||||
| import Hledger.Read.CsvReader as CsvReader | import Hledger.Read.CsvReader as CsvReader | ||||||
| import Hledger.Utils | import Hledger.Utils | ||||||
| import Prelude hiding (getContents, writeFile) | import Prelude hiding (getContents, writeFile) | ||||||
| import Hledger.Utils.UTF8IOCompat (hGetContents, writeFile) | import Hledger.Utils.UTF8IOCompat (writeFile) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| -- The available data file readers, each one handling a particular data | -- The available data file readers, each one handling a particular data | ||||||
| @ -77,14 +80,14 @@ journalEnvVar2          = "LEDGER" | |||||||
| journalDefaultFilename  = ".hledger.journal" | journalDefaultFilename  = ".hledger.journal" | ||||||
| 
 | 
 | ||||||
| -- | Which readers are worth trying for this (possibly unspecified) format, filepath, and data ? | -- | Which readers are worth trying for this (possibly unspecified) format, filepath, and data ? | ||||||
| readersFor :: (Maybe StorageFormat, Maybe FilePath, String) -> [Reader] | readersFor :: (Maybe StorageFormat, Maybe FilePath, Text) -> [Reader] | ||||||
| readersFor (format,path,s) = | readersFor (format,path,t) = | ||||||
|     dbg1 ("possible readers for "++show (format,path,elideRight 30 s)) $ |     dbg1 ("possible readers for "++show (format,path,textElideRight 30 t)) $ | ||||||
|     case format of |     case format of | ||||||
|      Just f  -> case readerForStorageFormat f of Just r  -> [r] |      Just f  -> case readerForStorageFormat f of Just r  -> [r] | ||||||
|                                                  Nothing -> [] |                                                  Nothing -> [] | ||||||
|      Nothing -> case path of Nothing  -> readers |      Nothing -> case path of Nothing  -> readers | ||||||
|                              Just p   -> case readersForPathAndData (p,s) of [] -> readers |                              Just p   -> case readersForPathAndData (p,t) of [] -> readers | ||||||
|                                                                              rs -> rs |                                                                              rs -> rs | ||||||
| 
 | 
 | ||||||
| -- | Find the (first) reader which can handle the given format, if any. | -- | Find the (first) reader which can handle the given format, if any. | ||||||
| @ -95,18 +98,18 @@ readerForStorageFormat s | null rs = Nothing | |||||||
|       rs = filter ((s==).rFormat) readers :: [Reader] |       rs = filter ((s==).rFormat) readers :: [Reader] | ||||||
| 
 | 
 | ||||||
| -- | Find the readers which think they can handle the given file path and data, if any. | -- | Find the readers which think they can handle the given file path and data, if any. | ||||||
| readersForPathAndData :: (FilePath,String) -> [Reader] | readersForPathAndData :: (FilePath,Text) -> [Reader] | ||||||
| readersForPathAndData (f,s) = filter (\r -> (rDetector r) f s) readers | readersForPathAndData (f,t) = filter (\r -> (rDetector r) f t) readers | ||||||
| 
 | 
 | ||||||
| -- try each reader in turn, returning the error of the first if all fail | -- try each reader in turn, returning the error of the first if all fail | ||||||
| tryReaders :: [Reader] -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal) | tryReaders :: [Reader] -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal) | ||||||
| tryReaders readers mrulesfile assrt path s = firstSuccessOrBestError [] readers | tryReaders readers mrulesfile assrt path t = firstSuccessOrBestError [] readers | ||||||
|   where |   where | ||||||
|     firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal) |     firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal) | ||||||
|     firstSuccessOrBestError [] []        = return $ Left "no readers found" |     firstSuccessOrBestError [] []        = return $ Left "no readers found" | ||||||
|     firstSuccessOrBestError errs (r:rs) = do |     firstSuccessOrBestError errs (r:rs) = do | ||||||
|       dbg1IO "trying reader" (rFormat r) |       dbg1IO "trying reader" (rFormat r) | ||||||
|       result <- (runExceptT . (rParser r) mrulesfile assrt path') s |       result <- (runExceptT . (rParser r) mrulesfile assrt path') t | ||||||
|       dbg1IO "reader result" $ either id show result |       dbg1IO "reader result" $ either id show result | ||||||
|       case result of Right j -> return $ Right j                       -- success! |       case result of Right j -> return $ Right j                       -- success! | ||||||
|                      Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying |                      Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying | ||||||
| @ -124,8 +127,8 @@ tryReaders readers mrulesfile assrt path s = firstSuccessOrBestError [] readers | |||||||
| -- | -- | ||||||
| -- A CSV conversion rules file may also be specified for use by the CSV reader. | -- A CSV conversion rules file may also be specified for use by the CSV reader. | ||||||
| -- Also there is a flag specifying whether to check or ignore balance assertions in the journal. | -- Also there is a flag specifying whether to check or ignore balance assertions in the journal. | ||||||
| readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal) | readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal) | ||||||
| readJournal mformat mrulesfile assrt mpath s = tryReaders (readersFor (mformat, mpath, s)) mrulesfile assrt mpath s | readJournal mformat mrulesfile assrt mpath t = tryReaders (readersFor (mformat, mpath, t)) mrulesfile assrt mpath t | ||||||
| 
 | 
 | ||||||
| -- | Read a Journal from this file (or stdin if the filename is -) or give | -- | Read a Journal from this file (or stdin if the filename is -) or give | ||||||
| -- an error message, using the specified data format or trying all known | -- an error message, using the specified data format or trying all known | ||||||
| @ -133,20 +136,9 @@ readJournal mformat mrulesfile assrt mpath s = tryReaders (readersFor (mformat, | |||||||
| -- conversion of that format. Also there is a flag specifying whether | -- conversion of that format. Also there is a flag specifying whether | ||||||
| -- to check or ignore balance assertions in the journal. | -- to check or ignore balance assertions in the journal. | ||||||
| readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> Bool -> FilePath -> IO (Either String Journal) | readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> Bool -> FilePath -> IO (Either String Journal) | ||||||
| readJournalFile mformat mrulesfile assrt f = | readJournalFile mformat mrulesfile assrt f = do | ||||||
|   readFileOrStdinAnyNewline f >>= readJournal mformat mrulesfile assrt (Just f) |   -- requireJournalFileExists f -- XXX ? | ||||||
| 
 |   readFileOrStdinAnyLineEnding f >>= readJournal mformat mrulesfile assrt (Just f) | ||||||
| -- | Read the given file, or standard input if the path is "-", using |  | ||||||
| -- universal newline mode. |  | ||||||
| readFileOrStdinAnyNewline :: String -> IO String |  | ||||||
| readFileOrStdinAnyNewline f = do |  | ||||||
|   requireJournalFileExists f |  | ||||||
|   h <- fileHandle f |  | ||||||
|   hSetNewlineMode h universalNewlineMode |  | ||||||
|   hGetContents h |  | ||||||
|   where |  | ||||||
|     fileHandle "-" = return stdin |  | ||||||
|     fileHandle f = openFile f ReadMode |  | ||||||
| 
 | 
 | ||||||
| -- | Call readJournalFile on each specified file path, and combine the | -- | Call readJournalFile on each specified file path, and combine the | ||||||
| -- resulting journals into one. If there are any errors, the first is | -- resulting journals into one. If there are any errors, the first is | ||||||
| @ -165,12 +157,13 @@ requireJournalFileExists :: FilePath -> IO () | |||||||
| requireJournalFileExists "-" = return () | requireJournalFileExists "-" = return () | ||||||
| requireJournalFileExists f = do | requireJournalFileExists f = do | ||||||
|   exists <- doesFileExist f |   exists <- doesFileExist f | ||||||
|   when (not exists) $ do |   when (not exists) $ do  -- XXX might not be a journal file | ||||||
|     hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f |     hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f | ||||||
|     hPrintf stderr "Please create it first, eg with \"hledger add\" or a text editor.\n" |     hPrintf stderr "Please create it first, eg with \"hledger add\" or a text editor.\n" | ||||||
|     hPrintf stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n" |     hPrintf stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n" | ||||||
|     exitFailure |     exitFailure | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| -- | Ensure there is a journal file at the given path, creating an empty one if needed. | -- | Ensure there is a journal file at the given path, creating an empty one if needed. | ||||||
| ensureJournalFileExists :: FilePath -> IO () | ensureJournalFileExists :: FilePath -> IO () | ||||||
| ensureJournalFileExists f = do | ensureJournalFileExists f = do | ||||||
| @ -211,9 +204,9 @@ defaultJournalPath = do | |||||||
| defaultJournal :: IO Journal | defaultJournal :: IO Journal | ||||||
| defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return | defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return | ||||||
| 
 | 
 | ||||||
| -- | Read a journal from the given string, trying all known formats, or simply throw an error. | -- | Read a journal from the given text, trying all known formats, or simply throw an error. | ||||||
| readJournal' :: String -> IO Journal | readJournal' :: Text -> IO Journal | ||||||
| readJournal' s = readJournal Nothing Nothing True Nothing s >>= either error' return | readJournal' t = readJournal Nothing Nothing True Nothing t >>= either error' return | ||||||
| 
 | 
 | ||||||
| tests_readJournal' = [ | tests_readJournal' = [ | ||||||
|   "readJournal' parses sample journal" ~: do |   "readJournal' parses sample journal" ~: do | ||||||
| @ -223,7 +216,7 @@ tests_readJournal' = [ | |||||||
| 
 | 
 | ||||||
| -- tests | -- tests | ||||||
| 
 | 
 | ||||||
| samplejournal = readJournal' $ unlines | samplejournal = readJournal' $ T.unlines | ||||||
|  ["2008/01/01 income" |  ["2008/01/01 income" | ||||||
|  ,"    assets:bank:checking  $1" |  ,"    assets:bank:checking  $1" | ||||||
|  ,"    income:salary" |  ,"    income:salary" | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ import Data.Functor.Identity | |||||||
| import Data.List.Compat | import Data.List.Compat | ||||||
| import Data.List.Split (wordsBy) | import Data.List.Split (wordsBy) | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
|  | -- import Data.Monoid | ||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| @ -44,8 +45,11 @@ import Hledger.Utils | |||||||
| -- | A parser of strings with generic user state, monad and return type. | -- | A parser of strings with generic user state, monad and return type. | ||||||
| type StringParser u m a = ParsecT String u m a | type StringParser u m a = ParsecT String u m a | ||||||
| 
 | 
 | ||||||
| -- | A string parser with journal-parsing state. | -- | A parser of strict text with generic user state, monad and return type. | ||||||
| type JournalParser m a = StringParser Journal m a | type TextParser u m a = ParsecT Text u m a | ||||||
|  | 
 | ||||||
|  | -- | A text parser with journal-parsing state. | ||||||
|  | type JournalParser m a = TextParser Journal m a | ||||||
| 
 | 
 | ||||||
| -- | A journal parser that runs in IO and can throw an error mid-parse. | -- | A journal parser that runs in IO and can throw an error mid-parse. | ||||||
| type ErroringJournalParser a = JournalParser (ExceptT String IO) a | type ErroringJournalParser a = JournalParser (ExceptT String IO) a | ||||||
| @ -55,14 +59,19 @@ runStringParser, rsp :: StringParser () Identity a -> String -> Either ParseErro | |||||||
| runStringParser p s = runIdentity $ runParserT p () "" s | runStringParser p s = runIdentity $ runParserT p () "" s | ||||||
| rsp = runStringParser | rsp = runStringParser | ||||||
| 
 | 
 | ||||||
|  | -- | Run a string parser with no state in the identity monad. | ||||||
|  | runTextParser, rtp :: TextParser () Identity a -> Text -> Either ParseError a | ||||||
|  | runTextParser p t = runIdentity $ runParserT p () "" t | ||||||
|  | rtp = runTextParser | ||||||
|  | 
 | ||||||
| -- | Run a journal parser with a null journal-parsing state. | -- | Run a journal parser with a null journal-parsing state. | ||||||
| runJournalParser, rjp :: Monad m => JournalParser m a -> String -> m (Either ParseError a) | runJournalParser, rjp :: Monad m => JournalParser m a -> Text -> m (Either ParseError a) | ||||||
| runJournalParser p s = runParserT p mempty "" s | runJournalParser p t = runParserT p mempty "" t | ||||||
| rjp = runJournalParser | rjp = runJournalParser | ||||||
| 
 | 
 | ||||||
| -- | Run an error-raising journal parser with a null journal-parsing state. | -- | Run an error-raising journal parser with a null journal-parsing state. | ||||||
| runErroringJournalParser, rejp :: ErroringJournalParser a -> String -> IO (Either String a) | runErroringJournalParser, rejp :: ErroringJournalParser a -> Text -> IO (Either String a) | ||||||
| runErroringJournalParser p s = runExceptT $ runJournalParser p s >>= either (throwError.show) return | runErroringJournalParser p t = runExceptT $ runJournalParser p t >>= either (throwError.show) return | ||||||
| rejp = runErroringJournalParser | rejp = runErroringJournalParser | ||||||
| 
 | 
 | ||||||
| genericSourcePos :: SourcePos -> GenericSourcePos | genericSourcePos :: SourcePos -> GenericSourcePos | ||||||
| @ -70,13 +79,13 @@ genericSourcePos p = GenericSourcePos (sourceName p) (sourceLine p) (sourceColum | |||||||
| 
 | 
 | ||||||
| -- | Given a parsec ParsedJournal parser, file path and data string, | -- | Given a parsec ParsedJournal parser, file path and data string, | ||||||
| -- parse and post-process a ready-to-use Journal, or give an error. | -- parse and post-process a ready-to-use Journal, or give an error. | ||||||
| parseAndFinaliseJournal :: ErroringJournalParser ParsedJournal -> Bool -> FilePath -> String -> ExceptT String IO Journal | parseAndFinaliseJournal :: ErroringJournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal | ||||||
| parseAndFinaliseJournal parser assrt f s = do | parseAndFinaliseJournal parser assrt f txt = do | ||||||
|   t <- liftIO getClockTime |   t <- liftIO getClockTime | ||||||
|   y <- liftIO getCurrentYear |   y <- liftIO getCurrentYear | ||||||
|   ep <- runParserT parser nulljournal{jparsedefaultyear=Just y} f s |   ep <- runParserT parser nulljournal{jparsedefaultyear=Just y} f txt | ||||||
|   case ep of |   case ep of | ||||||
|     Right pj -> case journalFinalise t f (T.pack s) assrt pj of |     Right pj -> case journalFinalise t f txt assrt pj of | ||||||
|                         Right j -> return j |                         Right j -> return j | ||||||
|                         Left e  -> throwError e |                         Left e  -> throwError e | ||||||
|     Left e   -> throwError $ show e |     Left e   -> throwError $ show e | ||||||
| @ -271,7 +280,7 @@ modifiedaccountnamep = do | |||||||
| -- spaces (or end of input). Also they have one or more components of | -- spaces (or end of input). Also they have one or more components of | ||||||
| -- at least one character, separated by the account separator char. | -- at least one character, separated by the account separator char. | ||||||
| -- (This parser will also consume one following space, if present.) | -- (This parser will also consume one following space, if present.) | ||||||
| accountnamep :: Monad m => StringParser u m AccountName | accountnamep :: Monad m => TextParser u m AccountName | ||||||
| accountnamep = do | accountnamep = do | ||||||
|     astr <- do |     astr <- do | ||||||
|       c <- nonspace |       c <- nonspace | ||||||
| @ -338,9 +347,9 @@ test_amountp = do | |||||||
| -- | Parse an amount from a string, or get an error. | -- | Parse an amount from a string, or get an error. | ||||||
| amountp' :: String -> Amount | amountp' :: String -> Amount | ||||||
| amountp' s = | amountp' s = | ||||||
|   case runParser (amountp <* eof) mempty "" s of |   case runParser (amountp <* eof) mempty "" (T.pack s) of | ||||||
|     Right t -> t |     Right amt -> amt | ||||||
|     Left err -> error' $ show err -- XXX should throwError |     Left err  -> error' $ show err -- XXX should throwError | ||||||
| 
 | 
 | ||||||
| -- | Parse a mixed amount from a string, or get an error. | -- | Parse a mixed amount from a string, or get an error. | ||||||
| mamountp' :: String -> MixedAmount | mamountp' :: String -> MixedAmount | ||||||
| @ -585,7 +594,7 @@ followingcommentandtagsp mdefdate = do | |||||||
|   -- Save the starting position and preserve all whitespace for the subsequent re-parsing, |   -- Save the starting position and preserve all whitespace for the subsequent re-parsing, | ||||||
|   -- to get good error positions. |   -- to get good error positions. | ||||||
|   startpos <- getPosition |   startpos <- getPosition | ||||||
|   commentandwhitespace <- do |   commentandwhitespace :: String <- do | ||||||
|     let semicoloncommentp' = (:) <$> char ';' <*> anyChar `manyTill` eolof |     let semicoloncommentp' = (:) <$> char ';' <*> anyChar `manyTill` eolof | ||||||
|     sp1 <- many spacenonewline |     sp1 <- many spacenonewline | ||||||
|     l1  <- try semicoloncommentp' <|> (newline >> return "") |     l1  <- try semicoloncommentp' <|> (newline >> return "") | ||||||
| @ -596,13 +605,13 @@ followingcommentandtagsp mdefdate = do | |||||||
|   -- pdbg 0 $ "comment:"++show comment |   -- pdbg 0 $ "comment:"++show comment | ||||||
| 
 | 
 | ||||||
|   -- Reparse the comment for any tags. |   -- Reparse the comment for any tags. | ||||||
|   tags <- case runStringParser (setPosition startpos >> tagsp) commentandwhitespace of |   tags <- case runTextParser (setPosition startpos >> tagsp) $ T.pack commentandwhitespace of | ||||||
|             Right ts -> return ts |             Right ts -> return ts | ||||||
|             Left e   -> throwError $ show e |             Left e   -> throwError $ show e | ||||||
|   -- pdbg 0 $ "tags: "++show tags |   -- pdbg 0 $ "tags: "++show tags | ||||||
| 
 | 
 | ||||||
|   -- Reparse the comment for any posting dates. Use the transaction date for defaults, if provided. |   -- Reparse the comment for any posting dates. Use the transaction date for defaults, if provided. | ||||||
|   epdates <- liftIO $ rejp (setPosition startpos >> postingdatesp mdefdate) commentandwhitespace |   epdates <- liftIO $ rejp (setPosition startpos >> postingdatesp mdefdate) $ T.pack commentandwhitespace | ||||||
|   pdates <- case epdates of |   pdates <- case epdates of | ||||||
|               Right ds -> return ds |               Right ds -> return ds | ||||||
|               Left e   -> throwError e |               Left e   -> throwError e | ||||||
| @ -645,14 +654,14 @@ commentStartingWithp cs = do | |||||||
| -- >>> commentTags "\na b:, \nd:e, f" | -- >>> commentTags "\na b:, \nd:e, f" | ||||||
| -- [("b",""),("d","e")] | -- [("b",""),("d","e")] | ||||||
| -- | -- | ||||||
| commentTags :: String -> [Tag] | commentTags :: Text -> [Tag] | ||||||
| commentTags s = | commentTags s = | ||||||
|   case runStringParser tagsp s of |   case runTextParser tagsp s of | ||||||
|     Right r -> r |     Right r -> r | ||||||
|     Left _  -> [] -- shouldn't happen |     Left _  -> [] -- shouldn't happen | ||||||
| 
 | 
 | ||||||
| -- | Parse all tags found in a string. | -- | Parse all tags found in a string. | ||||||
| tagsp :: StringParser u Identity [Tag] | tagsp :: TextParser u Identity [Tag] | ||||||
| tagsp = -- do | tagsp = -- do | ||||||
|   -- pdbg 0 $ "tagsp" |   -- pdbg 0 $ "tagsp" | ||||||
|   many (try (nontagp >> tagp)) |   many (try (nontagp >> tagp)) | ||||||
| @ -661,7 +670,7 @@ tagsp = -- do | |||||||
| -- | -- | ||||||
| -- >>> rsp nontagp "\na b:, \nd:e, f" | -- >>> rsp nontagp "\na b:, \nd:e, f" | ||||||
| -- Right "\na " | -- Right "\na " | ||||||
| nontagp :: StringParser u Identity String | nontagp :: TextParser u Identity String | ||||||
| nontagp = -- do | nontagp = -- do | ||||||
|   -- pdbg 0 "nontagp" |   -- pdbg 0 "nontagp" | ||||||
|   -- anyChar `manyTill` (lookAhead (try (tagorbracketeddatetagsp Nothing >> return ()) <|> eof)) |   -- anyChar `manyTill` (lookAhead (try (tagorbracketeddatetagsp Nothing >> return ()) <|> eof)) | ||||||
| @ -675,7 +684,7 @@ nontagp = -- do | |||||||
| -- >>> rsp tagp "a:b b , c AuxDate: 4/2" | -- >>> rsp tagp "a:b b , c AuxDate: 4/2" | ||||||
| -- Right ("a","b b") | -- Right ("a","b b") | ||||||
| -- | -- | ||||||
| tagp :: Monad m => StringParser u m Tag | tagp :: Monad m => TextParser u m Tag | ||||||
| tagp = do | tagp = do | ||||||
|   -- pdbg 0 "tagp" |   -- pdbg 0 "tagp" | ||||||
|   n <- tagnamep |   n <- tagnamep | ||||||
| @ -685,12 +694,12 @@ tagp = do | |||||||
| -- | | -- | | ||||||
| -- >>> rsp tagnamep "a:" | -- >>> rsp tagnamep "a:" | ||||||
| -- Right "a" | -- Right "a" | ||||||
| tagnamep :: Monad m => StringParser u m String | tagnamep :: Monad m => TextParser u m String | ||||||
| tagnamep = -- do | tagnamep = -- do | ||||||
|   -- pdbg 0 "tagnamep" |   -- pdbg 0 "tagnamep" | ||||||
|   many1 (noneOf ": \t\n") <* char ':' |   many1 (noneOf ": \t\n") <* char ':' | ||||||
| 
 | 
 | ||||||
| tagvaluep :: Monad m => StringParser u m String | tagvaluep :: Monad m => TextParser u m String | ||||||
| tagvaluep = do | tagvaluep = do | ||||||
|   -- ptrace "tagvalue" |   -- ptrace "tagvalue" | ||||||
|   v <- anyChar `manyTill` (void (try (char ',')) <|> eolof) |   v <- anyChar `manyTill` (void (try (char ',')) <|> eolof) | ||||||
| @ -746,14 +755,14 @@ datetagp mdefdate = do | |||||||
|     (do |     (do | ||||||
|         setPosition startpos |         setPosition startpos | ||||||
|         datep) -- <* eof) |         datep) -- <* eof) | ||||||
|     v |     (T.pack v) | ||||||
|   case ep |   case ep | ||||||
|     of Left e  -> throwError $ show e |     of Left e  -> throwError $ show e | ||||||
|        Right d -> return ("date"++n, d) |        Right d -> return ("date"++n, d) | ||||||
| 
 | 
 | ||||||
| --- ** bracketed dates | --- ** bracketed dates | ||||||
| 
 | 
 | ||||||
| -- tagorbracketeddatetagsp :: Monad m => Maybe Day -> StringParser u m [Tag] | -- tagorbracketeddatetagsp :: Monad m => Maybe Day -> TextParser u m [Tag] | ||||||
| -- tagorbracketeddatetagsp mdefdate = | -- tagorbracketeddatetagsp mdefdate = | ||||||
| --   bracketeddatetagsp mdefdate <|> ((:[]) <$> tagp) | --   bracketeddatetagsp mdefdate <|> ((:[]) <$> tagp) | ||||||
| 
 | 
 | ||||||
| @ -807,7 +816,7 @@ bracketeddatetagsp mdefdate = do | |||||||
|         eof |         eof | ||||||
|         return (md1,md2) |         return (md1,md2) | ||||||
|     ) |     ) | ||||||
|     s |     (T.pack s) | ||||||
|   case ep |   case ep | ||||||
|     of Left e          -> throwError $ show e |     of Left e          -> throwError $ show e | ||||||
|        Right (md1,md2) -> return $ catMaybes |        Right (md1,md2) -> return $ catMaybes | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ import Data.Char (toLower, isDigit, isSpace) | |||||||
| import Data.List.Compat | import Data.List.Compat | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import Data.Ord | import Data.Ord | ||||||
| -- import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import Data.Time.Calendar (Day) | import Data.Time.Calendar (Day) | ||||||
| #if MIN_VERSION_time(1,5,0) | #if MIN_VERSION_time(1,5,0) | ||||||
| @ -63,16 +63,16 @@ format :: String | |||||||
| format = "csv" | format = "csv" | ||||||
| 
 | 
 | ||||||
| -- | Does the given file path and data look like it might be CSV ? | -- | Does the given file path and data look like it might be CSV ? | ||||||
| detect :: FilePath -> String -> Bool | detect :: FilePath -> Text -> Bool | ||||||
| detect f s | detect f t | ||||||
|   | f /= "-"  = takeExtension f == '.':format  -- from a file: yes if the extension is .csv |   | f /= "-"  = takeExtension f == '.':format  -- from a file: yes if the extension is .csv | ||||||
|   | otherwise = length (filter (==',') s) >= 2 -- from stdin: yes if there are two or more commas |   | otherwise = T.length (T.filter (==',') t) >= 2 -- from stdin: yes if there are two or more commas | ||||||
| 
 | 
 | ||||||
| -- | Parse and post-process a "Journal" from CSV data, or give an error. | -- | Parse and post-process a "Journal" from CSV data, or give an error. | ||||||
| -- XXX currently ignores the string and reads from the file path | -- XXX currently ignores the string and reads from the file path | ||||||
| parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal | parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal | ||||||
| parse rulesfile _ f s = do | parse rulesfile _ f t = do | ||||||
|   r <- liftIO $ readJournalFromCsv rulesfile f s |   r <- liftIO $ readJournalFromCsv rulesfile f t | ||||||
|   case r of Left e -> throwError e |   case r of Left e -> throwError e | ||||||
|             Right j -> return j |             Right j -> return j | ||||||
| 
 | 
 | ||||||
| @ -87,7 +87,7 @@ parse rulesfile _ f s = do | |||||||
| -- 4. parse the rules file | -- 4. parse the rules file | ||||||
| -- 5. convert the CSV records to a journal using the rules | -- 5. convert the CSV records to a journal using the rules | ||||||
| -- @ | -- @ | ||||||
| readJournalFromCsv :: Maybe FilePath -> FilePath -> String -> IO (Either String Journal) | readJournalFromCsv :: 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 mrulesfile csvfile csvdata = | ||||||
|  handle (\e -> return $ Left $ show (e :: IOException)) $ do |  handle (\e -> return $ Left $ show (e :: IOException)) $ do | ||||||
| @ -117,7 +117,7 @@ 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 csvdata |              `fmap` parseCsv parsecfilename (T.unpack csvdata) | ||||||
|   dbg1IO "first 3 csv records" $ take 3 records |   dbg1IO "first 3 csv records" $ take 3 records | ||||||
| 
 | 
 | ||||||
|   -- identify header lines |   -- identify header lines | ||||||
| @ -607,7 +607,7 @@ transactionFromCsvRecord sourcepos rules record = t | |||||||
|     status      = |     status      = | ||||||
|       case mfieldtemplate "status" of |       case mfieldtemplate "status" of | ||||||
|         Nothing  -> Uncleared |         Nothing  -> Uncleared | ||||||
|         Just str -> either statuserror id $ runParser (statusp <* eof) mempty "" $ render str |         Just str -> either statuserror id $ runParser (statusp <* eof) mempty "" $ T.pack $ render str | ||||||
|           where |           where | ||||||
|             statuserror err = error' $ unlines |             statuserror err = error' $ unlines | ||||||
|               ["error: could not parse \""++str++"\" as a cleared status (should be *, ! or empty)" |               ["error: could not parse \""++str++"\" as a cleared status (should be *, ! or empty)" | ||||||
| @ -619,7 +619,7 @@ transactionFromCsvRecord sourcepos rules record = t | |||||||
|     precomment  = maybe "" render $ mfieldtemplate "precomment" |     precomment  = maybe "" render $ mfieldtemplate "precomment" | ||||||
|     currency    = maybe (fromMaybe "" mdefaultcurrency) render $ mfieldtemplate "currency" |     currency    = maybe (fromMaybe "" mdefaultcurrency) render $ mfieldtemplate "currency" | ||||||
|     amountstr   = (currency++) $ negateIfParenthesised $ getAmountStr rules record |     amountstr   = (currency++) $ negateIfParenthesised $ getAmountStr rules record | ||||||
|     amount      = either amounterror (Mixed . (:[])) $ runParser (amountp <* eof) mempty "" amountstr |     amount      = either amounterror (Mixed . (:[])) $ runParser (amountp <* eof) mempty "" $ T.pack amountstr | ||||||
|     amounterror err = error' $ unlines |     amounterror err = error' $ unlines | ||||||
|       ["error: could not parse \""++amountstr++"\" as an amount" |       ["error: could not parse \""++amountstr++"\" as an amount" | ||||||
|       ,showRecord record |       ,showRecord record | ||||||
|  | |||||||
| @ -82,7 +82,7 @@ import Control.Monad | |||||||
| import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError) | import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError) | ||||||
| import qualified Data.Map.Strict as M | import qualified Data.Map.Strict as M | ||||||
| import Data.Monoid | import Data.Monoid | ||||||
| -- import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| import Data.Time.LocalTime | import Data.Time.LocalTime | ||||||
| @ -112,14 +112,14 @@ format :: String | |||||||
| format = "journal" | format = "journal" | ||||||
| 
 | 
 | ||||||
| -- | Does the given file path and data look like it might be hledger's journal format ? | -- | Does the given file path and data look like it might be hledger's journal format ? | ||||||
| detect :: FilePath -> String -> Bool | detect :: FilePath -> Text -> Bool | ||||||
| detect f s | detect f t | ||||||
|   | f /= "-"  = takeExtension f `elem` ['.':format, ".j"] -- from a known file name: yes if the extension is this format's name or .j |   | f /= "-"  = takeExtension f `elem` ['.':format, ".j"] -- from a known file name: yes if the extension is this format's name or .j | ||||||
|   | otherwise = regexMatches "(^|\n)[0-9]+.*\n[ \t]+" s   -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented) |   | otherwise = regexMatches "(^|\n)[0-9]+.*\n[ \t]+" $ T.unpack t   -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented) | ||||||
| 
 | 
 | ||||||
| -- | Parse and post-process a "Journal" from hledger's journal file | -- | Parse and post-process a "Journal" from hledger's journal file | ||||||
| -- format, or give an error. | -- format, or give an error. | ||||||
| parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal | parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal | ||||||
| parse _ = parseAndFinaliseJournal journalp | parse _ = parseAndFinaliseJournal journalp | ||||||
| 
 | 
 | ||||||
| --- * parsers | --- * parsers | ||||||
| @ -190,7 +190,7 @@ includedirectivep = do | |||||||
|     liftIO $ runExceptT $ do |     liftIO $ runExceptT $ do | ||||||
|       let curdir = takeDirectory (sourceName parentpos) |       let curdir = takeDirectory (sourceName parentpos) | ||||||
|       filepath <- expandPath curdir filename `orRethrowIOError` (show parentpos ++ " locating " ++ filename) |       filepath <- expandPath curdir filename `orRethrowIOError` (show parentpos ++ " locating " ++ filename) | ||||||
|       txt      <- readFile' filepath         `orRethrowIOError` (show parentpos ++ " reading " ++ filepath) |       txt      <- readFileAnyLineEnding filepath `orRethrowIOError` (show parentpos ++ " reading " ++ filepath) | ||||||
|       (ej1::Either ParseError ParsedJournal) <- |       (ej1::Either ParseError ParsedJournal) <- | ||||||
|         runParserT  |         runParserT  | ||||||
|            (choice' [journalp |            (choice' [journalp | ||||||
| @ -203,7 +203,7 @@ includedirectivep = do | |||||||
|         (throwError |         (throwError | ||||||
|           . ((show parentpos ++ " in included file " ++ show filename ++ ":\n") ++) |           . ((show parentpos ++ " in included file " ++ show filename ++ ":\n") ++) | ||||||
|           . show) |           . show) | ||||||
|         (return . journalAddFile (filepath, T.pack txt)) |         (return . journalAddFile (filepath, txt)) | ||||||
|         ej1 |         ej1 | ||||||
|   case ej of |   case ej of | ||||||
|     Left e       -> throwError e |     Left e       -> throwError e | ||||||
| @ -311,10 +311,10 @@ aliasdirectivep = do | |||||||
|   alias <- accountaliasp |   alias <- accountaliasp | ||||||
|   addAccountAlias alias |   addAccountAlias alias | ||||||
| 
 | 
 | ||||||
| accountaliasp :: Monad m => StringParser u m AccountAlias | accountaliasp :: Monad m => TextParser u m AccountAlias | ||||||
| accountaliasp = regexaliasp <|> basicaliasp | accountaliasp = regexaliasp <|> basicaliasp | ||||||
| 
 | 
 | ||||||
| basicaliasp :: Monad m => StringParser u m AccountAlias | basicaliasp :: Monad m => TextParser u m AccountAlias | ||||||
| basicaliasp = do | basicaliasp = do | ||||||
|   -- pdbg 0 "basicaliasp" |   -- pdbg 0 "basicaliasp" | ||||||
|   old <- rstrip <$> many1 (noneOf "=") |   old <- rstrip <$> many1 (noneOf "=") | ||||||
| @ -323,7 +323,7 @@ basicaliasp = do | |||||||
|   new <- rstrip <$> anyChar `manyTill` eolof  -- don't require a final newline, good for cli options |   new <- rstrip <$> anyChar `manyTill` eolof  -- don't require a final newline, good for cli options | ||||||
|   return $ BasicAlias (T.pack old) (T.pack new) |   return $ BasicAlias (T.pack old) (T.pack new) | ||||||
| 
 | 
 | ||||||
| regexaliasp :: Monad m => StringParser u m AccountAlias | regexaliasp :: Monad m => TextParser u m AccountAlias | ||||||
| regexaliasp = do | regexaliasp = do | ||||||
|   -- pdbg 0 "regexaliasp" |   -- pdbg 0 "regexaliasp" | ||||||
|   char '/' |   char '/' | ||||||
| @ -433,7 +433,7 @@ transactionp = do | |||||||
|   code <- codep <?> "transaction code" |   code <- codep <?> "transaction code" | ||||||
|   description <- strip <$> descriptionp |   description <- strip <$> descriptionp | ||||||
|   comment <- try followingcommentp <|> (newline >> return "") |   comment <- try followingcommentp <|> (newline >> return "") | ||||||
|   let tags = commentTags comment |   let tags = commentTags $ T.pack comment | ||||||
|   postings <- postingsp (Just date) |   postings <- postingsp (Just date) | ||||||
|   n <- incrementTransactionCount |   n <- incrementTransactionCount | ||||||
|   return $ txnTieKnot $ Transaction n sourcepos date edate status code description comment tags postings "" |   return $ txnTieKnot $ Transaction n sourcepos date edate status code description comment tags postings "" | ||||||
|  | |||||||
| @ -57,8 +57,8 @@ import Control.Monad | |||||||
| import Control.Monad.IO.Class (liftIO) | import Control.Monad.IO.Class (liftIO) | ||||||
| import Control.Monad.Except (ExceptT) | import Control.Monad.Except (ExceptT) | ||||||
| import Data.Maybe (fromMaybe) | import Data.Maybe (fromMaybe) | ||||||
| -- import Data.Text (Text) | import Data.Text (Text) | ||||||
| -- import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import Test.HUnit | import Test.HUnit | ||||||
| import Text.Parsec hiding (parse) | import Text.Parsec hiding (parse) | ||||||
| import System.FilePath | import System.FilePath | ||||||
| @ -76,15 +76,15 @@ format :: String | |||||||
| format = "timeclock" | format = "timeclock" | ||||||
| 
 | 
 | ||||||
| -- | Does the given file path and data look like it might be timeclock.el's timeclock format ? | -- | Does the given file path and data look like it might be timeclock.el's timeclock format ? | ||||||
| detect :: FilePath -> String -> Bool | detect :: FilePath -> Text -> Bool | ||||||
| detect f s | detect f t | ||||||
|   | f /= "-"  = takeExtension f == '.':format -- from a known file name: yes if the extension is this format's name |   | f /= "-"  = takeExtension f == '.':format -- from a known file name: yes if the extension is this format's name | ||||||
|   | otherwise = regexMatches "(^|\n)[io] " s  -- from stdin: yes if any line starts with "i " or "o " |   | otherwise = regexMatches "(^|\n)[io] " $ T.unpack t  -- from stdin: yes if any line starts with "i " or "o " | ||||||
| 
 | 
 | ||||||
| -- | Parse and post-process a "Journal" from timeclock.el's timeclock | -- | Parse and post-process a "Journal" from timeclock.el's timeclock | ||||||
| -- format, saving the provided file path and the current time, or give an | -- format, saving the provided file path and the current time, or give an | ||||||
| -- error. | -- error. | ||||||
| parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal | parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal | ||||||
| parse _ = parseAndFinaliseJournal timeclockfilep | parse _ = parseAndFinaliseJournal timeclockfilep | ||||||
| 
 | 
 | ||||||
| timeclockfilep :: ErroringJournalParser ParsedJournal | timeclockfilep :: ErroringJournalParser ParsedJournal | ||||||
|  | |||||||
| @ -37,6 +37,8 @@ import Control.Monad.Except (ExceptT) | |||||||
| import Data.Char (isSpace) | import Data.Char (isSpace) | ||||||
| import Data.List (foldl') | import Data.List (foldl') | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
|  | import Data.Text (Text) | ||||||
|  | import qualified Data.Text as T | ||||||
| import Test.HUnit | import Test.HUnit | ||||||
| import Text.Parsec hiding (parse) | import Text.Parsec hiding (parse) | ||||||
| import System.FilePath | import System.FilePath | ||||||
| @ -57,13 +59,13 @@ format :: String | |||||||
| format = "timedot" | format = "timedot" | ||||||
| 
 | 
 | ||||||
| -- | Does the given file path and data look like it might contain this format ? | -- | Does the given file path and data look like it might contain this format ? | ||||||
| detect :: FilePath -> String -> Bool | detect :: FilePath -> Text -> Bool | ||||||
| detect f s | detect f t | ||||||
|   | f /= "-"  = takeExtension f == '.':format -- from a file: yes if the extension matches the format name |   | f /= "-"  = takeExtension f == '.':format -- from a file: yes if the extension matches the format name | ||||||
|   | otherwise = regexMatches "(^|\n)[0-9]" s  -- from stdin: yes if we can see a possible timedot day entry (digits in column 0) |   | otherwise = regexMatches "(^|\n)[0-9]" $ T.unpack t  -- from stdin: yes if we can see a possible timedot day entry (digits in column 0) | ||||||
| 
 | 
 | ||||||
| -- | Parse and post-process a "Journal" from the timedot format, or give an error. | -- | Parse and post-process a "Journal" from the timedot format, or give an error. | ||||||
| parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal | parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal | ||||||
| parse _ = parseAndFinaliseJournal timedotfilep | parse _ = parseAndFinaliseJournal timedotfilep | ||||||
| 
 | 
 | ||||||
| timedotfilep :: ErroringJournalParser ParsedJournal | timedotfilep :: ErroringJournalParser ParsedJournal | ||||||
|  | |||||||
| @ -37,6 +37,8 @@ import Control.Monad (liftM) | |||||||
| -- import Data.List | -- import Data.List | ||||||
| -- import Data.Maybe | -- import Data.Maybe | ||||||
| -- import Data.PPrint | -- import Data.PPrint | ||||||
|  | import Data.Text (Text) | ||||||
|  | import qualified Data.Text.IO as T | ||||||
| import Data.Time.Clock | import Data.Time.Clock | ||||||
| import Data.Time.LocalTime | import Data.Time.LocalTime | ||||||
| -- import Data.Text (Text) | -- import Data.Text (Text) | ||||||
| @ -134,13 +136,31 @@ firstJust ms = case dropWhile (==Nothing) ms of | |||||||
|     [] -> Nothing |     [] -> Nothing | ||||||
|     (md:_) -> md |     (md:_) -> md | ||||||
| 
 | 
 | ||||||
| -- | Read a file in universal newline mode, handling whatever newline convention it may contain. | -- | Read a file in universal newline mode, handling any of the usual line ending conventions. | ||||||
| readFile' :: FilePath -> IO String | readFile' :: FilePath -> IO String | ||||||
| readFile' name =  do | readFile' name =  do | ||||||
|   h <- openFile name ReadMode |   h <- openFile name ReadMode | ||||||
|   hSetNewlineMode h universalNewlineMode |   hSetNewlineMode h universalNewlineMode | ||||||
|   hGetContents h |   hGetContents h | ||||||
| 
 | 
 | ||||||
|  | -- | Read a file in universal newline mode, handling any of the usual line ending conventions. | ||||||
|  | readFileAnyLineEnding :: FilePath -> IO Text | ||||||
|  | readFileAnyLineEnding path =  do | ||||||
|  |   h <- openFile path ReadMode | ||||||
|  |   hSetNewlineMode h universalNewlineMode | ||||||
|  |   T.hGetContents h | ||||||
|  | 
 | ||||||
|  | -- | Read the given file, or standard input if the path is "-", using | ||||||
|  | -- universal newline mode. | ||||||
|  | readFileOrStdinAnyLineEnding :: String -> IO Text | ||||||
|  | readFileOrStdinAnyLineEnding f = do | ||||||
|  |   h <- fileHandle f | ||||||
|  |   hSetNewlineMode h universalNewlineMode | ||||||
|  |   T.hGetContents h | ||||||
|  |   where | ||||||
|  |     fileHandle "-" = return stdin | ||||||
|  |     fileHandle f = openFile f ReadMode | ||||||
|  | 
 | ||||||
| -- | Total version of maximum, for integral types, giving 0 for an empty list. | -- | Total version of maximum, for integral types, giving 0 for an empty list. | ||||||
| maximum' :: Integral a => [a] -> a | maximum' :: Integral a => [a] -> a | ||||||
| maximum' [] = 0 | maximum' [] = 0 | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ module Hledger.Utils.Parse where | |||||||
| 
 | 
 | ||||||
| import Data.Char | import Data.Char | ||||||
| import Data.List | import Data.List | ||||||
|  | -- import Data.Text (Text) | ||||||
|  | -- import qualified Data.Text as T | ||||||
| import Text.Parsec | import Text.Parsec | ||||||
| import Text.Printf | import Text.Printf | ||||||
| 
 | 
 | ||||||
| @ -31,15 +33,15 @@ showParseError e = "parse error at " ++ show e | |||||||
| showDateParseError :: ParseError -> String | showDateParseError :: ParseError -> String | ||||||
| showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e) | showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e) | ||||||
| 
 | 
 | ||||||
| nonspace :: (Stream [Char] m Char) => ParsecT [Char] st m Char | nonspace :: (Stream s m Char) => ParsecT s st m Char | ||||||
| nonspace = satisfy (not . isSpace) | nonspace = satisfy (not . isSpace) | ||||||
| 
 | 
 | ||||||
| spacenonewline :: (Stream [Char] m Char) => ParsecT [Char] st m Char | spacenonewline :: (Stream s m Char) => ParsecT s st m Char | ||||||
| spacenonewline = satisfy (`elem` " \v\f\t") | spacenonewline = satisfy (`elem` " \v\f\t") | ||||||
| 
 | 
 | ||||||
| restofline :: (Stream [Char] m Char) => ParsecT [Char] st m String | restofline :: (Stream s m Char) => ParsecT s st m String | ||||||
| restofline = anyChar `manyTill` newline | restofline = anyChar `manyTill` newline | ||||||
| 
 | 
 | ||||||
| eolof :: (Stream [Char] m Char) => ParsecT [Char] st m () | eolof :: (Stream s m Char) => ParsecT s st m () | ||||||
| eolof = (newline >> return ()) <|> eof | eolof = (newline >> return ()) <|> eof | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -71,17 +71,17 @@ import Hledger.Utils.String (charWidth) | |||||||
| -- lowercase = map toLower | -- lowercase = map toLower | ||||||
| -- uppercase = map toUpper | -- uppercase = map toUpper | ||||||
| 
 | 
 | ||||||
| -- -- | Remove leading and trailing whitespace. | -- | Remove leading and trailing whitespace. | ||||||
| -- strip :: String -> String | textstrip :: Text -> Text | ||||||
| -- strip = lstrip . rstrip | textstrip = textlstrip . textrstrip | ||||||
| 
 | 
 | ||||||
| -- -- | Remove leading whitespace. | -- | Remove leading whitespace. | ||||||
| -- lstrip :: String -> String | textlstrip :: Text -> Text | ||||||
| -- lstrip = dropWhile (`elem` " \t") :: String -> String -- XXX isSpace ? | textlstrip = T.dropWhile (`elem` " \t") :: Text -> Text -- XXX isSpace ? | ||||||
| 
 | 
 | ||||||
| -- -- | Remove trailing whitespace. | -- | Remove trailing whitespace. | ||||||
| -- rstrip :: String -> String | textrstrip = T.reverse . textlstrip . T.reverse | ||||||
| -- rstrip = reverse . lstrip . reverse | textrstrip :: Text -> Text | ||||||
| 
 | 
 | ||||||
| -- -- | Remove trailing newlines/carriage returns. | -- -- | Remove trailing newlines/carriage returns. | ||||||
| -- chomp :: String -> String | -- chomp :: String -> String | ||||||
| @ -94,9 +94,9 @@ import Hledger.Utils.String (charWidth) | |||||||
| -- elideLeft width s = | -- elideLeft width s = | ||||||
| --     if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s | --     if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s | ||||||
| 
 | 
 | ||||||
| -- elideRight :: Int -> String -> String | textElideRight :: Int -> Text -> Text | ||||||
| -- elideRight width s = | textElideRight width t = | ||||||
| --     if length s > width then take (width - 2) s ++ ".." else s |     if T.length t > width then T.take (width - 2) t <> ".." else t | ||||||
| 
 | 
 | ||||||
| -- -- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it. | -- -- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it. | ||||||
| -- -- Works on multi-line strings too (but will rewrite non-unix line endings). | -- -- Works on multi-line strings too (but will rewrite non-unix line endings). | ||||||
|  | |||||||
| @ -95,8 +95,8 @@ postAddForm = do | |||||||
|                     | map fst acctparams == [1..num] && |                     | map fst acctparams == [1..num] && | ||||||
|                       map fst amtparams `elem` [[1..num], [1..num-1]] = [] |                       map fst amtparams `elem` [[1..num], [1..num-1]] = [] | ||||||
|                     | otherwise = ["the posting parameters are malformed"] |                     | otherwise = ["the posting parameters are malformed"] | ||||||
|           eaccts = map (parsewith (accountnamep <* eof) . strip . T.unpack . snd) acctparams |           eaccts = map (runParser (accountnamep <* eof) () "" . T.pack . strip . T.unpack . snd) acctparams | ||||||
|           eamts  = map (runParser (amountp <* eof) mempty "" . strip . T.unpack . snd) amtparams |           eamts  = map (runParser (amountp <* eof) mempty "" . T.pack . strip . T.unpack . snd) amtparams | ||||||
|           (accts, acctErrs) = (rights eaccts, map show $ lefts eaccts) |           (accts, acctErrs) = (rights eaccts, map show $ lefts eaccts) | ||||||
|           (amts', amtErrs)  = (rights eamts, map show $ lefts eamts) |           (amts', amtErrs)  = (rights eamts, map show $ lefts eamts) | ||||||
|           amts | length amts' == num = amts' |           amts | length amts' == num = amts' | ||||||
|  | |||||||
| @ -32,6 +32,9 @@ module Hledger.Cli ( | |||||||
|                      module System.Console.CmdArgs.Explicit |                      module System.Console.CmdArgs.Explicit | ||||||
|               ) |               ) | ||||||
| where | where | ||||||
|  | import Data.Monoid ((<>)) | ||||||
|  | import Data.Text (Text) | ||||||
|  | import qualified Data.Text as T | ||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| import System.Console.CmdArgs.Explicit | import System.Console.CmdArgs.Explicit | ||||||
| import Test.HUnit | import Test.HUnit | ||||||
| @ -80,16 +83,16 @@ tests_Hledger_Cli = TestList | |||||||
|    in TestList |    in TestList | ||||||
|    [ |    [ | ||||||
|     "apply account directive 1" ~: sameParse |     "apply account directive 1" ~: sameParse | ||||||
|                            ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" ++ |                            ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" <> | ||||||
|                             "apply account outer\n2008/12/07 Two\n  aigh  $-2\n  bee  $2\n" ++ |                             "apply account outer\n2008/12/07 Two\n  aigh  $-2\n  bee  $2\n" <> | ||||||
|                             "apply account inner\n2008/12/07 Three\n  gamma  $-3\n  delta  $3\n" ++ |                             "apply account inner\n2008/12/07 Three\n  gamma  $-3\n  delta  $3\n" <> | ||||||
|                             "end apply account\n2008/12/07 Four\n  why  $-4\n  zed  $4\n" ++ |                             "end apply account\n2008/12/07 Four\n  why  $-4\n  zed  $4\n" <> | ||||||
|                             "end apply account\n2008/12/07 Five\n  foo  $-5\n  bar  $5\n" |                             "end apply account\n2008/12/07 Five\n  foo  $-5\n  bar  $5\n" | ||||||
|                            ) |                            ) | ||||||
|                            ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" ++ |                            ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" <> | ||||||
|                             "2008/12/07 Two\n  outer:aigh  $-2\n  outer:bee  $2\n" ++ |                             "2008/12/07 Two\n  outer:aigh  $-2\n  outer:bee  $2\n" <> | ||||||
|                             "2008/12/07 Three\n  outer:inner:gamma  $-3\n  outer:inner:delta  $3\n" ++ |                             "2008/12/07 Three\n  outer:inner:gamma  $-3\n  outer:inner:delta  $3\n" <> | ||||||
|                             "2008/12/07 Four\n  outer:why  $-4\n  outer:zed  $4\n" ++ |                             "2008/12/07 Four\n  outer:why  $-4\n  outer:zed  $4\n" <> | ||||||
|                             "2008/12/07 Five\n  foo  $-5\n  bar  $5\n" |                             "2008/12/07 Five\n  foo  $-5\n  bar  $5\n" | ||||||
|                            ) |                            ) | ||||||
| 
 | 
 | ||||||
| @ -124,7 +127,7 @@ tests_Hledger_Cli = TestList | |||||||
|   --     `is` "aa:aa:aaaaaaaaaaaaaa") |   --     `is` "aa:aa:aaaaaaaaaaaaaa") | ||||||
| 
 | 
 | ||||||
|   ,"default year" ~: do |   ,"default year" ~: do | ||||||
|     j <- readJournal Nothing Nothing True Nothing defaultyear_journal_str >>= either error' return |     j <- readJournal Nothing Nothing True Nothing defaultyear_journal_txt >>= either error' return | ||||||
|     tdate (head $ jtxns j) `is` fromGregorian 2009 1 1 |     tdate (head $ jtxns j) `is` fromGregorian 2009 1 1 | ||||||
|     return () |     return () | ||||||
| 
 | 
 | ||||||
| @ -187,8 +190,8 @@ sample_journal_str = unlines | |||||||
|  ] |  ] | ||||||
| -} | -} | ||||||
| 
 | 
 | ||||||
| defaultyear_journal_str :: String | defaultyear_journal_txt :: Text | ||||||
| defaultyear_journal_str = unlines | defaultyear_journal_txt = T.unlines | ||||||
|  ["Y2009" |  ["Y2009" | ||||||
|  ,"" |  ,"" | ||||||
|  ,"01/01 A" |  ,"01/01 A" | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import Data.Char (toUpper, toLower) | |||||||
| import Data.List.Compat | import Data.List.Compat | ||||||
| import qualified Data.Set as S | import qualified Data.Set as S | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| -- import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import Data.Time.Calendar (Day) | import Data.Time.Calendar (Day) | ||||||
| import Data.Typeable (Typeable) | import Data.Typeable (Typeable) | ||||||
| @ -183,7 +183,7 @@ dateAndCodeWizard EntryState{..} = do | |||||||
|     where |     where | ||||||
|       parseSmartDateAndCode refdate s = either (const Nothing) (\(d,c) -> return (fixSmartDate refdate d, c)) edc |       parseSmartDateAndCode refdate s = either (const Nothing) (\(d,c) -> return (fixSmartDate refdate d, c)) edc | ||||||
|           where |           where | ||||||
|             edc = runParser (dateandcodep <* eof) mempty "" $ lowercase s |             edc = runParser (dateandcodep <* eof) mempty "" $ T.pack $ lowercase s | ||||||
|             dateandcodep :: Monad m => JournalParser m (SmartDate, String) |             dateandcodep :: Monad m => JournalParser m (SmartDate, String) | ||||||
|             dateandcodep = do |             dateandcodep = do | ||||||
|                 d <- smartdate |                 d <- smartdate | ||||||
| @ -244,13 +244,18 @@ accountWizard EntryState{..} = do | |||||||
|    line $ green $ printf "Account %d%s%s: " pnum (endmsg::String) (showDefault def) |    line $ green $ printf "Account %d%s%s: " pnum (endmsg::String) (showDefault def) | ||||||
|     where |     where | ||||||
|       canfinish = not (null esPostings) && postingsBalanced esPostings |       canfinish = not (null esPostings) && postingsBalanced esPostings | ||||||
|  |       parseAccountOrDotOrNull :: String -> Bool -> String -> Maybe String | ||||||
|       parseAccountOrDotOrNull _  _ "."       = dbg1 $ Just "." -- . always signals end of txn |       parseAccountOrDotOrNull _  _ "."       = dbg1 $ Just "." -- . always signals end of txn | ||||||
|       parseAccountOrDotOrNull "" True ""     = dbg1 $ Just ""  -- when there's no default and txn is balanced, "" also signals end of txn |       parseAccountOrDotOrNull "" True ""     = dbg1 $ Just ""  -- when there's no default and txn is balanced, "" also signals end of txn | ||||||
|       parseAccountOrDotOrNull def@(_:_) _ "" = dbg1 $ Just def -- when there's a default, "" means use that |       parseAccountOrDotOrNull def@(_:_) _ "" = dbg1 $ Just def -- when there's a default, "" means use that | ||||||
|       parseAccountOrDotOrNull _ _ s          = dbg1 $ either (const Nothing) ((T.unpack <$>) . validateAccount) $ runParser (accountnamep <* eof) esJournal "" s -- otherwise, try to parse the input as an accountname |       parseAccountOrDotOrNull _ _ s          = dbg1 $ fmap T.unpack $ | ||||||
|  |         either (const Nothing) validateAccount $ | ||||||
|  |           runParser (accountnamep <* eof) esJournal "" (T.pack s) -- otherwise, try to parse the input as an accountname | ||||||
|  |         where | ||||||
|  |           validateAccount :: Text -> Maybe Text | ||||||
|  |           validateAccount t | no_new_accounts_ esOpts && not (t `elem` journalAccountNames esJournal) = Nothing | ||||||
|  |                             | otherwise = Just t | ||||||
|       dbg1 = id -- strace |       dbg1 = id -- strace | ||||||
|       validateAccount s | no_new_accounts_ esOpts && not (s `elem` journalAccountNames esJournal) = Nothing |  | ||||||
|                         | otherwise = Just s |  | ||||||
| 
 | 
 | ||||||
| amountAndCommentWizard EntryState{..} = do | amountAndCommentWizard EntryState{..} = do | ||||||
|   let pnum = length esPostings + 1 |   let pnum = length esPostings + 1 | ||||||
| @ -271,8 +276,8 @@ amountAndCommentWizard EntryState{..} = do | |||||||
|    maybeRestartTransaction $ |    maybeRestartTransaction $ | ||||||
|    line $ green $ printf "Amount  %d%s: " pnum (showDefault def) |    line $ green $ printf "Amount  %d%s: " pnum (showDefault def) | ||||||
|     where |     where | ||||||
|       parseAmountAndComment = either (const Nothing) Just . runParser (amountandcommentp <* eof) noDefCommodityJPS "" |       parseAmountAndComment = either (const Nothing) Just . runParser (amountandcommentp <* eof) nodefcommodityj "" . T.pack | ||||||
|       noDefCommodityJPS = esJournal{jparsedefaultcommodity=Nothing} |       nodefcommodityj = esJournal{jparsedefaultcommodity=Nothing} | ||||||
|       amountandcommentp :: Monad m => JournalParser m (Amount, String) |       amountandcommentp :: Monad m => JournalParser m (Amount, String) | ||||||
|       amountandcommentp = do |       amountandcommentp = do | ||||||
|         a <- amountp |         a <- amountp | ||||||
| @ -378,7 +383,7 @@ ensureOneNewlineTerminated = (++"\n") . reverse . dropWhile (=='\n') . reverse | |||||||
| registerFromString :: String -> IO String | registerFromString :: String -> IO String | ||||||
| registerFromString s = do | registerFromString s = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   j <- readJournal' s |   j <- readJournal' $ T.pack s | ||||||
|   return $ postingsReportAsText opts $ postingsReport ropts (queryFromOpts d ropts) j |   return $ postingsReportAsText opts $ postingsReport ropts (queryFromOpts d ropts) j | ||||||
|       where |       where | ||||||
|         ropts = defreportopts{empty_=True} |         ropts = defreportopts{empty_=True} | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| {-# LANGUAGE CPP, ScopedTypeVariables, DeriveDataTypeable, FlexibleContexts #-} |  | ||||||
| {-| | {-| | ||||||
| 
 | 
 | ||||||
| Common cmdargs modes and flags, a command-line options type, and | Common cmdargs modes and flags, a command-line options type, and | ||||||
| @ -6,6 +5,8 @@ related utilities used by hledger commands. | |||||||
| 
 | 
 | ||||||
| -} | -} | ||||||
| 
 | 
 | ||||||
|  | {-# LANGUAGE CPP, ScopedTypeVariables, DeriveDataTypeable, FlexibleContexts #-} | ||||||
|  | 
 | ||||||
| module Hledger.Cli.CliOptions ( | module Hledger.Cli.CliOptions ( | ||||||
| 
 | 
 | ||||||
|   -- * cmdargs flags & modes |   -- * cmdargs flags & modes | ||||||
| @ -71,6 +72,8 @@ import Data.Functor.Compat ((<$>)) | |||||||
| import Data.List.Compat | import Data.List.Compat | ||||||
| import Data.List.Split (splitOneOf) | import Data.List.Split (splitOneOf) | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
|  | -- import Data.Text (Text) | ||||||
|  | import qualified Data.Text as T | ||||||
| import Safe | import Safe | ||||||
| import System.Console.CmdArgs | import System.Console.CmdArgs | ||||||
| import System.Console.CmdArgs.Explicit | import System.Console.CmdArgs.Explicit | ||||||
| @ -384,7 +387,7 @@ getCliOpts mode' = do | |||||||
| 
 | 
 | ||||||
| -- | Get the account name aliases from options, if any. | -- | Get the account name aliases from options, if any. | ||||||
| aliasesFromOpts :: CliOpts -> [AccountAlias] | aliasesFromOpts :: CliOpts -> [AccountAlias] | ||||||
| aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp () ("--alias "++quoteIfNeeded a) a) | aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp () ("--alias "++quoteIfNeeded a) $ T.pack a) | ||||||
|                   . alias_ |                   . alias_ | ||||||
| 
 | 
 | ||||||
| -- | Get the (tilde-expanded, absolute) journal file path from | -- | Get the (tilde-expanded, absolute) journal file path from | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| {-# LANGUAGE CPP #-} |  | ||||||
| {-| | {-| | ||||||
| 
 | 
 | ||||||
| A ledger-compatible @register@ command. | A ledger-compatible @register@ command. | ||||||
| 
 | 
 | ||||||
| -} | -} | ||||||
| 
 | 
 | ||||||
|  | {-# LANGUAGE CPP, OverloadedStrings #-} | ||||||
|  | 
 | ||||||
| module Hledger.Cli.Register ( | module Hledger.Cli.Register ( | ||||||
|   registermode |   registermode | ||||||
|  ,register |  ,register | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user