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.