63

What setDateFormat option for NSDateFormatter do I use to get a month-day's ordinal suffix?

e.g. the snippet below currently produces:
3:11 PM Saturday August 15

What must I change to get:
3:11 PM Saturday August 15th

NSDate *date = [NSDate date];
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[dateFormatter setDateFormat:@"h:mm a EEEE MMMM d"];
NSString *dateString = [dateFormatter stringFromDate:date]; 
NSLog(@"%@", dateString);

In PHP, I'd use this for the case above:
<?php echo date('h:m A l F jS') ?>

Is there an NSDateFormatter equivalent to the S option in the PHP formatting string?

Matt Andersen
  • 4,816
  • 3
  • 24
  • 19

17 Answers17

95

None of these answers were as aesthetically pleasing as what I'm using, so I thought I would share:


Swift 3:

func daySuffix(from date: Date) -> String {
    let calendar = Calendar.current
    let dayOfMonth = calendar.component(.day, from: date)
    switch dayOfMonth {
    case 1, 21, 31: return "st"
    case 2, 22: return "nd"
    case 3, 23: return "rd"
    default: return "th"
    }
}

Objective-C:

- (NSString *)daySuffixForDate:(NSDate *)date {
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSInteger dayOfMonth = [calendar component:NSCalendarUnitDay fromDate:date];
    switch (dayOfMonth) {
        case 1:
        case 21:
        case 31: return @"st";
        case 2:
        case 22: return @"nd";
        case 3:
        case 23: return @"rd";
        default: return @"th";
    }
}

Obviously, this only works for English.

cbh2000
  • 2,183
  • 21
  • 17
  • 2
    This is the best answer! – Sheharyar May 31 '14 at 21:13
  • @cbh2000 Can you include how to concatenate this into the date string. I am not sure why dateFormatter.dateFormat = "F \(daySuffix(date)) MM yy" isn't working, thanks! – Ben Sullivan Mar 02 '16 at 12:12
  • 3
    @BenSullivan If you're going to do it that way, if should be `"F '\(daySuffix(date))' MM yy"`. Notice the ticks, which tells NSDateFormatter that "th," "st," and "rd" shouldn't be interpreted as a special symbol. – cbh2000 Mar 10 '16 at 21:59
  • 1
    No need for those `fallthrough`s, you can just put the cases together: `case 1, 21, 31:` etc – Marcus Jun 30 '16 at 15:30
  • Actually they are needed, right? They are covering all the 'th' cases. – alasker Oct 23 '16 at 14:58
  • @alasker The 'th' cases are covered by the `default` case. The `case 1, 21, 31: ` is just shorthand for `case 1: \n case 21: \n case 31: `. – cbh2000 Oct 25 '16 at 17:10
  • Yeah I see that, thought he was talking about the default case. – alasker Oct 25 '16 at 19:59
  • Worth noting that if you embed the ordinal string in the date format string using single quotes `'\(thus)'` you'll need to set the format string for each date that you wish formatted since the embedded call is only computed once. – Robin Macharg Mar 01 '18 at 11:46
  • 5
    This answer doesn't handle localization at all. Please instead use an ordinal number formatter to format numbers like this! – Warpling May 24 '20 at 15:28
