10
\$\begingroup\$

Inspired by this question: Fraction class implemented in Objective-C, I have written what I feel is an improved version of the Fraction class in Objective-C.

As per the tips in this answer, the class is immutable (and a mutable subclass might eventually happen). Besides improving on the existing functionality and changing to an immutable object, I also added the necessary methods for comparing fractions to other objects, other fractions, and importantly adding the compare: method so that the fractions can be sorted!

Fraction.h

#import <Foundation/Foundation.h>

@interface Fraction : NSObject
/*
    An immutable class for representing and doing math 
        with rational numbers represented as fractions 
        with a numerator and denominator part.
 */

@property (readonly) NSInteger numerator;
@property (readonly) NSInteger denominator;

- (instancetype)initWithNumerator:(NSInteger)numerator 
                      denominator:(NSInteger)denominator;

+ (instancetype)notANumber;
+ (instancetype)zero;
+ (instancetype)one;
+ (instancetype)fractionWithInteger:(NSInteger)integer;
+ (instancetype)fractionWithReciprocalOf:(NSInteger)integer;
+ (instancetype)fractionWithNumerator:(NSInteger)numerator 
                          denominator:(NSInteger)denominator;
+ (instancetype)fractionWithFraction:(Fraction *)fraction;

// TODO: init with floating points.

- (instancetype)fractionByAddingFraction:(Fraction *)fraction;
- (instancetype)fractionBySubtractingFraction:(Fraction *)fraction;
- (instancetype)fractionByMultiplyingByFraction:(Fraction *)fraction;
- (instancetype)fractionByDividingByFraction:(Fraction *)fraction;

- (instancetype)reduced;
- (instancetype)reciprocal;

- (double)doubleValue;
- (float)floatValue;
- (int)intValue;
- (long)longValue;
- (NSInteger)integerValue;

- (BOOL)isEqualToFraction:(Fraction *)fraction;
- (NSComparisonResult)compare:(Fraction *)fraction;

@end

@interface NSNumber(Fraction)

+ (instancetype)numberWithFraction:(Fraction *)fraction;

@end

Fraction.m

#import "Fraction.h"

NSInteger greatestCommonDenominator(NSInteger first, NSInteger second) {
    return second == 0 ? 
        first : greatestCommonDenominator(second, first % second);
}

@implementation Fraction {
    BOOL _isNotANumber;
    NSInteger _numerator;
    NSInteger _denominator;
}

// Init methods
- (instancetype)initWithNumerator:(NSInteger)numerator 
                      denominator:(NSInteger)denominator {
    self = [super init];
    if (self) {
        _isNotANumber = (denominator == 0);
        _numerator = numerator;
        _denominator = denominator;
    }
    return self;
}

- (instancetype)init {
    return [self initWithNumerator:0 denominator:1];
}

// Factory methods
+ (instancetype)notANumber {
    return [[self alloc] initWithNumerator:0 denominator:0];
}

+ (instancetype)zero {
    return [[self alloc] initWithNumerator:0 denominator:1];
}

+ (instancetype)one {
    return [[self alloc] initWithNumerator:1 denominator:1];
}

+ (instancetype)fractionWithInteger:(NSInteger)integer {
    return [[self alloc] initWithNumerator:integer denominator:1];
}

+ (instancetype)fractionWithReciprocalOf:(NSInteger)integer {
    return [[self alloc] initWithNumerator:1 denominator:integer];
}

+ (instancetype)fractionWithNumerator:(NSInteger)numerator 
                          denominator:(NSInteger)denominator {
    return [[self alloc] initWithNumerator:numerator 
                               denominator:denominator];
}

+ (instancetype)fractionWithFraction:(Fraction *)fraction {
    return [[self alloc] initWithNumerator:fraction.numerator 
                               denominator:fraction.denominator];
}

// Math operations
- (instancetype)fractionByAddingFraction:(Fraction *)fraction {
    if (_isNotANumber || fraction->_isNotANumber) {
        return [[self class] notANumber];
    } else {
        NSInteger newNumerator = (self.numerator * fraction.denominator) + 
            (self.denominator * fraction.denominator);
        NSInteger newDenominator = self.denominator * fraction.denominator;
        Fraction *newFraction = 
            [[self class] fractionWithNumerator:newNumerator
                                    denominator:newDenominator];
        return [newFraction reduced];
    }
}

- (instancetype)fractionBySubtractingFraction:(Fraction *)fraction {
    if (_isNotANumber || fraction->_isNotANumber) {
        return [[self class] notANumber];
    } else {
        NSInteger newNumerator = (self.numerator * fraction.denominator) - 
            (self.denominator * fraction.denominator);
        NSInteger newDenominator = self.denominator * fraction.denominator;
        Fraction *newFraction = 
            [[self class] fractionWithNumerator:newNumerator 
                                    denominator:newDenominator];
        return [newFraction reduced];
    }
}

- (instancetype)fractionByMultiplyingByFraction:(Fraction *)fraction {
    if (_isNotANumber || fraction->_isNotANumber) {
        return [[self class] notANumber];
    } else {
        NSInteger newNumerator = self.numerator * fraction.numerator;
        NSInteger newDenominator = self.denominator * fraction.denominator;
        Fraction *newFraction = 
            [[self class] fractionWithNumerator:newNumerator 
                                    denominator:newDenominator];
        return [newFraction reduced];
    }
}

