2

I have an app that presents data that expires every 30 seconds (precisely, at h/m/s 11:30:00, 11:30:30, 11:31:00, etc).

I can get the current time, but I am unsure on how to calculate the time between now and the nearest thirty seconds.

Anything I've found is in Objective-C, and I've been unable to convert it.

Here's what I tried:

func nearestThirtySeconds() -> Date? {
        var components = NSCalendar.current.dateComponents([.second], from: self)
        let second = components.second ?? 30
        components.second = second >= 30 ? 60 - second : -second
        return Calendar.current.date(byAdding: components, to: self)
    }

But this returns the nearest minute (I think, it always returns a definite minute)

Any ideas?

Will
  • 4,942
  • 2
  • 22
  • 47

4 Answers4

9

You can round the seconds to the nearest multiple of 30, and then add the difference between the rounded and the original value to the date:

extension Date {
    func nearestThirtySeconds() -> Date {
        let cal = Calendar.current
        let seconds = cal.component(.second, from: self)
        // Compute nearest multiple of 30:
        let roundedSeconds = lrint(Double(seconds) / 30) * 30
        return cal.date(byAdding: .second, value: roundedSeconds - seconds, to: self)!
    }
}

That should be good enough to display the rounded time, however it is not exact: A Date includes also fractional seconds, so for example "11:30:10.123" would become "11:30:00.123" and not "11:30:00.000". Here is another approach which solves that problem:

extension Date {
    func nearestThirtySeconds() -> Date {
        let cal = Calendar.current
        let startOfMinute = cal.dateInterval(of: .minute, for: self)!.start
        var seconds = self.timeIntervalSince(startOfMinute)
        seconds = (seconds / 30).rounded() * 30
        return startOfMinute.addingTimeInterval(seconds)
    }
}

Now seconds is the time interval since the start of the current minute (including fractional seconds). That interval is rounded to the nearest multiple of 30 and added to the start of the minute.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
8

I used the answer by Martin R to write a more generic version to round by any time period.

Answer is outdated and only works with time, check gist for the latest version.

https://gist.github.com/casperzandbergenyaacomm/83c6a585073fd7da2e1fbb97c9bcd38a

extension Date {

    func rounded(on amount: Int, _ component: Calendar.Component) -> Date {
        let cal = Calendar.current
        let value = cal.component(component, from: self)

        // Compute nearest multiple of amount:
        let roundedValue = lrint(Double(value) / Double(amount)) * amount
        let newDate = cal.date(byAdding: component, value: roundedValue - value, to: self)!

        return newDate.floorAllComponents(before: component)
    }

    func floorAllComponents(before component: Calendar.Component) -> Date {
        // All components to round ordered by length
        let components = [Calendar.Component.year, .month, .day, .hour, .minute, .second, .nanosecond]

        guard let index = components.index(of: component) else {
            fatalError("Wrong component")
        }

        let cal = Calendar.current
        var date = self

        components.suffix(from: index + 1).forEach { roundComponent in
            let value = cal.component(roundComponent, from: date) * -1
            date = cal.date(byAdding: roundComponent, value: value, to: date)!
        }

        return date
    }
}

To round to x minutes you need to also floor the seconds so this also contains the floor method I wrote.

How to use:

let date: Date = Date() // 10:16:34
let roundedDate0 = date.rounded(on: 30, .second) // 10:16:30
let roundedDate1 = date.rounded(on: 15, .minute) // 10:15:00 
let roundedDate2 = date.rounded(on: 1, .hour)    // 10:00:00 
Casper Zandbergen
  • 3,419
  • 2
  • 25
  • 49
3

There's a Cocoapod called 'SwiftDate' that is great for date formatting and manipulation that can applied to your Date() instance.

Example of how to use the date rounding method:

let date = Date()                       // 2018-10-01 23:05:29
date.dateRoundedAt(at: .toFloor5Mins)   // 2018-10-01 23:05:00
date.dateRoundedAt(at: .toCeil5Mins)    // 2018-10-01 23:10:00
date.dateRoundedAt(at: .toFloorMins(1)) // 2018-10-01 23:05:00
date.dateRoundedAt(at: .toCeilMins(1))  // 2018-10-01 23:06:00

(For reference, check out the documentation at https://cocoapods.org/pods/SwiftDate)

1
let now = Date()
var timeInterval = now.timeIntervalSinceReferenceDate
timeInterval += 30 - timeInterval.truncatingRemainder(dividingBy: 30)
let rounded = Date(timeIntervalSinceReferenceDate: timeInterval)
print("\(now) rounded to nearest 30 seconds is \(rounded)")
Josh Homann
  • 15,933
  • 3
  • 30
  • 33
  • You should use calendar calculations. This can break in some weird time zones. – Sulthan Jun 24 '17 at 14:07
  • while thats generally true if you are looking to be accurate, here you are explicity rounding the time and discarding the accuracy so i don't think it matters much if you are off by a leap second or whatever, but if you care, then you can use the same math with DateComponents.second. The formula in general is to add the number you want to round to then subtract the remainder after dividing by that number. – Josh Homann Jun 24 '17 at 14:18
  • Here is an example of a "weird" time zone offset: https://stackoverflow.com/a/44661976/1187415. So it depends on whether you want to round the local time or UTC time. – Martin R Jun 24 '17 at 14:26