-
-
Save MrMage/f96627e354c07cb8ba9246760576c9d9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private class ElementWithGroups { | |
let element: ActivityWrapper | |
var groups: [DetailedGroupSpecifier] | |
@inlinable init(element: ActivityWrapper, groups: [DetailedGroupSpecifier]) { | |
self.element = element | |
self.groups = groups | |
} | |
} | |
private func bucketElements( | |
_ elements: ContiguousArray<ElementWithGroups>, | |
durationThreshold: TimeInterval, attachActivitiesToLeaves: Bool) | |
-> (resultingGroups: ContiguousArray<ActivityListHierarchyNode>, activitiesWithoutGroup: ContiguousArray<ActivityWrapper>) { | |
// This is the actual "slow" path. | |
let byGroup = elements.toDictWithRepeatedKeys { $0.groups.popLast() } | |
var resultingGroups = ContiguousArray(byGroup.byKey.map { groupAndElements -> ActivityListHierarchyNode in | |
let children = bucketElements( | |
ContiguousArray(groupAndElements.value.value), | |
durationThreshold: durationThreshold, attachActivitiesToLeaves: attachActivitiesToLeaves) | |
return ActivityListHierarchyNode( | |
dateRange: groupAndElements.key.dateRange, type: groupAndElements.key, | |
children: children.resultingGroups, | |
attachedActivities: attachActivitiesToLeaves ? children.activitiesWithoutGroup : nil) | |
}) | |
applyDurationThreshold(to: &resultingGroups, threshold: durationThreshold) | |
return (resultingGroups: resultingGroups, | |
activitiesWithoutGroup: ContiguousArray(byGroup.withNilKey.map { $0.element })) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum DetailedGroupSpecifier: Hashable { | |
// - MARK: Nestedtypes. | |
// This is a class in order to reduce its memory usage inside the enum. | |
public final class TaskActivityProperties: CustomDebugStringConvertible, Hashable { | |
public let cachedTitle: CachedString? | |
public let dateRange: DateRangeHolder? | |
public let cachedNotes: CachedString? | |
init(cachedTitle: CachedString?, dateRange: DateRangeHolder?, cachedNotes: CachedString?) { | |
self.cachedTitle = cachedTitle | |
self.dateRange = dateRange | |
self.cachedNotes = cachedNotes | |
} | |
// Can't be syncthesized, because this is not a struct. (Also below.) | |
public func hash(into hasher: inout Hasher) { | |
hasher.combine(cachedTitle) | |
hasher.combine(dateRange) | |
hasher.combine(cachedNotes) | |
} | |
@inlinable public static func ==(_ A: TaskActivityProperties, _ B: TaskActivityProperties) -> Bool { | |
return A.cachedTitle == B.cachedTitle && A.dateRange == B.dateRange && A.cachedNotes == B.cachedNotes | |
} | |
} | |
// This is a class in order to reduce its memory usage inside the enum. | |
public final class AppActivityProperties: CustomDebugStringConvertible, Hashable { | |
public let application: Application? | |
public let titlePath: TitlePathHolder? | |
public let dateRange: DateRangeHolder? | |
init(application: Application?, titlePath: TitlePathHolder?, dateRange: DateRangeHolder?) { | |
self.application = application | |
self.titlePath = titlePath | |
self.dateRange = dateRange | |
} | |
public func hash(into hasher: inout Hasher) { | |
hasher.combine(application?.id) | |
hasher.combine(titlePath) | |
hasher.combine(dateRange) | |
} | |
@inlinable public static func ==(_ A: AppActivityProperties, _ B: AppActivityProperties) -> Bool { | |
return A.application == B.application && A.titlePath == B.titlePath && A.dateRange == B.dateRange | |
} | |
} | |
// This is a class in order to reduce its memory usage inside the enum. | |
public final class RawAppActivityProperties: CustomDebugStringConvertible, Hashable { | |
public let activity: AppActivityWithStrings | |
public let project: ProjectWrapper? | |
public let application: Application? | |
init(activity: AppActivityWithStrings, project: ProjectWrapper?, application: Application?) { | |
self.activity = activity | |
self.project = project | |
self.application = application | |
} | |
public func hash(into hasher: inout Hasher) { | |
hasher.combine(activity.id) | |
hasher.combine(project?.id) | |
hasher.combine(application?.id) | |
} | |
@inlinable public static func ==(_ A: RawAppActivityProperties, _ B: RawAppActivityProperties) -> Bool { | |
return A.activity == B.activity && A.project == B.project && A.application == B.application | |
} | |
} | |
public enum ProjectChainLocation: Hashable { | |
case leaf | |
case leafParent, topLevel, secondLevel | |
} | |
// - MARK: Cases. | |
case global | |
case project(ProjectWrapper?, endDate: Date?, location: ProjectChainLocation) | |
static func project(_ project: ProjectWrapper?, endDate: Date?) -> DetailedGroupSpecifier { | |
return .project(project, endDate: endDate, location: .leaf) | |
} | |
case taskActivityTitle(CachedString?) | |
case taskActivity(TaskActivity?, isPartOfTitleGroup: Bool) | |
case application(Application, endDate: Date?) | |
case appActivity(AppActivityWithStrings, isPartOfTitlePathHolder: Bool) | |
case titleAndPath(TitlePathHolder) | |
case timeBlockWithEndDate(Date) | |
case timeGroup(dateRange: DateRangeHolder, timeGroupingMode: TimeGroupingMode) | |
case taskActivityProperties(TaskActivityProperties) | |
case appActivityProperties(AppActivityProperties) | |
case rawTaskActivity(TaskActivity, ProjectWrapper?) | |
case rawAppActivity(RawAppActivityProperties) | |
case belowDurationThreshold(TimeInterval) | |
// - MARK: `Hashable` Conformance (removing this does not affect performance). | |
public func hash(into hasher: inout Hasher) { | |
switch self { | |
case .global: return | |
case let .project(project, endDate, location): | |
hasher.combine(project?.id ?? -1) | |
hasher.combine(endDate?.hashValue ?? -1) | |
hasher.combine(location) | |
case let .taskActivityTitle(title): | |
hasher.combine(title?.hashValue ?? -1) | |
case let .taskActivity(taskActivity, isPartOfTitleGroup): | |
hasher.combine(taskActivity?.id ?? -1) | |
hasher.combine(isPartOfTitleGroup) | |
case let .application(application, endDate): | |
hasher.combine(application.id) | |
hasher.combine(endDate?.hashValue ?? -1) | |
case let .appActivity(appActivity, isPartOfTitlePathHolder): | |
hasher.combine(appActivity.id) | |
// We could also hash the start date, but the case of identical IDs but differing start dates is very rare, | |
// so we only cover that in `isEqual` for slightly improved performance. | |
hasher.combine(isPartOfTitlePathHolder) | |
case let .titleAndPath(titlePathHolder): | |
hasher.combine(titlePathHolder.titleHolder?.id ?? -1) | |
hasher.combine(titlePathHolder.pathHolder?.id ?? -1) | |
case let .timeBlockWithEndDate(endDate): | |
hasher.combine(endDate) | |
case let .timeGroup(dateRange, timeGroupingMode): | |
hasher.combine(dateRange.startDate) | |
hasher.combine(dateRange.endDate) | |
hasher.combine(timeGroupingMode) | |
case let .taskActivityProperties(taskActivityProperties): | |
hasher.combine(taskActivityProperties) | |
case let .appActivityProperties(appActivityProperties): | |
hasher.combine(appActivityProperties) | |
case let .rawTaskActivity(taskActivity, projectWrapper): | |
hasher.combine(taskActivity.id) | |
hasher.combine(projectWrapper?.id ?? -1) | |
case let .rawAppActivity(rawAppActivityProperties): | |
hasher.combine(rawAppActivityProperties) | |
case let .belowDurationThreshold(timeInterval): | |
hasher.combine(timeInterval) | |
} | |
} | |
@inlinable public static func ==(A: DetailedGroupSpecifier, B: DetailedGroupSpecifier) -> Bool { | |
switch (A, B) { | |
case (.global, .global): return true | |
case let (.project(projectA, endDateA, locationA), .project(projectB, endDateB, locationB)): | |
return (projectA === projectB || projectA == projectB) | |
&& endDateA == endDateB && locationA == locationB | |
case let (.taskActivityTitle(titleA), .taskActivityTitle(titleB)): | |
return titleA === titleB || titleA == titleB | |
case let (.taskActivity(activityA, isPartOfTitleGroupA), | |
.taskActivity(activityB, isPartOfTitleGroupB)): | |
return (activityA === activityB || activityA == activityB) | |
&& isPartOfTitleGroupA == isPartOfTitleGroupB | |
case let (.application(applicationA, endDateA), .application(applicationB, endDateB)): | |
return (applicationA === applicationB || applicationA.id == applicationB.id) | |
&& endDateA == endDateB | |
case let (.appActivity(activityA, isPartOfTitlePathHolderA), .appActivity(activityB, isPartOfTitlePathHolderB)): | |
// We must compare activities (not IDs) here, to ensure that the activity start dates are also identical. | |
return (activityA === activityB || activityA == activityB) | |
&& isPartOfTitlePathHolderA == isPartOfTitlePathHolderB | |
case let (.titleAndPath(titleAndPathA), .titleAndPath(titleAndPathB)): return titleAndPathA == titleAndPathB | |
case let (.timeBlockWithEndDate(endDateA), .timeBlockWithEndDate(endDateB)): return endDateA == endDateB | |
case let (.timeGroup(dateRangeA, modeA), .timeGroup(dateRangeB, modeB)): | |
return modeA == modeB && dateRangeA == dateRangeB | |
case let (.taskActivityProperties(propertiesA), .taskActivityProperties(propertiesB)): | |
return propertiesA === propertiesB || propertiesA == propertiesB | |
case let (.appActivityProperties(propertiesA), .appActivityProperties(propertiesB)): | |
return propertiesA === propertiesB || propertiesA == propertiesB | |
case let (.belowDurationThreshold(thresholdA), .belowDurationThreshold(thresholdB)): return thresholdA == thresholdB | |
case let (.rawTaskActivity(activityA, projectA), .rawTaskActivity(activityB, projectB)): | |
return (activityA === activityB || activityA == activityB) | |
&& (projectA === projectB || projectA == projectB) | |
case let (.rawAppActivity(propertiesA), .rawAppActivity(propertiesB)): | |
return propertiesA === propertiesB || propertiesA == propertiesB | |
default: return false | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public final class Box<T> { | |
public var value: T | |
public init(value: T) { | |
self.value = value | |
} | |
} | |
public extension Sequence { | |
// This is functionally similar to `Dictionary(grouping:by:)`, but slightly faster. | |
func toDictWithRepeatedKeys<K: Hashable>(toKeyConverter: (Iterator.Element) -> K?) | |
-> (byKey: [K: Box<ContiguousArray<Self.Element>>], withNilKey: ContiguousArray<Self.Element>) { | |
// Boxing the arrays here tremendously improves performance, as they otherwise need to be copied on every append. | |
var byKey: [K: Box<ContiguousArray<Self.Element>>] | |
let underestimatedCount = estimateDictionarySize(self.underestimatedCount) | |
if underestimatedCount > 1 { | |
byKey = Dictionary(minimumCapacity: estimateDictionarySize(underestimatedCount)) | |
} else { | |
// If `underestimatedCount` is less than one, there's a good chance the dictionary might stay completely | |
// empty. In that case, we want to defer the dictionary storage allocation altogether until we actually | |
// need it. | |
byKey = Dictionary() | |
} | |
var withNilKey: ContiguousArray<Self.Element> = [] | |
for val in self { | |
let key = toKeyConverter(val) | |
if let key = key { | |
if let currentBox = byKey[key] { | |
currentBox.value.append(val) | |
} else { | |
byKey[key] = Box(value: [val]) | |
} | |
} else { | |
withNilKey.append(val) | |
} | |
} | |
return (byKey: byKey, withNilKey: withNilKey) | |
} | |
// This is a fully-specialized variant of the generic method above. Unfortunately, using it does not affect performance, either. | |
func toDictWithRepeatedKeysDetailedGroupSpecifier(toKeyConverter: (Iterator.Element) -> DetailedGroupSpecifier?) | |
-> (byKey: [DetailedGroupSpecifier: Box<ContiguousArray<Self.Element>>], withNilKey: ContiguousArray<Self.Element>) { | |
// Boxing the arrays here tremendously improves performance, as they otherwise need to be copied on every append. | |
var byKey: [DetailedGroupSpecifier: Box<ContiguousArray<Self.Element>>] | |
let underestimatedCount = estimateDictionarySize(self.underestimatedCount) | |
if underestimatedCount > 1 { | |
byKey = Dictionary(minimumCapacity: estimateDictionarySize(underestimatedCount)) | |
} else { | |
// If `underestimatedCount` is less than one, there's a good chance the dictionary might stay completely | |
// empty. In that case, we want to defer the dictionary storage allocation altogether until we actually | |
// need it. | |
byKey = Dictionary() | |
} | |
var withNilKey: ContiguousArray<Self.Element> = [] | |
for val in self { | |
let key = toKeyConverter(val) | |
if let key = key { | |
if let currentBox = byKey[key] { | |
currentBox.value.append(val) | |
} else { | |
byKey[key] = Box(value: [val]) | |
} | |
} else { | |
withNilKey.append(val) | |
} | |
} | |
return (byKey: byKey, withNilKey: withNilKey) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment