C. Keith Ray

C. Keith Ray writes about and develops software in multiple platforms and languages, including iOS® and Macintosh®.
Keith's Résumé (pdf)

Friday, August 12, 2016

CGRect Extensions in Swift

While I was implementing drawing code, I found I wanted several CGRect functions that did not exist in Swift 2 and Swift 3. This is the Swift 2 code and tests that I implemented.  The following explanation of that code can be copied into a playground if you desire.

/*
  CGRectExtensions.playground

  Created by C. Keith Ray on 2016.08.06.
  Copyright © 2016 by C. Keith Ray. All rights reserved.
  Permission is granted for anyone to use and modify this
  code for any purpose, as long as this copyright message
  is retained.
*/

import Cocoa
import CoreGraphics

/* These are the constructors available for CGRect:

 CGRect()
 CGRect(origin: CGPoint, size: CGSize)
 CGRect(x: CGFloat, y: CGFloat, width: CGFloat, 
     height: CGFloat)
 CGRect(x: Double, y: Double, width: Double, height: Double)
 CGRect(x: Int, y: Int, width: Int, height: Int)

 I'd like to be able to create CGRects given a height 
 and width, and a center-point. Creating a new constructor 
 isn't hard, we just add it to an extension.
*/

extension CGRect {
    public init(center: CGPoint, size: CGSize) {
        // This assumes width and height are positive numbers.
        let origin = CGPoint(x: center.x - size.width/2.0,
            y: center.y - size.height/2.0)
        self.init(origin: origin, size: size)
    }
}

let a = CGRect(center: CGPoint(x:25,y:15), size: CGSize(width:20,height:10))
print("a.origin.x = \(a.origin.x)")
print("a.origin.y = \(a.origin.y)")
print("a.size.width = \(a.size.width)")
print("a.size.height = \(a.size.height)")
/* prints:
a.origin.x = 15.0
a.origin.y = 10.0
a.size.width = 20.0
a.size.height = 10.0
*/

/*
 We'd also like to get the center of the rectangle easily.
*/

extension CGRect {
    public var center: CGPoint {
        get { return CGPoint(x: self.midX, y: self.midY) }
    }
}

print("a.center = \(a.center)")
// a.center = (25.0, 15.0)

/*
 I'd like to be able to get a rectangle that is close to
 an existing rectangle, but is a different size.
 For example, a taller and thinner rectangle at the same
 origin: ("inc" stands for "increment")

 let b = a.incHeightBy(5).incWidthBy(-5)

 Also, I don't want to modify the original rectangle. Using
 existing features we could do this instead:

 let b = a // copy a
 b.size.height += 5
 b.size.width -= 5

 but you can see that takes more lines of code.
*/

extension CGRect {
    public func incWidthBy(deltaX: CGFloat) -> CGRect {
        var result = self.standardized;
        result.size.width += deltaX
        return result
    }
    
    public func incHeightBy(deltaY: CGFloat) -> CGRect {
        var result = self.standardized;
        result.size.height += deltaY
        return result
    }
}

/* 
 We call self.standardized because rectangles can have 
 negative width and height (which puts the origin at the 
 opposite corner than usual) but we don't want to deal 
 with that in our math. The existing CGRect functions
 call self.standardized, too.
*/

let b = a.incHeightBy(5).incWidthBy(-5)
print("a = \(a)") // a = (15.0, 10.0, 20.0, 10.0)
print("b = \(b)") // b = (15.0, 10.0, 15.0, 15.0)

/*
 I want similar functions to change the origin. A 
 rectangle with the same width and height, but a 
 different origin can be gotten by:

 let c = a.incXBy(2).incYBy(3)

 Again, not changing the original rectangle. An 
 alternative would be:

 let c = a // copy a
 a.origin.x += 2
 a.origin.y += 3

 (Note: "incOriginBy(dx:dy:)" is essentially the 
 same as the already-existing function 
 "offsetBy(dx:dy:)")
*/

extension CGRect {
    public func incXBy(deltaX: CGFloat) -> CGRect {
        var result = self.standardized
        result.origin.x += deltaX
        return result
    }
    
    public func incYBy(deltaY: CGFloat) -> CGRect {
        var result = self.standardized
        result.origin.y += deltaY
        return result
    }
}

let c = a.incXBy(2).incYBy(3)
print("a = \(a)") // a = (15.0, 10.0, 20.0, 10.0)
print("c = \(c)") // c = (17.0, 13.0, 20.0, 10.0)

/*
 To be complete, we can also increment origin and size 
 using pairs of floats.
*/

