Permalink
Cannot retrieve contributors at this time
#!/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.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 -H` | |
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 |