0

First of: sorry for the ambigous question title... Second: I am a complete beginner...

I have a very simple data type called Task. A Task had a price, an amount and a unit. The unit can be days or hours.

I want to calculate the total amount and total price for a list of Tasks. For the price there is no issue, I multiply each price with the amount for the given task and it adds up. But for the amount I am stuck.

I cannot easely fold a list of hours and days. Say I have a task with 1 hour and one with 1 day, both of them have an amount of 1, but the total should be in one unit. I chose 'hour' as the base unit.

So to recap, this is my type:

-- data --

-- the task is parsed from json, but I would like the `unit` type to be
-- custom like `Hour | Day` instead of String. But that is an aeson issue?
data Task = {
  amount :: Double,
  price :: Double,
  unit :: String
}

-- functions --

taskPrice :: Task -> Double
taskPrice t = fromIntegral (price t) * amount t

calcPrice :: [Task] -> Double
calcPrice [] = 0
calcPrice [x] = taskPrice x
calcPrice (x:xs) = foldl (\acc x -> acc + (taskPrice x)) (taskPrice x) xs

-- this version will add days and hours like they are equal...
calcInvoiceHours :: [Task] -> Double
calcInvoiceHours [] = 0
calcInvoiceHours [x] = amount x
calcInvoiceHours (x:xs) = foldl (\acc x -> acc + (amount x)) (amount x) xs

Every Task knows it's own type, but a switch case inside the pattern matching is not the way to go I guess... In some naive pseudo code I would write:

calcInvoiceHours :: [Task] -> Double
calcInvoiceHours [] = 0
calcInvoiceHours [x -> unit x === Hour] = amount x
calcInvoiceHours [x -> unit x === Day] = (amount x) * 8 -- work 8 hours a day
calcInvoiceHours (x:xs) = foldl (\acc x -> acc + (amount x)) (amount x)

I know this is wrong, but I do not know how to do it. I might be running before I can walk here, but it seems like such a simple task (no pun intended).

Thanks!

== UPDATE ==

I found a way! But if this is not the good way, I would be glad to hear how I can improve! Plus I am still searching to parse the strings 'hours' and 'days' to a concrete custome type Hour | Day from json.

taskHours :: Task -> Double
taskHours (Task _ amount _ unit)
  | unit == "hours" = amount
  | otherwise = amount * 8

calcInvoiceHours :: [Task] -> Double
calcInvoiceHours [] = 0
calcInvoiceHours [x] = taskHours x
calcInvoiceHours (x:xs) = foldl (\acc x -> acc + (taskHours x)) (taskHours x) xs
4
  • Are you familiar with sum types? Commented Jul 20, 2016 at 18:28
  • No, I am readying the documentation right now, thank you! I could make it work with the update above and also with the suggestion below. Commented Jul 20, 2016 at 18:39
  • Perhaps this question would be better suited to Code Review? Commented Jul 20, 2016 at 23:40
  • This may be on-topic for Code Review if a) the code works and b) the code isn't hypothetical or incomplete in any way Commented Jul 21, 2016 at 0:01

2 Answers 2

4

You don't really need an explicit fold here. You just need a combination of map and sum.

inHours :: Task -> Double
inHours (Task amt _ "hours") = amt
inHours (Task amt _ "days") = 8 * amt

taskPrice :: Task -> Double
taskPrice t = (price t) * (inHours t)

calcPrice :: [Task] -> Double
calcPrice = sum . (map taskPrice)

calcInvoiceHours :: [Task] -> Double
calcInvoiceHours = sum . (map inHours)
Sign up to request clarification or add additional context in comments.

2 Comments

The braces on the right hand side of your functions are unnecessary, btw. E.g., taskPrice t = price t * inHours t
I tend to overuse parentheses, mostly out of habit from when I couldn't remember exactly how currying and function application interacted with infix operators.
1

You've left the treatment of time as a largely implicit part of your data model. I would model timespans as a self-contained abstract data type, with functions to calculate new timespans and to project various components of the timespan. This allows you to perform calculations in a canonical unit of your choice (I've chosen "ticks") in a manner that is opaque to code which uses the TimeSpan type.

-- Make the type abstract, so users can't directly manipulate ticks,
-- by keeping the TimeSpan constructor private to this module
newtype TimeSpan = TimeSpan Integer

-- you could pack these two into a Monoid instance if you like, or use GeneralizedNewtypeDeriving and get it without writing any code
zero :: TimeSpan
zero = TimeSpan 0
plus :: TimeSpan -> TimeSpan -> TimeSpan
plus (TimeSpan x) (TimeSpan y) = TimeSpan (x + y)

ticksPerHour :: Fractional a => a
ticksPerHour = 10000 * 1000 * 60 * 60  -- 10,000 ticks per millisecond

fromHours :: Float -> TimeSpan
fromHours = TimeSpan . hoursToTicks
    where hoursToTicks x = round (x * ticksPerHour)

toHours :: TimeSpan -> Float
toHours (TimeSpan x) = ticksToHours x
    where ticksToHours x = fromIntegral x / ticksPerHour

-- similar functions to measure the timespan in days, etc

This is just how you'd do it in an object-oriented language. C#'s TimeSpan is a struct (don't get me started on how bad C# structs are at data abstraction) which internally counts a private number of ticks, with public methods to convert a TimeSpan into various other measuring schemes such as hours or days. It's also more-or-less what you'll find in Haskell's Data.Time.Clock module.

Now you can use the abstract TimeSpan type directly in your code, delegating to your TimeSpan library to do the adding and subtracting in a units-independent manner.

data Task = Task {
    taskLength :: TimeSpan,
    taskPrice :: Money  -- omitted: a similar treatment to build an abstract Money type
}

totalTime :: [Task] -> TimeSpan
totalTime = foldl' plus zero . map taskLength

totalTimeHours :: [Task] -> Float
totalTimeHours = toHours . totalTime

1 Comment

Thank you for your answer! This is a bit much for me to grasp at the time, but I will keep it in mind while growing in the Haskell mindset/syntax

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.