mudrat_projector 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +49 -0
  7. data/README.md +46 -0
  8. data/Rakefile +26 -0
  9. data/bin/testrb +3 -0
  10. data/finance_fu.txt +41 -0
  11. data/lib/mudrat_projector/account.rb +75 -0
  12. data/lib/mudrat_projector/amortizer.rb +103 -0
  13. data/lib/mudrat_projector/banker_rounding.rb +11 -0
  14. data/lib/mudrat_projector/chart_of_accounts.rb +119 -0
  15. data/lib/mudrat_projector/date_diff.rb +169 -0
  16. data/lib/mudrat_projector/projection.rb +45 -0
  17. data/lib/mudrat_projector/projector.rb +71 -0
  18. data/lib/mudrat_projector/schedule.rb +45 -0
  19. data/lib/mudrat_projector/scheduled_transaction.rb +45 -0
  20. data/lib/mudrat_projector/tax_calculation.rb +154 -0
  21. data/lib/mudrat_projector/tax_calculator.rb +144 -0
  22. data/lib/mudrat_projector/tax_values_by_year.yml +102 -0
  23. data/lib/mudrat_projector/transaction.rb +71 -0
  24. data/lib/mudrat_projector/transaction_entry.rb +126 -0
  25. data/lib/mudrat_projector/transaction_handler.rb +19 -0
  26. data/lib/mudrat_projector/validator.rb +49 -0
  27. data/lib/mudrat_projector/version.rb +3 -0
  28. data/lib/mudrat_projector.rb +27 -0
  29. data/mudrat_projector.gemspec +28 -0
  30. data/test/integrations/long_term_projection_test.rb +42 -0
  31. data/test/integrations/mortgage_test.rb +85 -0
  32. data/test/integrations/self_employed_tax_calculation_test.rb +44 -0
  33. data/test/models/accounts_test.rb +39 -0
  34. data/test/models/chart_of_accounts_test.rb +170 -0
  35. data/test/models/date_diff_test.rb +117 -0
  36. data/test/models/projection_test.rb +62 -0
  37. data/test/models/projector_test.rb +168 -0
  38. data/test/models/schedule_test.rb +68 -0
  39. data/test/models/scheduled_transaction_test.rb +47 -0
  40. data/test/models/tax_calculator_test.rb +145 -0
  41. data/test/models/transaction_entry_test.rb +37 -0
  42. data/test/models/transaction_handler_test.rb +67 -0
  43. data/test/models/transaction_test.rb +66 -0
  44. data/test/models/validator_test.rb +45 -0
  45. data/test/test_helper.rb +69 -0
  46. metadata +204 -0
