Format list output in Haskell?
Asked Answered
A

3

7

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?

Asch answered 8/5, 2011 at 18:27 Comment(0)
S
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"
Shawna answered 8/5, 2011 at 18:32 Comment(5)
Ahhh ive seen something similar to this today - how does it work?Asch
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??Asch
@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.Shawna
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?Asch
You could do something like mapM_ (putStrLn . fooToLine) testData, though you'd either have to change the Foos to tuples or vice versa.Shawna
T
14

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.

Tabatha answered 8/5, 2011 at 18:31 Comment(4)
If you want the column width to adapt to the widest item you need some more code.Arundell
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?Asch
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.Tabatha
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...Asch
W
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
Wobble answered 9/5, 2011 at 15:48 Comment(1)
This is my favorite pretty-printer now for tables. I like that it's simple and composable.Jihad
S
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"
Shawna answered 8/5, 2011 at 18:32 Comment(5)
Ahhh ive seen something similar to this today - how does it work?Asch
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??Asch
@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.Shawna
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?Asch
You could do something like mapM_ (putStrLn . fooToLine) testData, though you'd either have to change the Foos to tuples or vice versa.Shawna

© 2022 - 2024 — McMap. All rights reserved.