I have a list of custom data objects which track an increasing total value on a daily basis using one field total
. Another field in the custom data type is the value new
. Using a csv file I have read in the values for date
and total
and am trying to calculate and set the values for new
from these values.
data item = Item{
date :: Day,
total :: Int,
new :: Int
}
Before
date | total | new |
---|---|---|
01/01/2021 | 0 | 0 |
02/01/2021 | 2 | 0 |
03/01/2021 | 6 | 0 |
04/01/2021 | 15 | 0 |
After
date | total | new |
---|---|---|
01/01/2021 | 0 | 0 |
02/01/2021 | 2 | 2 |
03/01/2021 | 6 | 4 |
04/01/2021 | 15 | 9 |
My understanding is that in haskell I should be trying to avoid the use of for
loops which iterate over a list until the final row is reached, for example using a loop control which terminates upon reaching a value equal to the length of the list.
Instead I have tried to create a function which assigns the value of new which can used with map
to update each item in the list. My problem is that such a function requires access to both the item being updated, as well as the previous item's value for total
and I'm unsure how to implement this in haskell.
--Set daily values by mapping single update function across list
calcNew:: [Item] -> Int -> [Item]
calcNew items = map updateOneItem items
-- takes an item and a value to fill the new field
updateOneItem :: Item -> Int -> Item
updateOneItem item x = Item date item total item x
Is it possible to populate that value while using map
? If not, is a recursive solution required?
CodePudding user response:
We can do this by zipping the input list with itself, shifted by one step.
Assuming you have a list of items already populated with total
values, which you want to update to contain the correct new
values (building an updated copy of course),
type Day = Int
data Item = Item{ -- data Item, NB
date :: Day,
total :: Int,
new :: Int
} deriving Show
calcNews :: [Item] -> [Item]
calcNews [] = []
calcNews totalsOK@(t:ts) = t : zipWith f ts totalsOK
where
f this prev = this{ new = total this - total prev }
This gives us
> calcNews [Item 1 0 0, Item 2 2 0, Item 3 5 0, Item 4 10 0]
[Item {date = 1, total = 0, new = 0},Item {date = 2, total = 2, new = 2},
Item {date = 3, total = 5,new = 3},Item {date = 4, total = 10, new = 5}]
Of course zipWith f x y == map (\(a,b) -> f a b) $ zip x y
, as we saw in your previous question, so zipWith
is like a binary map
.
Sometimes (though not here) we might need access to the previously calculated value as well, to calculate the next value. To arrange for that we can create the result by zipping the input with the shifted version of the result itself:
calcNews2 :: [Item] -> [Item]
calcNews2 [] = []
calcNews2 (t:totalsOK) = newsOK
where
newsOK = t : zipWith f totalsOK newsOK
f tot nw = tot{ new = total tot - total nw }