extension CGRect {
    public func incOriginBy(dx deltaX: CGFloat
                            dy deltaY: CGFloat) -> CGRect {
        var result = self.standardized
        result.origin.x += deltaX
        result.origin.y += deltaY
        return result
    }
    public func incSizeBy(dx deltaX: CGFloat
                          dy deltaY: CGFloat) -> CGRect {
        var result = self.standardized;
        result.size.width += deltaX
        result.size.height += deltaY
        return result
    }
}

let d = CGRect(x: 10, y: 20, width: 30, height: 40)
let e = d.incOriginBy(dx: 5, dy: 10)
let f = d.incSizeBy(dx: 15, dy: 20)
print("d = \(d)")  // d = (10.0, 20.0, 30.0, 40.0)
print("e = \(e)")  // e = (15.0, 30.0, 30.0, 40.0)
print("f = \(f)")  // f = (10.0, 20.0, 45.0, 60.0)

/*
 We made center as a getter, but we can also make a 
 function that returns a new rectangle with a different 
 center. I'll call that newCenter.
*/

extension CGRect {
    public func newCenter(cxy: CGPoint) -> CGRect {
        let me = self.standardized
        let result = CGRect(center: cxy, size: me.size)
        return result
    }
}

let g = a.newCenter(CGPoint(x: 50, y: 30))
print("a.center = \(a.center)")
print("a = \(a)")
print("g = \(g)")
print("g.center = \(g.center)")
/* prints
a.center = (25.0, 15.0)
a = (15.0, 10.0, 20.0, 10.0)
g = (40.0, 25.0, 20.0, 10.0)
g.center = (50.0, 30.)
*/

/*
 and, last but not least, an overload of newCenter taking 
 x,y instead of CGPoint.
*/

extension CGRect {
    public func newCenter(newX newX: CGFloat, newY: CGFloat
            -> CGRect {
        let me = self.standardized
        let result = CGRect(center: CGPoint(x: newX, y: newY), 
                              size: me.size)
        return result
    }
}

let h = a.newCenter(newX: 50, newY: 30)
print("a.center = \(a.center)")
print("a = \(a)")
print("h = \(h)")
print("h.center = \(h.center)")
/* prints
a.center = (25.0, 15.0)
a = (15.0, 10.0, 20.0, 10.0)
h = (40.0, 25.0, 20.0, 10.0)
h.center = (50.0, 30.0)
*/

/*
 One CGRect function I don't need to implement is "insetBy",
 which reduces the size of a rectangle, but keeps the same 
 center by altering the origin.
*/

let i = h.insetBy(dx: 3, dy: 3)
print("h = \(h)")
print("i = \(i)")
print("i.center = \(i.center)")
print("h.center = \(h.center)")
/* prints
h = (40.0, 25.0, 20.0, 10.0)
i = (43.0, 28.0, 14.0, 4.0)
i.center = (50.0, 30.0)
h.center = (50.0, 30.0)
*/

//
//  CGRectExtensions.swift
//
//  Created by C. Keith Ray on 2016.08.06.
//  Copyright © 2016 by C. Keith Ray. All rights reserved.
//  Permission is granted for anyone to use and modify this
//  code for any purpose, as long as this copyright message
//  is retained.
//

import Foundation
import CoreGraphics

extension CGRect {
    
    public init(center: CGPoint, size: CGSize) {
        let origin = CGPoint(x: center.x - size.width/2.0,
            y: center.y - size.height/2.0)
        self.init(origin: origin, size: size)
    }
    
    public var center: CGPoint {
        get { return CGPoint(x: self.midX, y: self.midY) }
    }
    
    public func newCenter(cxy: CGPoint) -> CGRect {
        let me = self.standardized
        let result = CGRect(center: cxy, size: me.size)
        return result
    }
    
