diff --git a/TsParse.hs b/TsParse.hs
--- a/TsParse.hs
+++ b/TsParse.hs
@@ -26,6 +26,7 @@
     -- ** Basic types
     Dollars
   , Shares
+  , TxnType
 
     -- ** Transaction Detail By Source
   , BySource(..)
@@ -36,6 +37,7 @@
   , BySourcePosting(..)
 
     -- ** Transaction Detail By Fund
+  , FundName
   , ByFund(..)
   , ByFundBeginningBal(..)
   , ByFundGainLoss(..)
@@ -98,7 +100,7 @@
 genDollars = D.Decimal <$> pure 2 <*> genMantissa
 
 newtype DollarsRen = DollarsRen { unDollarsRen :: Rendered Dollars }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 instance Arbitrary DollarsRen where
   arbitrary = do
@@ -110,7 +112,7 @@
 genShares = D.Decimal <$> pure 4 <*> genMantissa
 
 newtype SharesRen = SharesRen { unSharesRen :: Rendered Dollars }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 instance Arbitrary SharesRen where
   arbitrary = do
@@ -140,7 +142,7 @@
 data Rendered a = Rendered
   { ast :: a
   , rendering :: String
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 
 genWord :: Gen (Rendered String)
@@ -165,7 +167,7 @@
   return $ Rendered (map ast ws) withSpaces
 
 newtype WordsRen = WordsRen { unWordsRen :: Rendered [String] }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 instance Arbitrary WordsRen where
   arbitrary = WordsRen <$> genWords
@@ -175,6 +177,16 @@
 
 #endif
 
+dollarSign :: Parser ()
+dollarSign = () <$ optional (P.char '$')
+
+space :: Parser ()
+space = () <$ optional (P.char ' ')
+
+digits :: Parser String
+digits = fmap (filter (/= ',')) (P.many1 (P.oneOf "0123456789,"))
+         <?> "digits"
+
 -- | Parses a single decimal value. Recognizes negative signs. Strips
 -- out dollar signs and commas.
 --
@@ -183,16 +195,18 @@
 -- consumes the entire string if there is one.
 decimal :: Parser D.Decimal
 decimal = do
-  ws <- fmap (map (filter (not . (`elem` "$, ")))) words
-  (isNeg, num) <- case ws of
-    [] -> fail "empty string, cannot parse decimal"
-    x:[] -> return (False, x)
-    x:y:[] -> if x == "-"
-              then return (True, y)
-              else fail "could not parse decimal: too many words"
-    _ -> fail "could not parse decimal, too many words"
-  dec <- case readMaybe num of
-    Nothing -> fail $ "could not parse decimal: " ++ num
+  dollarSign
+  maybeNeg <- optional (P.char '-')
+  let isNeg = maybe False (const True) maybeNeg
+  dollarSign
+  space
+  dollarSign
+  whole <- digits
+  _ <- P.char '.'
+  frac <- digits
+  let numStr = whole ++ "." ++ frac
+  dec <- case readMaybe (whole ++ "." ++ frac) of
+    Nothing -> fail $ "could not parse decimal: " ++ numStr
     Just r -> return r
   return $ if isNeg then negate dec else dec
 
@@ -202,7 +216,7 @@
 prop_Decimal = testRendered decimal . unDecimalRen
 
 newtype DecimalRen = DecimalRen { unDecimalRen :: Rendered D.Decimal }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 instance Arbitrary DecimalRen where
   arbitrary = do
@@ -274,7 +288,7 @@
   c -> c
 
 newtype DayRen = DayRen { unDayRen :: Rendered T.Day }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 instance Arbitrary DayRen where
   arbitrary =
@@ -300,8 +314,7 @@
   _ -> fail $ "could not read string: " ++ s
 
 columnBreak :: Parser ()
-columnBreak
-  = () <$ P.char ' ' <* P.char ' ' <* P.many (P.char ' ')
+columnBreak = () <$ P.many (P.char ' ')
 
 
 class Pretty a where
@@ -319,22 +332,27 @@
 instance Pretty T.Day where
   pretty = Y.text . show
 
+-- | A list of words that indicates the transaction type.  Each string
+-- in this list will not have any spaces in it.
+type TxnType = [String]
+
 data BySourcePosting = BySourcePosting
   { bspPayrollOffice :: String
   , bspPostingDate :: T.Day
-  , bspTxnType :: [String]
+  , bspTxnType :: TxnType
   , bspTraditional :: Dollars
   , bspRoth :: Dollars
   , bspAutomatic :: Dollars
   , bspMatching :: Dollars
   , bspTotal :: Dollars
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
+
 data RenTxnBySource = RenTxnBySource
   { unRenTxnBySource :: Rendered BySourcePosting }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 
 instance Arbitrary RenTxnBySource where
@@ -350,11 +368,16 @@
     let rAst = BySourcePosting (ast rPayroll) (ast rPostingDate)
           (ast rTxnType) (ast rTrad) (ast rRoth) (ast rAuto)
           (ast rMatch) (ast rTot)
-    ren <- columns [ rendering rPayroll, rendering rPostingDate,
-                     rendering rTxnType,
-                     rendering rTrad,
-                     rendering rRoth, rendering rAuto,
-                     rendering rMatch, rendering rTot ]
+        clmns = [ return $ rendering rPayroll       , columnSpaceOne
+                , return $ rendering rPostingDate   , columnSpaceOne
+                , return $ rendering rTxnType       , columnSpaceTwo
+                , return $ rendering rTrad          , columnSpaceOne
+                , return $ rendering rRoth          , columnSpaceOne
+                , return $ rendering rAuto          , columnSpaceOne
+                , return $ rendering rMatch         , columnSpaceOne
+                , return $ rendering rTot
+                ]
+    ren <- fmap concat . sequence $ clmns
     leader <- fmap (flip replicate ' ') Q.arbitrarySizedIntegral
     return . RenTxnBySource $ Rendered rAst (leader ++ ren ++ "\n")
 
@@ -364,13 +387,20 @@
 genInterleaved :: Gen a -> [a] -> Gen [a]
 genInterleaved g = sequence . intersperse g . map return
 
-columnSpacer :: Gen String
-columnSpacer = fmap (f . abs) Q.arbitrarySizedIntegral
+-- | Generates at least two spaces.
+columnSpaceTwo :: Gen String
+columnSpaceTwo = fmap (f . abs) Q.arbitrarySizedIntegral
   where
     f i = "  " ++ replicate i ' '
 
+-- | Generates at least one space.
+columnSpaceOne :: Gen String
+columnSpaceOne = fmap (f . abs) Q.arbitrarySizedIntegral
+  where
+    f i = " " ++ replicate i ' '
+
 columns :: [String] -> Gen String
-columns = fmap concat . genInterleaved columnSpacer
+columns = fmap concat . genInterleaved columnSpaceTwo
 
 
 #endif
@@ -424,7 +454,7 @@
   , bssAuto :: Dollars
   , bssMatching :: Dollars
   , bssTotal :: Dollars
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -509,7 +539,7 @@
 
 newtype FundNameRen = FundNameRen
   { unFundNameRen :: Rendered [String] }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 instance Arbitrary FundNameRen where
   arbitrary = do
@@ -532,7 +562,7 @@
   , bfpTotal :: Dollars
   , bfpSharePrice :: Dollars
   , bfpNumShares :: Shares
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -548,10 +578,15 @@
   leader <- fmap (flip replicate ' ') Q.arbitrarySizedIntegral
   let rAst = ByFundPosting (ast rdy) (ast rty) (ast rtrad) (ast rroth)
                        (ast rtot) (ast rpri) (ast rsha)
-  ren <- columns [ rendering rdy,
-                   rendering rty,
-                   rendering rtrad, rendering rroth,
-                   rendering rtot, rendering rpri, rendering rsha ]
+  let clmns = [ return $ rendering rdy            , columnSpaceOne
+              , return $ rendering rty            , columnSpaceTwo
+              , return $ rendering rtrad          , columnSpaceOne
+              , return $ rendering rroth          , columnSpaceOne
+              , return $ rendering rtot           , columnSpaceOne
+              , return $ rendering rpri           , columnSpaceOne
+              , return $ rendering rsha
+              ]
+  ren <- fmap concat . sequence $ clmns
   return $ Rendered rAst (leader ++ ren ++ "\n")
 
 prop_txnByFund :: QP.Property
@@ -595,7 +630,7 @@
   { bfbbSharePrice :: Dollars
   , bfbbNumShares :: Shares
   , bfbbDollarBalance :: Dollars
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -640,7 +675,7 @@
 -- FUND@ section.
 data ByFundGainLoss = ByFundGainLoss
   { bfglDollarBalance :: Dollars }
-  deriving (Eq, Show)
+  deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -675,7 +710,7 @@
   { bfebSharePrice :: Dollars
   , bfebNumShares :: Shares
   , bfebDollarBalance :: Dollars
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -722,7 +757,7 @@
   , bsTxns :: [BySourcePosting]
   , bsGainLoss :: BySourceGainLoss
   , bsEndingBal :: BySourceEndingBal
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -865,15 +900,19 @@
            <?> "transactions by source summary")
   return $ BySource begBal txns gainLoss endBal
 
+-- | The name of a fund, eg @C Fund@. This is a list of words; each
+-- word will not contain any spaces.
+type FundName = [String]
+
 -- | A single fund in the @YOUR TRANSACTION DETAIL BY FUND@ section
 -- (e.g. the @G Fund@, @L 2040 Fund@, etc.)
 data ByFund = ByFund
-  { bfFundName :: [String]
+  { bfFundName :: FundName
   , bfBeginningBal :: ByFundBeginningBal
   , bfPostings :: [ByFundPosting]
   , bfGainLoss :: ByFundGainLoss
   , bfEndingBal :: ByFundEndingBal
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 #ifdef test
 
@@ -980,7 +1019,7 @@
 data TspStatement = TspStatement
   { tspDetailBySource :: BySource
   , tspDetailByFund :: [ByFund]
-  } deriving (Eq, Show)
+  } deriving (Eq, Ord, Show)
 
 instance Pretty TspStatement where
   pretty x = Y.vcat
diff --git a/tsparse.cabal b/tsparse.cabal
--- a/tsparse.cabal
+++ b/tsparse.cabal
@@ -4,7 +4,7 @@
 -- The name of the package.
 name:                tsparse
 
-version:             0.2.0.0
+version:             0.4.0.0
 
 -- A short (one-line) description of the package.
 synopsis: Parses U.S. federal Thrift Savings Plan PDF quarterly statements
