Truncate Date to Calendar.Component in Swift

I’m frequently wanting to work with truncated dates in Swift apps, especially when dealing with Swift Charts. Foundation provides Calendar.startOfDay(for:) to get the first moment of a day, and that’s been really useful. The extension below will do something similar for an arbitrary1 Calendar.Component to truncate that date to the first moment of that component.

extension Date {
  /// Returns the first moment of a given Calendar.Component, as a Date.
  ///
  /// For example, pass in `.hour` called on `Date()`, if you want the start of
  /// the current hour of the current day.
  /// Supported Calendar.Components are .year, .month, .day, .hour, .minute,
  /// .second. Passing an unsupported component will cause `self` to be returned
  /// unchanged.
  /// Calling `.start(of: .day)` is equivalent to calling `.startOfDay`.
  /// - parameter to: The smallest component you want
  /// - returns: The first moment of the given date.
  func start(of component: Calendar.Component) -> Date {
    let allComponents = [Calendar.Component.year, .month, .day, .hour, .minute, .second]
    guard let idx = allComponents.firstIndex(of: component) else { return self }
    let components = Calendar.current.dateComponents(
      Set<Calendar.Component>(allComponents[0...idx]),
      from: self
    )
    return Calendar.current.date(from: components)!
  }
}

This has been super useful for me in building time-series charts from objects with timestamps. For example, here is the data that backs a chart showing the number of events per minute over the last hour:

  var eventsByMinute: OrderedDictionary<Date, Int> {
    let oneHourAgo = Calendar.current.date(byAdding: .hour, value: -1, to: Date())!
    return events
      .filter { $0.time >= oneHourAgo }
      .reduce(into: [:]) { result, event in
        result[event.time.start(of: .minute), default: 0] += 1
      }
  }

This is quick-and-dirty and doesn’t consider time zones, but hopefully it’s useful to you if you need something similar.

  1. Provided that the component is one of the ones specified in allComponents. It’s easy enough to add additional ones if they’re useful to you.