51
NSDate *date = [NSDate date];
NSDateFormatter *prefixDateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[prefixDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[prefixDateFormatter setDateFormat:@"h:mm a EEEE MMMM d"];
NSString *prefixDateString = [prefixDateFormatter stringFromDate:date];
NSDateFormatter *monthDayFormatter = [[[NSDateFormatter alloc] init] autorelease];
[monthDayFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[monthDayFormatter setDateFormat:@"d"];     
int date_day = [[monthDayFormatter stringFromDate:date] intValue];  
NSString *suffix_string = @"|st|nd|rd|th|th|th|th|th|th|th|th|th|th|th|th|th|th|th|th|th|st|nd|rd|th|th|th|th|th|th|th|st";
NSArray *suffixes = [suffix_string componentsSeparatedByString: @"|"];
NSString *suffix = [suffixes objectAtIndex:date_day];   
NSString *dateString = [prefixDateString stringByAppendingString:suffix];   
NSLog(@"%@", dateString);
Matt Andersen
  • 4,816
  • 3
  • 24
  • 19
  • 2
    The only improvement on this i might make would be to simply create the array instead of generating it from componentsSeparatedByString. This could be done easily with the Array literal syntax @[ @"st", @"nd", @"rd", ... ]; – ericg Mar 04 '13 at 00:24
  • 1
    You have a small bug in the code: NSString *suffix = [suffixes objectAtIndex:date_day]; That should be changed to NSString *suffix = [suffixes objectAtIndex:date_day-1]; because we always count from 0 :) . Otherwise good solution ! – Petar Mar 06 '13 at 16:43
  • 7
    How is this a valid answer since it completely disregards the user's locale? NSDateFormatter simply does not support the feature. – David James Apr 08 '14 at 14:29
  • To provide these suffixes for English speaking users but not offend the rest of the world, wrap the code in: `if ([[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0] isEqualToString:@"en"])` – David James Apr 08 '14 at 14:48
  • Yeah, this just won't work because it's only valid for English speakers. – Thane Brimhall May 15 '14 at 18:41
  • 11
    I down-voted this answer because creating a string for each individual day, splitting the string, then using the day as the index is fragile and obfuscated. Read: it would be easy to make a mistake, it might crash, and it's hard to follow. I'm sure Matt's a better programmer now, but I don't agree with the 40+ upvoters here. – cbh2000 May 23 '14 at 21:53
  • 6
    iOS 9 has support for this with the NSNumberFormatter: http://stackoverflow.com/a/31033712/276626 – Paul Solt Mar 24 '16 at 18:38
28

This is easily done as of iOS9

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterOrdinalStyle;
NSArray<NSNumber *> *numbers = @[@1, @2, @3, @4, @5];

for (NSNumber *number in numbers) {
    NSLog(@"%@", [formatter stringFromNumber:number]);
}
// "1st", "2nd", "3rd", "4th", "5th"

Taken from NSHipster

Swift 2.2:

let numberFormatter = NSNumberFormatter()
numberFormatter.numberStyle = .OrdinalStyle
let numbers: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for number in numbers {
    print(numberFormatter.stringFromNumber(number)!)
}
Paul Solt
  • 8,375
  • 5
  • 41
  • 46
Gerard
  • 4,818
  • 5
  • 51
  • 80
  • The methods below, while they work, look hacky. I want something like this, but how would I get that to work with an NSDateFormatter? I receive a string that contains the number for the date. – Jake T. Apr 15 '16 at 13:03
  • As you pointed, that the minimum iOS version for this is iOS9, so be aware of that – jomafer Oct 18 '16 at 12:29
  • 2
    That is nice, but still we could use something that works with NSDateFormatter. – Graham Perks Jun 28 '17 at 15:10
15

Here's another implementation for a method to generate the suffix. The suffixes it produces are only valid in English and may not be correct in other languages:

- (NSString *)suffixForDayInDate:(NSDate *)date
{
    NSInteger day = [[[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] components:NSDayCalendarUnit fromDate:date] day];
    if (day >= 11 && day <= 13) {
        return @"th";
    } else if (day % 10 == 1) {
        return @"st";
    } else if (day % 10 == 2) {
        return @"nd";
    } else if (day % 10 == 3) {
        return @"rd";
    } else {
        return @"th";
    }
}
SDJMcHattie
  • 1,690
  • 1
  • 15
  • 21
12

This is already implemented in the Foundation.

let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .ordinal
numberFormatter.locale = Locale.current

numberFormatter.string(for: 1) //Should produce 1st
numberFormatter.string(for: 2) //Should produce 2nd
numberFormatter.string(for: 3) //Should produce 3rd
numberFormatter.string(for: 4) //Should produce 4th
ogantopkaya
  • 474
  • 6
  • 8
10

Date formatters on Mac OS 10.5 and the iPhone use TR35 as their format specifier standard. This spec doesn't allow for such a suffix on any date; if you want one, you'll have to generate it yourself.

Tim
  • 59,527
  • 19
  • 156
  • 165
9

This will do the formatting in two steps: first, create a sub-string that is the day with an appropriate suffix, then create a format string for the remaining parts, plugging in the already-formatted day.

func ordinalDate(date: Date) -> String {
    let ordinalFormatter = NumberFormatter()
    ordinalFormatter.numberStyle = .ordinal
    let day = Calendar.current.component(.day, from: date)
    let dayOrdinal = ordinalFormatter.string(from: NSNumber(value: day))!

    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "h:mm a EEEE MMMM '\(dayOrdinal)'"
    return dateFormatter.string(from: Date())
}

Since the ordinal day is built by NumberFormatter, it should work in all languages, not just English.

You could get a format string ordered for the current locale by replacing the assignment to dateFormat with this:

dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "h:mm a EEEE MMMM d", options: 0, locale: dateFormatter.locale)?.replacingOccurrences(of: "d", with: "'\(dayOrdinal)'")

