table-0.1.0.0: Main.hs
{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-}
module Main where
import Data.List
import Data.Monoid
import Text.Printf
import Options.Applicative
import Data.List.Split (splitOn)
import Control.Applicative
import Data.Char (isDigit, isSpace)
import Control.Monad (when)
import System.Process (readProcess)
import qualified Text.CSV as CSV
data Options = Options {
splitter :: String -> [String] -- delimiter splitting function
, csvInput :: Bool
, suppressRowDividers :: Bool
, printHeaderDivider :: Bool
, maxWidth :: Int
}
parseOptions :: Parser Options
parseOptions = Options
<$> (setDelimiter <|> whiteSpaceDelimiter <|> pure (splitOn "\t"))
<*> switch (short 'c' <> help "Parse input as CSV")
<*> switch (short 'R' <> help "Don't print row dividers")
<*> switch (short 'H' <> help "Print header row divider")
<*> parseMaxWidth
setDelimiter =
splitOn <$> strOption (metavar "DELIM" <> short 'd' <> help "Input field delimiter. Default is TAB (\\t).")
whiteSpaceDelimiter = flag'
words
(short 's' <> help "Use any run of whitespace as input field delimiter")
parseMaxWidth = read <$> strOption (
value "0" <> short 'w' <> metavar "WIDTH"
<> help "Max table width. Defaults to value of `tput cols` command.")
opts = info (helper <*> parseOptions)
(fullDesc
<> progDesc "Pretty format TSV input into table with aligned and wrapped cells"
<> header "table"
<> footer "https://github.com/danchoi/table")
main = do
Options {..} <- execParser opts
s <- getContents
let rawRows = if csvInput
then case CSV.parseCSV "" s of
Left err -> error $ "Error: " ++ show err
Right xs' -> stripBlankRows xs'
else map splitter . lines $ s
let initialWidths = getCellWidths rawRows
-- Adjust the max width subtract padding for gutters on side and ' | ' between cells
maxWidth' <- do
if maxWidth == 0
then (read <$> readProcess "tput" ["cols"] [])
else (return maxWidth)
let adjMaxWidth = maxWidth' - 2 - ((length initialWidths - 1) * 3)
let adjustedWidths = adjustWidths adjMaxWidth initialWidths
let rows = mkCells adjustedWidths rawRows
mapM_ (\(n, row) -> do
when (not suppressRowDividers || (printHeaderDivider && n == 1)) $
putStrLn $ printDivider 1 $ map width row
putStrLn . printRow 1 $ row
) $ zip [0..] rows
when (not suppressRowDividers) $
putStrLn $ printDivider 1 $ map width (head rows)
stripBlankRows :: [[String]] -> [[String]]
stripBlankRows xs = [x | x <- xs, all (> 0) $ map length x ]
adjustWidths :: Int -> [Int] -> [Int]
adjustWidths maxWidth xs | sum xs <= maxWidth = xs
| otherwise = adjustWidths maxWidth $ reduceWidest xs
reduceWidest :: [Int] -> [Int]
reduceWidest xs = let m = maximum xs
in [ if x == m then x - 1 else x | x <- xs ]
data Cell = Cell {
content :: [String]
, width :: Int
, height :: Int
, isNumeric :: Bool
} deriving (Show)
-- | get initial column widths
getCellWidths :: [[String]] -> [Int]
getCellWidths rows = map (maximum . map length) . transpose $ rows
{- Each row is represented as Cell, which contains dimension information. The
first value is the text content; the second is the normalized of the column
width for that cell. -}
mkCells :: [Int] -> [[String]] -> [[Cell]]
mkCells columnWidths rows =
let cols = transpose rows
colCells = map (\(width, cell) -> addCellDimensions width cell) $ zip columnWidths cols
in transpose colCells
{- Input is a column of strings. Wraps data in a Cell, which adds width and
height to each cell in a column, and also a flag if the column looks numeric,
which determines the alignment. Also wraps stings to max cell width -}
addCellDimensions :: Int -> [String] -> [Cell]
addCellDimensions maxWidth xs =
let w = min (maximum . map length $ xs) maxWidth
xs' = map (wrapString w) xs -- wrapped string content
numeric = all (all isDigit) (if length xs > 1 then (tail xs) else xs)
in map (\lines -> Cell lines w (length lines) numeric) xs'
wrapString :: Int -> String -> [String]
wrapString maxWidth x = map trim . wrapLine maxWidth $ x
-- | prints a row of cells with dividers
-- gutter is the width of the blank space at left and right of table
printRow :: Int -> [Cell] -> String
printRow gutter xs =
let rowHeight = maximum $ map height xs
subcells :: [[String]]
subcells = map content xs
lines = map (\n ->
let ss :: [String]
ss = map (cellLine n) xs
in formatRow ss)
[0..(rowHeight - 1)]
in mconcat $ intersperse "\n" $ lines
where formatRow :: [String] -> String
formatRow ss =
mconcat [margin gutter ' ' , (intercalate " | " ss) , margin gutter ' ']
-- prints the nth line of a cell
cellLine :: Int -> Cell -> String
cellLine n Cell {..} =
if n < length content
then printf fmt (content !! n)
else printf fmt ""
where fmt | isNumeric = "%" ++ show width ++ "s"
| otherwise = "%-" ++ show width ++ "s"
margin :: Int -> Char -> String
margin w c = take w $ repeat c
printDivider :: Int -> [Int] -> String
printDivider gutter widths =
mconcat [margin gutter '-'
, (intercalate "-+-"
$ map (\w -> take w $ repeat '-') widths)
, margin gutter '-']
------------------------------------------------------------------------
-- Word wrapping
-- taken from http://moreindirection.blogspot.com/2010/08/blog-post.html
trim :: String -> String
trim = trimAndReverse . trimAndReverse
where trimAndReverse = reverse . dropWhile isSpace
reverseBreak :: (a -> Bool) -> [a] -> ([a], [a])
reverseBreak f xs = (reverse before, reverse after)
where (after, before) = break f $ reverse xs
wrapLine :: Int -> String -> [String]
wrapLine maxLen line
| length line <= maxLen = [line]
| any isSpace beforeMax = beforeSpace : (wrapLine maxLen $ afterSpace ++ afterMax)
| otherwise = beforeMax : wrapLine maxLen afterMax
where (beforeMax, afterMax) = splitAt maxLen line
(beforeSpace, afterSpace) = reverseBreak isSpace beforeMax