I am trying to convert CSV files into a table using the Boxes package
.
I am using IHaskell (Jupyter notebook extension) to do data analysis and would like to include nice looking tables. I could write a program to convert the CSV to a Jupyter markdown native table but that's not as fluid a process as generating the table on the fly.
Below is the code which produces a reasonable looking table but I had to do some manual stuff which made me think I wasn't using Boxes correctly.
There doesn't appear to be a way to set a fixed width Box (with padding) and the actual data centered within it. It's easy to get it to center the data but the box width is dependent on the width of the data resulting in a 'jagged' column.
My complicated code is below .. I've been doing Haskell for awhile but I still consider myself a novice.
Any comments greatly appreciated.
Tom
module Main (main) where
{-
Notes:
- This converst a CSV into a table
- Am I using the Boxes package but I had to do a lot
of work, using only punctuateH, punctuateV from the
package.
- Is there a better way to do it?
- Things I had to do 'manually'
- Center the data in a fixed width box. If I didn't
do this the columns would not align since the data
has various widths.
- Add the outside borders
- The input is rows of data separated by commas .. (CSV)
on,pinput,pout,eff,ledi
5.0e-6,8.43764e-2,7.88486e-2,0.934486,8.63554e-3
5.4e-5,3.04731,2.90032,0.951765,0.214559
1.03e-4,6.34257,6.03162,0.950973,0.434331
- Produces,
|--------------------------------------------------------------|
| on | pinput | pout | eff | ledi |
| ------------------------------------------------------------ |
| 5.0e-6 | 8.43764e-2 | 7.88486e-2 | 0.934486 | 8.63554e-3|
| ------------------------------------------------------------ |
| 5.4e-5 | 3.04731 | 2.90032 | 0.951765 | 0.214559 |
| ------------------------------------------------------------ |
| 1.03e-4 | 6.34257 | 6.03162 | 0.950973 | 0.434331 |
|--------------------------------------------------------------|
-}
import Text.PrettyPrint.Boxes
replace :: Eq a => a -> a -> [a] -> [a]
replace a b = map $ \c -> if c == a then b else c
setLen :: Int -> String -> String
setLen n str =
let fullPad = n - (length str)
halfPadLen = truncate $ (fromIntegral fullPad) / (2.0::Double)
halfPad = replicate halfPadLen ' '
in
if (even fullPad)
then halfPad ++ str ++ halfPad
else halfPad ++ " " ++ str ++ halfPad
padEnds :: String -> String
padEnds str =
let theLines = lines str
tableW = (length (head theLines)) + 2
allData = concatMap (\aLine -> "|" ++ aLine ++ "|\n") theLines
bar = replicate (tableW-2) '-'
in "|" ++ bar ++ "|\n" ++ allData ++ "|" ++ bar ++ "|\n"
renderTable :: String -> String
renderTable str = toStr ( lines (replace ',' ' ' str) )
where
toStr :: [String] -> String
toStr theLines =
let allBoxes = map (\aLine -> makeBoxes aLine) theLines
hs = map (\row -> punctuateH center1 (text " | ") row) allBoxes
box = punctuateV center1 (text (replicate 60 '-')) hs
rawStr = render box
in padEnds rawStr
makeBoxes :: String -> [Box]
makeBoxes aLine =
let sameLen = map (setLen 10) (words aLine)
in map text sameLen
main :: IO ()
main = do
fileData <- readFile "test2.csv"
putStrLn $ renderTable fileData
Yes, I think you're using boxes wrong. Here's what my go would look like:
import Prelude hiding ((<>)) -- ugh
import Data.List
import Text.PrettyPrint.Boxes
table :: [[String]] -> Box
table sss = border <> punctuateH center1 sep cs <> border where
border = vtext (replicate height '|')
sep = vtext (take height (cycle "-|"))
height = maxPosOn rows cs
cs = map column (transpose sss)
column :: [String] -> Box
column ss = sep // punctuateV center1 sep (map text ss) // sep where
sep = text (replicate (maxPosOn length ss+2) '-')
vtext :: String -> Box
vtext = vcat center1 . map char
maxPosOn :: (a -> Int) -> [a] -> Int
maxPosOn f = maximum . (0:) . map f
Try it out in ghci:
> printBox $ table [["on","pinput","pout","eff","ledi"],["5.0e-6","8.43764e-2","7.88486e-2","0.934486","8.63554e-3"],["5.4e-5","3.04731","2.90032","0.951765","0.214559"],["1.03e-4","6.34257","6.03162","0.950973","0.434331"]]
|-----------------------------------------------------------|
| on | pinput | pout | eff | ledi |
|-----------------------------------------------------------|
| 5.0e-6 | 8.43764e-2 | 7.88486e-2 | 0.934486 | 8.63554e-3 |
|-----------------------------------------------------------|
| 5.4e-5 | 3.04731 | 2.90032 | 0.951765 | 0.214559 |
|-----------------------------------------------------------|
| 1.03e-4 | 6.34257 | 6.03162 | 0.950973 | 0.434331 |
|-----------------------------------------------------------|
Related
Is there an easy way to delete all occurrences of a char in an OCaml string ?
I thought I could use this clean function :
let clean =
function
| ' ' | '[' | ']' | '\n' | '>' -> ''
| x -> x
in
But you can't use '' in OCaml.
So I came with this solution :
let delete =
function
| ' ' | '[' | ']' | '\n' | '>' -> true
| _ -> false
in
let char_list, size =
String.fold_left
(fun (acc, count) c ->
if delete c then acc, count
else c::acc, count+1) ([],0) path_string
in
let char_list = ref ## List.rev char_list in
let aux() =
match !char_list with
| [] -> failwith "unexpected"
| x :: xs -> char_list := xs; x
in
let cleaned_string = String.init size (fun _ -> aux()) in
cleaned_string
But it's big - with python it's just s.replace('>','') to suppress one - and only work with OCaml 4.13 which is not supported for my installation.
If you want to build a string dynamically, you should use a buffer
let remove_chars erase s =
let b = Buffer.create 10 in
String.iter (fun c -> if not (erase c) then Buffer.add_char b c);
Buffer.contents b
(Building a list of characters is extremely inefficient.)
The closest equivalent to python might be Str.global_replace:
Str.(global_replace (regexp ">") "" s)
or to do multiple characters at once you could do something like:
Str.(global_replace (regexp "[youChar1yourChar2yourChar3]") "" s)
So in your case :
Str.(global_replace (regexp "[][> \n]") "" s)
Solved
My function isn't working properly. When I do hex2dec "100" or "10000" it gives me 16 instead of the correct 256. When I do hex2dec "101" it gives me 17 instead of the correct 257.
hex2dec :: String -> Integer
hex2dec str = go (reverse str)
where go [] = 0
go (x:xs) = hexChar x + 16 * hex2dec xs
hexChar :: Char -> Integer
hexChar c
| c == '0' = 0
| c == '1' = 1
| c == '2' = 2
| c == '3' = 3
| c == '4' = 4
| c == '5' = 5
| c == '6' = 6
| c == '7' = 7
| c == '8' = 8
| c == '9' = 9
| c == 'a' = 10
| c == 'b' = 11
| c == 'c' = 12
| c == 'd' = 13
| c == 'e' = 14
| c == 'f' = 15
| otherwise = 0
I had to change "hex2dec" to "go".
hex2dec :: String -> Integer
hex2dec str = go (reverse str)
where go [] = 0
go (x:xs) = hexChar x + 16 * go xs
Notice that your functions are partial; not all Char values are valid hex digits. Instead, define a new type to represent them:
data Hex = H0 | H1 | H2 | H3
| H4 | H5 | H6 | H7
| H8 | H9 | HA | HB
| HC | HD | HE | HF deriving (Enum)
Deriving an Enum instance gives you hexChar (let's call it hexInt) for nearly for free; we switch from Int to Integer here.
hexInt :: Hex -> Integer
hexInt = fromIntegral . fromEnum
With this in place, you can use Horner's Rule to reduce a list of Hex values to an Integer.
hex2dec :: [Hex] -> Integer
hex2dec = foldr (\d acc -> hexInt d + 16*acc) 0
To generalize this from [Hex] to String, we first define a function to convert a Char to a Maybe Hex
import Data.List
charToHex :: Char -> Maybe Hex
charToHex c = fmap toEnum $ c `elemIndex` "0123456789abcdef"
-- elemIndex returns a Maybe Int "between" Just 0 and Just 15;
-- fmap toEnum converts that to a Maybe Hex.
If you wanted to be explicit, charToHex is of course simply
charToHex '0' = Just H0
-- ...
charToHex 'f' = Just H15
charToHex _ = Nothing
Then we want a function that can handle any failures while mapping charToHex over a String.
str2dec :: String -> Maybe Integer
str2dec = fmap hex2dec . traverse charToHex
where traverse charToHex :: String -> Maybe [Hex] returns Nothing if any call to charToHex on the input string returns Nothing.
I want to make a function that takes in a string of multiple "grades" of varying length and convert it to a list of grades.
Grade is just a data structure that looks like this (just an arbitrary grading system):
data Grade = A+ | A | A- | B+ | B | B- | P | F
deriving (Show, Eq)
As you can see, the grades have varying length. If they had length 1 or consistent length, this would have been much easier.
Here is the function that I want to make:
This is what the string input looks like "PA+FABA+B-A"
stringToGrade :: String -> Grade
stringToGrade stringGrade
| stringGrade == "A+" = A+
| stringGrade == "A" = A
-- and so on
extractGrades :: String -> [Grade]
extractGrades stringGrades = case stringGrades of
[] -> []
x:y:ys
| x == "A" && y == "+" -> [stringToGrade (x : y)] : extractGrades ys
| x == "A" -> [stringToGrade x] : extractGrades y:ys
-- and so on
As you can see, this is not going anywhere.
Is there an elegant and easy way I cam do this instead of had coding everything?
We can apply pattern matching so to match a string prefix. Here's an example:
foo :: String -> [Int]
foo [] = []
foo ('h':'e':'l':'l':'o':rest) = 1 : foo rest
foo ('b':'o':'b':rest) = 2 : foo rest
foo ('b':rest) = 3 : foo rest
foo _ = error "foo: invalid input syntax"
Sample usage:
foo "hellobbobbobhello" ==> [1,3,2,2,1]
You can split the string into tokens using combination of split functions.
split (keepDelimsR $ oneOf "+-") "PA+FABA+B-A"
will create this form, where the suffixes are attached.
["PA+","FABA+","B-","A"]
Now, you can split this further with a custom splitter
splitInit [] = []
splitInit [x] = [[x]]
splitInit [x,y] = [[x,y]]
splitInit (x:xs) = [x] : splitInit xs
a combination will give you
concatMap splitInit $ split (keepDelimsR $ oneOf "+-") "PA+FABA+B-A"
["P","A+","F","A","B","A+","B-","A"]
where you can map through your constructors
i have to make Haskell function called markDups that processes a string, replacing all repeated occurrences of a character with the underscore, "_", character.
here is my code i did so far.
makeBar :: Char -> [Char] -> [Char]
makeBar c (x:xs) | c == x = '_':makeBar c xs --turn into a "_"
| otherwise = x:makeBar c xs--ignore and move on
when I run this, here is my output with error message
output should be like this
what should I do?
This seems to work:
import Data.Set
main = putStrLn (markDups "hello world" empty)
markDups :: [Char] -> Set Char -> [Char]
markDups [] set = []
markDups (x:rest) set
| member x set = '_':(markDups rest set)
| otherwise = x:(markDups rest (insert x set))
For convenient analysis of data I'd like to use a library which for the following code:
data SomeType = A [String] Int | B | C Int deriving (Eq, Ord, Show)
main = do
let theData = A ["a", "b", "c"] 9 : C 3 : B : []
putStr $ treeString theData -- `treeString` is the implied library function
would produce an output similar to the following:
- A:
| - - a
| | - b
| | - c
| - 9
- C:
| - 3
- B
Is there such a library? Or maybe a better approach to such a problem?
Data.Tree has drawTree and drawForest functions with similar formatting, so you can write a function to convert your data structure to a Tree String and then use drawTree.
import Data.Tree
data SomeType = A [String] Int | B | C Int deriving (Eq, Ord, Show)
toTree :: SomeType -> Tree String
toTree (A xs n) = Node "A" [Node "*" (map (flip Node []) xs), Node (show n) []]
toTree B = Node "B" []
toTree (C n) = Node "C" [Node (show n) []]
main = do
let theData = A ["a", "b", "c"] 9 : C 3 : B : []
putStr $ drawTree (Node "*" (map toTree theData))
Output:
*
|
+- A
| |
| +- *
| | |
| | +- a
| | |
| | +- b
| | |
| | `- c
| |
| `- 9
|
+- C
| |
| `- 3
|
`- B
Adding to hammar's answer. Here's how one can do a generic conversion to Data.Tree:
import Data.Tree
import Data.Generics
import Control.Applicative
dataTree = fix . genericTree
where
genericTree :: Data a => a -> Tree String
genericTree = dflt `extQ` string
where
string x = Node x []
dflt a = Node (showConstr (toConstr a)) (gmapQ genericTree a)
fix (Node name forest)
| name == "(:)"
, a : b : [] <- forest
= Node "*" $ (fix a) : (subForest $ fix b)
| otherwise = Node name $ fix <$> forest
But for this to work on one's data types, they must have an instance of Data, which can easily be achieved by adding a {-# LANGUAGE DeriveDataTypeable #-} pragma and making the type derive from Typeable and Data like so:
data SomeType = A [String] Int | B | C Int | D [[String]]
deriving (Typeable, Data)