1

I have list of ingredients like below in NSArray as NSString object.

"1/2 Cup Milk",
"125 grams Cashew",
"2 green onions",
"1/4 Cup Sugar",
"1/6 Spoon Salt",
"3/2 XYZ",
"One cup water",
"Almond oil"

And I want to sort them like below.

"1/6 Spoon Salt",
"1/4 Cup Sugar",
"1/2 Cup Milk",
"3/2 XYZ",
"2 green onions",
"125 grams Cashew",
"Almond oil",
"One cup water",

Attempt 1: Sort using localizedStandardCompare & NSNumericSearch both results are same.

Result :

"1/2 Cup Milk",
"1/4 Cup Sugar",
"1/6 Spoon Salt",
"2 green onions",
"3/2 XYZ",
"125 grams Cashew",
"Almond oil",
"One cup water"

I know it is possible but somehow I am unable to figure this out.

If anyone has done similar thing you can guide me.

Thanks, In advance.

Pratik Mistry
  • 2,905
  • 1
  • 22
  • 35
  • Have you attempted to do a numeric sort on the array of strings (there are many examples showing how)? I don't know if it properly handles fractions. – rmaddy Oct 03 '16 at 18:54
  • See http://stackoverflow.com/questions/2846301/how-to-do-a-natural-sort-on-an-nsarray and let us know if it works with your fractions. – rmaddy Oct 03 '16 at 18:58
  • @rmaddy Ok let me try this one. – Pratik Mistry Oct 03 '16 at 18:59
  • @rmaddy Sadly, no, that doesn't handle fractions. Nor can it handle "One cup" (though it doesn't sound like he wants that sorted as "1", anyway). – Rob Oct 03 '16 at 19:19
  • Yes @Rob I don't want One Cup sorted but if it can do than it's a great thing. – Pratik Mistry Oct 03 '16 at 19:21
  • @rmaddy tried and updated question with results. – Pratik Mistry Oct 03 '16 at 19:35
  • I didn't think the numeric sort would handle the fractions but it was worth trying. This means you will have to write your own sorting code. Something along the lines of extracting the first word of each string and seeing if it is a number or fraction and handling it accordingly. – rmaddy Oct 03 '16 at 20:05

2 Answers2

2

You can define a category on NSString to compare numbers (including fractions):

@interface NSString (Number)
- (NSNumber * _Nullable)numberValue;
- (NSComparisonResult)compareNumber:(NSString *)string;
@end

@implementation NSString (Number)

- (NSNumber *)numberValue {
    NSError *error;
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^\\s*(\\d+)\\s*/\\s*(\\d+)" options:0 error:&error];
    NSAssert(regex, @"%@", error.localizedDescription);

    NSTextCheckingResult *match = [regex firstMatchInString:self options:0 range:NSMakeRange(0, self.length)];
    if (match) {
        float numerator = [[self substringWithRange:[match rangeAtIndex:1]] floatValue];
        float denominator = [[self substringWithRange:[match rangeAtIndex:2]] floatValue];
        return denominator ? @(numerator / denominator) : nil;
    }

    regex = [NSRegularExpression regularExpressionWithPattern:@"^\\s*(\\d+)" options:0 error:&error];
    match = [regex firstMatchInString:self options:0 range:NSMakeRange(0, self.length)];
    if (match) {
        return @([self floatValue]);
    }

    // if you don't want it to recognize spelt out numbers, comment the following bit of code

    regex = [NSRegularExpression regularExpressionWithPattern:@"^\\s*(\\S+)" options:0 error:&error];
    match = [regex firstMatchInString:self options:0 range:NSMakeRange(0, self.length)];
    if (match) {
        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
        formatter.numberStyle = NSNumberFormatterSpellOutStyle;
        return [formatter numberFromString:[[self substringWithRange:[match rangeAtIndex:1]] lowercaseString]];
    }

    return nil;
}

- (NSComparisonResult)compareNumber:(NSString *)string {
    NSNumber *number1 = [self numberValue];
    NSNumber *number2 = [string numberValue];

    if (number1 && !number2) {
        return NSOrderedAscending;
    }

    if (number2 && !number1) {
        return NSOrderedDescending;
    }

    if (number1 && number2) {
        NSComparisonResult numericComparison = [number1 compare:number2];
        if (numericComparison != NSOrderedSame) {
            return numericComparison;
        }
    }

    return [self caseInsensitiveCompare:string];
}

@end

You can then sort the ingredients like so:

