Skip to content

Commit 9dac577

Browse files
committed
Better account name validation, isNameValid function on account
1 parent 5ae878c commit 9dac577

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+841
-126
lines changed

SwiftBeanCountModel/Account.swift

+54-5
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ public enum AccountType: String {
3838

3939
/// AccountItems have a name item, which is the last part of the name and and `AccountType`
4040
public protocol AccountItem {
41+
4142
/// Last part of the name, for **Assets:Cash:CAD** this would be **CAD**
4243
var nameItem: String { get }
4344
/// Type, see `AccountType`
4445
var accountType: AccountType { get }
46+
4547
}
4648

4749
/// A group of accounts.
@@ -93,6 +95,14 @@ public class AccountGroup: AccountItem {
9395
/// It does hot hold any `Transaction`s
9496
public class Account: AccountItem {
9597

98+
/// Errors an account can throw
99+
public enum AccoutError: Error {
100+
/// an invalid account name
101+
case invaildName(String)
102+
}
103+
104+
static let nameSeperator = Character(":")
105+
96106
/// Full quilified name of the account, e.g. Assets:Cash:CAD
97107
public let name: String
98108

@@ -112,17 +122,20 @@ public class Account: AccountItem {
112122

113123
/// Last part of the name, for **Assets:Cash:CAD** this would be **CAD**
114124
public var nameItem: String {
115-
return String(describing: name.split(separator: ":").last!)
125+
return String(describing: name.split(separator: Account.nameSeperator).last!)
116126
}
117127

118128
/// Creates an Account
119129
///
120130
/// - Parameters:
121-
/// - name: full name of the account
122-
/// - accountType: type of the account
123-
public init(name: String, accountType: AccountType) {
131+
/// - name: a vaild name for the account
132+
/// - Throws: AccoutError.invaildName in case the account name is invalid
133+
public init(name: String) throws {
134+
guard Account.isNameValid(name) else {
135+
throw AccoutError.invaildName(name)
136+
}
124137
self.name = name
125-
self.accountType = accountType
138+
self.accountType = Account.getAccountType(for: name)
126139
}
127140

128141
///
@@ -153,6 +166,42 @@ public class Account: AccountItem {
153166
return true
154167
}
155168

169+
/// Checks if a given name for an account is valid
170+
///
171+
/// This includes that the name start with one of the base groups and is correctly formattet with seperators
172+
///
173+
/// - Parameter name: String to check
174+
/// - Returns: if the name is valid
175+
public class func isNameValid(_ name: String) -> Bool {
176+
guard !name.isEmpty else {
177+
return false
178+
}
179+
for type in AccountType.allValues() {
180+
if name.starts(with: type.rawValue + String(Account.nameSeperator)) // has to start with one base account followed by a seperator
181+
&& name.last != Account.nameSeperator // is not allowed to end in a seperator
182+
&& name.range(of: "\(Account.nameSeperator)\(Account.nameSeperator)") == nil { // no account item is allowed to be empty
183+
return true
184+
}
185+
}
186+
return false
187+
}
188+
189+
/// Gets the `AccountType` from a name string
190+
///
191+
/// In case of an invalid account name the function might just return .assets
192+
///
193+
/// - Parameter name: valid account name
194+
/// - Returns: `AccountType` of the account with this name
195+
private class func getAccountType(for name: String) -> AccountType {
196+
var type = AccountType.asset
197+
for accountType in AccountType.allValues() {
198+
if name.starts(with: accountType.rawValue ) {
199+
type = accountType
200+
}
201+
}
202+
return type
203+
}
204+
156205
}
157206

158207
extension Account: CustomStringConvertible {

SwiftBeanCountModel/Ledger.swift

+7-9
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,20 @@ public class Ledger {
6060
/// This function ensures that there is exactly one object per Account
6161
///
6262
/// - Parameter name: account name
63-
/// - Returns: Account
63+
/// - Returns: Account or nil if the name is invalid
6464
public func getAccountBy(name: String) -> Account? {
6565
if self.account[name] == nil {
66-
var account: Account!
66+
guard let account = try? Account(name: name) else {
67+
return nil
68+
}
6769
var group: AccountGroup!
68-
let nameItems = name.split(separator: ":").map { String($0) }
70+
let nameItems = name.split(separator: Account.nameSeperator).map { String($0) }
6971
for (index, nameItem) in nameItems.enumerated() {
7072
switch index {
7173
case 0:
72-
guard let accountGroup = accountGroup[nameItem] else {
73-
return nil
74-
}
75-
group = accountGroup
76-
account = Account(name: name, accountType: accountGroup.accountType)
74+
group = accountGroup[nameItem]
7775
case nameItems.count - 1:
78-
group.accounts[name] = account
76+
group.accounts[nameItem] = account
7977
default:
8078
if group.accountGroups[nameItem] == nil {
8179
group.accountGroups[nameItem] = AccountGroup(nameItem: nameItem, accountType: group.accountType)
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Updated for 0.24.0
2+
3+
excluded:
4+
- Carthage
5+
6+
opt_in_rules:
7+
- attributes
8+
- closure_end_indentation
9+
- closure_spacing
10+
- conditional_returns_on_newline
11+
- empty_count
12+
- explicit_init
13+
- extension_access_modifier
14+
- fatal_error_message
15+
- first_where
16+
- implicit_return
17+
- let_var_whitespace
18+
- multiline_parameters
19+
- nimble_operator
20+
- no_extension_access_modifier
21+
- number_separator
22+
- object_literal
23+
- operator_usage_whitespace
24+
- overridden_super_call
25+
- private_outlet
26+
- prohibited_super_call
27+
- redundant_nil_coalescing
28+
- sorted_imports
29+
- switch_case_on_newline
30+
- vertical_parameter_alignment_on_call
31+
- unneeded_parentheses_in_closure_argument
32+
- trailing_closure
33+
- joined_default_parameter
34+
- single_test_class
35+
- pattern_matching_keywords
36+
- contains_over_first_not_nil
37+
- array_init
38+
- multiline_arguments
39+
- literal_expression_end_indentation
40+
- strict_fileprivate
41+
- sorted_first_last
42+
- override_in_extension
43+
44+
disabled_rules:
45+
- trailing_comma
46+
- force_try
47+
48+
line_length: 175
49+
function_body_length: 30

SwiftBeanCountModelTests/AccountTests.swift

+48-19
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,22 @@ class AccountTests: XCTestCase {
1616
let date20170610 = Date(timeIntervalSince1970: 1_497_078_000)
1717

1818
let amount = Amount(number: Decimal(1), commodity: Commodity(symbol: "EUR"))
19+
let accountName = "Assets:Cash"
20+
let invalidNames = ["Assets", "Liabilities", "Income", "Expenses", "Equity", "Assets:", "Assets:Test:", "Assets:Test:", "Assets:Test::Test", "💰", ""]
21+
let validNames = ["Assets:Cash", "Assets:Cash:Test:Test:A", "Assets:Cash:💰", "Assets:Cash:Ca💰h:Test:💰", "Liabilities:Test", "Income:Test", "Expenses:Test", "Equity:Test"]
22+
23+
func testInit() {
24+
for name in validNames {
25+
XCTAssertNoThrow(try Account(name: name))
26+
}
27+
for name in invalidNames {
28+
XCTAssertThrowsError(try Account(name: name))
29+
}
30+
}
1931

2032
func testDescription() {
2133
let name = "Assets:Cash"
22-
let accout = Account(name: name, accountType: .asset)
34+
let accout = try! Account(name: name)
2335
XCTAssertEqual(String(describing: accout), "")
2436
accout.opening = date20170608
2537
XCTAssertEqual(String(describing: accout), "2017-06-08 open \(name)")
@@ -32,7 +44,7 @@ class AccountTests: XCTestCase {
3244

3345
func testDescriptionSpecialCharacters() {
3446
let name = "Assets:💰"
35-
let accout = Account(name: name, accountType: .asset)
47+
let accout = try! Account(name: name)
3648
XCTAssertEqual(String(describing: accout), "")
3749
accout.opening = date20170608
3850
XCTAssertEqual(String(describing: accout), "2017-06-08 open \(name)")
@@ -44,13 +56,21 @@ class AccountTests: XCTestCase {
4456
}
4557

4658
func testNameItem() {
47-
XCTAssertEqual(Account(name: "Assets:Cash", accountType: .asset).nameItem, "Cash")
48-
XCTAssertEqual(Account(name: "Assets:A:B:C:D:E:Cash", accountType: .asset).nameItem, "Cash")
49-
XCTAssertEqual(Account(name: "Assets:💰", accountType: .asset).nameItem, "💰")
59+
XCTAssertEqual(try! Account(name: "Assets:Cash").nameItem, "Cash")
60+
XCTAssertEqual(try! Account(name: "Assets:A:B:C:D:E:Cash").nameItem, "Cash")
61+
XCTAssertEqual(try! Account(name: "Assets:💰").nameItem, "💰")
62+
}
63+
64+
func testAccountType() {
65+
XCTAssertEqual(try! Account(name: "Assets:Test").accountType, AccountType.asset)
66+
XCTAssertEqual(try! Account(name: "Liabilities:Test").accountType, AccountType.liability)
67+
XCTAssertEqual(try! Account(name: "Income:Test").accountType, AccountType.income)
68+
XCTAssertEqual(try! Account(name: "Expenses:Test").accountType, AccountType.expense)
69+
XCTAssertEqual(try! Account(name: "Equity:Test").accountType, AccountType.equity)
5070
}
5171

5272
func testIsPostingValid_NotOpenPast() {
53-
let account = Account(name: "name", accountType: .asset)
73+
let account = try! Account(name: accountName)
5474
let transaction = Transaction(metaData: TransactionMetaData(date: Date(timeIntervalSince1970: 0),
5575
payee: "Payee",
5676
narration: "Narration",
@@ -61,14 +81,14 @@ class AccountTests: XCTestCase {
6181
}
6282

6383
func testIsPostingValid_NotOpenPresent() {
64-
let account = Account(name: "name", accountType: .asset)
84+
let account = try! Account(name: accountName)
6585
let transaction = Transaction(metaData: TransactionMetaData(date: Date(), payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
6686
let posting = Posting(account: account, amount: Amount(number: Decimal(1), commodity: Commodity(symbol: "EUR")), transaction: transaction)
6787
XCTAssertFalse(account.isPostingValid(posting))
6888
}
6989

7090
func testIsPostingValid_BeforeOpening() {
71-
let account = Account(name: "name", accountType: .asset)
91+
let account = try! Account(name: accountName)
7292
account.opening = date20170609
7393

7494
let transaction1 = Transaction(metaData: TransactionMetaData(date: Date(timeIntervalSince1970: 0),
@@ -85,7 +105,7 @@ class AccountTests: XCTestCase {
85105
}
86106

87107
func testIsPostingValid_AfterOpening() {
88-
let account = Account(name: "name", accountType: .asset)
108+
let account = try! Account(name: accountName)
89109
account.opening = date20170609
90110

91111
let transaction1 = Transaction(metaData: TransactionMetaData(date: date20170609, payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
@@ -98,7 +118,7 @@ class AccountTests: XCTestCase {
98118
}
99119

100120
func testIsPostingValid_BeforeClosing() {
101-
let account = Account(name: "name", accountType: .asset)
121+
let account = try! Account(name: accountName)
102122
account.opening = date20170609
103123
account.closing = date20170609
104124
let transaction = Transaction(metaData: TransactionMetaData(date: date20170609, payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
@@ -107,7 +127,7 @@ class AccountTests: XCTestCase {
107127
}
108128

109129
func testIsPostingValid_AfterClosing() {
110-
let account = Account(name: "name", accountType: .asset)
130+
let account = try! Account(name: accountName)
111131
account.opening = date20170609
112132
account.closing = date20170609
113133
let transaction = Transaction(metaData: TransactionMetaData(date: date20170610, payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
@@ -116,7 +136,7 @@ class AccountTests: XCTestCase {
116136
}
117137

118138
func testIsPostingValid_WithoutCommodity() {
119-
let account = Account(name: "name", accountType: .asset)
139+
let account = try! Account(name: accountName)
120140
account.opening = date20170608
121141

122142
let transaction1 = Transaction(metaData: TransactionMetaData(date: date20170609, payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
@@ -129,7 +149,7 @@ class AccountTests: XCTestCase {
129149
}
130150

131151
func testIsPostingValid_CorrectCommodity() {
132-
let account = Account(name: "name", accountType: .asset)
152+
let account = try! Account(name: accountName)
133153
account.commodity = amount.commodity
134154
account.opening = date20170608
135155
let transaction = Transaction(metaData: TransactionMetaData(date: date20170609, payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
@@ -138,7 +158,7 @@ class AccountTests: XCTestCase {
138158
}
139159

140160
func testIsPostingValid_WrongCommodity() {
141-
let account = Account(name: "name", accountType: .asset)
161+
let account = try! Account(name: accountName)
142162
account.commodity = Commodity(symbol: "\(amount.commodity.symbol)1")
143163
account.opening = date20170608
144164
let transaction = Transaction(metaData: TransactionMetaData(date: date20170609, payee: "Payee", narration: "Narration", flag: Flag.complete, tags: []))
@@ -147,16 +167,16 @@ class AccountTests: XCTestCase {
147167
}
148168

149169
func testEqual() {
150-
let name1 = "Asset:Cash"
151-
let name2 = "Asset:💰"
170+
let name1 = "Assets:Cash"
171+
let name2 = "Assets:💰"
152172
let commodity1 = Commodity(symbol: "EUR")
153173
let commodity2 = Commodity(symbol: "💵")
154174
let date1 = date20170608
155175
let date2 = date20170609
156176

157-
let account1 = Account(name: name1, accountType: .asset)
158-
let account2 = Account(name: name1, accountType: .asset)
159-
let account3 = Account(name: name2, accountType: .asset)
177+
let account1 = try! Account(name: name1)
178+
let account2 = try! Account(name: name1)
179+
let account3 = try! Account(name: name2)
160180

161181
// equal
162182
XCTAssertEqual(account1, account2)
@@ -186,4 +206,13 @@ class AccountTests: XCTestCase {
186206
account2.closing = date1
187207
}
188208

209+
func testIsAccountNameVaild() {
210+
for name in validNames {
211+
XCTAssert(Account.isNameValid(name))
212+
}
213+
for name in invalidNames {
214+
XCTAssertFalse(Account.isNameValid(name))
215+
}
216+
}
217+
189218
}

SwiftBeanCountModelTests/LedgerTests.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class LedgerTests: XCTestCase {
4141
XCTAssertEqual(cash1, cash2)
4242
XCTAssertNil(ledger.getAccountBy(name: "Invalid"))
4343
XCTAssertEqual(ledger.accounts.count, 2)
44+
XCTAssertNil(ledger.getAccountBy(name: "Assets:Invalid:"))
45+
XCTAssertEqual(ledger.accounts.count, 2)
46+
XCTAssertNil(ledger.getAccountBy(name: "Assets::Invalid"))
47+
XCTAssertEqual(ledger.accounts.count, 2)
4448
}
4549

4650
func testAccountGroups() {
@@ -85,7 +89,7 @@ class LedgerTests: XCTestCase {
8589
let accountName = "Assets:Cash"
8690
let transactionMetaData = TransactionMetaData(date: Date(timeIntervalSince1970: 1_496_991_600), payee: "Payee", narration: "Narration", flag: Flag.complete, tags: [])
8791
let transaction = Transaction(metaData: transactionMetaData)
88-
let account = Account(name: accountName, accountType: .asset)
92+
let account = try! Account(name: accountName)
8993
let posting = Posting(account: account, amount: Amount(number: Decimal(10), commodity: Commodity(symbol: "EUR")), transaction: transaction)
9094
transaction.postings.append(posting)
9195
let ledger = Ledger()

0 commit comments

Comments
 (0)