RTFM first(where:)

While I'm on a roll...

Some times, one get's lucky...

I need to un-complexify this code - so that I can modify it and - OMG - it's dense.

And, yes, I added the two print statements.

// how to unwrap this formula with first:where...

guard let latestClose = self.first?.close,

let priorClose = self.first(where: {

!Calendar.current.isDate(Date(timeIntervalSince1970: $0.timestamp), inSameDayAs: latestDate)

})?.close else {

print(" didn't find latesClose maybe \(String(describing: self.first?.close)).")

print(" didn't find priorClose maybe...")

return 0

}


Luckly, you can find the Swift Language Reference page to first(where:) it's description is a bit skimmpy:

Returns the first element of the sequence that satisfies the given predicate.

Yet we get the nice bit - a predicate is a closure!
Parameters

predicate

A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.

I think that is what the Declaration syntax means:

func first(where predicate: (Self.Element) throws -> Bool) rethrows -> Self.Element?

But unfortunately the manual authors wanted to be very concise and used the newer syntax shortcut techniques in that simple closure. But once your around these things for long enough - one starts to grasp the simplest of these comparator closures. In the Discussion the closure passed into the where: parameter does a comparison to less than zero of the $0 (the first value passed in).

With Xcode's Quick Help - you can find out that "self.first" is:
The first element of the collection.

The outer function is an extension on Array named: computeChangePercentage().


Now that we understand the first(where:) construct, let's parse the code above.

We've got a nice method guard construct - this allows the coder to gracefully check for the input matching expected values and bail out if there are poor/bad values. The first let is doing an implisit nil check on first - the first item in the self type which should be a collection of some type. It then sets the new constant latestClose. The second let (nested inside the guard statement) is complex.... it eventually sets the priorClose constant to the close value of the collection where the date is NOT (did you miss the BANG!) the same as the date in the first parameter of the closure. Yeah, that hurts your head... the trick here is having some context for what should be happening and why we want the day after the current day. It is the prior day's closing stock market price we are after, so that we can calculate the precent change.

If my friend Lance was here I would be writing the unit tests for the function before dissecting that compound Guard statement that iterates the collection for the day-after's closing price. I'm using the Playground feature of Xcode to test this function. And it would save me lots of trouble and headache... but, hey this is a learning oppertunity. Don't do as I do; do as I say you SHOULD do. Write a unit-test for the function, write several... to test many options like only ONE day in the collection. Price going up trend & going down trend, don't forget to test price staying the same (like on a Saturday or Sunday). One test is just not sufficcent.

See the Reverse TDD series on TDD.Academy.

Here's an article on converting IF statements to Guard statements and the rational behind such a refactoring. In this instance I want to go back the other way. To reduce complexity in the compound Guard statements. At least until I fix the *now* obvious logic of the method. It only works for single daily changes, not for weeks, months, or years. And I feel that this code is getting the market trends backward (green for downward trends).

Notice how clean and readable the example code is using a guard statement in the Playground.


// a first pass at inverting the complexification.

guard let latestClose = self.first?.close else {

return 0

}

guard let priorClose = self.first(where: {

!Calendar.current.isDate(Date(timeIntervalSince1970: $0.timestamp), inSameDayAs: latestDate)

})?.close else {

return 0

}

Now the first guard statement is very understandable - let's expand the second guard statement. The key is that inSameDayAs parameter and functionality. That function of isDate is very powerful. We most likely want to keep that functionality - but check our collection for multiple days somehow.

In this exploration I've noticed that the original author has the collection first/last mapped to the inverse of newest/oldest Dates.

Here's my current attempt to de-complexify the code. I'm not saying it's better - just that a caveman like me can understand it.


// cannot use this when the from/to date range is same day.

// SORT dependent - makes for flaky code

extension Array where Element == CandleStick {

func computeChangePercentage() -> Double {

let symbol = self.first!.symbol

print("First record \(symbol) at ts \(self.first!.timestamp) is \(self.first!.close) price.")

print("Last record \(symbol) at ts \(self.last!.timestamp) is \(self.last!.close) price.")

let newestDate = Date(timeIntervalSince1970: self.first!.timestamp)

let latestDate = Date(timeIntervalSince1970: self.last!.timestamp)

print("\(symbol) CandleStick.computeChangePercentage() for \(newestDate) )")

print("\(symbol) CandleStick.computeChangePercentage() for \(latestDate) )")

// a third pass at inverting the complexification.

guard let newestClose = self.first?.close else {

return 0

}

// guard let priorClose = self.first(where: {

// !Calendar.current.isDate(Date(timeIntervalSince1970: $0.timestamp), inSameDayAs: latestDate)

// })?.close else {

// return 0

// }

var priorClose = 0.0

priorClose = self.first(where: {

!Calendar.current.isDate(Date(timeIntervalSince1970: $0.timestamp),

inSameDayAs: newestDate)

} )?.close ?? 0.0 // get the closing price for next day

if priorClose == 0.0 { // check for bad values e.g. 0.0

return 0

}


let dailyChange = 1 - (priorClose/newestClose)

print("\(symbol) dailyChange where (1 - priorClose / newestClose) on newestDate \(newestDate) ")

print("dailyChange where 1 - \(priorClose) / \(newestClose) ")

return dailyChange

}

}



Running the Playground I get:


First record AAPL at ts 1661477237.514956 is 187.0 price.

Last record AAPL at ts 1661390837.522008 is 191.0 price.

AAPL CandleStick.computeChangePercentage() for 2022-08-26 01:27:17 +0000 )

AAPL CandleStick.computeChangePercentage() for 2022-08-25 01:27:17 +0000 )

AAPL dailyChange where (1 - priorClose / newestClose) on newestDate 2022-08-26 01:27:17 +0000

dailyChange where 1 - 191.0 / 187.0

Change is -0.021390374331550888.




The mere fact that a down treand on the market now has a negitive change precentage is a great indicator that the function is closer than before. I still only have one example data set in the Playground to test with. But it's easy to change.