NSArray *ingredients = @[@"1/2 Cup Milk",
                         @"125 grams Cashew",
                         @"2 green onions",
                         @"1/4 Cup Sugar",
                         @"1/6 Spoon Salt",
                         @"3/2 XYZ",
                         @"One cup water",
                         @"Almond oil"];

NSArray *sorted = [ingredients sortedArrayUsingComparator:^NSComparisonResult(NSString  * _Nonnull obj1, NSString  * _Nonnull obj2) {
    return [obj1 compareNumber:obj2];
}];

That yields:

"1/6 Spoon Salt",
"1/4 Cup Sugar",
"1/2 Cup Milk",
"One cup water",
"3/2 XYZ",
"2 green onions",
"125 grams Cashew",
"Almond oil"

I must confess that the NSNumberFormatter logic for spelt out numbers isn't robust (it recognizes "Thirty-Two", but not "Thirty Two"), but you can play with that if you want. Or you might pull that entirely.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
2

It seems Rob posted his nice answer while I was busy writing my own. Since I spent the time I may as well post mine. They are similar but different.

I started with an extension to NSString as well:

@interface NSString (NumberSort)

- (nullable NSNumber *)leadingNumber;

@end

@implementation NSString (NumberSort)

- (nullable NSNumber *)leadingNumber {
    if (self.length < 1) return nil;

    // See if the string starts with a digit
    unichar first = [self characterAtIndex:0];
    if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:first]) {
        // It does so now get the first word (number)
        NSString *numStr = self;
        NSRange spaceRange = [self rangeOfString:@" "];
        if (spaceRange.location != NSNotFound) {
            numStr = [self substringToIndex:spaceRange.location];
        }

        // Now see if the leading number is actually a fraction
        NSRange slashRange = [numStr rangeOfString:@"/"];
        if (slashRange.location != NSNotFound) {
            // It's a fraction. Compute its value
            NSString *numeratorStr = [[numStr substringToIndex:slashRange.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
            NSString *denominatorStr = [[numStr substringFromIndex:slashRange.location + slashRange.length] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

            NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
            fmt.numberStyle = NSNumberFormatterDecimalStyle;
            NSNumber *numerator = [fmt numberFromString:numeratorStr];
            NSNumber *denominator = [fmt numberFromString:denominatorStr];
            if (numerator && denominator) {
                return @([numerator doubleValue] / [denominator doubleValue]);
            }
        } else {
            // Not a fraction, convert number string to number
            NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
            fmt.numberStyle = NSNumberFormatterDecimalStyle;
            NSNumber *num = [fmt numberFromString:numStr];

            return num;
        }
    } else {
        // See if string starts with spelled out number
        NSString *numStr = self;
        NSRange spaceRange = [self rangeOfString:@" "];
        if (spaceRange.location != NSNotFound) {
            numStr = [self substringToIndex:spaceRange.location];
        }

        NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
        fmt.numberStyle = NSNumberFormatterSpellOutStyle;
        NSNumber *num = [fmt numberFromString:[numStr lowercaseString]];

        return num;
    }

    return nil;
}

@end

Then I used a simple comparator block to process the array:

NSArray *ingrediants = @[
    @"1/2 Cup Milk",
    @"125 grams Cashew",
    @"2 green onions",
    @"1/4 Cup Sugar",
    @"1/6 Spoon Salt",
    @"3/2 XYZ",
    @"One cup water",
    @"Almond oil"
];

NSArray *sorted = [ingrediants sortedArrayUsingComparator:^NSComparisonResult(NSString * _Nonnull str1, NSString * _Nonnull str2) {
    NSNumber *num1 = [str1 leadingNumber];
    NSNumber *num2 = [str2 leadingNumber];

    if (num1) {
        if (num2) {
            return [num1 compare:num2];
        } else {
            return NSOrderedAscending;
        }
    } else {
        if (num2) {
            return NSOrderedDescending;
        } else {
            return [str1 compare:str2 options:NSCaseInsensitiveSearch];
        }
    }
}];

NSLog(@"Ordered: %@", sorted);

Output:

Ordered: (
    "1/6 Spoon Salt",
    "1/4 Cup Sugar",
    "1/2 Cup Milk",
    "One cup water",
    "3/2 XYZ",
    "2 green onions",
    "125 grams Cashew",
    "Almond oil"
)

My code suffers from a similar problem when it comes to handling spelled out numbers. My code, as-is, only handles one-word numbers. It wouldn't take too much to handle multi-word spelled out numbers if needed.

My code also requires that there be no spaces in any fraction. Again, a little work could work around this limitation.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • Glad to help. Don't forget that you can up vote any number of answers in addition to accepting one. – rmaddy Oct 04 '16 at 05:25