Skip to content

Instantly share code, notes, and snippets.

@MrMage
Last active July 23, 2019 11:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrMage/f96627e354c07cb8ba9246760576c9d9 to your computer and use it in GitHub Desktop.
Save MrMage/f96627e354c07cb8ba9246760576c9d9 to your computer and use it in GitHub Desktop.
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 }))
}
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
}
}
}
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