Pratik Mistry Pratik Mistry - 2 months ago 7
Objective-C Question

How to sort ingredients inside NSString? Ex. 1/2 Cup Milk, 1/4 Spoon Powder

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.

Rob Rob
Answer

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.