    public func newCenter(newX newX: CGFloat, newY: CGFloat
            -> CGRect {
        let me = self.standardized
        let result = CGRect(center: CGPoint(x: newX, y: newY), 
                              size: me.size)
        return result
    }
    
    public func incXBy(deltaX: CGFloat) -> CGRect {
        var result = self.standardized
        result.origin.x += deltaX
        return result
    }
    
    public func incYBy(deltaY: CGFloat) -> CGRect {
        var result = self.standardized
        result.origin.y += deltaY
        return result
    }
    
    public func incOriginBy(dx deltaX: CGFloat
                            dy deltaY: CGFloat) -> CGRect {
        var result = self.standardized
        result.origin.x += deltaX
        result.origin.y += deltaY
        return result
    }
    
    public func incWidthBy(deltaX: CGFloat) -> CGRect {
        var result = self.standardized;
        result.size.width += deltaX
        return result
    }
    
    public func incHeightBy(deltaY: CGFloat) -> CGRect {
        var result = self.standardized;
        result.size.height += deltaY
        return result
    }
    
    public func incSizeBy(dx deltaX: CGFloat
                          dy deltaY: CGFloat) -> CGRect {
        var result = self.standardized;
        result.size.width += deltaX
        result.size.height += deltaY
        return result
    }
}

//
//  CGRectExtensionsTests.swift
//
//  Created by C. Keith Ray on 2016.08.06.
//  Permission is granted for anyone to use and modify this
//  code for any purpose, as long as this copyright message
//  is retained.

import XCTest

class CGRectExtensionsTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
    }
    
    override func tearDown() {
        super.tearDown()
    }
    
