Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 182 lines (146 sloc) 4.21 KB
#!/usr/bin/env ruby
require 'yaml'
require 'cymbal'
require 'term/ansicolor'
require 'date'
##
# Give Numeric a helper .to_d for decimals
class Numeric
def to_d
format '%.2f', self
end
end
def pad(line)
string, _, number = line.rpartition(' ')
pad = 60 - string.size - number.split('.').first.size
string + ' ' + '-' * pad + ' ' + number
end
## a_puts prints with padding to align decimals
def a_puts(line, bold = false)
new_line = pad(line)
line = bold && STDOUT.tty? ? Term::ANSIColor.bold(new_line) : new_line
puts line
end
##
# Calculator for tax burden
class TaxCalc # rubocop:disable Metrics/ClassLength
def initialize(params = {})
@config_file = params[:config]
@year = params[:year] || (Date.today.year - 0).to_s
end
def summary
puts 'Taxes:'
taxes.each { |tax| print_tax_summary(tax) }
puts
totals
end
private
def totals
a_puts "Total Income: #{income.to_d}"
a_puts "Total Assessed: #{total_assessed.to_d}"
a_puts "Total Withheld: #{total_withheld.to_d}"
a_puts "Total Owed: #{total_owed.to_d}", true
end
def print_tax_summary(tax)
puts " #{tax.split(':').last}"
print_deduction_summary(tax)
a_puts " Taxable Income: #{taxable_income(tax).to_d}"
puts ' Taxes Per Bracket:'
per_bracket_cost(tax).each do |rate, cost|
a_puts " #{rate}: #{cost.to_d}"
end
print_tax_totals(tax)
end
def print_deduction_summary(tax)
puts ' Deductions:'
deductions[tax.split(':').last.to_sym].each do |x|
a_puts " #{x[:name]}: #{x[:amount].to_d}"
end
end
def print_tax_totals(tax)
a_puts " Total Assessed: #{tax_assessed(tax).to_d}"
a_puts " Total Withheld: #{withholding[tax].to_d}"
a_puts " Total Owed: #{tax_owed(tax).to_d}", true
end
def config
@config ||= Cymbal.symbolize(YAML.safe_load(File.read(@config_file)))
end
def get_total(account)
if account.to_s.start_with? '_tax_:'
return withholding[account.to_s.split(':').last.to_sym]
end
res = `ledger register #{account} -p '#{@year}' -s`
return 0 if res.size.zero?
res.split.last.tr('$,', '').to_f
end
def income
@income ||= config[:taxable].reduce(0) do |sum, account|
sum + get_total(account) * -1
end
end
def withholding
@withholding ||= taxes.map { |tax| [tax, get_total(tax)] }.to_h
end
def deductions
@deductions ||= config[:deductions].each_with_object({}) do |item, acc|
parse_deduction_amount(item)
item[:from] = parse_exemptions(item[:from])
item[:from].each do |tax|
acc[tax] ||= []
acc[tax] << item
end
end
end
def parse_deduction_amount(item)
item[:amount] ||= get_total(item[:name])
item[:amount] = item[:cap] if item[:cap] && item[:cap] < item[:amount]
end
def withholding_name(short_name)
[config[:withholding_basename], short_name].compact.join(':')
end
def brackets
@brackets ||= config[:brackets].map { |k, v| [withholding_name(k), v] }.to_h
end
def taxes
@taxes ||= brackets.keys
end
def tax_owed(tax)
tax_assessed(tax) - withholding[tax]
end
def tax_assessed(tax)
per_bracket_cost(tax).map(&:last).reduce(:+)
end
def per_bracket_cost(tax)
tmp_income = taxable_income(tax)
brackets[tax].reverse.map do |bracket|
next if tmp_income < bracket[:starts]
cost = bracket_rate_calc(bracket, tmp_income)
tmp_income = bracket[:starts] - 1
[bracket[:rate], cost]
end.compact
end
def bracket_rate_calc(bracket, income)
(income - bracket[:starts] + 1) * bracket[:rate]
end
def taxable_income(tax)
tax = tax.split(':').last.to_sym
income - deductions[tax].reduce(0) { |a, e| a + e[:amount] }
end
def total_withheld
withholding.map(&:last).reduce(:+)
end
def total_assessed
taxes.reduce(0) { |a, e| a + tax_assessed(e) }
end
def total_owed
taxes.reduce(0) { |a, e| a + tax_owed(e) }
end
def parse_exemptions(from)
return taxes.map { |x| x.split(':').last.to_sym } unless from
from = [from] unless from.is_a? Array
from.map(&:to_sym)
end
end
config_file = ARGV.shift || raise("Usage: #{$PROGRAM_NAME} TAX_FILE")
year = ARGV.shift
TaxCalc.new(config: config_file, year: year).summary