Search code examples
listhaskellmap-function

Calculating the value of a field based on the difference between the values of another field in two adjacent positions using haskell


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?


Solution

  • 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 }