Skip to content

Commit

Permalink
fix: Correct calculations for month-level durations (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbmorley committed Jan 25, 2024
1 parent de5d6aa commit d65839f
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 9 deletions.
4 changes: 4 additions & 0 deletions macos/Overview.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
D8BAC1242B5F60EE00D6A98A /* SimilarEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAC1232B5F60EE00D6A98A /* SimilarEvents.swift */; };
D8C0ADAC2B5C3FDA00E77BDC /* material-icons-license in Resources */ = {isa = PBXBuildFile; fileRef = D8C0ADAB2B5C3FDA00E77BDC /* material-icons-license */; };
D8C296AB2B5777FA00286301 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C296AA2B5777FA00286301 /* URL.swift */; };
D8D0897C2B625BE0003272F3 /* DateComponentsFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D0897B2B625BE0003272F3 /* DateComponentsFormatter.swift */; };
D8D3B3D628E7CB8700A610D4 /* Diligence in Frameworks */ = {isa = PBXBuildFile; productRef = D8D3B3D528E7CB8700A610D4 /* Diligence */; };
D8D3B3DB28E7D6EF00A610D4 /* overview-license in Resources */ = {isa = PBXBuildFile; fileRef = D8D3B3DA28E7D69700A610D4 /* overview-license */; };
D8DCDDD925F664440083DF48 /* OverviewApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DCDDD825F664440083DF48 /* OverviewApp.swift */; };
Expand Down Expand Up @@ -82,6 +83,7 @@
D8BAC1232B5F60EE00D6A98A /* SimilarEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarEvents.swift; sourceTree = "<group>"; };
D8C0ADAB2B5C3FDA00E77BDC /* material-icons-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "material-icons-license"; sourceTree = "<group>"; };
D8C296AA2B5777FA00286301 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
D8D0897B2B625BE0003272F3 /* DateComponentsFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateComponentsFormatter.swift; sourceTree = "<group>"; };
D8D3B3D328E7CAB000A610D4 /* diligence */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = diligence; path = ../diligence; sourceTree = "<group>"; };
D8D3B3DA28E7D69700A610D4 /* overview-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "overview-license"; sourceTree = "<group>"; };
D8DCDDD525F664440083DF48 /* Overview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Overview.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -141,6 +143,7 @@
children = (
D81507DE25FD0D5300290DD2 /* Calendar.swift */,
D832648428E8EAA300D1C1B7 /* Date.swift */,
D8D0897B2B625BE0003272F3 /* DateComponentsFormatter.swift */,
D8FA1F442AD79EFE00E18E26 /* EKCalendar.swift */,
D8FA1F462AD79F1D00E18E26 /* EKCalendarItem.swift */,
D8805D9F2603A0A700C23C11 /* EKEventStore.swift */,
Expand Down Expand Up @@ -427,6 +430,7 @@
D8FE3AD92911789500C6F7FE /* Summary.swift in Sources */,
D8FE3ADD29117B8000C6F7FE /* CalendarEvent.swift in Sources */,
D8BAC11E2B5F451C00D6A98A /* CalendarInstance.swift in Sources */,
D8D0897C2B625BE0003272F3 /* DateComponentsFormatter.swift in Sources */,
D8DCDE0825F6F9410083DF48 /* MonthView.swift in Sources */,
D8DCDE1225F6FD060083DF48 /* YearView.swift in Sources */,
D8FA1F472AD79F1D00E18E26 /* EKCalendarItem.swift in Sources */,
Expand Down
41 changes: 41 additions & 0 deletions macos/Overview/Extensions/DateComponentsFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2021-2024 Jason Morley
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation

extension DateComponentsFormatter {

// Curiously this functionality isn't provided out of the box, making it far from obvious how to use
// `DateComponentsFormatter` to correctly render the duration of a date interval in the context of it's start date.
// We use this to ensure that 1 month can be correctly displayed as 28 or 29 days in February, but 31 days in
// January.
func string(from dateInterval: DateInterval) -> String? {
return string(from: dateInterval.start, to: dateInterval.end)
}

func string(from dateComponents: DateComponents, startDate: Date) -> String? {
let calendar = self.calendar ?? Calendar.autoupdatingCurrent
guard let endDate = calendar.date(byAdding: dateComponents, to: startDate) else {
return nil
}
return string(from: startDate, to: endDate)
}

}
8 changes: 4 additions & 4 deletions macos/Overview/Interface/MonthView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ struct MonthView: View {

var title: String { dateFormatter.string(from: summary.dateInterval.start) }

func format(dateComponents: DateComponents) -> String {
guard let result = dateComponentsFormatter.string(from: dateComponents) else {
func format(dateComponents: DateComponents, startDate: Date) -> String {
guard let result = dateComponentsFormatter.string(from: dateComponents, startDate: startDate) else {
return "Unknown"
}
return result
Expand All @@ -71,15 +71,15 @@ struct MonthView: View {
Text("\(summary.uniqueItems.count) events")
.foregroundStyle(.secondary)
Spacer()
Text(format(dateComponents: summary.duration(calendar: calendar)))
Text(format(dateComponents: summary.duration(calendar: calendar), startDate: summary.startDate))
}
}
Divider()
.foregroundStyle(.secondary)
}
HStack {
Spacer()
Text(format(dateComponents: summary.duration(calendar: calendar)))
Text(format(dateComponents: summary.duration(calendar: calendar), startDate: summary.startDate))
.foregroundStyle(.secondary)
}
}
Expand Down
8 changes: 8 additions & 0 deletions macos/Overview/Model/MonthlySummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@
import Foundation

typealias MonthlySummary = Summary<[CalendarInstance], SimilarEvents>

extension MonthlySummary {

func duration(calendar: Calendar) -> DateComponents {
calendar.date(byAdding: items.map { $0.duration(calendar: calendar) }, to: dateInterval.start)
}

}
4 changes: 2 additions & 2 deletions macos/Overview/Model/SimilarEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ extension SimilarEvents {
var uniqueItems: [Item] { Array(Set(items)) }

func duration(calendar: Calendar) -> DateComponents {
calendar.date(byAdding: uniqueItems.map { $0.duration(calendar: calendar, bounds: dateInterval) },
to: dateInterval.start)
return calendar.date(byAdding: uniqueItems.map { $0.duration(calendar: calendar, bounds: dateInterval) },
to: dateInterval.start)
}


Expand Down
6 changes: 3 additions & 3 deletions macos/Overview/Model/Summary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ struct Summary<Context, Item>: Identifiable {
var items: [Item]
}

extension Summary where Context == [CalendarInstance], Item == SimilarEvents {
extension Summary {

func duration(calendar: Calendar) -> DateComponents {
calendar.date(byAdding: items.map { $0.duration(calendar: calendar) }, to: dateInterval.start)
var startDate: Date {
return dateInterval.start
}

}

0 comments on commit d65839f

Please sign in to comment.