Note the advice from several others that creating formatters is expensive, so you should cache and reuse them in code that is called frequently.

Steve Madsen
  • 13,465
  • 4
  • 49
  • 67
  • Are you sure the custom format string is always accepted as a valid date format by DateFormatter? – mattsson Nov 07 '17 at 13:03
  • Not sure what you mean, but the stuff inside 'single quotes' is a literal part of the formatted string. – Steve Madsen Nov 08 '17 at 16:42
  • Right, so the string ends up being something like `"h:mm a EEEE MMMM '1st'"`. How can you be sure the date formatter supports this string since it's no longer using proper date format syntax? – mattsson Nov 10 '17 at 09:31
  • I mean, how do you know putting it in single quotes makes the date formatter ignore it? – mattsson Nov 10 '17 at 09:46
  • 2
    Because that's what the documentation says it does: [Date Formatting Guide](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DataFormatting/Articles/dfDateFormatting10_4.html). Fixed formats use the syntax defined by the [Unicode Technical Standard #35](http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns). – Steve Madsen Nov 11 '17 at 17:53
4

Matt Andersen's answer is quite elaborate, and so is SDJMcHattie. But NSDateFormatter is quite heavy on the cpu and if you call this 100x you really see the impact, so here is a combined solution derived from the answers above. (Please note that the above are still correct)

NSDateFormatter is crazily expensive to create. Create once and reuse, but beware: it's not thread safe, so one per thread.

Assuming self.date = [NSDate date];

   - (NSString *)formattedDate{

    static NSDateFormatter *_dateFormatter = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _dateFormatter = [[NSDateFormatter alloc] init];
        _dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        _dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
    });

    _dateFormatter.dateFormat = [NSString stringWithFormat:@"h:mm a EEEE MMMM d'%@'", [self suffixForDayInDate:self.date]];
   NSString *date = [_dateFormatter stringFromDate:self.date];

    return date;
}

/* SDJMcHattie's code, this is more convenient than using an array */

- (NSString *)suffixForDayInDate:(NSDate *)date{
    NSInteger day = [[[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] components:NSDayCalendarUnit fromDate:date] day];
    if (day >= 11 && day <= 13) {
        return @"th";
    } else if (day % 10 == 1) {
        return @"st";
    } else if (day % 10 == 2) {
        return @"nd";
    } else if (day % 10 == 3) {
        return @"rd";
    } else {
        return @"th";
    }
}

Output: 3:11 PM Saturday August 15th

Edwin
  • 3,812
  • 2
  • 21
  • 16
4

None of the answers uses the ordinal number style already present in Number Formatter in swift.

    var dateString: String {
       let calendar = Calendar.current
       let dateComponents = calendar.component(.day, from: date)
       let numberFormatter = NumberFormatter()
       numberFormatter.numberStyle = .ordinal
       let day = numberFormatter.string(from: dateComponents as NSNumber)
       let dateFormatter = DateFormatter()
       dateFormatter.dateFormat = "MMM"
       return day! + dateFormatter.string(from: date)
    }
Dhiraj Das
  • 174
  • 9
2

