0

I want to write a method that provides a human-readable string representation of arrays, with comma-spaces when necessary (incl. the Oxford Comma), and a conjoining " and " at the end.

For example, say I have these arrays:

NSArray *nun = @[];
NSArray *won = @[@"The Loneliest"];
NSArray *too = @[@"Peas", @"Pod"];
NSArray *tree = @[@"Apple", @"Falls", @"Far, Far Away"];

I want to write a method like:

+ (NSString*) humanReadableListFromArray: (NSArray*) arr
{
  // magic
}

And when I pass my arrays through, I want them to look like this:

@""
@"The Loneliest"
@"Peas and Pod"
@"Apple, Falls, and Far, Far Away"

Note that the first one, having exactly 1 item, is just the first item without decoration. The second, having exactly 2 items, has no commas, but does have the conjoining " and ". The third, having more than 2 items, includes a comma-space between each item, and the last item includes an additional and just after the comma-space and before the item.

Is there a short way to do this in Objective C? I've done this in languages like Java, but I know that Objective C features methods like -(NSString*)[NSArray componentsJoinedByString:] that might assist in this.


This question is not answered by Replace last comma in string with an "and". (Objective-C) because it does not address commas in the last array item. I've also looked into questions like Objective-C Simplest way to create comma separated string from an array of objects, and they don't mention this nice part of human readability.

Community
  • 1
  • 1
Ky -
  • 30,724
  • 51
  • 192
  • 308
  • duplicate of: http://stackoverflow.com/questions/5614588/replace-last-comma-in-string-with-an-and-objective-c create the comma separated list and then follow this link to replace the last comma. That should get you where you want to go. – Nico Jul 08 '15 at 18:22
  • @NicolásCarlo That doesn't work if the last item has a comma in it. See my edit for explanation. – Ky - Jul 08 '15 at 18:25
  • 1
    There is no "short way" to do this in Objective-C. You need to write a for loop and add the "and" when needed. Update your question with what you have tried. – rmaddy Jul 08 '15 at 18:39

6 Answers6

6

That third party library @timgcarlson mentions sounds promising. Here's what I'd do natively...

