8

I am having trouble trying to format the output a of a list of my own type in Haskell.

I would like something like this:

Make  | Model | Years(this is a list)    <- this would be the headers if you like
-------------------
Item1 | Item1 | Item1s,Item1s           
Item2 | Item2 | Item2s,Items2,Items2

^ This would be the data loaded from my String String [Int] type.

How would I do this in Haskell?

3 Answers 3

15

Generally, we use "pretty printing" libraries to do nice formatted output. The standard one that you should know is Text.PrettyPrint. Given a data type, you can walk that type, building up a well-formated document.

An example:

import Text.PrettyPrint
import Data.List

-- a type for records
data T = T { make  :: String
           , model :: String
           , years :: [Int] }
    deriving Show

-- test data
test =
    [ T "foo" "avenger" [1990, 1992]
    , T "bar" "eagle"   [1980, 1982]
    ]

-- print lists of records: a header, then each row
draw :: [T] -> Doc
draw xs =
    text "Make\t|\tModel\t|\tYear"
   $+$
    vcat (map row xs)
 where
    -- print a row
    row t = foldl1 (<|>) [ text (make t)
                         , text (model t)
                         , foldl1 (<^>) (map int (years t))
                         ]

-- helpers
x <|> y = x <> text "\t|\t" <> y
x <^> y = x <> text "," <+> y

Testing:

main = putStrLn (render (draw test))

Results in:

Make    |   Model   |   Year
foo     |   avenger |   1990, 1992
bar     |   eagle   |   1980, 1982

The ability to quickly write pretty printers is an incredibly useful skill.

Sign up to request clarification or add additional context in comments.

4 Comments

If you want the column width to adapt to the widest item you need some more code.
id prefer not to use these libraries, i like the method below but im not sure how to use a set of test data with it?
You'd read the test data from a file or database. Edit: ah, I see, you're looking at how to do IO as well. Look for other questions on this site about io + haskell.
its in a function: testData = [("type1","type2",["item1","item12"], "type2","type3",["item2","item22"])] yeahh thats how the data is coming in from my own format/type...
6

Here is a generalized table generator. It calculates the column widths to fit the widest row. The ColDesc type allows you to specify, for each column, the title alignment, the title string, the data alignment, and a function to format the data.

import Data.List (transpose, intercalate)

-- a type for records
data T = T { make  :: String
           , model :: String
           , years :: [Int] }
    deriving Show

-- a type for fill functions
type Filler = Int -> String -> String

-- a type for describing table columns
data ColDesc t = ColDesc { colTitleFill :: Filler
                         , colTitle     :: String
                         , colValueFill :: Filler
                         , colValue     :: t -> String
                         }

-- test data
test =
    [ T "foo" "avenger" [1990, 1992]
    , T "bar" "eagle"   [1980, 1982, 1983]
    ]

-- functions that fill a string (s) to a given width (n) by adding pad
-- character (c) to align left, right, or center
fillLeft c n s = s ++ replicate (n - length s) c
fillRight c n s = replicate (n - length s) c ++ s
fillCenter c n s = replicate l c ++ s ++ replicate r c
    where x = n - length s
          l = x `div` 2
          r = x - l

-- functions that fill with spaces
left = fillLeft ' '
right = fillRight ' '
center = fillCenter ' '

-- converts a list of items into a table according to a list
-- of column descriptors
showTable :: [ColDesc t] -> [t] -> String
showTable cs ts =
    let header = map colTitle cs
        rows = [[colValue c t | c <- cs] | t <- ts]
        widths = [maximum $ map length col | col <- transpose $ header : rows]
        separator = intercalate "-+-" [replicate width '-' | width <- widths]
        fillCols fill cols = intercalate " | " [fill c width col | (c, width, col) <- zip3 cs widths cols]
    in
        unlines $ fillCols colTitleFill header : separator : map (fillCols colValueFill) rows

Running:

putStrLn $ showTable [ ColDesc center "Make"  left  make
                     , ColDesc center "Model" left  model
                     , ColDesc center "Year"  right (intercalate ", " . map show . years)
                     ] test

Results in:

Make |  Model  |       Year      
-----+---------+-----------------
foo  | avenger |       1990, 1992
bar  | eagle   | 1980, 1982, 1983

1 Comment

This is my favorite pretty-printer now for tables. I like that it's simple and composable.
4

Something like this?

import Data.List (intercalate)
data Foo = Foo String String [Int]

fooToLine :: Foo -> String
fooToLine (Foo a b cs) = a ++ " | " ++ b ++ " | " ++ intercalate ", " (map show cs)

Now, you can do

>>> fooToLine (Foo "Hello" "World" [1, 2, 3])
"Hello | World | 1, 2, 3"

5 Comments

Ahhh ive seen something similar to this today - how does it work?
How would i use test data for this? so you dont actually give in anything frm te users end its just given in from antoehr function called testData??
@Ash: I've added a simple example. Note that this assumes that your strings are of the same length, otherwise the columns won't align. See Don Stewart's answer for how to handle aligning columns of different width.
so how would i make this call another function for the test data? this is the other function: testData = [("type1","type2",["item1","item12"], "type2","type3",["item2","item22"])] how can i put that in there so it doesnt require input from the user?
You could do something like mapM_ (putStrLn . fooToLine) testData, though you'd either have to change the Foos to tuples or vice versa.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.