-1

I have an extension to normalize a CGRect to a specified CGSize

extension CGRect {
    /// If you have a rectangle that needs to be normalized to another width and height you can use this function
    /// - Parameter size: The size you want to normalize to
    /// - Returns: The normalized Rect
    public func normalize(toSize size: CGSize) -> CGRect {
        let width = 1.0 / size.width
        let height = 1.0 / size.height
        let scale = CGAffineTransform.identity.scaledBy(x: width, y: height)
        let newSize = self.applying(scale)
        return newSize
    }
}

Now I test it like this.

//Will Work with a 100 X 100 canvas first

let firstCanvas = CGSize(width: 100, height: 100)

let nonNormalRect = CGRect(x: 50, y: 50, width: 10, height: 10)

let normalizedRect = nonNormalRect.normalize(toSize: firstCanvas)

XCTAssert(normalizedRect.origin.x == 0.5)
XCTAssert(normalizedRect.origin.y == 0.5)
//Fails
XCTAssert(normalizedRect.width == 0.1)
//Fails
XCTAssert(normalizedRect.height == 0.1)

The new CGSize fails the test because it is a long floating point value close to 0.1

po normalizedRect.size
▿ (0.09999999999999998, 0.09999999999999998)
  - width : 0.09999999999999998
  - height : 0.09999999999999998

How come the CGAffineTransform is "too precise" in this case?

Jon Vogel
  • 5,244
  • 1
  • 39
  • 54
  • 2
    That's just how floating point numbers are, on a computer. You can never expect mere `==` to work. – matt Dec 03 '20 at 19:52
  • 2
    Does this answer your question? [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – Gereon Dec 03 '20 at 19:59
  • 2
    I don't understand why this questions immediately gets down votes? It's a wellformed question and I have learned something from the answers. I could not find the answer I wanted on Google so I came here. Stackoverflow is starting to feel broken. – Jon Vogel Dec 03 '20 at 20:00
  • 2
    Just to add to the other points that were already raised: do check out https://github.com/apple/swift-evolution/blob/master/proposals/0259-approximately-equal.md – Gereon Dec 03 '20 at 20:04
  • I gave it a downvote because I don't think it will help anyone else—the question isn't relevant to the problem, so it can't be searched for. But maybe I'm thinking about it wrong? Perhaps teaching one person (you), here, will allow for other people to be helped, indirectly, and that's enough to not warrant an unexplained downvote? I'm currently of the belief that Q/A here should be usable by other people, but I'm willing to have my mind changed if it gets codified. –  Dec 03 '20 at 20:51

4 Answers4

4

This is why the gods gave you XCTAssertEqual with an accuracy parameter:

https://developer.apple.com/documentation/xctest/2919914-xctassertequal

(And why you should never use mere XCTAssert with an == inside the expression, but that's a broader topic.)

Floating point math on a computer always has some slippage in its representation. See, always see, https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Also it seems you may be reinventing the wheel here? There is already a built-in function for scaling a rect into another rect. – matt Dec 03 '20 at 19:57
  • I just looked through the CGRect documentation and either missed what you are referring to or it's somewhere else. Can you please link the function you are talking about? Thank you! – Jon Vogel Dec 03 '20 at 20:04
  • 1
    I was thinking of e.g. AVFoundation's `AVMakeRect` – matt Dec 03 '20 at 20:12
  • AVMakeRect looks cool, and is a new one on me. Matt, you should add that as part of your answer for maximum visibility. It looks like it will only scale DOWN though, based on the `boundingRect` parameter. – Duncan C Dec 03 '20 at 20:54
3

First, see Is floating point math broken? 0.1 cannot be represented precisely in binary floating point (1/10 cannot be resolved in a finite number of binary digits, just as 1/3 cannot be resolved in a finite number of decimal digits).

== is rarely the correct tool for floating point.

The first tool you probably want here is CGRect.integral which will create a CGRect that is aligned to integer coordinates and is promised to be at least as large as the original rect. Since this is promised to be integer values, == will generally be fine.

The other tool you may want is abs(value1 - value2) < small_value. This is to check if a value is "close" to some other value. What makes sense for small_value depends on your problem, but in your case, something like 0.5 or 0.1 might be fine.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
1

You misunderstand. Binary floating point math will often have very small differences between a calculated result and the expected value.

1/100 will not equal 0.01 exactly. It is just the nature of binary floating point math. You will need to compare the actual result to the expected result and make sure it is within a certain threshold. (Maybe within 0.0001 of the expected value?

You could add an extension to CGFloat to check if two values are close to each other:

extension CGFloat {
    func isCloseTo(_ value: CGFloat, threshold: CGFloat = 0.01) -> Bool {
        return abs(self - value) < threshold
    }
}
Duncan C
  • 128,072
  • 22
  • 173
  • 272
0

Everything they're saying about floating point is correct. However, you should simplify your math. (And recognize that you're just doing division, not "normalization"). You'll get a cleaner result—in this case, the tests will pass.

Note: this is not the approach I take for "CGFloat2s", but it's the relevant portion of it.

public extension CGRect {
  static func / (rect: Self, size: CGSize) -> Self {
    var rect = rect
    rect.size /= size
    rect.origin /= size
    return rect
  }
}
extension CGSize {
  static func /= (dividend: inout Self, divisor: Self) {
    let simd = SIMD2(dividend) / .init(divisor)
    dividend = .init(width: simd.x, height: simd.y)
  }
}

extension CGPoint {
  static func /= (dividend: inout Self, divisor: CGSize) {
    let simd = SIMD2(dividend) / .init(divisor)
    dividend = .init(x: simd.x, y: simd.y)
  }
}
extension SIMD2 where Scalar == CGFloat.NativeType {
  init(_ size: CGSize) {
    self.init(size.width.native, size.height.native)
  }

  init(_ point: CGPoint) {
    self.init(point.x.native, point.y.native)
  }
}