This will give string in format "10:10 PM Saturday, 2nd August"

   -(NSString*) getTimeInString:(NSDate*)date
    {
        NSString* string=@"";
        NSDateComponents *components = [[NSCalendar currentCalendar] components: NSCalendarUnitDay fromDate:date];

        if(components.day == 1 || components.day == 21 || components.day == 31){
             string = @"st";
        }else if (components.day == 2 || components.day == 22){
            string = @"nd";
        }else if (components.day == 3 || components.day == 23){
             string = @"rd";
        }else{
             string = @"th";
        }

        NSDateFormatter *prefixDateFormatter = [[NSDateFormatter alloc] init];    [prefixDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
        [prefixDateFormatter setDateFormat:[NSString stringWithFormat:@"h:mm a EEEE, d'%@' MMMM",string]];

        NSString *dateString = [prefixDateFormatter stringFromDate:date];

        return dateString;
    }
Yogesh Lolusare
  • 2,162
  • 1
  • 24
  • 35
1

I just used some of the answers on here to solve this problem myself, but I think my solution is just a little bit different from some of the solutions here.

I like using NumberFormatter to properly handle ordinal formatting (with localization), and DateFormatter for the rest, but I don't like that some of these solutions require re-building the date formatter per-use (which is expensive).

Here's what I'm using, which should give decent localization by way way of leaning on Apple's APIs, and shouldn't be too heavy on processing because of the static creation of the formatters (requires iOS 9.0+):

// ...in a class that needs ordinal date formatting

static let stringFormatDateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    // A date format, replacing `d` with `'%@'` string format placeholder
    formatter.dateFormat = "MMM '%@', YYYY" 
    return formatter
}()

static let ordinalFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.numberStyle = .ordinal
    return formatter
}()

