parent
55138d429f
commit
a7ec081bd9
@ -0,0 +1,85 @@ |
||||
// |
||||
// Context+Calculations.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 20/02/2023. |
||||
// |
||||
|
||||
import Foundation |
||||
import CoreData |
||||
|
||||
extension NSManagedObjectContext { |
||||
|
||||
func compute(activity: Activity, stats: [Stat], filter: Filter?) throws -> [StatValue] { |
||||
|
||||
// Step 1: |
||||
// - Create the summing expression on the amount attribute. |
||||
// - Name the expression result as 'amountTotal'. |
||||
// - Assign the expression result data type as a Double. |
||||
|
||||
var expressions: [NSExpressionDescription] = [] |
||||
|
||||
for stat in stats { |
||||
let expression = NSExpressionDescription() |
||||
expression.expression = NSExpression(forFunction: stat.function, arguments:[NSExpression(forKeyPath: stat.field)]) |
||||
expression.name = stat.stringIdentifier; |
||||
expression.expressionResultType = NSAttributeType.doubleAttributeType |
||||
|
||||
expressions.append(expression) |
||||
} |
||||
|
||||
// Step 2: |
||||
// - Create the fetch request for the Movement entity. |
||||
// - Indicate that the fetched properties are those that were |
||||
// described in `expression`. |
||||
// - Indicate that the result type is a dictionary. |
||||
|
||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Record") |
||||
|
||||
var predicates: [NSPredicate] = [] |
||||
predicates.append(NSPredicate(format: "activity = %@", activity)) |
||||
if let filter { |
||||
predicates.append(filter.predicate) |
||||
} |
||||
fetchRequest.predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: predicates) |
||||
|
||||
fetchRequest.propertiesToFetch = expressions |
||||
fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType |
||||
|
||||
// Step 3: |
||||
// - Execute the fetch request which returns an array. |
||||
// - There will only be one result. Get the first array |
||||
// element and assign to 'resultMap'. |
||||
// - The summed amount value is in the dictionary as |
||||
// 'amountTotal'. This will be summed value. |
||||
|
||||
var statValues: [StatValue] = [] |
||||
|
||||
let results = try self.fetch(fetchRequest) |
||||
if let resultMap = results.first as? [String : Any] { |
||||
|
||||
for stat in stats { |
||||
|
||||
if let result = resultMap[stat.stringIdentifier] { |
||||
|
||||
var value: NSDecimalNumber? = nil |
||||
switch result { |
||||
case let double as Double: |
||||
value = NSDecimalNumber(value: double) |
||||
case let integer as Int: |
||||
value = NSDecimalNumber(integerLiteral: integer) |
||||
default: |
||||
print("unmanaged value type = \(String(describing: result))") |
||||
break |
||||
} |
||||
|
||||
if let value { |
||||
let sv = StatValue(stat: stat, value: value) |
||||
statValues.append(sv) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return statValues |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
// |
||||
// Filter.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 20/02/2023. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
fileprivate extension Int { |
||||
|
||||
var startOfYear: NSDate { |
||||
let components: DateComponents = DateComponents(year: self) |
||||
if let date = Calendar.current.date(from: components) { |
||||
return date as NSDate |
||||
} |
||||
return NSDate() |
||||
} |
||||
|
||||
} |
||||
|
||||
struct Month { |
||||
var month: Int |
||||
var year: Int |
||||
|
||||
var start: NSDate { |
||||
let components: DateComponents = DateComponents(year: self.year, month: self.month) |
||||
if let date = Calendar.current.date(from: components) { |
||||
return date as NSDate |
||||
} |
||||
return NSDate() |
||||
} |
||||
|
||||
var end: NSDate { |
||||
let start = self.start as Date |
||||
if let end = Calendar.current.date(byAdding: .month, value: 1, to: start) { |
||||
return end as NSDate |
||||
} |
||||
return NSDate() |
||||
} |
||||
} |
||||
|
||||
enum Filter { |
||||
case year(_ year: Int) |
||||
case month(_ month: Month) |
||||
|
||||
var predicate: NSPredicate { |
||||
switch self { |
||||
case .year(let year): |
||||
return NSPredicate(format: "start >= %@ AND end < %@", year.startOfYear, (year + 1).startOfYear) |
||||
case .month(let month): |
||||
return NSPredicate(format: "start >= %@ AND end < %@", month.start, month.end) |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,78 @@ |
||||
// |
||||
// Stat.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 20/02/2023. |
||||
// |
||||
|
||||
import Foundation |
||||
import CoreData |
||||
|
||||
enum Stat: Int, CaseIterable { |
||||
case count |
||||
case totalDuration |
||||
case averageDuration |
||||
|
||||
var localizedName: String { |
||||
switch self { |
||||
case .count: return NSLocalizedString("Count", comment: "") |
||||
case .totalDuration: return NSLocalizedString("Duration", comment: "") |
||||
case .averageDuration: return NSLocalizedString("Average duration", comment: "") |
||||
} |
||||
} |
||||
|
||||
var field: String { |
||||
switch self { |
||||
case .count: |
||||
return "duration" |
||||
case .totalDuration: |
||||
return "duration" |
||||
case .averageDuration: |
||||
return "duration" |
||||
} |
||||
} |
||||
|
||||
var function: String { |
||||
switch self { |
||||
case .count: |
||||
return "count:" |
||||
case .totalDuration: |
||||
return "sum:" |
||||
case .averageDuration: |
||||
return "average:" |
||||
} |
||||
} |
||||
|
||||
var stringIdentifier: String { |
||||
return self.function + self.field |
||||
} |
||||
|
||||
var type: NSAttributeType { |
||||
switch self { |
||||
case .count: |
||||
return .integer32AttributeType |
||||
default: |
||||
return .doubleAttributeType |
||||
} |
||||
} |
||||
} |
||||
|
||||
struct StatValue: Identifiable { |
||||
|
||||
var id: Int { return self.stat.rawValue } |
||||
|
||||
var stat: Stat |
||||
var value: NSDecimalNumber |
||||
|
||||
var formattedValue: String { |
||||
|
||||
switch self.stat { |
||||
case .averageDuration, .totalDuration: |
||||
return self.value.doubleValue.minuteSecond |
||||
default: |
||||
let formatter: NumberFormatter = NumberFormatter() |
||||
return formatter.string(from: self.value) ?? "--" |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,60 @@ |
||||
// |
||||
// ActivityView.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 20/02/2023. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
fileprivate enum TimeFrame: Int, Identifiable, CaseIterable { |
||||
|
||||
var id: Int { return self.rawValue } |
||||
|
||||
case all |
||||
case year |
||||
case month |
||||
|
||||
var localizedString: String { |
||||
switch self { |
||||
case .all: return NSLocalizedString("All", comment: "") |
||||
case .year: return NSLocalizedString("Year", comment: "") |
||||
case .month: return NSLocalizedString("Month", comment: "") |
||||
} |
||||
} |
||||
} |
||||
|
||||
struct ActivityView: View { |
||||
|
||||
@Environment(\.managedObjectContext) private var viewContext |
||||
|
||||
var activity: Activity |
||||
|
||||
@State private var selectedTimeFrame: TimeFrame = .all |
||||
|
||||
var body: some View { |
||||
|
||||
VStack { |
||||
Picker("", selection: $selectedTimeFrame) { |
||||
ForEach(TimeFrame.allCases) { timeFrame in |
||||
Text(timeFrame.localizedString) |
||||
} |
||||
}.pickerStyle(.segmented) |
||||
|
||||
StatsView(activity: self.activity) |
||||
.environment(\.managedObjectContext, viewContext) |
||||
.background(.red) |
||||
.foregroundColor(.white) |
||||
RecordsView(activity: self.activity) |
||||
.environment(\.managedObjectContext, viewContext) |
||||
.frame(maxWidth: .infinity) |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
struct ActivityView_Previews: PreviewProvider { |
||||
static var previews: some View { |
||||
ActivityView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext)) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue