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