func ordinalDateString(from date: Date) -> String {

    // Pull the day from the date
    let day = NSCalendar.current.component(.day, from: date)

    // Create the ordinal, fall back to non-ordinal number due to optionality
    let dayOrdinal = Self.ordinalFormatter.string(for: day) ?? "\(day)"

    // Create the formatter with placeholder for day (e.g. "Jan %@, 2011")
    let dateStringFormat = Self.stringFormatDateFormatter.string(from: date)

    // Inject ordinal ("Jan 10th, 2011")
    return String(format: dateStringFormat, dayOrdinal)
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
tbogosia
  • 315
  • 1
  • 10
0

Or if you want the suffix for any number:

extension Int {

    public func suffix() -> String {
        let absSelf = abs(self)

        switch (absSelf % 100) {

        case 11...13:
            return "th"
        default:
            switch (absSelf % 10) {
            case 1:
                return "st"
            case 2:
                return "nd"
            case 3:
                return "rd"
            default:
                return "th"
            }
        }
    }
}

The thinking being that there are 5 possibilities for positive numbers. It's first place digit is 1 being "st". It's second place digit is 2 being "2nd". It's third place digit is 3 being "rd". Any other case is "th", or if it's second place digit is 1, then the above rules do not apply and it is "th".

Modulo 100 gives us the digit's last two numbers, so we can check for 11 to 13. Modulo 10 gives us the digit's last number, so we can check for 1, 2, 3 if not caught by the first condition.

Try that extension in playgrounds:

let a = -1 

a.suffix() // "st"

let b = 1112 

b.suffix() // "th"

let c = 32 

c.suffix() // "nd"

Would love to see if there is an even shorter way to write this using binary operations and/or an array!

Dave Thomas
  • 3,667
  • 2
  • 33
  • 41
-1
   - (void)viewDidLoad
{ 
  NSDate *date = [NSDate date];
        NSDateFormatter *prefixDateFormatter = [[[NSDateFormatter alloc] init] autorelease];
        [prefixDateFormatter setDateFormat:@"yyy-dd-MM"];
        date = [prefixDateFormatter dateFromString:@"2014-6-03"]; //enter yourdate
        [prefixDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
        [prefixDateFormatter setDateFormat:@"EEEE MMMM d"];

        NSString *prefixDateString = [prefixDateFormatter stringFromDate:date];

        NSDateFormatter *monthDayFormatter = [[[NSDateFormatter alloc] init] autorelease];
        [monthDayFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];

        [monthDayFormatter setDateFormat:@"d"];
        int date_day = [[monthDayFormatter stringFromDate:date] intValue];
        NSString *suffix_string = @"|st|nd|rd|th|th|th|th|th|th|th|th|th|th|th|th|th|th|th|th|th|st|nd|rd|th|th|th|th|th|th|th|st";
        NSArray *suffixes = [suffix_string componentsSeparatedByString: @"|"];
        NSString *suffix = [suffixes objectAtIndex:date_day];
        NSString *dateString = [prefixDateString stringByAppendingString:suffix];
        NSLog(@"%@", dateString);

}
Paresh Hirpara
  • 487
  • 3
  • 10
-1

I added these two methods to NSDate with a category NSDate+Additions.

\- (NSString *)monthDayYear 
{

    NSDateFormatter * dateFormatter = NSDateFormatter.new;
    [dateFormatter setDateFormat:@"MMMM d*, YYYY"];
    NSString *dateString = [dateFormatter stringFromDate:self];

    return [dateString stringByReplacingOccurrencesOfString:@"*" withString:[self ordinalSuffixForDay]];
}

\- (NSString *)ordinalSuffixForDay {

NSDateFormatter * dateFormatter = NSDateFormatter.new;
[dateFormatter setDateFormat:@"d"];
NSString *dateString = [dateFormatter stringFromDate:self];
NSString *suffix = @"th";

if ([dateString length] == 2 && [dateString characterAtIndex:0] == '1') {
    return suffix;
}

switch ([dateString characterAtIndex:[dateString length]-1]) {
    case '1':
        suffix = @"st";
        break;
    case '2':
        suffix = @"nd";
        break;
    case '3':
        suffix = @"rd";
        break;
}

return suffix;
}

You could make them more efficient by combining them and indexing the one's place digit of the day within your format string as the switch point. I opted to separate the functionality so the ordinal suffixes can be called separately for different date formats.

Lytic
  • 1
  • 1
-1
- (NSString *)dayWithSuffixForDate:(NSDate *)date {

    NSInteger day = [[[NSCalendar currentCalendar] components:NSDayCalendarUnit fromDate:date] day];

    NSString *dayOfMonthWithSuffix, *suffix  = nil ;

    if(day>0 && day <=31)
    {

        switch (day)
        {
            case 1:
            case 21:
            case 31: suffix =  @"st";
                break;
            case 2:
            case 22: suffix = @"nd";
                break;
            case 3:
            case 23: suffix = @"rd";
                break;
            default: suffix = @"th";
                break;
        }


            dayOfMonthWithSuffix = [NSString stringWithFormat:@"%ld%@", (long)day , suffix];
    }


    return dayOfMonthWithSuffix;
}
-1

Swift 5 - Number Formatter + Date Formatter

You can use the ordinal number style already present in NumberFormatter in swift.

func formatted(date: Date, in calendar: Calendar = Calendar.current) -> String {
    let numberFormatter = NumberFormatter()
    numberFormatter.numberStyle = .ordinal
    let day = calendar.component(.day, from: date)

    let dateFormat: String
    if let dayOrdinal = numberFormatter.string(from: NSNumber(integerLiteral: day)) {
        dateFormat = "E, '\(dayOrdinal)' MMM yyyy 'at' h:mma"
    } else {
        dateFormat = "E, d MMM yyyy 'at' h:mma"
    }

    let formatter = DateFormatter()
    formatter.dateFormat = dateFormat
    formatter.amSymbol = "am"
    formatter.pmSymbol = "pm"

    return formatter.string(from: date)
}

Obs: This way you will avoid the force unwrap, you can use a custom Calendar and, if needed, you can define date Format: String outside the function (just pass it through the method declaration).

Enrique
  • 1,586
  • 17
  • 14
-4

The NSDateFormatter documentation says that all the format options it supports are listed in TR35.

Why do you want this? If you're making something for a machine to parse, you should use ISO 8601 format, or RFC 2822 format if you have to. Neither one of those requires or allows an ordinal suffix.

If you're showing dates to the user, you should use one of the formats from the user's locale settings.

Community
  • 1
  • 1
Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • 1
    Obviously the reason is that he'd like to format something that the user will be reading. In fact, it seems to me you aught to rarely have a machine parsing a date sent from another one... it seems like you'd choose to pass the date as long milliseconds since Jan 1, 1970. – ArtOfWarfare Sep 22 '12 at 23:59