- (NSString *)humanReadableListFromArray:(NSArray *)array withOxfordStyle:(BOOL)oxford {
    if (array.count == 0) return @"";
    if (array.count == 1) return array[0];
    if (array.count == 2) return [array componentsJoinedByString:@" and "];

    NSArray *firstItems = [array subarrayWithRange:NSMakeRange(0, array.count-1)];
    NSString *lastItem = [array lastObject];
    NSString *lastDelimiter = (oxford)? @", and " : @" and ";
    return [NSString stringWithFormat:@"%@%@%@",
        [firstItems componentsJoinedByString:@", "], lastDelimiter, lastItem];
}
danh
  • 62,181
  • 10
  • 95
  • 136
  • 1
    Beat me to the punch. :) All you need from there is a switch for whether the >=3 case uses an Oxford comma. – rickster Jul 08 '15 at 18:42
  • Nice idea @rickster, regarding optional style. Please see if that edit captures what you meant. – danh Jul 08 '15 at 18:55
  • Nice. Strunk and White would be proud. Or at least maybe [Weird Al would](http://www.vevo.com/watch/weird-al-yankovic/Word-Crimes/USRV81400343). – rickster Jul 08 '15 at 18:56
3

TTTFormatterKit has exactly what you need.

Here is the example from the project's README...

NSArray *list = [NSArray arrayWithObjects:@"Russel", @"Spinoza", @"Rawls", nil];
TTTArrayFormatter *arrayFormatter = [[TTTArrayFormatter alloc] init];
[arrayFormatter setUsesSerialDelimiter:NO]; // Omit Oxford Comma
NSLog(@"%@", [arrayFormatter stringFromArray:list]); // "Russell, Spinoza and Rawls"
timgcarlson
  • 3,017
  • 25
  • 52
  • This looks awesome! Unfortunately this is an enterprise application, so there's a lot of red tape to go through to add a library; I doubt they'll let me for just this one use. I'll definitely remember it for my personal projects, though! – Ky - Jul 08 '15 at 19:52
2

As of iOS 13, there is now built-in support for this: (NS)ListFormatter, which is even better than the other answers listed here, because it handles localization.

Kelan
  • 2,296
  • 1
  • 15
  • 17
1

Not tested:

+ (NSString*)humanReadableListFromArray:(NSArray*) arr
{
   NSUInteger count = [arr count];
   if (count == 0)
        return @"";
   else if (count == 1)
        return arr[0];

   NSString *subarrayStr;
   if (count == 2) {
       subarrayStr = arr[0];
   } else {
       NSArray *subarray = [arr subarrayWithRange:NSMakeRange(0, count - 1)];
       subarrayStr = [subarray componentsJoinedByString:@", "];
   }

   return [[subarrayStr stringByAppendingString:@" and "] stringByAppendingString:arr[count - 1]];
}
Droppy
  • 9,691
  • 1
  • 20
  • 27
1

Here is how to do it in Swift 3:

extension Collection where Iterator.Element == String {
    func joinedWithComma() -> String {
        var string = joined(separator: ", ")

        if let lastCommaRange = string.range(of: ", ", options: .backwards) {
            string.replaceSubrange(lastCommaRange, with: " and ")
        }

        return string
    }
}

["A", "B", "C"].joinedWithComma() // returns "A, B and C"
phatmann
  • 18,161
  • 7
  • 61
  • 51
  • This breaks down when the last list item has a comma in it. Like `["(1, 2)", "(0, 0)", "(5, 4)"]`. This erroneously outputs `"(1, 2), (0, 0), (5, and 4)"` – Ky - Feb 06 '17 at 21:09
  • 2
    It is a very naive algorithm. Please improve on it if you have time. – phatmann Feb 11 '17 at 01:04
1

Here's a Swift 4 version. It supports commas within items, the optional use of the oxford comma and a max item count. If the number of elements is higher than maxItemCount it shortens the list and adds etc. to the end of the list. Note: It is assumed that this function is used to add lists to the end of a sentence and so there is no punctuation after "etc", otherwise you would end up with 2 periods e.g. "I like many fruits: apples, pears, strawberries, etc.."

extension Array where Iterator.Element == String {
    func joinedWithComma(useOxfordComma: Bool = false, maxItemCount: Int = -1) -> String {
        let result: String
        if maxItemCount >= 0 && count > maxItemCount {
            result = self[0 ..< maxItemCount].joined(separator: ", ") + ", etc"
        } else if count >= 2 {
            let lastIndex = count - 1
            let extraComma = (useOxfordComma && count > 2) ? "," : ""
            result = self[0 ..< lastIndex].joined(separator: ", ") + extraComma + " and " + self[lastIndex]
        } else if count == 1 {
            result = self[0]
        } else {
            result = ""
        }

        return result
    }
}
Cortis Clark
  • 169
  • 1
  • 5
  • Why use Int with all negatives meaning the same thing, when you could use a options UInt? – Ky - Sep 11 '18 at 16:08
  • @BenLeggiero did you mean "optional" instead of "options"? Sure, that would work, and can remove the check for >= 0, but then I have an additional if let statement (or a non-nil check and and two force unwraps.) Technically I should probably be using a guard against maxItemCount less than 3 because "etc." doesn't make sense to use (in English) without at least two items before it, but that puts more of a burden on the caller to have to remember the valid ranges for maxItemCount. – Cortis Clark Sep 11 '18 at 19:04
  • @BenLeggiero To me, -1 has a sensical mathematical meaning in this context (admittedly in a round about way and only to a programmer). -1 is 0xFFFFF... (number of F's depends on the integer size) and so has an equivalent bit pattern to UInt.max which is as close as we can get to infinite within the range (infinite is what is trying to be expressed). In fact its better, in my mind, than UInt.max itself since UInt.max is not infinite and would be unclear as to whether it semantically means use "etc." only when the count reaches that high or never use "etc.". – Cortis Clark Sep 11 '18 at 19:06
  • Yes, sorry; autocorrect decided "options" was the better word :P – Ky - Sep 11 '18 at 19:22
  • First off, your answer is great. +1! It follows practices that are time tested and takes advantage of great Swift features like default argument values and immutability. I just wanted your perspective on your approach. That said, Apple along with the rest of the industry are moving away from Magic Numbers, whose meanings or macros you have to remember (like `NSNotFound`), toward semantic values (like `nil`/`null` or specialized enumerated cases). – Ky - Sep 11 '18 at 19:29
  • In your example, all negative numbers are the same, meaning you're delegating ~9 quintillion possible values to mean the same thing. I think that's silly, and would probably make the type a `UInt8?` (since we're designing this for humans, 256 is plenty of list items, and after that you might as well not limit it). Plus, the `Optional` enum is so lightweight that it's hardly boxing at all. I've created [a gist with some changes I'd make](https://gist.github.com/BenLeggiero/dbe7f2f8845087e46ac94485955abaab). Again, not saying yours is bad; just trying to say my perspective – Ky - Sep 11 '18 at 19:30