Base fixes and test harness

This commit is contained in:
alexandrev-tibco
2026-02-01 11:12:57 +01:00
parent f5b13ec924
commit e328767c4a
39 changed files with 3575 additions and 142 deletions
+59 -26
View File
@@ -46,6 +46,19 @@ private func createWidgetContainer() -> NSPersistentContainer? {
return container
}
private func fetchCurrencyCode(from container: NSPersistentContainer?) -> String {
guard let container = container else { return "EUR" }
let context = container.viewContext
let request = NSFetchRequest<NSManagedObject>(entityName: "AppSettings")
request.fetchLimit = 1
if let settings = try? context.fetch(request).first,
let code = settings.value(forKey: "currency") as? String,
!code.isEmpty {
return code
}
return "EUR"
}
// MARK: - Widget Entry
struct InvestmentWidgetEntry: TimelineEntry {
@@ -60,6 +73,7 @@ struct InvestmentWidgetEntry: TimelineEntry {
let categoryEvolution: [CategorySeries]
let categoryTotals: [(name: String, value: Decimal, color: String)]
let goals: [GoalSummary]
let currencyCode: String
}
struct CategorySeries: Identifiable {
@@ -124,7 +138,8 @@ struct InvestmentWidgetProvider: TimelineProvider {
],
goals: [
GoalSummary(name: "Target", targetAmount: 75000, targetDate: nil)
]
],
currencyCode: "EUR"
)
}
@@ -145,8 +160,10 @@ struct InvestmentWidgetProvider: TimelineProvider {
private func fetchData() -> InvestmentWidgetEntry {
let isPremium = UserDefaults(suiteName: appGroupIdentifier)?.bool(forKey: sharedPremiumKey) ?? false
let container = createWidgetContainer()
let currencyCode = fetchCurrencyCode(from: container)
guard let container = createWidgetContainer() else {
guard let container = container else {
return InvestmentWidgetEntry(
date: Date(),
isPremium: isPremium,
@@ -158,7 +175,8 @@ struct InvestmentWidgetProvider: TimelineProvider {
trendLabels: [],
categoryEvolution: [],
categoryTotals: [],
goals: []
goals: [],
currencyCode: currencyCode
)
}
@@ -344,7 +362,8 @@ struct InvestmentWidgetProvider: TimelineProvider {
trendLabels: trendLabels,
categoryEvolution: categoryEvolution,
categoryTotals: categoryTotalsData.map { (name: $0.name, value: $0.value, color: $0.color) },
goals: goals
goals: goals,
currencyCode: currencyCode
)
}
}
@@ -360,7 +379,7 @@ struct SmallWidgetView: View {
.font(.caption)
.foregroundColor(.secondary)
Text(entry.totalValue.compactCurrencyString)
Text(entry.totalValue.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.title2.weight(.bold))
.minimumScaleFactor(0.7)
.lineLimit(1)
@@ -369,7 +388,7 @@ struct SmallWidgetView: View {
Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right")
.font(.caption2)
Text(entry.dayChange.compactCurrencyString)
Text(entry.dayChange.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.caption.weight(.medium))
Text(String(format: "%.1f%% since last", entry.dayChangePercentage))
@@ -398,7 +417,7 @@ struct MediumWidgetView: View {
.font(.caption)
.foregroundColor(.secondary)
Text(entry.totalValue.compactCurrencyString)
Text(entry.totalValue.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.title.weight(.bold))
.minimumScaleFactor(0.7)
.lineLimit(1)
@@ -407,7 +426,7 @@ struct MediumWidgetView: View {
Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right")
.font(.caption2)
Text(entry.dayChange.compactCurrencyString)
Text(entry.dayChange.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.caption.weight(.medium))
Text("(\(String(format: "%.1f%%", entry.dayChangePercentage)))")
@@ -425,7 +444,8 @@ struct MediumWidgetView: View {
TrendLineChartView(
points: entry.trendPoints,
labels: entry.trendLabels,
goal: entry.goals.first
goal: entry.goals.first,
currencyCode: entry.currencyCode
)
.frame(height: 70)
} else {
@@ -462,7 +482,7 @@ struct MediumWidgetView: View {
.foregroundColor(.secondary)
.lineLimit(1)
Text(source.value.shortCurrencyString)
Text(source.value.shortCurrencyString(currencyCode: entry.currencyCode))
.font(.caption.weight(.medium))
}
}
@@ -490,7 +510,7 @@ struct LargeWidgetView: View {
.font(.caption)
.foregroundColor(.secondary)
Text(entry.totalValue.compactCurrencyString)
Text(entry.totalValue.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.title2.weight(.bold))
.minimumScaleFactor(0.7)
.lineLimit(1)
@@ -499,7 +519,7 @@ struct LargeWidgetView: View {
Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right")
.font(.caption2)
Text(entry.dayChange.compactCurrencyString)
Text(entry.dayChange.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.caption.weight(.medium))
Text("(\(String(format: "%.1f%%", entry.dayChangePercentage)))")
@@ -521,7 +541,8 @@ struct LargeWidgetView: View {
CombinedCategoryChartView(
series: entry.categoryEvolution,
labels: entry.trendLabels,
goal: entry.goals.first
goal: entry.goals.first,
currencyCode: entry.currencyCode
)
.frame(height: 98)
@@ -538,7 +559,7 @@ struct LargeWidgetView: View {
Spacer()
Text(category.value.shortCurrencyString)
Text(category.value.shortCurrencyString(currencyCode: entry.currencyCode))
.font(.caption.weight(.medium))
}
}
@@ -601,7 +622,7 @@ struct AccessoryRectangularView: View {
.font(.caption2)
.foregroundColor(.secondary)
Text(entry.totalValue.compactCurrencyString)
Text(entry.totalValue.compactCurrencyString(currencyCode: entry.currencyCode))
.font(.headline)
HStack(spacing: 4) {
@@ -623,6 +644,7 @@ struct TrendLineChartView: View {
let points: [Decimal]
let labels: [String]
let goal: GoalSummary?
var currencyCode: String = "EUR"
private var values: [Double] {
points.map { NSDecimalNumber(decimal: $0).doubleValue }
@@ -646,11 +668,11 @@ struct TrendLineChartView: View {
var body: some View {
HStack(alignment: .center, spacing: 6) {
VStack(alignment: .leading, spacing: 2) {
Text(Decimal(maxValue).shortCurrencyString)
Text(Decimal(maxValue).shortCurrencyString(currencyCode: currencyCode))
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text(Decimal(minValue).shortCurrencyString)
Text(Decimal(minValue).shortCurrencyString(currencyCode: currencyCode))
.font(.caption2)
.foregroundColor(.secondary)
}
@@ -715,6 +737,7 @@ struct CombinedCategoryChartView: View {
let series: [CategorySeries]
let labels: [String]
let goal: GoalSummary?
var currencyCode: String = "EUR"
private var pointsCount: Int {
series.first?.points.count ?? 0
@@ -742,11 +765,11 @@ struct CombinedCategoryChartView: View {
var body: some View {
HStack(alignment: .center, spacing: 6) {
VStack(alignment: .leading, spacing: 2) {
Text(Decimal(maxValue).shortCurrencyString)
Text(Decimal(maxValue).shortCurrencyString(currencyCode: currencyCode))
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text(Decimal(0).shortCurrencyString)
Text(Decimal(0).shortCurrencyString(currencyCode: currencyCode))
.font(.caption2)
.foregroundColor(.secondary)
}
@@ -889,7 +912,8 @@ struct PortfolioJournalWidgetBundle: WidgetBundle {
trendLabels: ["Aug", "Sep", "Oct", "Nov", "Dec", "Jan"],
categoryEvolution: [],
categoryTotals: [],
goals: []
goals: [],
currencyCode: "EUR"
)
}
@@ -911,7 +935,8 @@ struct PortfolioJournalWidgetBundle: WidgetBundle {
trendLabels: ["Aug", "Sep", "Oct", "Nov", "Dec", "Jan"],
categoryEvolution: [],
categoryTotals: [],
goals: []
goals: [],
currencyCode: "EUR"
)
}
@@ -961,11 +986,12 @@ struct PortfolioJournalWidgetBundle: WidgetBundle {
],
goals: [
GoalSummary(name: "Target", targetAmount: 120000, targetDate: nil)
]
],
currencyCode: "EUR"
)
}
extension Decimal {
var compactCurrencyString: String {
func compactCurrencyString(currencyCode: String) -> String {
let absValue = (self as NSDecimalNumber).doubleValue.magnitude
let sign = (self as NSDecimalNumber).doubleValue < 0 ? -1.0 : 1.0
@@ -986,14 +1012,13 @@ extension Decimal {
let value = (self as NSDecimalNumber).doubleValue / divisor
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode
formatter.maximumFractionDigits = value < 10 && suffix != "" ? 1 : 0
formatter.minimumFractionDigits = 0
// Use current locale currency symbol
let currencySymbol = formatter.currencySymbol ?? ""
let formattedNumber: String
do {
// Use a plain decimal formatter to better control digits
let nf = NumberFormatter()
nf.numberStyle = .decimal
nf.maximumFractionDigits = formatter.maximumFractionDigits
@@ -1004,8 +1029,16 @@ extension Decimal {
return "\(currencySymbol)\(formattedNumber)\(suffix)"
}
func shortCurrencyString(currencyCode: String) -> String {
return compactCurrencyString(currencyCode: currencyCode)
}
var compactCurrencyString: String {
return compactCurrencyString(currencyCode: "EUR")
}
var shortCurrencyString: String {
return compactCurrencyString
return shortCurrencyString(currencyCode: "EUR")
}
}