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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +49 -0
- data/README.md +46 -0
- data/Rakefile +26 -0
- data/bin/testrb +3 -0
- data/finance_fu.txt +41 -0
- data/lib/mudrat_projector/account.rb +75 -0
- data/lib/mudrat_projector/amortizer.rb +103 -0
- data/lib/mudrat_projector/banker_rounding.rb +11 -0
- data/lib/mudrat_projector/chart_of_accounts.rb +119 -0
- data/lib/mudrat_projector/date_diff.rb +169 -0
- data/lib/mudrat_projector/projection.rb +45 -0
- data/lib/mudrat_projector/projector.rb +71 -0
- data/lib/mudrat_projector/schedule.rb +45 -0
- data/lib/mudrat_projector/scheduled_transaction.rb +45 -0
- data/lib/mudrat_projector/tax_calculation.rb +154 -0
- data/lib/mudrat_projector/tax_calculator.rb +144 -0
- data/lib/mudrat_projector/tax_values_by_year.yml +102 -0
- data/lib/mudrat_projector/transaction.rb +71 -0
- data/lib/mudrat_projector/transaction_entry.rb +126 -0
- data/lib/mudrat_projector/transaction_handler.rb +19 -0
- data/lib/mudrat_projector/validator.rb +49 -0
- data/lib/mudrat_projector/version.rb +3 -0
- data/lib/mudrat_projector.rb +27 -0
- data/mudrat_projector.gemspec +28 -0
- data/test/integrations/long_term_projection_test.rb +42 -0
- data/test/integrations/mortgage_test.rb +85 -0
- data/test/integrations/self_employed_tax_calculation_test.rb +44 -0
- data/test/models/accounts_test.rb +39 -0
- data/test/models/chart_of_accounts_test.rb +170 -0
- data/test/models/date_diff_test.rb +117 -0
- data/test/models/projection_test.rb +62 -0
- data/test/models/projector_test.rb +168 -0
- data/test/models/schedule_test.rb +68 -0
- data/test/models/scheduled_transaction_test.rb +47 -0
- data/test/models/tax_calculator_test.rb +145 -0
- data/test/models/transaction_entry_test.rb +37 -0
- data/test/models/transaction_handler_test.rb +67 -0
- data/test/models/transaction_test.rb +66 -0
- data/test/models/validator_test.rb +45 -0
- data/test/test_helper.rb +69 -0
- 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
|