- (instancetype)fractionByDividingByFraction:(Fraction *)fraction {
        Fraction *divisor = [fraction reciprocal];
        return [self fractionByMultiplyingByFraction:divisor];
}

- (instancetype)reduced {
    if (_isNotANumber) {
        return [[self class] notANumber];
    } else {
        NSInteger gcd = greatestCommonDenominator(self.numerator,
                                                  self.denominator);
        NSInteger newNumerator = self.numerator / gcd;
        NSInteger newDenominator = self.denominator / gcd;
        return [[self class] fractionWithNumerator:newNumerator
                                       denominator:newDenominator];
    }
}

- (instancetype)reciprocal {
    if (_isNotANumber) {
        return [[self class] notANumber];
    } else {
        return [[self class] fractionWithNumerator:self.denominator 
                                       denominator:self.numerator];
    }
}

// Property methods
- (NSInteger)numerator {
    return _isNotANumber ? NAN : _numerator;
}

- (NSInteger)denominator {
    return _isNotANumber ? NAN : _denominator;
}

// "extractor" methods?
- (double)doubleValue {
    return _isNotANumber ? NAN : (double)self.numerator / (double)self.denominator;
}

- (float)floatValue {
    return _isNotANumber ? NAN : (float)self.numerator / (float)self.denominator;
}

- (int)intValue {
    return _isNotANumber ? NAN : (int)self.numerator / (int)self.denominator;
}

- (long)longValue {
    return _isNotANumber ? NAN : (long)self.numerator / (long)self.denominator;
}

- (NSInteger)integerValue {
    return _isNotANumber ? NAN : self.numerator / self.denominator;
}


- (NSString *)description {
    return [NSString stringWithFormat:@"%li/%li", 
               (long)self.numerator, 
               (long)self.denominator
            ];
}

// Comparison methods
- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    } if ([object respondsToSelector:@selector(doubleValue)]) {
        return [self doubleValue] == [object doubleValue];
    } else {
        return NO;
    }
}

- (BOOL)isEqualToFraction:(Fraction *)fraction {
    if (self == fraction) {
        return YES;
    } else {
        return [self doubleValue] == [fraction doubleValue];
    }
}

- (NSComparisonResult)compare:(Fraction *)fraction {
    double left = [self doubleValue];
    double right = [fraction doubleValue];

    if (left > right) {
        return NSOrderedDescending;
    } else if (left < right) {
        return NSOrderedAscending;
    } else {
        return NSOrderedSame;
    }
}

@end

@implementation NSNumber(Fraction)

+ (instancetype)numberWithFraction:(Fraction *)fraction {
    return [self numberWithDouble:[fraction doubleValue]];
}

@end

I didn't feel the need to provide an NSDecimalNumber category, as it is a subclass of NSNumber, so by simply having the NSNumber category here, the numberWithFraction: method can be used to instantiate either an NSNumber or NSDecimalNumber object.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

One big issue I have with your code is that all of of your comparisons are based off of the fraction's double value. This can lead to many bugs down the line that result in questions like this.


To fix this: you can do one of two things: define or pass an epsilon (such as .00001) to be used for comparison.

Or even better, use the denominator and numerator to compare fractions.

To do so, I would utilize memoization (to make this performant for repeated calls on the same fraction), and to to store the fraction in reduced form. Then you do a simple comparison on the reduced form like so:

  • Fraction 1: \$\dfrac{x}{y}\$
  • Fraction 2: \$\dfrac{a}{b}\$

Compare \$x*b\$ to \$a*y\$:

  1. if \$x*b > a*y\$, Fraction 1 is bigger.
  2. if \$x*b < a*y\$, Fraction 2 is bigger.
  3. if \$x*b = a*y\$, Fraction 1 is equal to Fraction 2.
\$\endgroup\$
3
  • \$\begingroup\$ Welp, ignore my comment. I see you're recommending the method take an epsilon as an argument. \$\endgroup\$
    – nhgrif
    Jul 18, 2014 at 22:40
  • \$\begingroup\$ Using x*b > ... doesn't prevent overflows (undefined behavior since NSInteger is a 64 bits signed int) \$\endgroup\$ Jul 20, 2014 at 19:19
  • \$\begingroup\$ That problem can be avoided by forcing the numerator and denominator to be 32 bits, or by storing the product in a 128 bit integer. However, there will always be an overflow problem unless you utilize a mathematical integer (with indefinite precision). My answer focused on just the mathematical aspect, but it is indeed important to note these limitations. \$\endgroup\$
    – mleyfman
    Jul 20, 2014 at 19:49
3
\$\begingroup\$

There's one issue with - (instancetype)fractionByAddingFraction:(Fraction *)fraction:

 NSInteger newNumerator = (self.numerator * fraction.denominator) + 
            (self.denominator * fraction.denominator);

should be

 NSInteger newNumerator = (self.numerator * fraction.denominator) + 
            (self.denominator * fraction.numerator);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.