-- | Commodity parsers. Commodity names fall into three groups: -- -- * Level 1 commodities. These can have a broad selection of -- characters, including spaces. The downside is that they need to be -- quoted when they appear in a file. (They don't have to be quoted -- from the command line, presuming that a single command line -- argument captures the entire commodity name and nothing else.) -- -- * Level 2 commodities. The first sub-commodity begins with a letter -- or a symbol. All other characters may be nearly any other -- character. Spaces however are not permitted. -- -- * Level 3 commodities. All charcters must be letters or symbols. In -- addition, the first character cannot be a @+@ or a @-@. module Penny.Copper.Commodity ( -- * Level 1 commodities lvl1Char, lvl1Cmdty, quotedLvl1Cmdty, commandLineCmdty, -- * Level 2 commodities lvl2FirstChar, lvl2OtherChars, lvl2Cmdty, -- * Level 3 commodities lvl3FirstChar, lvl3OtherChars, lvl3Cmdty, -- * Helpers when parsing from a file leftSideCmdty, rightSideCmdty, -- * Rendering renderQuotedLvl1, renderLvl2, renderLvl3 ) where import Control.Applicative ((<*>), (<$>), (*>), (<|>)) import Control.Monad (guard) import Data.Text ( pack, Text, cons, snoc, singleton ) import Text.Parsec ( satisfy, many, char, sepBy1, many1, (), between, option, sepBy ) import Text.Parsec.Text ( Parser ) import qualified Penny.Lincoln.Bits as B import Data.List.NonEmpty (NonEmpty((:|)), fromList) import Penny.Copper.Util (listIsOK, firstCharOfListIsOK) import qualified Penny.Copper.Util as U import qualified Penny.Lincoln.HasText as HT import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ), unsafeTextNonEmpty ) -- | Most liberal set of letters allowed in a commodity. lvl1Char :: Char -> Bool lvl1Char c = (category || specific) && notBanned where category = U.rangeLettersToSymbols c specific = c == ' ' notBanned = not $ c `elem` ['"', ':'] -- | A sub commodity comprised of the most liberal characters. lvl1SubCmdty :: Parser B.SubCommodity lvl1SubCmdty = f <$> m "sub commodity" where m = many1 (satisfy lvl1Char) f cs = B.SubCommodity (TextNonEmpty (head cs) (pack $ tail cs)) -- | A commodity that might have spaces inside of the name. To parse -- this when it is in a ledger file, it must be quoted; use -- quotedLvl1Cmdty for that. This parser can be used directly for values -- entered from the command line. lvl1Cmdty :: Parser B.Commodity lvl1Cmdty = (B.Commodity . fromList) <$> sepBy1 lvl1SubCmdty (char ':') "commodity with spaces" -- | A commodity that may have spaces in the name; is wrapped inside -- of double quotes. quotedLvl1Cmdty :: Parser B.Commodity quotedLvl1Cmdty = between q q lvl1Cmdty "quoted commodity" where q = char '"' -- | Allows only letters and symbols. lvl2FirstChar :: Char -> Bool lvl2FirstChar c = U.rangeLetters c || U.rangeMathCurrency c lvl2OtherChars :: Char -> Bool lvl2OtherChars c = category && notBanned where category = U.rangeLettersToSymbols c notBanned = not $ c `elem` ['"', ':'] lvl2FirstSubCmdty :: Parser B.SubCommodity lvl2FirstSubCmdty = f <$> firstLet <*> restLet e where e = "sub commodity, first character is letter or symbol" firstLet = satisfy lvl2FirstChar restLet = many (satisfy lvl2OtherChars) f l1 lr = B.SubCommodity (TextNonEmpty l1 (pack lr)) lvl2OtherSubCmdty :: Parser B.SubCommodity lvl2OtherSubCmdty = f <$> ls e where e = "sub commodity" ls = many1 (satisfy lvl2OtherChars) f = B.SubCommodity . unsafeTextNonEmpty lvl2Cmdty :: Parser B.Commodity lvl2Cmdty = f <$> firstSub <*> restSubs e where e = "commodity, first character is letter or symbol" firstSub = lvl2FirstSubCmdty restSubs = option [] $ char ':' *> sepBy1 lvl2OtherSubCmdty (char ':') f s1 sr = B.Commodity (s1 :| sr) lvl3OtherChars :: Char -> Bool lvl3OtherChars c = U.rangeLetters c || U.rangeMathCurrency c lvl3FirstChar :: Char -> Bool lvl3FirstChar c = lvl3OtherChars c && (not $ c `elem` "+-") lvl3FirstSubCmdty :: Parser B.SubCommodity lvl3FirstSubCmdty = f <$> c <*> cs e where e = "first sub commodity, letters and symbols only, " ++ "first character not a + or -" f c1 cr = B.SubCommodity (TextNonEmpty c1 (pack cr)) c = satisfy lvl3FirstChar cs = many (satisfy lvl3OtherChars) lvl3OtherSubCmdty :: Parser B.SubCommodity lvl3OtherSubCmdty = f <$> ls e where e = "sub commodity, letters and symbols only" f = B.SubCommodity . unsafeTextNonEmpty ls = many1 (satisfy lvl3OtherChars) lvl3Cmdty :: Parser B.Commodity lvl3Cmdty = f <$> p1 <*> pr e where f cf cs = B.Commodity (cf :| cs) p1 = lvl3FirstSubCmdty pr = option [] $ char ':' *> sepBy lvl3OtherSubCmdty (char ':') e = "commodity, letters and symbols only" -- | A commodity being read in from the command line, where the -- commodity is guaranteed to be the only thing to parse. commandLineCmdty :: Parser B.Commodity commandLineCmdty = lvl1Cmdty -- | A commodity on the left side of a quantity in a ledger file. leftSideCmdty :: Parser B.Commodity leftSideCmdty = quotedLvl1Cmdty <|> lvl3Cmdty "commodity to the left of the quantity" -- | A commodity on the right side of a quantity in a ledger file. rightSideCmdty :: Parser B.Commodity rightSideCmdty = quotedLvl1Cmdty <|> lvl2Cmdty "commodity to the right of the quantity" -- | Render a quoted Level 1 commodity. Fails if any character does -- not satisfy lvl1Char. renderQuotedLvl1 :: B.Commodity -> Maybe Text renderQuotedLvl1 ca@(B.Commodity c) = do guard $ listIsOK lvl1Char ca return $ '"' `cons` HT.text (HT.Delimited (singleton ':') (HT.textList c)) `snoc` '"' -- | Render a Level 2 commodity. Fails if the first character is not a -- letter or a symbol, or if any other character is a space. renderLvl2 :: B.Commodity -> Maybe Text renderLvl2 (B.Commodity c) = do guard $ firstCharOfListIsOK lvl2FirstChar c guard $ listIsOK lvl2OtherChars c return $ HT.text (HT.Delimited (singleton ':') (HT.textList c)) -- | Render a Level 3 commodity. Fails if any character is not a -- letter or a symbol. renderLvl3 :: B.Commodity -> Maybe Text renderLvl3 (B.Commodity c) = do guard $ listIsOK lvl3OtherChars c guard $ firstCharOfListIsOK lvl3FirstChar c return $ HT.text (HT.Delimited (singleton ':') (HT.textList c))