2

In my iOS app, I have a shape class, built with CGPoints. I save it to a file using encodeCGPoint:forKey. I read it back in. That all works.

However, the CGPoint values I read in are not exactly equal to the values I saved it. The low bits of the CGFloat values aren't stable. So CGPointEqualToPoint returns NO, which means my isEqual method returns NO. This causes me trouble and pain.

Obviously, serializing floats precisely has been a hassle since the beginning of time. But in this situation, what is the best approach? I can think of several:

  • write out the x and y values using encodeFloat instead of encodeCGPoint (would that help at all?)
  • multiply my x and y values by 256.0 before saving them (they're all going to be between -1 and 1, roughly, so this might help?)
  • write out the x and y values using encodeDouble instead of encodeCGPoint (still might round the lowest bit incorrectly?)
  • cast to NSUInteger and write them out using encodeInt32 (icky, but it would work, right?)
  • accept the loss of precision, and implement my isEqual method to use within-epsilon comparison rather than CGPointEqualToPoint (sigh)

EDIT-ADD: So the second half of the problem, which I was leaving out for simplicity, is that I have to implement the hash method for these shape objects.

Hashing floats is also a horrible pain (see " Good way to hash a float vector? "), and it turns out it more or less nullifies my question. The toolkit's encodeCGPoint method rounds its float values in an annoying way -- it's literally printing them to a string with the %g format -- so there's no way I can use it and still make hashing reliable.

Therefore, I'm forced to write my own encodePoint function. As long as I'm doing that, I might as well write one that encodes the value exactly. (Copy two 32-bit floats into a 64-bit integer field, and no, it's not portable, but this is iOS-only and I'm making that tradeoff.)

With reliable exact storage of CGPoints, I can go back to exact comparison and any old hash function I want. Tolerance ranges do nothing for me, so I'm just not using them for this application.

If I wanted hashing and tolerance comparisons, I'd be comparing values within a tolerance of N significant figures, not a fixed distance epsilon. (That is, I'd want 0.123456 to compare close to 0.123457, but I'd also want 1234.56 to compare close to 1234.57.) That would be stable against floating-point math errors, for both large and small values. I don't have sample code for that, but start with the frexpf() function and it shouldn't be too hard.

Community
  • 1
  • 1
Andrew Plotkin
  • 1,176
  • 1
  • 8
  • 9

2 Answers2

1

Directly comparing floating point numbers is usually not the right game plan. Try one of the many other options. The best solution for your problem is probably your last suggestion; I don't know why there's a "sigh" there, though. A double precision floating point number has about 16 decimal digits worth of precision - there's a very good chance that your program doesn't actually need that much precision.

Carl Norum
  • 219,201
  • 40
  • 422
  • 469
  • Carl, a CGFloat is not double precision (on iOS). However, I have to agree that CGPointEqualToPoint is not very useful doing a bit-wise compare of the coordinates. Roll your own isEqual and stay clear from CGPointEqualToPoint, which seems to be useful only when you are directly copying coordinates around. – Steven Kramer Jul 14 '11 at 20:54
  • Thanks Steven; I didn't actually look up the actual types. The same kind of reasoning stands. Even a single precision floating point number has 7 digits of decimal precision, which has *got* to be enough for the OP's purposes. – Carl Norum Jul 14 '11 at 21:11
  • Yeah, 7 digits should get your UIView *incredibly* close to the screen pixel where you want it displayed ;-) – Steven Kramer Jul 14 '11 at 21:23
  • Right. Of course when I wrote this math originally I *was* just copying coordinates around. Then I started copying them to a file and back again, hoping that would be transparent to the math. Obviously not. – Andrew Plotkin Jul 15 '11 at 05:06
  • As for the "sigh" -- it's because there's an additional complication: I need to implement the hash method. The naive idea (hash = round to nearest 0.001; isequal if within 0.001) doesn't work. I'll have to do some extra rounding to ensure that the hash values are always equal for equal points. – Andrew Plotkin Jul 15 '11 at 15:38
1

Use the epsilon method, because the "low bits of the CGFloat values aren't stable" problem surfaces any time there's an implicit conversion between float and double (often in framework code. tgmath.h is useful for avoiding this in your own code.)

I use the following functions (the tolerance defaulting to 0.5 because that's useful in the common case for CGGeometry):

BOOL OTValueNearToValueWithTolerance(CGFloat v1, CGFloat v2, CGFloat tolerance)
{
    return (fabs(v1 - v2) <= tolerance);
}

BOOL OTPointNearToPointWithTolerance(CGPoint p1, CGPoint p2, CGFloat tolerance)
{
    return (OTValueNearToValueWithTolerance(p1.x, p2.x, tolerance) && OTValueNearToValueWithTolerance(p1.y, p2.y, tolerance));
}

BOOL OTSizeNearToSizeWithTolerance(CGSize s1, CGSize s2, CGFloat tolerance)
{
    return (OTValueNearToValueWithTolerance(s1.width, s2.width, tolerance) && OTValueNearToValueWithTolerance(s1.height, s2.height, tolerance));
}

BOOL OTRectNearToRectWithTolerance(CGRect r1, CGRect r2, CGFloat tolerance)
{
    return (OTPointNearToPointWithTolerance(r1.origin, r2.origin, tolerance) && OTSizeNearToSizeWithTolerance(r1.size, r2.size, tolerance));
}

BOOL OTValueNearToValue(CGFloat v1, CGFloat v2)
{
    return OTValueNearToValueWithTolerance(v1, v2, 0.5);
}

BOOL OTPointNearToPoint(CGPoint p1, CGPoint p2)
{
    return OTPointNearToPointWithTolerance(p1, p2, 0.5);
}

BOOL OTSizeNearToSize(CGSize s1, CGSize s2)
{
    return OTSizeNearToSizeWithTolerance(s1, s2, 0.5);
}

BOOL OTRectNearToRect(CGRect r1, CGRect r2)
{
    return OTRectNearToRectWithTolerance(r1, r2, 0.5);
}

BOOL OTPointNearToEdgeOfRect(CGPoint point, CGRect rect, CGFloat amount, CGRectEdge edge)
{
    CGRect nearRect, otherRect;
    CGRectDivide(rect, &nearRect, &otherRect, amount, edge);
    return CGRectContainsPoint(nearRect, point);
}
hatfinch
  • 3,095
  • 24
  • 35