From 9888f1972fd6f27050dba198df003484fd18858c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Jun 2026 06:16:03 +0000 Subject: [PATCH 1/7] Initial plan From a147444a60477953315b4e675f8737e9065725aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Jun 2026 06:20:54 +0000 Subject: [PATCH 2/7] Add ignored payee duplicate settings --- .../Settings/GeneralSettingsView.swift | 10 +- .../IgnoredPayeeDuplicateSettings.swift | 92 +++++++++++++++++++ .../Settings/SettingsTableView.swift | 10 +- .../SettingsTableViewDataSource.swift | 37 ++++++++ SwiftBeanCountApp/Settings/SettingsView.swift | 14 +++ SwiftBeanCountApp/Tabs/Payees.swift | 49 ++++++---- .../Tabs/Payees/PayeeDuplicateDetector.swift | 7 +- 7 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift diff --git a/SwiftBeanCountApp/Settings/GeneralSettingsView.swift b/SwiftBeanCountApp/Settings/GeneralSettingsView.swift index 7859c7e..1c7a026 100644 --- a/SwiftBeanCountApp/Settings/GeneralSettingsView.swift +++ b/SwiftBeanCountApp/Settings/GeneralSettingsView.swift @@ -17,12 +17,18 @@ struct GeneralSettingsView: View { let payees: [String: String] let accounts: [String: String] let descriptions: [String: String] + let ignoredPayeeDuplicates: [IgnoredPayeeDuplicatePair]? let dateTolerance: String - init(payees: [String: String], accounts: [String: String], descriptions: [String: String], dateTolerance: String) { + init(payees: [String: String], + accounts: [String: String], + descriptions: [String: String], + ignoredPayeeDuplicates: [IgnoredPayeeDuplicatePair]? = nil, + dateTolerance: String) { self.payees = payees self.accounts = accounts self.descriptions = descriptions + self.ignoredPayeeDuplicates = ignoredPayeeDuplicates self.dateTolerance = dateTolerance } @@ -143,6 +149,7 @@ struct GeneralSettingsView: View { for (description, account) in settingsFile.accounts { Settings.setAccountMapping(key: description, account: account) } + IgnoredPayeeDuplicateSettings.replaceAll(with: settingsFile.ignoredPayeeDuplicates ?? []) if let dateTolerance = Int(settingsFile.dateTolerance) { Settings.dateToleranceInDays = dateTolerance self.dateTolerance = dateTolerance @@ -153,6 +160,7 @@ struct GeneralSettingsView: View { SettingsFile(payees: Settings.allPayeeMappings, accounts: Settings.allAccountMappings, descriptions: Settings.allDescriptionMappings, + ignoredPayeeDuplicates: IgnoredPayeeDuplicateSettings.allPairs(), dateTolerance: "\(Settings.dateToleranceInDays)") } diff --git a/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift b/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift new file mode 100644 index 0000000..bdc83d8 --- /dev/null +++ b/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift @@ -0,0 +1,92 @@ +// +// IgnoredPayeeDuplicateSettings.swift +// SwiftBeanCountApp +// +// Created by Copilot on 2026-06-06. +// + +import Foundation + +struct IgnoredPayeeDuplicatePair: Codable, Hashable { + let payee1: String + let payee2: String + + init(payee1: String, payee2: String) { + let trimmedPayee1 = payee1.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedPayee2 = payee2.trimmingCharacters(in: .whitespacesAndNewlines) + if Self.shouldSwap(trimmedPayee1, trimmedPayee2) { + self.payee1 = trimmedPayee2 + self.payee2 = trimmedPayee1 + } else { + self.payee1 = trimmedPayee1 + self.payee2 = trimmedPayee2 + } + } + + private static func shouldSwap(_ left: String, _ right: String) -> Bool { + let normalizedLeft = left.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) + let normalizedRight = right.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) + if normalizedLeft == normalizedRight { + return left > right + } + return normalizedLeft > normalizedRight + } +} + +enum IgnoredPayeeDuplicateSettings { + + private static let key = "ignoredPayeeDuplicates" + + static func allPairs() -> [IgnoredPayeeDuplicatePair] { + guard let data = UserDefaults.standard.data(forKey: key), + let pairs = try? JSONDecoder().decode([IgnoredPayeeDuplicatePair].self, from: data) else { + return [] + } + return normalized(pairs) + } + + static func allPairsSet() -> Set { + Set(allPairs()) + } + + static func add(_ pair: IgnoredPayeeDuplicatePair) { + var pairs = allPairsSet() + pairs.insert(pair) + save(Array(pairs)) + } + + static func remove(_ pair: IgnoredPayeeDuplicatePair) { + var pairs = allPairsSet() + pairs.remove(pair) + save(Array(pairs)) + } + + static func replaceAll(with pairs: [IgnoredPayeeDuplicatePair]) { + save(pairs) + } + + private static func save(_ pairs: [IgnoredPayeeDuplicatePair]) { + let normalizedPairs = normalized(pairs) + guard !normalizedPairs.isEmpty else { + UserDefaults.standard.removeObject(forKey: key) + return + } + guard let data = try? JSONEncoder().encode(normalizedPairs) else { + return + } + UserDefaults.standard.set(data, forKey: key) + } + + private static func normalized(_ pairs: [IgnoredPayeeDuplicatePair]) -> [IgnoredPayeeDuplicatePair] { + Array(Set(pairs + .map { IgnoredPayeeDuplicatePair(payee1: $0.payee1, payee2: $0.payee2) } + .filter { !$0.payee1.isEmpty && !$0.payee2.isEmpty } + )) + .sorted { + if $0.payee1 == $1.payee1 { + return $0.payee2.localizedCaseInsensitiveCompare($1.payee2) == .orderedAscending + } + return $0.payee1.localizedCaseInsensitiveCompare($1.payee1) == .orderedAscending + } + } +} diff --git a/SwiftBeanCountApp/Settings/SettingsTableView.swift b/SwiftBeanCountApp/Settings/SettingsTableView.swift index 9aec466..341bb0c 100644 --- a/SwiftBeanCountApp/Settings/SettingsTableView.swift +++ b/SwiftBeanCountApp/Settings/SettingsTableView.swift @@ -79,12 +79,17 @@ struct SettingsTableView: View { .contextMenu(forSelectionType: T.ID.self) { ids in let items = ids.map { id in allData.first { $0.id == id } } if let item = items.first { - Button("Edit") { edit(item!) }.keyboardShortcut(.defaultAction) + if T.isEditable { + Button("Edit") { edit(item!) }.keyboardShortcut(.defaultAction) + } Button("Delete", role: .destructive) { delete(item!.id) }.keyboardShortcut(.delete) } else { EmptyView() } } primaryAction: { ids in + guard T.isEditable else { + return + } let items = ids.map { id in allData.first { $0.id == id } } if let item = items.first { edit(item!) @@ -108,6 +113,9 @@ struct SettingsTableView: View { } #endif .onKeyPress(.return) { + guard T.isEditable else { + return .ignored + } if let editing, editing == selected { endEditing() return .handled diff --git a/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift b/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift index 497f769..404bc74 100644 --- a/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift +++ b/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift @@ -11,6 +11,7 @@ import SwiftUI protocol SettingsTableViewDataSource: Identifiable { // swiftlint:disable:this file_types_order static var keyName: String { get } static var hasValue2: Bool { get } + static var isEditable: Bool { get } static var value1Name: String { get } static var value2Name: String { get } @@ -26,6 +27,11 @@ protocol SettingsTableViewDataSource: Identifiable { // swiftlint:disable:this f func delete() } +extension SettingsTableViewDataSource { + + static var isEditable: Bool { true } +} + struct DescriptionPayeeMapping: SettingsTableViewDataSource { static var keyName: String { "Imported Description" } static var hasValue2: Bool { true } @@ -100,3 +106,34 @@ struct PayeeAccountMapping: SettingsTableViewDataSource { Settings.setAccountMapping(key: key, account: nil) } } + +struct IgnoredPayeeDuplicateMapping: SettingsTableViewDataSource { + static var keyName: String { "Payee 1" } + static var hasValue2: Bool { false } + static var isEditable: Bool { false } + static var value1Name: String { "Payee 2" } + static var value2Name: String { "" } + + let id = UUID() + let key: String + let payee2: String + + var value1: String { payee2 } + var value2: String { "" } + + static func load() -> [Self] { + IgnoredPayeeDuplicateSettings.allPairs().map { Self(key: $0.payee1, payee2: $0.payee2) } + } + + func setValue1(_: String) { + // empty + } + + func setValue2(_: String) { + // empty + } + + func delete() { + IgnoredPayeeDuplicateSettings.remove(IgnoredPayeeDuplicatePair(payee1: key, payee2: payee2)) + } +} diff --git a/SwiftBeanCountApp/Settings/SettingsView.swift b/SwiftBeanCountApp/Settings/SettingsView.swift index a327472..2cf55f8 100644 --- a/SwiftBeanCountApp/Settings/SettingsView.swift +++ b/SwiftBeanCountApp/Settings/SettingsView.swift @@ -40,6 +40,15 @@ struct SettingsView: View { Spacer() }.padding() } + SwiftUI.Tab("Ignored Duplicates", systemImage: "xmark.circle") { + HStack { + VStack { + SettingsTableView() + Spacer() + } + Spacer() + }.padding() + } } .frame(minWidth: 900, minHeight: 500) #else @@ -60,6 +69,11 @@ struct SettingsView: View { } label: { Text("Account Mapping") } + NavigationLink { + SettingsTableView().padding() + } label: { + Text("Ignored Duplicates") + } } .navigationTitle("Settings") } detail: { diff --git a/SwiftBeanCountApp/Tabs/Payees.swift b/SwiftBeanCountApp/Tabs/Payees.swift index 80e9d3f..cbb6f4e 100644 --- a/SwiftBeanCountApp/Tabs/Payees.swift +++ b/SwiftBeanCountApp/Tabs/Payees.swift @@ -135,22 +135,31 @@ struct Payees: View { private var duplicateList: some View { List(duplicates) { duplicate in - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(duplicate.payee1).bold() - Text("\(duplicate.countPayee1)") - Text("↔").foregroundColor(.secondary) - Text(duplicate.payee2).bold() - Text("\(duplicate.countPayee2)") - } - HStack { - Text(duplicate.reason) - .font(.caption) - .foregroundColor(.secondary) - Spacer() - Text("Confidence: \(Int(duplicate.confidence * 100))%") - .font(.caption) - .foregroundColor(confidenceColor(duplicate.confidence)) + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(duplicate.payee1).bold() + Text("\(duplicate.countPayee1)") + Text("↔").foregroundColor(.secondary) + Text(duplicate.payee2).bold() + Text("\(duplicate.countPayee2)") + } + HStack { + Text(duplicate.reason) + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text("Confidence: \(Int(duplicate.confidence * 100))%") + .font(.caption) + .foregroundColor(confidenceColor(duplicate.confidence)) + } + } + Button("Not a Duplicate") { + markAsNotDuplicate(duplicate) + } + .buttonStyle(.bordered) + .controlSize(.small) } } .padding(.vertical, 2) @@ -198,6 +207,14 @@ struct Payees: View { } } + private func markAsNotDuplicate(_ duplicate: PayeeDuplicate) { + let ignoredPair = IgnoredPayeeDuplicatePair(payee1: duplicate.payee1, payee2: duplicate.payee2) + IgnoredPayeeDuplicateSettings.add(ignoredPair) + duplicates.removeAll { + IgnoredPayeeDuplicatePair(payee1: $0.payee1, payee2: $0.payee2) == ignoredPair + } + } + } #Preview { diff --git a/SwiftBeanCountApp/Tabs/Payees/PayeeDuplicateDetector.swift b/SwiftBeanCountApp/Tabs/Payees/PayeeDuplicateDetector.swift index 6e911f7..959956d 100644 --- a/SwiftBeanCountApp/Tabs/Payees/PayeeDuplicateDetector.swift +++ b/SwiftBeanCountApp/Tabs/Payees/PayeeDuplicateDetector.swift @@ -37,14 +37,14 @@ enum PayeeDuplicateDetector { counts[payee, default: 0] += 1 } let sortedCounts = counts.sorted { $0.key.lowercased() < $1.key.lowercased() }.map { ($0.key, $0.value) } - let duplicates = Self.findDuplicates(in: counts) + let duplicates = Self.findDuplicates(in: counts, ignoring: IgnoredPayeeDuplicateSettings.allPairsSet()) return (sortedCounts, duplicates) } /// Finds potential duplicate payees from a list of payee names /// - Parameter payees: Array of payee names /// - Returns: Array of potential duplicates sorted by confidence (highest first) - private static func findDuplicates(in payees: [String: Int]) -> [PayeeDuplicate] { + private static func findDuplicates(in payees: [String: Int], ignoring ignoredPairs: Set) -> [PayeeDuplicate] { var duplicates = [PayeeDuplicate]() let payeeList = payees.map(\.0).sorted() @@ -52,6 +52,9 @@ enum PayeeDuplicateDetector { for j in (i + 1).. Date: Sat, 6 Jun 2026 06:21:27 +0000 Subject: [PATCH 3/7] Clarify ignored duplicate settings behavior --- SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift b/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift index 404bc74..dcbd300 100644 --- a/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift +++ b/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift @@ -126,11 +126,11 @@ struct IgnoredPayeeDuplicateMapping: SettingsTableViewDataSource { } func setValue1(_: String) { - // empty + // Not editable - ignored duplicate pairs can only be removed. } func setValue2(_: String) { - // empty + // Not editable - ignored duplicate pairs can only be removed. } func delete() { From b8f15f023a27c8ce038e9fee295fe35784f9953b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Jun 2026 06:22:07 +0000 Subject: [PATCH 4/7] Normalize ignored duplicate pair decoding --- .../Settings/IgnoredPayeeDuplicateSettings.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift b/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift index bdc83d8..ea510d5 100644 --- a/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift +++ b/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift @@ -8,6 +8,11 @@ import Foundation struct IgnoredPayeeDuplicatePair: Codable, Hashable { + private enum CodingKeys: String, CodingKey { + case payee1 + case payee2 + } + let payee1: String let payee2: String @@ -23,6 +28,12 @@ struct IgnoredPayeeDuplicatePair: Codable, Hashable { } } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + try self.init(payee1: container.decode(String.self, forKey: .payee1), + payee2: container.decode(String.self, forKey: .payee2)) + } + private static func shouldSwap(_ left: String, _ right: String) -> Bool { let normalizedLeft = left.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) let normalizedRight = right.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) @@ -79,7 +90,6 @@ enum IgnoredPayeeDuplicateSettings { private static func normalized(_ pairs: [IgnoredPayeeDuplicatePair]) -> [IgnoredPayeeDuplicatePair] { Array(Set(pairs - .map { IgnoredPayeeDuplicatePair(payee1: $0.payee1, payee2: $0.payee2) } .filter { !$0.payee1.isEmpty && !$0.payee2.isEmpty } )) .sorted { From 82997a77d2a037943fa8b0d77d572c1973b676ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20K=C3=B6tte?= Date: Sat, 6 Jun 2026 22:02:30 -0700 Subject: [PATCH 5/7] fix lint --- .../Settings/GeneralSettingsView.swift | 16 +++--- .../SettingsTableViewDataSource.swift | 9 ++-- SwiftBeanCountApp/Settings/SettingsView.swift | 52 +++++++++---------- 3 files changed, 37 insertions(+), 40 deletions(-) diff --git a/SwiftBeanCountApp/Settings/GeneralSettingsView.swift b/SwiftBeanCountApp/Settings/GeneralSettingsView.swift index 1c7a026..2647650 100644 --- a/SwiftBeanCountApp/Settings/GeneralSettingsView.swift +++ b/SwiftBeanCountApp/Settings/GeneralSettingsView.swift @@ -17,14 +17,16 @@ struct GeneralSettingsView: View { let payees: [String: String] let accounts: [String: String] let descriptions: [String: String] - let ignoredPayeeDuplicates: [IgnoredPayeeDuplicatePair]? + let ignoredPayeeDuplicates: [IgnoredPayeeDuplicatePair] let dateTolerance: String - init(payees: [String: String], - accounts: [String: String], - descriptions: [String: String], - ignoredPayeeDuplicates: [IgnoredPayeeDuplicatePair]? = nil, - dateTolerance: String) { + init( + payees: [String: String], + accounts: [String: String], + descriptions: [String: String], + ignoredPayeeDuplicates: [IgnoredPayeeDuplicatePair], + dateTolerance: String + ) { self.payees = payees self.accounts = accounts self.descriptions = descriptions @@ -149,7 +151,7 @@ struct GeneralSettingsView: View { for (description, account) in settingsFile.accounts { Settings.setAccountMapping(key: description, account: account) } - IgnoredPayeeDuplicateSettings.replaceAll(with: settingsFile.ignoredPayeeDuplicates ?? []) + IgnoredPayeeDuplicateSettings.replaceAll(with: settingsFile.ignoredPayeeDuplicates) if let dateTolerance = Int(settingsFile.dateTolerance) { Settings.dateToleranceInDays = dateTolerance self.dateTolerance = dateTolerance diff --git a/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift b/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift index dcbd300..cc757fb 100644 --- a/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift +++ b/SwiftBeanCountApp/Settings/SettingsTableViewDataSource.swift @@ -27,11 +27,6 @@ protocol SettingsTableViewDataSource: Identifiable { // swiftlint:disable:this f func delete() } -extension SettingsTableViewDataSource { - - static var isEditable: Bool { true } -} - struct DescriptionPayeeMapping: SettingsTableViewDataSource { static var keyName: String { "Imported Description" } static var hasValue2: Bool { true } @@ -137,3 +132,7 @@ struct IgnoredPayeeDuplicateMapping: SettingsTableViewDataSource { IgnoredPayeeDuplicateSettings.remove(IgnoredPayeeDuplicatePair(payee1: key, payee2: payee2)) } } + +extension SettingsTableViewDataSource { + static var isEditable: Bool { true } +} diff --git a/SwiftBeanCountApp/Settings/SettingsView.swift b/SwiftBeanCountApp/Settings/SettingsView.swift index 2cf55f8..2235933 100644 --- a/SwiftBeanCountApp/Settings/SettingsView.swift +++ b/SwiftBeanCountApp/Settings/SettingsView.swift @@ -14,40 +14,24 @@ struct SettingsView: View { #if os(macOS) TabView { SwiftUI.Tab("General", systemImage: "gear") { - HStack { - VStack { - GeneralSettingsView() - Spacer() - } - Spacer() - }.padding() + settingsContainer { + GeneralSettingsView() + } } SwiftUI.Tab("Description Mapping", image: "DescriptionMapping") { - HStack { - VStack { - SettingsTableView() - Spacer() - } - Spacer() - }.padding() + settingsContainer { + SettingsTableView() + } } SwiftUI.Tab("Account Mapping", image: "AccountMapping") { - HStack { - VStack { - SettingsTableView() - Spacer() - } - Spacer() - }.padding() + settingsContainer { + SettingsTableView() + } } SwiftUI.Tab("Ignored Duplicates", systemImage: "xmark.circle") { - HStack { - VStack { - SettingsTableView() - Spacer() - } - Spacer() - }.padding() + settingsContainer { + SettingsTableView() + } } } .frame(minWidth: 900, minHeight: 500) @@ -82,6 +66,18 @@ struct SettingsView: View { #endif } + @ViewBuilder + private func settingsContainer(@ViewBuilder content: () -> Content) -> some View { + HStack { + VStack { + content() + Spacer() + } + Spacer() + } + .padding() + } + } #Preview { From 8d326529798d77686459237cc9f5d509dc4288e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20K=C3=B6tte?= Date: Sat, 6 Jun 2026 22:17:37 -0700 Subject: [PATCH 6/7] Fix view layout --- SwiftBeanCountApp/Tabs/Payees.swift | 59 ++++++++++++++++------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/SwiftBeanCountApp/Tabs/Payees.swift b/SwiftBeanCountApp/Tabs/Payees.swift index cbb6f4e..bcd97ec 100644 --- a/SwiftBeanCountApp/Tabs/Payees.swift +++ b/SwiftBeanCountApp/Tabs/Payees.swift @@ -135,35 +135,37 @@ struct Payees: View { private var duplicateList: some View { List(duplicates) { duplicate in - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .top) { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(duplicate.payee1).bold() - Text("\(duplicate.countPayee1)") - Text("↔").foregroundColor(.secondary) - Text(duplicate.payee2).bold() - Text("\(duplicate.countPayee2)") - } - HStack { - Text(duplicate.reason) - .font(.caption) - .foregroundColor(.secondary) - Spacer() - Text("Confidence: \(Int(duplicate.confidence * 100))%") - .font(.caption) - .foregroundColor(confidenceColor(duplicate.confidence)) - } - } - Button("Not a Duplicate") { - markAsNotDuplicate(duplicate) - } - .buttonStyle(.bordered) - .controlSize(.small) + duplicateView(duplicate) + } + } + + func duplicateView(_ duplicate: PayeeDuplicate) -> some View { + VStack(alignment: .leading) { + HStack { + Text(duplicate.payee1).bold() + Text("(\(duplicate.countPayee1))") + Text("↔").foregroundColor(.secondary) + Text(duplicate.payee2).bold() + Text("(\(duplicate.countPayee2))") + Spacer() + Button("Not a Duplicate") { + markAsNotDuplicate(duplicate) } + .buttonStyle(.bordered) + .controlSize(.small) + } + HStack { + Text(duplicate.reason) + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text("Confidence: \(Int(duplicate.confidence * 100))%") + .font(.caption) + .foregroundColor(confidenceColor(duplicate.confidence)) + .padding(.trailing, 8) } - .padding(.vertical, 2) } + .padding(.vertical, 2) } private func confidenceColor(_ confidence: Double) -> Color { @@ -220,3 +222,8 @@ struct Payees: View { #Preview { Payees().environmentObject(LedgerManager(URL(fileURLWithPath: "/Users/User/Download/Test.beancount"))) } + +#Preview { + Payees().duplicateView(PayeeDuplicate(payee1: "Test Sushi", countPayee1: 4, payee2: "Tes Sushi", countPayee2: 3, confidence: 0.4, reason: "1 character difference")) + .padding() +} From a056ba6287fbd9b470b8eb804eac99ecb842d74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20K=C3=B6tte?= Date: Sat, 6 Jun 2026 22:23:37 -0700 Subject: [PATCH 7/7] fix local and simplify --- .../IgnoredPayeeDuplicateSettings.swift | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift b/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift index ea510d5..542ec29 100644 --- a/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift +++ b/SwiftBeanCountApp/Settings/IgnoredPayeeDuplicateSettings.swift @@ -8,10 +8,6 @@ import Foundation struct IgnoredPayeeDuplicatePair: Codable, Hashable { - private enum CodingKeys: String, CodingKey { - case payee1 - case payee2 - } let payee1: String let payee2: String @@ -28,19 +24,11 @@ struct IgnoredPayeeDuplicatePair: Codable, Hashable { } } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - try self.init(payee1: container.decode(String.self, forKey: .payee1), - payee2: container.decode(String.self, forKey: .payee2)) - } - private static func shouldSwap(_ left: String, _ right: String) -> Bool { - let normalizedLeft = left.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) - let normalizedRight = right.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) - if normalizedLeft == normalizedRight { - return left > right - } - return normalizedLeft > normalizedRight + // too keep consitent when changing locale, do not use localized compare + // Otherwise, after chaning locale, the order might change, so they would + // no longer match and show up incorrectly again + left > right } } @@ -89,9 +77,7 @@ enum IgnoredPayeeDuplicateSettings { } private static func normalized(_ pairs: [IgnoredPayeeDuplicatePair]) -> [IgnoredPayeeDuplicatePair] { - Array(Set(pairs - .filter { !$0.payee1.isEmpty && !$0.payee2.isEmpty } - )) + Array(Set(pairs.filter { !$0.payee1.isEmpty && !$0.payee2.isEmpty })) .sorted { if $0.payee1 == $1.payee1 { return $0.payee2.localizedCaseInsensitiveCompare($1.payee2) == .orderedAscending