0

I have two NSArrays, each containing NSStrings.

How do I check they are equivalent, ignoring case and order?

I've been playing around with NSSets and NSPredicates, but not having much luck.

Ev.
  • 7,109
  • 14
  • 53
  • 87

2 Answers2

5

Simple (case insensitive)

NSSet* one = [[NSCountedSet setWithArray:firstArray] valueForKey:@"lowercaseString"];
NSSet* two = [[NSCountedSet setWithArray:secondArray] valueForKey:@"lowercaseString"];

return [one isEqualToSet:two];

Complex (case and diacritic insensitive)

This solution is more complex, because it requires a special string comparison. You can either accomplish this by iterating sorted arrays using this method, or, if you wish to use valueForKey:, using the following code.

@interface LNComparisonHelper : NSObject

- (instancetype)initWithString:(NSString*)str;
@property (nonatomic, copy, readonly) string;

@end

@implementation LNComparisonHelper

- (instancetype)initWithString:(NSString*)str
{
    self = [super init];
    if(self) { _str = [str copy]; }
    return self;
}

- (BOOL)isEqual:(LNComparisonHelper*)other
{
    return [_str compare:other.string options:(DiacriticInsensitiveSearch | CaseInsensitiveSearch)] == OrderedSame;
}

@end

@interface NSString (ComparisonWrapper)

- (id) ln_casediacriticInsensitiveComparator;

@end

@implementation NSString (ComparisonWrapper)

- (id) ln_casediacriticInsensitiveComparator
{
    return [[LNComparisonHelper alloc] initWithString:self];
}

@end

NSSet* one = [[NSCountedSet setWithArray:firstArray] valueForKey:@"ln_casediacriticInsensitiveComparator"];
NSSet* two = [[NSCountedSet setWithArray:secondArray] valueForKey:@"ln_casediacriticInsensitiveComparator"];

return [one isEqualToSet:two];
Community
  • 1
  • 1
Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • Nice! Is there any concern with the "lowercaseString" on non-english characters? – Ev. Mar 09 '15 at 00:38
  • 1
    Nice blog post on KVC and usages such as this: [Fun with KVC](http://www.noodlesoft.com/blog/2009/06/30/fun-with-kvc/). – zaph Mar 09 '15 at 00:43
  • @Ev. Yes, all locale considerations apply. Also, diacritic insensitivity is not accomplished here, if you need it. – Léo Natan Mar 09 '15 at 00:43
  • Thanks @LeoNatan. Do you know a way to perform this with diacritic insensitivity? (I'm not being picky - I'm porting a Java app to ObjC and need equivalency). – Ev. Mar 09 '15 at 00:45
  • There is one failure mode, if one of the arrays has a duplicated string and the other does not, they will compare as equal. @["a", "b", "a"] will compare equal to @["a", "b"] or @["a", "b", "a"] and @["a", "b", "b"]. – zaph Mar 09 '15 at 00:48
  • 1
    @Ev. Look at `decomposedStringWithCanonicalMapping`, which you can then use to remove diacritics and then perform lowercase transform. You could probably create a category over `NSString` and use the method defined there with `valueForKey:`. – Léo Natan Mar 09 '15 at 00:48
  • 1
    @Zaph Fixed using `NSCountedSet `. – Léo Natan Mar 09 '15 at 00:50
  • @Ev. Tell me the requirements of your comparison. This is not easy, but your should first have an understanding of what you need. Perhaps a lowercase of `decomposedStringWithCompatibilityMapping` is your best option. – Léo Natan Mar 09 '15 at 01:07
  • @LeoNatan the app that I'm porting from takes into account localization and diacritic when comparing case of strings. Does localizedCaseInsensitiveCompare on NSString handle this? If yes, it might make more sense to just iterate over the sorted arrays, as Zaph suggested? – Ev. Mar 09 '15 at 01:22
  • @Ev. What you mean is `compare:options:` with `(DiacriticInsensitiveSearch | CaseInsensitiveSearch)` options. – Léo Natan Mar 09 '15 at 01:24
  • @LeoNatan sounds perfect to me. So is there a way to nicely implement this leaning on NSSet isEqualToSet? – Ev. Mar 09 '15 at 01:25
  • Unfortunately, no. `isEqualToSet:` uses `isEqual:` internally, which is no good. Any other solution gets complicated, unfortunately. – Léo Natan Mar 09 '15 at 01:27
  • @Ev. Your best bet is probably to iterate the arrays or sets. But see my edit for a solution that allows you to use `valueForKey:` with a category and a helper class. – Léo Natan Mar 09 '15 at 01:40
  • Interesting. Cool - nice work. I think I'll just iterate over arrays as @Zaph suggested. Not sure what to mark as the correct response. If either of you two would like to create an answer I'll be happy to mark that as correct? Or perhaps you, Leo, might like to mention the iteration solution in your answer? You guys are great. Thanks so much. – Ev. Mar 09 '15 at 01:43
  • The solution is getting rather complex. – zaph Mar 09 '15 at 03:04
1
  1. Compare array counts
  2. Copy into new NSMutableArrays
  3. Sort the arrays (sortUsingComparator: with NSCaseInsensitiveSearch and DiacriticInsensitiveSearch)
  4. By pairs in order compare strings (NSCaseInsensitiveSearch and DiacriticInsensitiveSearch)

As soon as there is a failure return NO
If no failure YES

Note: For reasonably sized arrays sorting has a low cost close to O(n).

zaph
  • 111,848
  • 21
  • 189
  • 228
  • With step 4, would you write up a loop? I was thinking I'd be able to do something like convert them to an NSSet, then do isEqualToSet while overriding the equality comparer (somehow). Thoughts? – Ev. Mar 09 '15 at 00:23
  • Just use a loop. As Kent Beck says: "Do The Simplest Thing That Could Possibly Work." Don't worry about performance unless it is proven to be needed. – zaph Mar 09 '15 at 00:24
  • Good advice. Do you know if the method I suggested is possible in objc? – Ev. Mar 09 '15 at 00:26
  • Sorry, I'm not into clever, I want the easiest to understand and debug as possible. Brian Kernighan: "If you're as clever as you can be when you write it, how will you ever debug it? – zaph Mar 09 '15 at 00:29
  • Oakie doak. Thanks for the input! – Ev. Mar 09 '15 at 00:32
  • Yup - it's a little more like what I was hoping for. I'm somewhat concerned with the lowercasestring for non-english characters... – Ev. Mar 09 '15 at 00:37