packages feed

penny-0.24.0.0: tests/Copper/Gen/Parsers.hs

{-# LANGUAGE OverloadedStrings #-}
module Copper.Gen.Parsers where

import Control.Applicative ((<$>), (<*>), pure)
import Control.Arrow (first)
import qualified Control.Monad.Trans.Writer as W
import Control.Monad.Trans.Class (lift)
import Data.List (nubBy, intersperse)
import qualified Penny.Lincoln as L
import qualified Penny.Copper as C
import qualified Penny.Copper.Render as CR
import qualified Data.Time as Time
import Data.Maybe (fromMaybe, catMaybes)
import Data.Monoid ((<>))
import qualified Penny.Steel.Sums as S

import qualified System.Random.Shuffle as Shuffle
import qualified Lincoln as TL
import qualified Copper.Gen.Terminals as T
import qualified Data.List.NonEmpty as NE
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.Text as X
import Data.Text (pack, snoc, cons)
import qualified Test.QuickCheck.Gen as G
import Test.QuickCheck.Gen (Gen)
import qualified Test.QuickCheck as Q
import Test.QuickCheck (arbitrary, suchThat)

--
-- * Helpers
--

optional :: Gen (a, X.Text) -> Gen (Maybe a, X.Text)
optional g = do
  b <- arbitrary
  if b
    then fmap (first Just) g
    else return (Nothing, X.empty)

interleave :: Gen (Maybe a) -> [a] -> Gen [a]
interleave g ls =
  let withGens = intersperse g (map (return . Just) ls)
  in fmap catMaybes . sequence $ withGens

leadingZero :: Show s => s -> X.Text
leadingZero s =
  let str = show s
  in if length str > 1
      then pack str
      else pack ('0':str)

surround
  :: Char -> Char -> (X.Text -> a) -> Gen Char -> Gen (a, X.Text)
surround o c m g = do
  x <- fmap pack $ G.listOf g
  return (m x, o `cons` x `snoc` c)

shuffle :: [a] -> Gen [a]
shuffle [] = return []
shuffle ls = G.MkGen $ \g _ -> Shuffle.shuffle' ls (length ls) g

white :: Gen X.Text
white = fmap pack (G.listOf T.white)

--
-- * Accounts
--

lvl1SubAcct :: Gen (L.SubAccount, X.Text)
lvl1SubAcct = do
  txt <- fmap pack $ G.listOf1 T.lvl1AcctChar
  return (L.SubAccount txt, txt)

lvl1FirstSubAcct :: Gen (L.SubAccount, X.Text)
lvl1FirstSubAcct = lvl1SubAcct

lvl1OtherSubAcct :: Gen (L.SubAccount, X.Text)
lvl1OtherSubAcct = do
  (ac, gtxt) <- lvl1SubAcct
  return (ac, ':' `cons` gtxt)

lvl1Acct :: Gen (L.Account, X.Text)
lvl1Acct = do
  s1 <- lvl1SubAcct
  sr <- G.listOf lvl1OtherSubAcct
  let subs = fst s1 : fmap fst sr
      txts = X.concat . map snd $ s1:sr
  return (L.Account subs, txts)

quotedLvl1Acct :: Gen (L.Account, X.Text)
quotedLvl1Acct = fmap f lvl1Acct
  where
    f (ac, gTxt) = (ac, '{' `cons` gTxt `snoc` '}')

lvl2FirstSubAcct :: Gen (L.SubAccount, X.Text)
lvl2FirstSubAcct = do
  l1 <- T.letter
  ls <- fmap pack $ G.listOf T.lvl2AcctOtherChar
  let txt = l1 `cons` ls
  return (L.SubAccount txt, txt)

lvl2OtherSubAcct :: Gen (L.SubAccount, X.Text)
lvl2OtherSubAcct = do
  cs <- fmap pack $ G.listOf1 T.lvl2AcctOtherChar
  return (L.SubAccount cs, ':' `cons` cs)

lvl2Acct :: Gen (L.Account, X.Text)
lvl2Acct = do
  a1 <- lvl2FirstSubAcct
  as <- G.listOf lvl2OtherSubAcct
  let a = L.Account (map fst (a1:as))
      g = X.concat . map snd $ (a1:as)
  return (a, g)

ledgerAcct :: Gen (L.Account, X.Text)
ledgerAcct = G.oneof [quotedLvl1Acct, lvl2Acct]

--
-- * Commodities
--

data QuotedLvl1Cmdty = QuotedLvl1Cmdty L.Commodity X.Text
  deriving Show

data Lvl2Cmdty = Lvl2Cmdty L.Commodity X.Text
  deriving Show

data Lvl3Cmdty = Lvl3Cmdty L.Commodity X.Text
  deriving Show

data Cmdty
  = L1 QuotedLvl1Cmdty
  | L2 Lvl2Cmdty
  | L3 Lvl3Cmdty
  deriving Show

unwrapCmdty :: Cmdty -> (L.Commodity, X.Text)
unwrapCmdty c = case c of
  L1 (QuotedLvl1Cmdty y x) -> (y, x)
  L2 (Lvl2Cmdty y x) -> (y, x)
  L3 (Lvl3Cmdty y x) -> (y, x)

lvl1Cmdty :: Gen (L.Commodity, X.Text)
lvl1Cmdty = do
  cs <- fmap X.pack (G.listOf1 T.lvl1CmdtyChar)
  return (L.Commodity cs, cs)

quotedLvl1Cmdty :: Gen QuotedLvl1Cmdty
quotedLvl1Cmdty = fmap f lvl1Cmdty
  where
    f (c, gx) = QuotedLvl1Cmdty c ('"' `cons` gx `snoc` '"')

lvl2Cmdty :: Gen Lvl2Cmdty
lvl2Cmdty = do
  c1 <- T.lvl2CmdtyFirstChar
  cs <- fmap pack $ G.listOf T.lvl2CmdtyOtherChar
  return (Lvl2Cmdty (L.Commodity $ c1 `cons` cs) (c1 `cons` cs))

lvl3Cmdty :: Gen Lvl3Cmdty
lvl3Cmdty = do
  cs <- fmap pack $ G.listOf1 T.lvl3CmdtyChar
  return (Lvl3Cmdty (L.Commodity cs) cs)

genCmdty :: Gen Cmdty
genCmdty = G.oneof [ fmap L1 quotedLvl1Cmdty
                   , fmap L2 lvl2Cmdty
                   , fmap L3 lvl3Cmdty ]

uniqueCmdtys :: Gen [Cmdty]
uniqueCmdtys = G.sized $ \s -> do
  ls <- (G.resize (min s 3) $ G.listOf1 genCmdty)
  let f c1 c2 = (fst . unwrapCmdty $ c1) == (fst . unwrapCmdty $ c2)
  return (nubBy f ls)


--
-- * Quantities
--


--
-- ## QtyRep
--

type Signif = Integer
type Places = Integer

class Ast a where
  ast :: Gen (a, X.Text)

instance Ast L.Digit where
  ast = Q.elements . map (\(d, c) -> (d, X.singleton c)) $
    zip [L.D0 .. L.D9] ['0'..'9']

genNonEmpty :: Gen a -> Gen (NonEmpty a)
genNonEmpty g = (:|) <$> g <*> Q.listOf g

instance Ast L.DigitList where
  ast = fmap f (genNonEmpty ast)
    where
      f ls = ( L.DigitList . fmap fst $ ls
             , X.concat . NE.toList . fmap snd $ ls)

instance Ast L.PeriodGrp where
  ast
    = fmap f
    . Q.elements
    $ [(' ', L.PGSpace), ('\x2009', L.PGThinSpace), (',', L.PGComma)]
    where
      f (c, e) = (e, X.singleton c)

instance Ast L.CommaGrp where
  ast = fmap f . Q.elements
    $ [(' ', L.CGSpace), ('\x2009', L.CGThinSpace), ('.', L.CGPeriod)]
    where
      f (c, e) = (e, X.singleton c)

instance Ast a => Ast (L.GroupedDigits a) where
  ast = do
    (d1a, d1x) <- ast
    (dsa, dsx) <- fmap unzip $ Q.listOf ast
    (ssa, ssx) <- fmap unzip $ Q.vectorOf (length dsa) ast
    let ra = L.GroupedDigits d1a (zip ssa dsa)
        rx = d1x <> (X.concat (map toTxt (zip ssx dsx)))
        toTxt (x1, x2) = x1 <> x2
    return (ra, rx)


wholeFrac
  :: (L.Digits a, Ast a)
  => L.Radix
  -> Gen (L.WholeFrac a, X.Text)
wholeFrac rad = do
  let hasNonZero (x, y) =
        TL.digitsHasNonZero (fst x) || TL.digitsHasNonZero (fst y)
  ((wa, wx), (fa, fx)) <- ((,) <$> ast <*> ast) `suchThat` hasNonZero
  let r = fromMaybe (error "wholeFracDigits: wholeFrac failed")
        (L.wholeFrac wa fa)
      rx = wx <> repRadix rad <> fx
  return (r, rx)

instance (Ast a, L.Digits a) => Ast (L.WholeOnly a) where
  ast = do
    (wa, wx) <- ast `suchThat` (TL.digitsHasNonZero . fst)
    let r = fromMaybe (error "generating WholeOnly failed")
                      (L.wholeOnly wa)
    return (r, wx)

wholeOrFrac
  :: (L.Digits a, Ast a)
  => L.Radix
  -> Gen (L.WholeOrFrac a, X.Text)
wholeOrFrac r = do
  left <- arbitrary
  if left
    then do
      (w, x) <- ast
      return (L.WholeOrFrac . Left $ w, x)
    else do
      (w, x) <- wholeFrac r
      return (L.WholeOrFrac . Right $ w, x)

instance Ast L.QtyRep where
  ast = do
    (qr, x) <- do
      grouped <- arbitrary
      if grouped
        then do
          left <- arbitrary
          if left
            then do
              (wf, x) <- wholeOrFrac L.Period
              return (L.QGrouped (Left wf), x)
            else do
              (wf, x) <- wholeOrFrac L.Comma
              return (L.QGrouped (Right wf), x)
        else do
          rx <- arbitrary
          (wf, x) <- wholeOrFrac rx
          return (L.QNoGrouping wf rx, x)
    let (b, e) = CR.quoteQtyRep qr
    return (qr, b <> x <> e)

repRadix :: L.Radix -> X.Text
repRadix r = X.singleton $ case r of
  L.Comma -> ','
  L.Period -> '.'


-- | Generate a random representation for a Qty.
representQty :: L.Qty -> Gen L.QtyRep
representQty q = L.qtyToRep <$> arbitrary <*> pure q

renderQty :: L.Qty -> Gen (L.QtyRep, X.Text)
renderQty q = do
  qr <- representQty q
  return (qr, CR.qtyRep qr)


--
-- * Amounts
--

spaceBetween :: X.Text -> L.SpaceBetween
spaceBetween x = if X.null x then L.NoSpaceBetween else L.SpaceBetween

leftCmdtyLvl1Amt
  :: QuotedLvl1Cmdty
  -> (L.QtyRep, X.Text)
  -> Gen ((L.Amount L.QtyRep, X.Text), L.SpaceBetween)
leftCmdtyLvl1Amt (QuotedLvl1Cmdty c xc) (q, xq) = do
  ws <- white
  let amt = L.Amount q c
  return ((amt, X.concat [xc, ws, xq]), spaceBetween ws)


leftCmdtyLvl3Amt
  :: Lvl3Cmdty
  -> (L.QtyRep, X.Text)
  -> Gen ((L.Amount L.QtyRep, X.Text), L.SpaceBetween)
leftCmdtyLvl3Amt (Lvl3Cmdty c xc) (q, xq) = do
  ws <- white
  let amt = L.Amount q c
  return ((amt, X.concat [xc, ws, xq]), spaceBetween ws)


leftSideCmdtyAmt
  :: Either QuotedLvl1Cmdty Lvl3Cmdty
  -> (L.QtyRep, X.Text)
  -> Gen ((L.Amount L.QtyRep, X.Text), L.SpaceBetween)
leftSideCmdtyAmt c q = case c of
  Left l1 -> leftCmdtyLvl1Amt l1 q
  Right l3 -> leftCmdtyLvl3Amt l3 q

rightSideCmdty :: Gen (Either QuotedLvl1Cmdty Lvl2Cmdty)
rightSideCmdty = G.oneof
  [ fmap Left quotedLvl1Cmdty
  , fmap Right lvl2Cmdty ]


rightSideCmdtyAmt
  :: Either QuotedLvl1Cmdty Lvl2Cmdty
  -> (L.QtyRep, X.Text)
  -> Gen ((L.Amount L.QtyRep, X.Text), L.SpaceBetween)
rightSideCmdtyAmt cty (q, xq) = do
  ws <- white
  let (c, xc) = case cty of
        Left (QuotedLvl1Cmdty ct x) -> (ct, x)
        Right (Lvl2Cmdty ct x) -> (ct, x)
      xr = X.concat [xq, ws, xc]
      amt = L.Amount q c
  return ((amt, xr), spaceBetween ws)


amount
  :: Cmdty
  -> (L.QtyRep, X.Text)
  -> Gen ((L.Amount L.QtyRep, X.Text), L.SpaceBetween, L.Side)
amount c q =
  let mkTrip sd (p, b) = (p, b, sd)
  in case c of
      L1 l1 -> G.oneof
        [ fmap (mkTrip L.CommodityOnLeft) (leftSideCmdtyAmt (Left l1) q)
        , fmap (mkTrip L.CommodityOnRight) (rightSideCmdtyAmt (Left l1) q)
        ]
      L2 l2 -> fmap (mkTrip L.CommodityOnRight)
                    (rightSideCmdtyAmt (Right l2) q)
      L3 l3 -> fmap (mkTrip L.CommodityOnLeft)
                    (leftSideCmdtyAmt (Right l3) q)

--
-- * Comments
--

comment :: Gen (C.Comment, X.Text)
comment = do
  x <- fmap pack $ G.listOf T.nonNewline
  ws <- white
  let txt = ('#' `cons` x `snoc` '\n') `X.append` ws
  return (C.Comment x, txt)

--
-- * DateTime
--

year :: Gen (Integer, X.Text)
year = do
  i <- G.choose (1492, 2400)
  return (i, pack . show $ i)

month :: Gen (Int, X.Text)
month = do
  i <- G.choose (1, 12)
  return (i, leadingZero i)


day :: Gen (Int, X.Text)
day = do
  i <- G.choose (1, 28)
  return (i, leadingZero i)

throwMaybe :: Monad m => String -> Maybe a -> m a
throwMaybe s = maybe (fail $ "could not generate " ++ s) return

date :: Gen (Time.Day, X.Text)
date = do
  (y, yTxt) <- year
  (m, mTxt) <- month
  (d, dTxt) <- day
  dt <- throwMaybe "date" $ Time.fromGregorianValid y m d
  s1 <- T.dateSep
  s2 <- T.dateSep
  let x = (yTxt `snoc` s1) `X.append` (mTxt `snoc` s2) `X.append` dTxt
  return (dt, x)


hours :: Gen (L.Hours, X.Text)
hours =  do
  h <- G.choose (0, 23)
  hr <- throwMaybe "hours" (L.intToHours h)
  let x = leadingZero h
  return (hr, x)

minutes :: Gen (L.Minutes, X.Text)
minutes = do
  m <- G.choose (0, 59)
  mi <- throwMaybe "minutes" (L.intToMinutes m)
  return (mi, ':' `cons` leadingZero m)

seconds :: Gen (L.Seconds, X.Text)
seconds = do
  s <- G.choose (0, 59)
  let _types = s :: Int
  se <- throwMaybe "seconds" (L.intToSeconds (fromIntegral s))
  return (se, ':' `cons` leadingZero s)


time :: Gen ((L.Hours, L.Minutes, Maybe L.Seconds), X.Text)
time = do
  (h, ht) <- hours
  (m, mt) <- minutes
  (s, st) <- optional seconds
  let x = ht `X.append` mt `X.append` st
  return ((h, m, s), x)

tzSign :: Gen (Int -> Int, X.Text)
tzSign = do
  s <- Q.arbitrary
  return $ if s
    then (id, X.singleton '+')
    else (negate, X.singleton '-')

tzNumber :: Gen (Int, X.Text)
tzNumber = do
  i <- G.choose (0, 840)
  return (i, X.justifyRight 4 '0' (pack . show $ i))

timeZone :: Gen (L.TimeZoneOffset, X.Text)
timeZone = do
  (s, st) <- tzSign
  (n, nt) <- tzNumber
  o <- throwMaybe "time zone" (L.minsToOffset (s n))
  return (o, st `X.append` nt)

timeWithZone ::
  Gen
  ((L.Hours, L.Minutes, Maybe L.Seconds, Maybe L.TimeZoneOffset), X.Text)
timeWithZone = do
  ((h, m, mays), xt) <- time
  ws <- white
  (o, xo) <- optional timeZone
  let x = xt `X.append` ws `X.append` xo
  return ((h, m, mays, o), x)

dateTime :: Gen (L.DateTime, X.Text)
dateTime = do
  (d, xd) <- date
  w <- white
  (t, xt) <- optional timeWithZone
  let ((h, m, s), tz) = case t of
        Nothing -> (L.midnight, L.noOffset)
        Just (hr, mn, mayS, mayTz) ->
          let sec = fromMaybe L.zeroSeconds mayS
              z = fromMaybe L.noOffset mayTz
          in ((hr, mn, sec), z)
  return (L.DateTime d h m s tz, xd `X.append` w `X.append` xt)

--
-- * DrCr
--


debit :: Gen (L.DrCr, X.Text)
debit = (\x -> (L.Debit, X.singleton x)) <$> T.lessThan

credit :: Gen (L.DrCr, X.Text)
credit = (\x -> (L.Credit, X.singleton x)) <$> T.greaterThan

drCr :: Gen (L.DrCr, X.Text)
drCr = G.oneof [debit, credit]

--
-- * Entry
--


entry
  :: Cmdty
  -> (L.DrCr, X.Text)
  -> (L.QtyRep, X.Text)
  -> Gen ((L.Entry L.QtyRep, X.Text), L.SpaceBetween, L.Side)
entry c (d, xd) q = f <$> white <*> amount c q
  where
    f w ((a, xa), sb, sd) = ((L.Entry d a, x), sb, sd)
      where
        x = X.concat [xd, w, xa]


genEntryGroup
  :: Cmdty
  -> Gen [((L.Entry L.QtyRep, X.Text), L.SpaceBetween, L.Side)]
genEntryGroup c = do
  dr <- debit
  cr <- credit
  (_, dq, cq) <- TL.genBalQtys
  dqWithX <- mapM renderQty dq
  cqWithX <- mapM renderQty cq
  des <- mapM (entry c dr) dqWithX
  ces <- mapM (entry c cr) cqWithX
  return $ des ++ ces


genEntryGroups
  :: Gen [((L.Entry L.QtyRep, X.Text), L.SpaceBetween, L.Side)]
genEntryGroups = do
  cs <- uniqueCmdtys
  fmap concat . mapM genEntryGroup $ cs


--
-- * Flag
--

flag :: Gen (L.Flag, X.Text)
flag = surround '[' ']' L.Flag T.flagChar


--
-- * Memo
--

postingMemoLine :: Gen (X.Text, X.Text)
postingMemoLine = do
  me <- fmap pack $ G.listOf T.nonNewline
  ws <- white
  return (me, '\'' `cons` me `snoc` '\n' `X.append` ws)

postingMemo :: Gen (L.Memo, X.Text)
postingMemo = do
  me <- G.listOf1 postingMemoLine
  let mem = L.Memo . map fst $ me
      ren = X.concat .  map snd $ me
  return (mem, ren)

transactionMemoLine :: Gen (X.Text, X.Text)
transactionMemoLine = do
  me <- fmap pack $ G.listOf T.nonNewline
  ws <- white
  return (me, ';' `cons` me `snoc` '\n' `X.append` ws)

transactionMemo :: Gen (L.Memo, X.Text)
transactionMemo = do
  me <- G.listOf1 transactionMemoLine
  let mem = L.Memo . map fst $ me
      ren = X.concat . map snd $ me
  return (mem, ren)

--
-- * Number
--

number :: Gen (L.Number, X.Text)
number = surround '(' ')' L.Number T.numberChar

--
-- * Payee
--

lvl1Payee :: Gen (L.Payee, X.Text)
lvl1Payee = do
  x <- fmap pack $ G.listOf T.quotedPayeeChar
  return (L.Payee x, x)

quotedLvl1Payee :: Gen (L.Payee, X.Text)
quotedLvl1Payee = do
  (p, xp) <- lvl1Payee
  return (p, '~' `cons` xp `snoc` '~')

lvl2Payee :: Gen (L.Payee, X.Text)
lvl2Payee = do
  l1 <- T.letter
  ls <- fmap pack $ G.listOf T.nonNewline
  let x = l1 `cons` ls
  return (L.Payee x, x)


--
-- * Price
--


fromCmdty
  :: Either QuotedLvl1Cmdty Lvl2Cmdty
  -> (L.From, X.Text)
fromCmdty e = case e of
  Left (QuotedLvl1Cmdty c x) -> (L.From c, x)
  Right (Lvl2Cmdty c x) -> (L.From c, x)


price :: Gen (L.PricePoint, X.Text)
price = do
  atSign <- T.atSign
  wsAt <- white
  fr <- G.oneof [ fmap Left lvl3Cmdty, fmap Right quotedLvl1Cmdty ]
  (dt, xdt) <- dateTime
  ws1 <- white
  let (fc, xfc) = case fr of
        Left (Lvl3Cmdty c x) -> (c, x)
        Right (QuotedLvl1Cmdty c x) -> (c, x)
  ws2 <- fmap pack (G.listOf1 T.white)
  qty <- arbitrary
  q <- renderQty qty
  let pdct x = (fst . unwrapCmdty $ x) /= fc
  toCmdty <- Q.suchThat genCmdty pdct
  (((L.Amount toQ t), xam), sb, sd) <- amount toCmdty q
  let (to, cpu) = (L.To t, L.CountPerUnit toQ)
  p <- throwMaybe "price" (L.newPrice (L.From fc) to cpu)
  ws3 <- white
  let pp = L.PricePoint dt p (Just sd) (Just sb) Nothing
      x = X.concat [ X.singleton atSign, wsAt, xdt, ws1, xfc,
                     ws2, xam, X.singleton '\n', ws3]
  return (pp, x)


--
-- * Tags
--

tag :: Gen (L.Tag, X.Text)
tag = do
  x <- fmap pack $ G.listOf T.tagChar
  w <- white
  return (L.Tag x, '*' `cons` x `X.append` w)

tags :: Gen (L.Tags, X.Text)
tags = fmap f $ G.listOf1 tag
  where
    f ls = (L.Tags . map fst $ ls, X.concat . map snd $ ls)

--
-- * TopLine
--

topLinePayee :: Gen (L.Payee, X.Text)
topLinePayee = G.oneof [quotedLvl1Payee, lvl2Payee]

topLineFlagNum :: Gen ((Maybe L.Flag, Maybe L.Number), X.Text)
topLineFlagNum = f <$> optional flag <*> white <*> optional number
  where
    f (fl, xfl) ws (nu, xnu) = ((fl, nu), X.concat [xfl, ws, xnu])


topLineCore :: Gen (L.TopLineCore, X.Text)
topLineCore = do
  (me, xme) <- optional transactionMemo
  (dt, xdt) <- dateTime
  w1 <- white
  ((fl, nu), xfn) <- topLineFlagNum
  w2 <- white
  (pa, xp) <- optional topLinePayee
  w3 <- white
  let tl = L.TopLineCore dt nu fl pa me
      x = X.concat [xme, xdt, w1, xfn, w2, xp, X.singleton '\n', w3]
  return (tl, x)


--
-- * Posting
--


liftMaybeGen :: Gen (a, X.Text) -> W.WriterT [X.Text] Gen (Maybe a)
liftMaybeGen g = do
  b <- lift arbitrary
  if b
    then do
      (r, x) <- lift g
      ws <- lift white
      W.tell [X.append x ws]
      return (Just r)
    else return Nothing

flagNumPayee
  :: Gen ((Maybe L.Flag, Maybe L.Number, Maybe L.Payee), X.Text)
flagNumPayee = do
  (r, ls) <- W.runWriterT
             $ (,,)
             <$> liftMaybeGen flag
             <*> liftMaybeGen number
             <*> liftMaybeGen quotedLvl1Payee
  ls' <- shuffle ls
  return (r, X.concat ls')

postingAcct :: Gen (L.Account, X.Text)
postingAcct = G.oneof [quotedLvl1Acct, lvl2Acct]


posting
 :: Maybe ((L.Entry L.QtyRep, X.Text), L.SpaceBetween, L.Side)
 -> Gen ((L.PostingCore, X.Text), Maybe (L.Entry L.QtyRep))
posting mayEn = do
  (mayFnp, xfnp) <- optional flagNumPayee
  let (fl, nu, pa) = fromMaybe (Nothing, Nothing, Nothing) mayFnp
  ws1 <- white
  (ac, xa) <- postingAcct
  ws2 <- white
  doTags <- Q.arbitrary
  (ts, xt) <- if doTags
                then tags
                else return (L.Tags [], X.empty)
  doMemo <- Q.arbitrary
  (pm, xm) <- if doMemo
                then do
                      (mo, xMo) <- postingMemo
                      return (Just mo, xMo)
                else return (Nothing, X.empty)
  ws3 <- white
  ws4 <- white
  ws5 <- white
  let (en, xe, sd, sb) = case mayEn of
        Nothing -> (Nothing, X.empty, Nothing, Nothing)
        Just ((jEn, jXe), jSd, jSb) -> (Just jEn, jXe, Just jSd, Just jSb)
      po = L.PostingCore pa nu fl ac ts pm sb sd
      txt = X.concat
            [ xfnp, ws1, xa, ws2, xt, ws3, xe,
              X.singleton '\n', ws4, xm, ws5]
  return ((po, txt), en)


postings :: Gen [((L.PostingCore, X.Text), Maybe (L.Entry L.QtyRep))]
postings = do
  egs <- genEntryGroups
  removeEn <- Q.arbitrary
  ps <- if removeEn
        then do
          p1 <- posting Nothing
          ps <- mapM posting . map Just . tail $ egs
          return (p1:ps)
        else mapM posting . map Just $ egs
  shuffle ps


--
-- * Transaction
--


transaction :: Gen ((L.TopLineCore, L.Ents L.PostingCore), X.Text)
transaction = do
  (tl, xtl) <- topLineCore
  pstgs <- postings
  let x = xtl `X.append` (X.concat . map (snd . fst) $ pstgs)
      es = map (\((pc, _), mayEn) -> (fmap Left mayEn, pc)) pstgs
      _types = es :: [ ( Maybe (Either (L.Entry L.QtyRep) a)
                       , L.PostingCore)]
  case L.ents es of
    Nothing -> fail $ "failed to create transaction: " ++ show pstgs
    Just r -> return ((tl, r), x)


--
-- * BlankLine
--

blankLine :: Gen (C.BlankLine, X.Text)
blankLine = fmap f white
  where
    f ws = (C.BlankLine, '\n' `cons` ws)


--
-- * Item
--


type TestItem
  = S.S4 (L.TopLineCore, L.Ents L.PostingCore)
         L.PricePoint
         C.Comment
         C.BlankLine

item :: Gen (TestItem, X.Text)
item = Q.oneof
  [ fmap (\(c, x) -> (S.S4c c, x)) comment
  , fmap (\(p, x) -> (S.S4b p, x)) price
  , fmap (\(t, x) -> (S.S4a t, x)) transaction
  , fmap (\(b, x) -> (S.S4d b, x)) blankLine
  ]

--
-- * Ledger
--

ledger :: Gen ([TestItem], X.Text)
ledger = f <$> white <*> Q.listOf item
  where
    f ws is = (map fst is
              , ws `X.append` (X.concat . map snd $ is))