Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 189 lines (160 sloc) 4.37 KB
#!/usr/bin/env ruby
# rubocop:disable Style/BlockComments
=begin
Example schema:
---
# A bill for comcast on the 5th of the month for $100
- name: comcast
amount: 100
when: 5
# Payday for $1000, every other friday
- name: payday
amount: -1000
frequency: biweekly
when: friday
# A bill for $200/mo on the 5th of each month starting in January 2016
- name: Annoying bill
amount: 200
when: 5
starts: 2016/01/01
# A bill that will stop being withdrawn after November 30th, 2015
- name: Another bill
amount: 100
when: 10
stops: 2015/11/30
---
Schema reference:
* name: any string to refer to this item in the output
* amount: dollar amount for this item (negative for income)
* frequency: what cycle the charge recurs on. Supported values:
* monthly (default)
* biweekly
* when: Used alongside frequency.
* monthly: set to day of month or array of days of month where activity occurs
* biweekly: set an example day where the action occurs (must be in the past)
* starts: optional start date as YYYY/MM/DD, item will only be processed after
this date
* stops: optional end date as YYYY/MM/DD, item will only be processed until this
date
=end
# rubocop:enable Style/BlockComments
require 'yaml'
require 'date'
require 'cymbal'
require 'mercenary'
# Base action object
class Action
def initialize(action, start, stop)
@name = action[:name]
@start = action[:starts] ? Date.parse(action[:starts]) : start
@stop = action[:stops] ? Date.parse(action[:stops]) : stop
@amount = action[:amount].to_i
@when = action[:when]
end
def entry(date)
[date, @name, @amount]
end
end
# Action that recurs monthly
class Monthly < Action
def initialize(*_) # rubocop:disable Naming/UncommunicativeMethodParamName
super
@when = [@when] unless @when.is_a? Array
end
def log
@when.flat_map do |date|
pointer = Date.new(Date.today.year, 1, date)
get_dates([], pointer, 0)
end
end
def get_dates(log, pointer, counter)
date = pointer >> counter
return log if date > @stop
return get_dates(log, pointer, counter + 1) if date < @start
log << entry(date)
get_dates(log, pointer, counter + 1)
end
end
# Action that recurs every week
class Weekly < Action
def log
pointer = Date.parse(@when)
get_dates([], pointer)
end
def get_dates(log, pointer)
return log if pointer > @stop
return get_dates(log, pointer + step) if pointer < @start
log << entry(pointer)
get_dates(log, pointer + step)
end
def step
7
end
end
# Action that reurs every other week
class Biweekly < Weekly
def step
14
end
end
# Budget projection class
class Budget
FREQUENCIES = {
monthly: Monthly,
weekly: Weekly,
biweekly: Biweekly
}.freeze
def initialize(params = {})
@options = params
end
def project!
balance = seed
write_line(start_date, 'initial value', seed, seed)
log.each do |date, name, amount|
balance -= amount
write_line(date, name, amount, balance)
end
end
private
def write_line(date, name, change, balance)
args = [date, balance.to_s.rjust(10), change.to_s.rjust(10), name]
puts args.join(' | ')
end
def log
@log ||= actions.reduce([]) do |array, action|
frequency = (action[:frequency] || :monthly).to_sym
action_class = FREQUENCIES[frequency]
raise("Invalid frequency: #{frequency}") unless action_class
array + action_class.new(action, start_date, stop_date).log
end.sort_by(&:first)
end
def actions
@actions ||= Cymbal.symbolize(
File.open(activity_file) { |fh| YAML.safe_load fh }
)
end
def activity_file
@activity_file ||= @options[:activity_file] || 'projections.yml'
end
def start_date
@start_date ||= Date.today
end
def stop_date
@stop_date ||= start_date >> (@options[:months] || 6).to_i
end
def seed
@seed ||= @options[:seed].to_i || 0
end
end
Mercenary.program(:budget) do |p|
p.description 'Tool for projecting budgets'
p.syntax 'budget [options] path/to/projections.yml'
# rubocop:disable Metrics/LineLength
p.option :months, '-m N', '--months N', 'How many months to project (default 6)'
p.option :seed, '-s N', '--seed N', 'Seed value to being operations with'
p.option :file, '-f PATH', '--file PATH', 'Path of projections YAML file'
# rubocop:enable Metrics/LineLength
p.action do |_, options|
Budget.new(options).project!
end
end