-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRetentionQueryGenerator.swift
More file actions
132 lines (116 loc) · 4.65 KB
/
RetentionQueryGenerator.swift
File metadata and controls
132 lines (116 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//
// RetentionQueryGenerator.swift
//
//
// Created by Daniel Jilg on 28.11.22.
//
import Foundation
public enum RetentionQueryGenerator {
public enum RetentionQueryGeneratorErrors: Error {
/// beginDate and endDate are less than one month apart
case datesTooClose
}
public static func generateRetentionQuery(
dataSource: String,
appID: String,
testMode: Bool,
beginDate: Date,
endDate: Date
) throws -> CustomQuery {
// If beginDate and endDate are less than 1m apart, this does not make sense as a query
let components = Calendar.current.dateComponents([.month], from: beginDate, to: endDate)
if (components.month ?? 0) < 1 {
throw RetentionQueryGeneratorErrors.datesTooClose
}
let months = splitIntoMonthLongIntervals(from: beginDate, to: endDate)
// Collect all Aggregators and PostAggregators
var aggregators = [Aggregator]()
var postAggregators = [PostAggregator]()
for month in months {
aggregators.append(aggregator(for: month))
}
for row in months {
for column in months where column >= row {
postAggregators.append(postAggregatorBetween(interval1: row, interval2: column))
}
}
// Combine query
return CustomQuery(
queryType: .groupBy,
dataSource: .init(dataSource),
filter: .and(.init(fields: [
.selector(.init(dimension: "appID", value: appID)),
.selector(.init(dimension: "isTestMode", value: testMode ? "true" : "false")),
])),
intervals: [QueryTimeInterval(beginningDate: beginDate, endDate: endDate)],
granularity: .all,
aggregations: aggregators.uniqued(),
postAggregations: postAggregators.uniqued()
)
}
static func numberOfMonthsBetween(beginDate: Date, endDate: Date) -> Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.month], from: beginDate, to: endDate)
return components.month ?? 0
}
static func splitIntoMonthLongIntervals(from fromDate: Date, to toDate: Date) -> [DateInterval] {
let calendar = Calendar.current
let numberOfMonths = numberOfMonthsBetween(beginDate: fromDate, endDate: toDate)
var intervals = [DateInterval]()
for month in 0 ... numberOfMonths {
let startOfMonth = calendar.date(byAdding: .month, value: month, to: fromDate)!.startOfMonth
let endOfMonth = startOfMonth.endOfMonth
let interval = DateInterval(start: startOfMonth, end: endOfMonth)
intervals.append(interval)
}
return intervals
}
// beginning of the month
static func beginningOfMonth(for date: Date) -> Date {
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: date)
return calendar.date(from: components)!
}
// end of the month
static func endOfMonth(for date: Date) -> Date {
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: date)
return calendar.date(byAdding: DateComponents(month: 1, day: -1), to: calendar.date(from: components)!)!
}
static func title(for interval: DateInterval) -> String {
"\(DateFormatter.iso8601.string(from: interval.start))_\(DateFormatter.iso8601.string(from: interval.end))"
}
static func aggregator(for interval: DateInterval) -> Aggregator {
.filtered(.init(
filter: .interval(.init(
dimension: "__time",
intervals: [.init(dateInterval: interval)]
)),
aggregator: .thetaSketch(.init(
name: "_\(title(for: interval))",
fieldName: "clientUser"
))
)
)
}
static func postAggregatorBetween(interval1: DateInterval, interval2: DateInterval) -> PostAggregator {
.thetaSketchEstimate(.init(
name: "retention_\(title(for: interval1))_\(title(for: interval2))",
field: .thetaSketchSetOp(.init(
func: .intersect,
fields: [
.fieldAccess(.init(type: .fieldAccess, fieldName: "_\(title(for: interval1))")),
.fieldAccess(.init(type: .fieldAccess, fieldName: "_\(title(for: interval2))")),
]
)
)
)
)
}
}
extension Sequence where Element: Hashable {
func uniqued() -> [Element] {
var set = Set<Element>()
return filter { set.insert($0).inserted }
}
}