Question

I am looking to display the amount of months from an NSDate object.

//Make Date Six Months In The Future
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *sixMonthsFromNow = [[NSDateComponents alloc] init];
[sixMonthsFromNow setMonth:6];
NSDate *finishDate = [calendar dateByAddingComponents:sixMonthsFromNow toDate:[NSDate date] options:0]; //Six Months Time

//Display
NSCalendarUnit requiredFormat = NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *dateComponents  = [calendar components:requiredFormat fromDate:[NSDate date] toDate:finishDate options:0];

NSLog(@"%d months %d days %d hours %d minutes %d seconds", [dateComponents month], [dateComponents day], [dateComponents hour], [dateComponents minute], [dateComponents second]);

This outputs: 5 months 29 days 23 hours 59 minutes 59 seconds

Which is great, but I only wish to display the amount of months.

If I limit to only months:

//Make Date Six Months In The Future
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *sixMonthsFromNow = [[NSDateComponents alloc] init];
[sixMonthsFromNow setMonth:6];
NSDate *finishDate = [calendar dateByAddingComponents:sixMonthsFromNow toDate:[NSDate date] options:0]; //Six Months Time

//Display
NSCalendarUnit requiredFormat = NSMonthCalendarUnit;
NSDateComponents *dateComponents  = [calendar components:requiredFormat fromDate:[NSDate date] toDate:finishDate options:0];
NSLog(@"%d months", [dateComponents month]);

This outputs: 5 months

Although this is technically correct, I would like to round the amount of days to make it output six months.

Is there an easy way to achieve this effect? I noticed there wasn't a rounding property on NSDateComponents. Will I have to manually check the amount of days and decide to round up?

My end goal is to not only limit the rounding effect to months, this should be able to round hours to days if i only supplied: NSDayCalendarUnit

Était-ce utile?

La solution

The following method could to what you want. The idea is that after computing the (rounded down) number of calendar units between start date and end date, add both that amount and one more to the start date and check which one is closer to the end date:

@interface NSCalendar (MyCategory)
-(NSInteger)roundedUnit:(NSCalendarUnit)unit fromDate:(NSDate *)fromDate toDate:(NSDate *)toDate;
@end

@implementation NSCalendar (MyCategory)
-(NSInteger)roundedUnit:(NSCalendarUnit)unit fromDate:(NSDate *)fromDate toDate:(NSDate *)toDate
{
    // Number of units between the two dates:
    NSDateComponents *comps = [self components:unit fromDate:fromDate toDate:toDate options:0];
    NSInteger value = [comps valueForComponent:unit];

    // Add (value) units to fromDate:
    NSDate *date1 = [self dateByAddingComponents:comps toDate:fromDate options:0];

    // Add (value + 1) units to fromDate:
    [comps setValue:(value + 1) forComponent:unit];
    NSDate *date2 = [self dateByAddingComponents:comps toDate:fromDate options:0];

    // Now date1 <= toDate < date2. Check which one is closer,
    // and return the corresponding value:
    NSTimeInterval diff1 = [toDate timeIntervalSinceDate:date1];
    NSTimeInterval diff2 = [date2 timeIntervalSinceDate:toDate];
    return (diff1 <= diff2 ? value : value + 1);
}
@end

And you would use it as

NSInteger months = [calendar roundedUnit:NSMonthCalendarUnit fromDate:[NSDate date] toDate:finishDate];

The code uses utility methods for NSDateComponents from https://github.com/henrinormak/NSDateComponents-HNExtensions/blob/master/README.md:

- (void)setValue:(NSInteger)value forComponent:(NSCalendarUnit)unit;
- (NSInteger)valueForComponent:(NSCalendarUnit)unit;

These methods are new in OS X 10.9, but not available in iOS 7.

Autres conseils

As the other commenter pointed out, you're calling NSDate twice, and the second date is slightly later than the first. Use the same date object twice and you won't get rounding errors.

As for how to round:

I don't think there is any rounding built into NSCalendars calendrical calculation methods.

You need to decide what that means to you. You might round at the halfway point. In that case, you could ask the calendar how many days there are in the current month, using the rangeOfUnit:inUnit:forDate method, and then add half that many days to the end date, then ask for the month. If you only want to "round up within a week, then only add a week to the end date before asking for the number of months difference.

I've got a similar problem:

let fmt1 = NSDateComponentsFormatter()

fmt1.allowedUnits = .CalendarUnitYear |
    .CalendarUnitMonth |
    .CalendarUnitDay

let dc1 = NSDateComponents()
dc1.month = 1
dc1.day = 15

// This occasionally outputs "1 month, 14 days" instead of
// "1 month, 15 days" when the formatter is constrained to
// years, months and days. If formatter is allowed to use
// all units, the output is "1 month, 14 days, 23 hours,
// 59 minutes, 59 seconds".
fmt1.stringFromDateComponents(dc1)

This can be fixed by specifying a positive amount of seconds and combining it with setting maximumUnitCount formatter parameter to a fixed value:

let fmt2 = NSDateComponentsFormatter()

fmt2.allowedUnits = .CalendarUnitYear |
    .CalendarUnitMonth |
    .CalendarUnitDay

fmt2.maximumUnitCount = 2
fmt2.collapsesLargestUnit = true

let dc2 = NSDateComponents()
dc2.month = 1
dc2.day = 15
dc2.second = 10

// This always outputs "1 month, 15 days"
fmt2.stringFromDateComponents(dc2)

This fix will probably work in your case too: just assign some positive amount of seconds to NSDateComponents before calling dateByAddingComponents:

[sixMonthsFromNow setMonth: 6];
[sixMonthsFromNow setSecond: 10];
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top