dc25
dc25

Reputation: 21

Reading file into lists

I have just recently started learning Haskell,and what I am trying to do is this:

I have a txt file like this

2000
booka 500

bookb 1000

bookc 250

bookd 250

and I want my output like this

booka 25%
bookb 50%
bookc 12.5%
bookd 12.5%

I can read the conten from the file with readFile but I dont know how to treat the content after

this is what I have

-- Imports needed
import System.IO
import Data.List

main = do
    putStrLn "Please insert the name of the file you want to process:"
    file <- getLine
    read_File file  

read_File file = do
h <- openFile file ReadMode
content <- readFile file

Upvotes: 2

Views: 1130

Answers (3)

Stefan Holdermans
Stefan Holdermans

Reputation: 8050

One approach is to break down the problem into three obvious parts:

  1. Parse the input.
  2. Process the input to obtain the results.
  3. Print the results.

Indeed, almost all data-processing problems can be broken down like this.

Parsing the input

Parsing the input materialises into reading the file and then breaking the contents up into a list of lines. The first of these lines contains a total value, the remaining lines form a table of rows consisting of a label and a value:

parseFile :: FilePath -> IO (Float, [(String, Float)])
parseFile fp = do
  contents <- readFile fp
  let (s : ss) = lines contents
  return (read s, parseTable ss)

The table is parsed by means of an auxiliary function that breaks each line up into a list of two words: one for the label, one for the value.

parseTable :: [String] ->   [(String, Float)]
parseTable []        = []
parseTable ("" : ss) = parseTable ss
parseTable (s : ss)  = let [s1, s2] = words s
                       in (s1, read s2) : parseTable ss

Processing the input

Processing the input is straightforward:

processTable :: Float -> [(String, Float)] -> [(String, Float)]
processTable sum table = [(label, (part / sum) * 100) | (label, part) <- table]

The second component of each row in the table is divided by the total value and then multiplied by hundred to yield a percentage.

Printing the results

A textual rendering of single row consisting, now, of a label and a percentage is easily obtained:

showRow :: (String, Float) -> String
showRow (label, perc) = label ++ " " ++ show perc ++ "%"

Putting it all together, a whole file is then processed by (1) parsing it to obtain a total value and a table, (2) processing the table to obtain a processed table, and (3) printing the processed table row by row:

processFile :: FilePath -> IO ()
processFile fp = do
  (sum, table) <- parseFile fp
  mapM_ (putStrLn . showRow) (processTable sum table)

For your example data, this generates almost the output as you specified it:

booka 25.0%
bookb 50.0%
bookc 12.5%
bookd 12.5%

That is, getting rid of trailing ".0"s in printed floating-point numbers is left as an exercise. ;)

Upvotes: 1

Shoe
Shoe

Reputation: 76240

To "store" the lines of the file you can just call:

fileContent <- readFile file
let fileLines = lines fileContent

from there you can use words for every line and it will return a list of [label, amount]. You can read the amount with read to place it back into some integral or fractional form.

Live solution

Upvotes: 0

bheklilr
bheklilr

Reputation: 54058

First of all, you should be opening the file with openFile, then reading the file with readFile. You only need the latter, it is a handy function that opens the file, reads it, returns the contents, then closes the file safely for you.

What you should be aiming to write is a pure function

processContents :: String -> Maybe (Float, [(String, Float)])

that can get the first number from the file and then each successive line. Since we want to ensure our code is robust, we should also handle failures in some way. For this problem, I think using Maybe is sufficient to handle errors in parsing. For this, we'll add a couple imports:

import Text.Read (readMaybe)
import Data.Maybe

processContents :: String -> Maybe (Float, [(String, Float)])
processContents c = do
    let ls = lines c
    -- If we can't get a single line, the whole operation fails
    firstLine <- listToMaybe ls
    -- If we can't parse the total, the whole operation fails
    total <- readMaybe firstLine
    let rest = drop 1 ls
    return (total, catMaybes $ map parseLine $ rest)

The listToMaybe function acts as a "safe head", it is defined as

listToMaybe (x:xs) = Just x
listToMaybe [] = Nothing

The readMaybe function has the type Read a => String -> Maybe a, and returns Nothing if it couldn't parse the value, making it easy to use in this case. The catMaybes function takes a list of Maybe as, extracts all the ones that aren't Nothing, and returns the rest as a list with the type catMaybes :: [Maybe a] -> [a].

Now we just need to implement parseLine:

parseLine :: String -> Maybe (String, Float)
parseLine line = case words line of
    (book:costStr:_) -> do
        cost <- readMaybe costStr
        return (book, cost)
    _ -> Nothing

This breaks the line up by spaces, grabs the first two elements, and parses them into a name and a number, if possible.

Now you have a function that could parse your example file into the value

Just (2000.0, [("booka", 500.0), ("bookb", 1000.0), ("bookc", 250.0), ("bookd", 250.0)])

Now it's just up to you to figure out how calculate the percentages and print it to the screen in your desired format.

Upvotes: 1

Related Questions