0

I'm working on a publisher for Swift/Combine

Given a stream of inputs, I want to record the max value. If the next number is lower, take one from the last recorded max value and emit that.

Input:  [1,2,3,4,5,2,3,3,1]
Output: [1,2,3,4,5,4,3,3,2]

I can do this easily with the following code, however, I really don't like the instance variable

var lastMaxInstanceValue: Float = 0

publisher
.map { newValue
  if newValue > lastMaxInstanceValue {
    lastMaxInstanceValue = newValue
  } else {
    lastMaxInstanceValue = max(0, lastMaxInstanceValue - 1)
  }
}
.assign(to: \.percentage, on: self)
.store(in: &cancellables)

So I wrote a publisher/subscriber here which encapsulates the map part above:

https://github.com/nthState/FallingMaxPublisher

With my publisher, the code turns into:

publisher
.fallingMax()
.assign(to: \.percentage, on: self)
.store(in: &cancellables)

My question is, is my GitHub publisher necessary? Can the value I want be calculated without having the extra variable?

Chris
  • 2,739
  • 4
  • 29
  • 57

1 Answers1

0

You can use scan operator to achieve this. Scan stores an accumulated value computed from each upstream value and the currently accumulated value. You do however, need to give it an initial value; based on your example I used 0:

publisher
   .scan(0, { max, value in
      value >= max ? value : max - 1
   })

You can implement a fallingMax operator for SignedIntegers - as in your github - like so:

extension Publisher where Output: SignedInteger {
    func fallingMax(initial: Output = 0, 
                    fadeDownAmount: Output = 1
                   ) -> AnyPublisher<Output, Failure> {

        self.scan(initial, { max, value in
            value >= max ? value : max - fadeDownAmount
        })
        .eraseToAnyPublisher()
    }
}

As per @Rob's suggestion, if you don't want to supply an initial value, which would instead use the first value as the initial output, you can use an Optional (notice the .compactMap to bring it back to non-optional):

extension Publisher where Output: SignedInteger {
    func fallingMax(initial: Output? = .none, 
                    fadeDownAmount: Output = 1
                   ) -> AnyPublisher<Output, Failure> {

        self.scan(initial, { max, value in
            max.map { value >= $0 ? value : $0 - fadeDownAmount } ?? value
        })
        .compactMap { $0 }
        .eraseToAnyPublisher()
    }
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • 1
    If you wanted to avoid magical sentinel values, one could make it optional, e.g. `.scan(Int?.none) { max($0 ?? $1, $1) }` – Rob Feb 20 '21 at 17:01
  • @Rob, definitely a good point (and needs to be followed by `.compactMap`), but it subtly changes OP's desired functionality if doesn't start from `0`, so I didn't do it. – New Dev Feb 20 '21 at 17:20