@@ -0,0 +1,45 @@
1
+ module MudratProjector
2
+ class Projection
3
+ attr :range
4
+
5
+ SequenceEntry = Struct.new :transaction, :batch_id do
6
+ def sort_key
7
+ [transaction.date, batch_id]
8
+ end
9
+
10
+ def <=> other_entry
11
+ sort_key <=> other_entry.sort_key
12
+ end
13
+ end
14
+
15
+ def initialize range: date_range, chart: chart_of_accounts
16
+ @chart = chart
17
+ @batch_id = 0
18
+ @range = range
19
+ @transaction_sequence = []
20
+ end
21
+
22
+ def << transaction
23
+ @transaction_sequence.push SequenceEntry.new(transaction, @batch_id)
24
+ end
25
+
26
+ def add_transaction_batch batch
27
+ batch.each do |transaction|
28
+ self << transaction
29
+ @batch_id += 1
30
+ end
31
+ end
32
+
33
+ def project!
34
+ freeze
35
+ transaction_sequence.each do |transaction|
36
+ @chart.apply_transaction transaction
37
+ yield transaction if block_given?
38
+ end
39
+ end
40
+
41
+ def transaction_sequence
42
+ @transaction_sequence.tap(&:sort!).map &:transaction
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ module MudratProjector
2
+ class Projector
3
+ extend Forwardable
4
+ extend BankerRounding
5
+
6
+ AccountDoesNotExist = Class.new StandardError
7
+ AccountExists = Class.new ArgumentError
8
+ BalanceError = Class.new StandardError
9
+ InvalidAccount = Class.new ArgumentError
10
+ InvalidTransaction = Class.new StandardError
11
+
12
+ attr :from
13
+
14
+ def initialize params = {}
15
+ @chart = params.fetch :chart, ChartOfAccounts.new
16
+ @from = params.fetch :from, ABSOLUTE_START
17
+ @transactions = []
18
+ @validator = Validator.new projector: self, chart: @chart
19
+ end
20
+
21
+ def_delegators :@chart, *%i(accounts account_balance apply_transaction balance fetch net_worth split_account)
22
+ def_delegators :@validator, *%i(must_be_balanced! validate_account! validate_transaction!)
23
+
24
+ def accounts= accounts
25
+ accounts.each do |account_id, params|
26
+ add_account account_id, params
27
+ end
28
+ end
29
+
30
+ def account_exists? account_id
31
+ @chart.exists? account_id
32
+ end
33
+
34
+ def add_account account_id, **params
35
+ validate_account! account_id, params
36
+ @chart.add_account account_id, params
37
+ end
38
+
39
+ def add_transaction params
40
+ if params.is_a? Transaction
41
+ transaction = params
42
+ else
43
+ klass = params.has_key?(:schedule) ? ScheduledTransaction : Transaction
44
+ transaction = klass.new params
45
+ validate_transaction! transaction
46
+ end
47
+ @transactions.push transaction
48
+ end
49
+
50
+ def balanced?
51
+ balance.zero?
52
+ end
53
+
54
+ def project to: end_of_projection, build_next: false, &block
55
+ must_be_balanced!
56
+ projection = Projection.new range: (from..to), chart: @chart
57
+ handler = TransactionHandler.new projection: projection
58
+ if build_next
59
+ handler.next_projector = self.class.new from: to + 1, chart: @chart
60
+ end
61
+ @transactions.each do |transaction| handler << transaction; end
62
+ projection.project! &block
63
+ handler.next_projector
64
+ end
65
+
66
+ def transactions= transactions
67
+ transactions.each do |transaction| add_transaction transaction; end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,45 @@
1
+ module MudratProjector
2
+ class Schedule
3
+ attr :count, :scalar, :unit
4
+
5
+ def initialize params = {}
6
+ @count = params.fetch :count, nil
7
+ @scalar = params.fetch :scalar
8
+ @unit = params.fetch :unit
9
+ end
10
+
11
+ def advance_over range
12
+ split_count_over(range).reduce range.begin do |date, factor|
13
+ @count -= factor if @count
14
+ yield [date, factor]
15
+ DateDiff.advance intervals: factor, unit: unit, from: date
16
+ end
17
+ end
18
+
19
+ def finished?
20
+ (count.nil? || count > 0) ? false : true
21
+ end
22
+
23
+ def split_count_over range
24
+ diff = DateDiff.date_diff unit: unit, from: range.begin, to: range.end
25
+ full_units, final_prorate = [@count, diff].compact.min.divmod 1
26
+ ([1] * full_units).tap do |list|
27
+ list.push final_prorate unless final_prorate.zero?
28
+ end
29
+ end
30
+
31
+ def serialize
32
+ {
33
+ scalar: scalar,
34
+ unit: unit,
35
+ }.tap { |h| h[:count] = count if count }
36
+ end
37
+
38
+ def slice range, &block
39
+ bits = []
40
+ advance_over range do |date, factor| bits.push [date, factor]; end
41
+ leftover = finished? ? nil : serialize
42
+ [bits, leftover]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module MudratProjector
2
+ class ScheduledTransaction < Transaction
3
+ attr :schedule
4
+
5
+ def initialize params = {}
6
+ @schedule = Schedule.new params.fetch :schedule
7
+ super params
8
+ end
9
+
10
+ def build_transactions intervals
11
+ intervals.map do |date, prorate|
12
+ clone_transaction new_date: date, multiplier: prorate
13
+ end
14
+ end
15
+
16
+ def clone_transaction new_date: date, new_schedule: nil, multiplier: 1
17
+ params = build_cloned_tranasction_params new_date, multiplier
18
+ return Transaction.new(params) if new_schedule.nil?
19
+ params[:schedule] = new_schedule
20
+ ScheduledTransaction.new params
21
+ end
22
+
23
+ def multiply entries, by: 1
24
+ entries.map do |entry| entry * by; end
25
+ end
26
+
27
+ def slice slice_date
28
+ intervals, next_schedule = schedule.slice(date..slice_date)
29
+ if next_schedule
30
+ leftover = clone_transaction new_date: slice_date + 1, new_schedule: next_schedule
31
+ end
32
+ [build_transactions(intervals), leftover]
33
+ end
34
+
35
+ private
36
+
37
+ def build_cloned_tranasction_params new_date, multiplier
38
+ {
39
+ date: new_date,
40
+ debits: multiply(debits, by: multiplier),
41
+ credits: multiply(credits, by: multiplier),
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,154 @@
1
+ module MudratProjector
2
+ class TaxCalculation
3
+ attr :values_hash
4
+ attr :w2_gross, :se_gross, :pretax_deductions, :other_gross
5
+ attr :projector, :household, :taxes_withheld
6
+
7
+ def initialize projector, household, values_hash
8
+ @values_hash = values_hash
9
+ @projector = projector
10
+ @w2_gross = 0
11
+ @se_gross = 0
12
+ @pretax_deductions = 0
13
+ @hsa_contribs = Hash.new { |h,k| h[k] = 0 }
14
+ @household = household
15
+ @itemized_deduction = 0
16
+ @taxes_withheld = 0
17
+ @other_gross = 0
18
+ extend_year_shim
19
+ end
20
+
21
+ def method_missing method_name, *args
22
+ if args.empty? && values_hash.has_key?(method_name)
23
+ values_hash.fetch method_name
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def << transaction
30
+ @w2_gross += transaction.salaries_and_wages
31
+ @se_gross += transaction.business_profit
32
+ @other_gross += transaction.dividend_income
33
+ @itemized_deduction += transaction.charitable_contributions
34
+ @itemized_deduction += transaction.interest_paid
35
+ @itemized_deduction += transaction.taxes_paid
36
+ @taxes_withheld += transaction.taxes_withheld
37
+ transaction.debits.each do |entry|
38
+ account = projector.fetch entry.account_id
39
+ if account.type == :asset && account.tag?(:hsa)
40
+ revenue = transaction.credits.select { |e| a = projector.fetch(e.account_id); a.type == :revenue && a.tag?(:salary)}
41
+ @pretax_deductions += revenue.reduce 0 do |s,e| s + e.amount; end
42
+ @hsa_contribs[account] += [entry.amount - pretax_deductions, 0].max
43
+ end
44
+ end
45
+ end
46
+
47
+ def adjustments
48
+ (self_employment_tax / 2) + hsa_contributions
49
+ end
50
+
51
+ def agi
52
+ total_income - adjustments
53
+ end
54
+
55
+ def brackets
56
+ bracket_caps = super.tap do |list|
57
+ list.push Float::INFINITY
58
+ end
59
+ values_hash.fetch(:bracket_rates).zip bracket_caps
60
+ end
61
+
62
+ def deduction
63
+ [itemized_deduction, standard_deduction].max
64
+ end
65
+
66
+ def effective_rate
67
+ (((gross - net) / gross) * 100).round 2
68
+ end
69
+
70
+ def exemption
71
+ personal_exemption * household.exemptions
72
+ end
73
+
74
+ def gross
75
+ w2_gross + se_gross + other_gross
76
+ end
77
+
78
+ def income_tax
79
+ amount_taxed = 0
80
+ brackets.reduce 0 do |sum, (rate, cap)|
81
+ if cap == Float::INFINITY
82
+ in_bracket = taxable_income - amount_taxed
83
+ else
84
+ in_bracket = [cap, taxable_income].min - amount_taxed
85
+ end
86
+ amount_taxed += in_bracket
87
+ t = (in_bracket * (rate / 100))
88
+ sum + t
89
+ end
90
+ end
91
+
92
+ def hsa_contributions
93
+ @hsa_contribs.reduce 0 do |sum, (account, amount)|
94
+ type = [:individual, :family, :senior].detect { |tag| account.tag? tag }
95
+ max = hsa_limit.fetch type
96
+ sum + [amount, max].min
97
+ end
98
+ end
99
+
100
+ def itemized_deduction
101
+ @itemized_deduction
102
+ end
103
+
104
+ def medicare_withheld
105
+ w2_gross * (hi_rate / 100)
106
+ end
107
+
108
+ def net
109
+ gross - taxes
110
+ end
111
+
112
+ def self_employment_tax
113
+ rate = (oasdi_rate + hi_rate) / 100
114
+ se_gross * (1 - rate) * (rate * 2)
115
+ end
116
+
117
+ def social_security_withheld
118
+ [w2_gross, oasdi_wage_base].min * (oasdi_rate / 100)
119
+ end
120
+
121
+ def taxable_income
122
+ agi - deduction - exemption
123
+ end
124
+
125
+ def taxes
126
+ withholding_tax + income_tax + self_employment_tax
127
+ end
128
+
129
+ def taxes_owed
130
+ taxes - taxes_withheld
131
+ end
132
+
133
+ def total_income
134
+ gross - pretax_deductions
135
+ end
136
+
137
+ def year
138
+ projector.from.year
139
+ end
140
+
141
+ def withholding_tax
142
+ medicare_withheld + social_security_withheld
143
+ end
144
+
145
+ private
146
+
147
+ def extend_year_shim
148
+ shim_module = "Year#{year}".to_sym
149
+ if TaxCalculator.const_defined? shim_module
150
+ extend TaxCalculator.const_get shim_module
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,144 @@
1
+ module MudratProjector
2
+ class TaxCalculator
3
+ module Year2012
4
+ # see http://en.wikipedia.org/wiki/Tax_Relief,_Unemployment_Insurance_Reauthorization,_and_Job_Creation_Act_of_2010
5
+ def social_security_withheld
6
+ super * (4.2.to_d / 6.2.to_d)
7
+ end
8
+ end
9
+
10
+ attr :household, :projector
11
+
12
+ HOUSEHOLD_TYPES = %i(married_filing_jointly married_filing_separately single
13
+ head_of_household)
14
+ EXPENSE_ACCOUNT_ID = :united_states_treasury
15
+
16
+ Household = Struct.new :filing_status, :exemptions do
17
+ def initialize *args
18
+ super
19
+ unless HOUSEHOLD_TYPES.include? filing_status
20
+ raise "Invalid filing status #{filing_status.inspect}"
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.project projector, to: end_date, household: nil
26
+ tax_calculator = new projector: projector, household: household
27
+ (projector.from.year..to.year).each do
28
+ calculation = tax_calculator.calculate!
29
+ yield calculation if block_given?
30
+ end
31
+ tax_calculator.projector
32
+ end
33
+
34
+ class TransactionWrapper
35
+ INCOME_VALUES = %i(business_profit dividend_income salaries_and_wages)
36
+ ADJUSTMENTS_VALUES = %i(other_adjustments)
37
+ DEDUCTIONS_VALUES = %i(charitable_contributions interest_paid taxes_paid)
38
+ CREDITS_VALUES = %i()
39
+
40
+ VALUES = [INCOME_VALUES, ADJUSTMENTS_VALUES, DEDUCTIONS_VALUES, CREDITS_VALUES].flatten 1
41
+
42
+ attr :calculator, :taxes_withheld
43
+ private :calculator
44
+
45
+ def initialize calculator, transaction
46
+ @calculator = calculator
47
+ @taxes_withheld = 0
48
+ @transaction = transaction
49
+ VALUES.each do |attr_name| instance_variable_set "@#{attr_name}", 0; end
50
+ end
51
+
52
+ VALUES.each do |attr_name| attr attr_name; end
53
+
54
+ def calculate!
55
+ @transaction.entries.each do |entry|
56
+ account = calculator.projector.fetch entry.account_id
57
+ if account.type == :revenue
58
+ @salaries_and_wages += entry.delta if account.tag? :salary
59
+ @business_profit += entry.delta if account.tag? :self_employed
60
+ @dividend_income += entry.delta if account.tag? :dividend
61
+ elsif account.type == :expense
62
+ @business_profit -= entry.delta if account.tag? :self_employed
63
+ @charitable_contributions +=
64
+ entry.delta if account.tag? "501c".to_sym
65
+ @interest_paid += entry.delta if account.tag? :mortgage_interest
66
+ @taxes_paid += entry.delta if account.tag? :tax
67
+ @taxes_withheld += entry.delta if entry.account_id == EXPENSE_ACCOUNT_ID
68
+ elsif account.type == :asset
69
+ @other_adjustments += entry.delta if account.tag?(:hsa) && entry.debit?
70
+ end
71
+ end
72
+ end
73
+
74
+ def debits
75
+ @transaction.debits
76
+ end
77
+
78
+ def credits
79
+ @transaction.credits
80
+ end
81
+ end
82
+
83
+ def initialize projector: nil, household: {}
84
+ @household = Household.new(
85
+ household.fetch(:filing_status),
86
+ household.fetch(:exemptions),
87
+ )
88
+ @projector = projector
89
+ @values_hash = parse_yaml
90
+ unless projector.account_exists? EXPENSE_ACCOUNT_ID
91
+ projector.add_account EXPENSE_ACCOUNT_ID, type: :expense
92
+ end
93
+ unless projector.account_exists? :owed_taxes
94
+ projector.add_account :owed_taxes, type: :liability
95
+ end
96
+ end
97
+
98
+ def calculate!
99
+ end_of_calendar_year = Date.new year, 12, 31
100
+ calculation = TaxCalculation.new projector, household, @values_hash
101
+ next_projector = projector.project to: end_of_calendar_year, build_next: true do |transaction|
102
+ calculation << TransactionWrapper.new(self, transaction).tap(&:calculate!)
103
+ end
104
+ final_transaction = Transaction.new(
105
+ date: end_of_calendar_year,
106
+ debit: { amount: calculation.taxes_owed, account_id: EXPENSE_ACCOUNT_ID },
107
+ credit: { amount: calculation.taxes_owed, account_id: :owed_taxes },
108
+ )
109
+ projector.apply_transaction final_transaction
110
+ @projector = next_projector
111
+ @values_hash = parse_yaml
112
+ calculation
113
+ end
114
+
115
+ def year
116
+ projector.from.year
117
+ end
118
+
119
+ private
120
+
121
+ def parse_yaml
122
+ yaml = File.expand_path '../tax_values_by_year.yml', __FILE__
123
+ by_year = YAML.load File.read(yaml)
124
+ max_year = by_year.keys.max
125
+ parsed = by_year.fetch([year, max_year].min).tap do |hash|
126
+ recursively_symbolize_keys! hash
127
+ end
128
+ hash = parsed.fetch household.filing_status
129
+ parsed.each_with_object hash do |(key, value), hash|
130
+ hash[key] = value unless HOUSEHOLD_TYPES.include? key
131
+ end
132
+ hash
133
+ end
134
+
135
+ def recursively_symbolize_keys! hash
136
+ hash.keys.each do |key|
137
+ value = hash.delete key
138
+ recursively_symbolize_keys! value if value.is_a? Hash
139
+ key = key.respond_to?(:to_sym) ? key.to_sym : key
140
+ hash[key] = value
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,102 @@
1
+ 2012:
2
+ oasdi_rate: 6.2
3
+ oasdi_wage_base: 110_100
4
+ hi_rate: 1.45
5
+ personal_exemption: 3_800
6
+ hsa_limit:
7
+ individual: 3_100
8
+ family: 6_250
9
+ senior: 1000
10
+ bracket_rates:
11
+ - 10.0
12
+ - 15.0
13
+ - 25.0
14
+ - 28.0
15
+ - 33.0
16
+ - 35.0
17
+ married_filing_jointly:
18
+ standard_deduction: 11_900
19
+ brackets:
20
+ - 17_400
21
+ - 70_700
22
+ - 142_700
23
+ - 217_450
24
+ - 388_350
25
+ married_filing_separately:
26
+ standard_deduction: 5_950
27
+ brackets:
28
+ - 8_700
29
+ - 35_350
30
+ - 71_350
31
+ - 108_725
32
+ - 194_175
33
+ single:
34
+ standard_deduction: 5_950
35
+ brackets:
36
+ - 8_700
37
+ - 35_350
38
+ - 85_650
39
+ - 178_650
40
+ - 388_350
41
+ head_of_household:
42
+ standard_deduction: 8_700
43
+ brackets:
44
+ - 12_400
45
+ - 47_350
46
+ - 122_300
47
+ - 190_050
48
+ - 388_350
49
+
50
+ 2013:
51
+ oasdi_rate: 6.2
52
+ oasdi_wage_base: 113_700
53
+ hi_rate: 1.45
54
+ personal_exemption: 3_900
55
+ hsa_limit:
56
+ individual: 3_250
57
+ family: 6_450
58
+ senior: 1000
59
+ bracket_rates:
60
+ - 10.0
61
+ - 15.0
62
+ - 25.0
63
+ - 28.0
64
+ - 33.0
65
+ - 35.0
66
+ - 39.6
67
+ married_filing_jointly:
68
+ standard_deduction: 12_200
69
+ brackets:
70
+ - 17_850
71
+ - 72_500
72
+ - 146_400
73
+ - 223_050
74
+ - 398_350
75
+ - 450_000
76
+ married_filing_separately:
77
+ standard_deduction: 6_100
78
+ brackets:
79
+ - 8_925
80
+ - 36_250
81
+ - 73_200
82
+ - 111_525
83
+ - 199_175
84
+ - 225_000
85
+ single:
86
+ standard_deduction: 6_100
87
+ brackets:
88
+ - 8_925
89
+ - 36_250
90
+ - 87_850
91
+ - 183_250
92
+ - 398_350
93
+ - 400_000
94
+ head_of_household:
95
+ standard_deduction: 8_950
96
+ brackets:
97
+ - 12_750
98
+ - 48_600
99
+ - 125_450
100
+ - 203_150
101
+ - 398_350
102
+ - 425_000
@@ -0,0 +1,71 @@
1
+ module MudratProjector
2
+ class Transaction
3
+ include Enumerable
4
+
5
+ attr :credits, :date, :debits
6
+
7
+ def initialize params = {}
8
+ @date = params.fetch :date
9
+ self.credits = extract_entry_params :credit, params
10
+ self.debits = extract_entry_params :debit, params
11
+ end
12
+
13
+ def balanced?
14
+ sum_credits = build_set_for_balance credits
15
+ sum_debits = build_set_for_balance debits
16
+ (sum_credits ^ sum_debits).empty?
17
+ end
18
+
19
+ def credits= credits
20
+ @credits = build_entries :credit, credits
21
+ end
22
+
23
+ def debits= debits
24
+ @debits = build_entries :debit, debits
25
+ end
26
+
27
+ def each &block
28
+ credits.each &block
29
+ debits.each &block
30
+ end
31
+
32
+ def slice slice_date
33
+ if date > slice_date
34
+ [[], self]
35
+ else
36
+ [[self], nil]
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def extract_entry_params credit_or_debit, params
43
+ entries = Array params["#{credit_or_debit}s".to_sym]
44
+ return entries unless params.has_key? credit_or_debit
45
+ unless entries.empty?
46
+ raise ArgumentError, "You cannot supply both #{credit_or_debit} and "\
47
+ "#{credit_or_debit}s"
48
+ end
49
+ [params.fetch(credit_or_debit)]
50
+ end
51
+
52
+ def build_entries credit_or_debit, entries
53
+ entries.map do |entry_params|
54
+ if entry_params.is_a? TransactionEntry
55
+ entry_params
56
+ else
57
+ TransactionEntry.public_send "new_#{credit_or_debit}", entry_params
58
+ end
59
+ end
60
+ end
61
+
62
+ def build_set_for_balance entries
63
+ hash = Hash.new { |h,k| h[k] = 0 }
64
+ entries.each do |entry|
65
+ balance_key = entry.class == TransactionEntry ? :fixed : entry.other_account_id
66
+ hash[balance_key] += entry.scalar
67
+ end
68
+ hash.reduce Set.new do |set, (_, value)| set << value; end
69
+ end
70
+ end
71
+ end