    func test_init_center() {
        let result = CGRect(center: CGPoint(
            x: 120.0y: 100.0), size: CGSize(width: 
            6.0, height: 4.0))
        let expected = CGRect(x: 120 - 3, y: 100 - 2
            width: 6.0height: 4.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_center() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.center
        let expected = CGPoint(x: 60.0, y: 120.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incXBy5() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incXBy(5.0)
        let expected = CGRect(x: 15.0, y: 20.0
            width: 100.0height: 200.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incXByMinus5() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incXBy(-5.0)
        let expected = CGRect(x: 5.0, y: 20.0
            width: 100.0height: 200.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incYBy5() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incYBy(5.0)
        let expected = CGRect(x: 10.0, y: 25.0
            width: 100.0height: 200.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incYByMinus5() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incYBy(-5.0)
        let expected = CGRect(x: 10.0, y: 15.0
            width: 100.0height: 200.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incOriginBy() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incOriginBy(dx: 5.0, dy: 6.0)
        let expected = CGRect(x: 15.0, y: 26.0
            width: 100.0height: 200.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incWidthBy() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incWidthBy(5.0)
        let expected = CGRect(x: 10.0, y: 20.0
            width: 105.0height: 200.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incHeightBy() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incHeightBy(5.0)
        let expected = CGRect(x: 10.0, y: 20.0
            width: 100.0height: 205.0)
        XCTAssertEqual(result, expected)
    }
    
    func test_incSizeBy() {
        let input = CGRect(x: 10.0, y: 20.0, width: 
            100.0height: 200.0)
        let result = input.incSizeBy(dx: 5.0, dy: 4.0)
        let expected = CGRect(x: 10.0, y: 20.0
            width: 105.0height: 204.0)
        XCTAssertEqual(result, expected)
    }
}


Thursday, August 27, 2015

DDMathParser

Today’s blog entry (and hopefully for a few more) is about Dave DeLong’s DDMathParser, which is hosted here:  https://github.com/davedelong/DDMathParser.

I found DDMathParser after I wrote a calculator app that was intended to provide very precise math, when I was looking to improve its parsing of equations.

I’m also a little obsessed with numeric precision, but only a smidgen.

Computers and programmers usually represent “real number” types using float or double in C/C++ (and similarly-named types in many other languages). These can store a certain number of digits and an exponent. If you exceed that number of digits, you are no longer doing math precisely, and that can be VERY important when you dealing with money.

Never use float or double to calculate quantities of money that are large or which need to be precise. (Who likes sloppy accounting?)

To illustrate the lack of precision that double gets you, we can run the command-line demo of DDMathParser:

Math Evaluator!
    Type a mathematical expression to evaluate it.
    Type "functions" to show available functions
    Type "operators" to show available operators
    Type "exit" to quit
> 300.1 ** 14
    Parsed: pow(300.1,14)
    Rewritten as: 4.805337947671661e+34
    Evaluated: 4.805337947671661e+34
> 300.1 ** 14 + 1
    Parsed: add(pow(300.1,14),1)
    Rewritten as: 4.805337947671661e+34
    Evaluated: 4.805337947671661e+34

As you can see here, (300.1 ** 14) prints out as 4.805337947671661e+34,
and (300.1 ** 14 + 1) prints out as              4.805337947671661e+34.

(I’ve made sure the two numbers line up so you can see that they look the same.)

They not only look the same, but when I use == to compare them, it says they are the same!

> 300.1 ** 14 == 300.1 ** 14 + 1
    Parsed: l_eq(pow(300.1,14),add(pow(300.1,14),1))
    Rewritten as: 1
    Evaluated: 1

However, Dave provides a “high precision” option. So let’s turn that on.

In main.c we have:

int main (int argc, const char * argv[]) {
    ...
    DDMathEvaluator *evaluator = [[DDMathEvaluator alloc] init];

and when we look at the header file DDMathEvaluator.h we see:

    @interface DDMathEvaluator : NSObject

    @property (nonatomic) BOOL usesHighPrecisionEvaluation; // default is NO

so let’s add a line to main.c:

int main (int argc, const char * argv[]) {
    ...
    DDMathEvaluator *evaluator = [[DDMathEvaluator alloc] init];
    [evaluator setUsesHighPrecisionEvaluation: YES];

and run the command-line demo:

> 300.1 ** 14
    Parsed: pow(300.1,14)
    Rewritten as: 48053379476716554740761909736735670.908
    Evaluated: 48053379476716554740761909736735670.908
> 300.1 ** 14 + 1
    Parsed: add(pow(300.1,14),1)
    Rewritten as: 48053379476716554740761909736735671.908
    Evaluated: 48053379476716554740761909736735671.908
> 300.1 ** 14 == 1 + 300.1 ** 14
    Parsed: l_eq(pow(300.1,14),add(1,pow(300.1,14)))
    Rewritten as: 0
    Evaluated: 0

This time, instead of (300.1 ** 14) printing out as 4.805337947671661e+34,
it prints out as 48053379476716554740761909736735670.908; and, (300.1 ** 14 + 1)
prints as        48053379476716554740761909736735671.908.

In default precision mode, DDMathParser uses NSNumber, which uses float in 32-bit applications, and double in 64-bit apps. (The Swift version currently uses Double instead of NSNumber and doesn’t implement a high precision mode.)

In high precision mode, DDMathParser uses NSDecimal. Apple has provided NSDecimal as a money-safe numeric type in Foundation/NSDecimal.h.

NSDecimal can represent some big numbers very precisely, but it has a limit on the magnitude and number of digits it allows. According to Apple’s documentation for NSDecimalNumber (which seems to use NSDecimal internally), NSDecimal can represent any number that can be expressed as (mantissa * 10 ** exponent) where mantissa is a decimal integer up to 38 digits long, and exponent is an integer from –128 through 127.

We can test these limits using factorial:

> 103!
    Parsed: factorial(103)
    Rewritten as: 9902 9007164861 8040754671 5254581773 3488000000 
                       0000000000 0000000000 0000000000 0000000000 
                       0000000000 0000000000 0000000000 0000000000 
                       0000000000 0000000000 0000000000 0000000000
    Evaluated: ...
> 104!
    Parsed: factorial(104)
    Rewritten as: NaN
    Evaluated: NaN

(I modified the output to group digits by tens.)

I picked factorial because DDMathParser’s code to compute factorial with NSDecimal explicitly looks for the error NSCalculationOverflow. I found only four other places checking for underflow/overflow/etc., so I conjecture that most math in DDMathParser isn’t checking for those kinds of errors. (I could be wrong.)

In comparison, Python has infinite-precision integers, making it very good for financial calculations. Check it out:

$ python
Python 2.7.10 (default, Jul 14 2015, 19:46:27) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import math
>>> math.factorial(103)

   9902 9007164861 8040754671 5254581773 3490901658 
        2211449248 3005280554 6998766658 4162228321 
        4144107388 3538492653 5163859772 9209322288 
        2134415149 8915840000 0000000000 0000000000L

>>> math.factorial(104)

1029901 6745145627 6238485838 6476504428 3053772454 
        9990721823 2549177688 7871732475 2871745427 
        0987168388 8003235965 7041416383 7769517974 
        1979175588 7247360000 0000000000 0000000000L

(I also modified this output to group digits by tens.) Notice that the DDMathParser/NSDecimal version of 103! isn’t the same as Python’s 103!.

There’s a lot to explore with DDMathParser. For one, how does it actually parse out equations? For two, can we convert it to Swift? For three, what might be different if we developed this code in a test-driven style?

Dave has already converted some (most?) of the DDMathParser code to Swift. I’d like to get some practice with Swift, so in the next blog entry of this series, I’ll try to write some unit tests for some DDMathParser classes.

Other ideas: If we add overloaded math operators for NSDecimalNumber, we can simplify how that class can be used. If we wrap NSDecimal or NSDecimalNumber in a Swift struct, that might be even better. Swiftifying NSDecimal or NSDecimalNumber could be a separate project, but since it’s a good practice to actually use the code that you write, I’d like to do that in the context of DDMathParser.