bouch 1.1.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.
@@ -0,0 +1,142 @@
1
+ # Bouch
2
+ [![pipeline status](https://gitlab.com/ssofos/bouch/badges/master/pipeline.svg)](https://gitlab.com/ssofos/bouch/commits/master) [![coverage report](https://gitlab.com/ssofos/bouch/badges/master/coverage.svg)](https://gitlab.com/ssofos/bouch/commits/master)
3
+
4
+ `Bouch` is the budget pouch. A simple tool to calculate and project your annual personal budget based on fiscal quarter expenditures, income, assets, and debts.
5
+
6
+ Use it to help establish an annual financial plan, set monetary goals, and gain perspective on your financial health.
7
+
8
+ ## Install
9
+
10
+ To install bouch via RubyGems simple execute this command from a command-line interface:
11
+
12
+ ```
13
+ gem install bouch
14
+ ```
15
+
16
+ To use bouch as part of your Ruby Project add this line to your Gemfile:
17
+
18
+ ```
19
+ gem 'bouch'
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Bouch takes a simple YAML file as its primary data input. This will be referred to as a budget pouch file.
25
+
26
+ There is an example budget pouch file called **pouch.example.yml**.
27
+
28
+ Use this budget pouch file and execute the following command on your favorite terminal emulator's CLI:
29
+
30
+ ```
31
+ bouch pouch.example.yml
32
+ ```
33
+
34
+ You should see the example budget summary output:
35
+
36
+ ```
37
+ ----------------------
38
+ Quarter 1: 3025.00
39
+ Quarter 2: 3025.00
40
+ Quarter 3: 3025.00
41
+ Quarter 4: 3025.00
42
+ ----------------------
43
+ Budget Annual Total: 12100.00
44
+ Budget Annual Income: 28808.00
45
+ Budget Income Percent: 42.00%
46
+ ----------------------
47
+ Assets Total: 1000.00
48
+ ----------------------
49
+ Debt Total: 420.00
50
+ Debt Ratio: 0.4200
51
+ Debt Ratio Percent: 42.00%
52
+ ----------------------
53
+ ```
54
+
55
+ ### Pouch Schema
56
+
57
+ To use bouch to calculate your own budget you must create a customized budget pouch file.
58
+
59
+ Budget pouch files are written in Ruby friendly YAML and currently use the following schema:
60
+
61
+ * Deeply nested mappings, also know as hashes
62
+ * Think of hashes as simple groupings of keys and values
63
+ * Primary hash keys:
64
+ * **Budget**
65
+ * **Salary**
66
+ * **Assets**
67
+ * **Debts**
68
+ * **Budget** nested keys and values:
69
+ * **Q1**
70
+ * foo
71
+ * Value: Integer or Float
72
+ * Equals a budget item's quarterly cost
73
+ * bar
74
+ * **cost**
75
+ * Value: Integer or Float
76
+ * Equals the monthly cost of any repeated payment
77
+ * **repeat**
78
+ * Value: true
79
+ * Enables quarterly auto-calculation of the repeat payment
80
+ * **Q2**
81
+ * Same nested schema as Q1
82
+ * **Q3**
83
+ * Same nested schema as Q1
84
+ * **Q4**
85
+ * Same nested schema as Q1
86
+ * **Salary** nested keys and values:
87
+ * **quantity**
88
+ * Value: Float or Integer
89
+ * Equals amount of after-tax money per salary pay period
90
+ * **frequency**
91
+ * Value: Integer
92
+ * Equals number of weeks for each salary pay period
93
+ * **Assets** nested keys and values:
94
+ * foo
95
+ * Value: Integer or Float
96
+ * Equals an asset's total valued amount
97
+ * **Debts** nested keys and values:
98
+ * bar
99
+ * Value: Integer or Float
100
+ * Equals a debt's total valued amount
101
+ * All non-primary nested keys are case insensitive
102
+
103
+ Here is an simple example of of budget pouch file:
104
+
105
+ ```
106
+ ---
107
+ Budget:
108
+ Q1:
109
+ Rent:
110
+ cost: 1000
111
+ repeat: true
112
+ foo: 20
113
+ bar: 5
114
+ Q2:
115
+ Rent:
116
+ cost: 1000
117
+ repeat: true
118
+ fizz: 20
119
+ buzz: 5
120
+ Q3:
121
+ Rent:
122
+ cost: 1000
123
+ repeat: true
124
+ bubble: 20
125
+ sort: 5
126
+ Q4:
127
+ Rent:
128
+ cost: 1000
129
+ repeat: true
130
+ baz: 20
131
+ qax: 5
132
+ Salary:
133
+ quantity: 1108.00
134
+ frequency: 2
135
+ Assets:
136
+ foo: 500
137
+ bar: 500
138
+ Debts:
139
+ baz: 120
140
+ qax: 300
141
+ ...
142
+ ```
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'bouch/cli'
6
+
7
+ Bouch::CLI.new(ARGV[0].freeze).start
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
4
+ require 'bouch/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'bouch'
8
+ s.date = '2018-06-09'
9
+ s.version = Bouch::VERSION
10
+ s.authors = ['Shane R. Sofos']
11
+ s.email = ['ssofos@gmail.com']
12
+ s.homepage = 'https://gitlab.com/ssofos/bouch'
13
+ s.license = 'GPL-3.0'
14
+ s.description = 'The Budget Pouch. Fast annual budget projections.'
15
+ s.summary =
16
+ 'A simple tool to calculate and project your ' +
17
+ 'annual personal budget based on fiscal quarters ' +
18
+ 'expenditures, income, assets, and debts.'
19
+
20
+ s.files = %x(git ls-files).split($INPUT_RECORD_SEPARATOR)
21
+ s.bindir = ['bin']
22
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
24
+ s.require_paths = ['lib']
25
+
26
+ s.add_development_dependency 'bundler'
27
+ s.add_development_dependency 'rake'
28
+ s.add_development_dependency 'rspec'
29
+ s.add_development_dependency 'rubocop', '<=0.56.0'
30
+ s.add_development_dependency 'simplecov'
31
+
32
+ s.required_ruby_version = '>= 2.4.0'
33
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'bouch/calc'
5
+
6
+ # Calculate and create simple financial budgets
7
+ # by parsing a single YAML file as input
8
+ class Bouch
9
+ attr_accessor :assets, :debts, :pouch, :quarters
10
+ include BouchCalculate
11
+
12
+ def initialize(file)
13
+ @assets = Array.new
14
+ @debts = Array.new
15
+ @pouch = YAML.safe_load(IO.read(file))
16
+ @quarters = Hash.new
17
+ end
18
+
19
+ # Summarize and show aggregate asset totals
20
+ def show_assets_total
21
+ calc_assets(@pouch['Assets']) if @assets.empty?
22
+ puts '----------------------'
23
+ puts format('%-30s %.2f', 'Assets Total:', @assets.sum)
24
+ end
25
+
26
+ # Summarize and show aggregate liabilities totals
27
+ def show_debts_total
28
+ calc_debts(@pouch['Debts']) if @debts.empty?
29
+ puts '----------------------'
30
+ puts format('%-30s %.2f', 'Debt Total:', @debts.sum)
31
+ end
32
+
33
+ # Summarize and show debt ratio
34
+ def show_debt_ratio
35
+ calc_assets(@pouch['Assets']) if @assets.empty?
36
+ calc_debts(@pouch['Debts']) if @debts.empty?
37
+ puts format('%-30s %.4f', 'Debt Ratio:', calc_debt_ratio(@debts.sum, @assets.sum))
38
+ puts format('%-30s %.2f%s', 'Debt Ratio Percent:',
39
+ calc_debt_ratio_percent(calc_debt_ratio(@debts.sum, @assets.sum)),
40
+ '%')
41
+ puts '----------------------'
42
+ end
43
+
44
+ # Summarize and show all financial quarter budgets
45
+ def show_quarters
46
+ calc_quarters_raw(@pouch['Budget']) if @quarters.empty?
47
+ puts '----------------------'
48
+ 4.times do |n|
49
+ puts format('%-30s %.2f', 'Quarter ' + (n + 1).to_s + ':', @quarters[n.to_s].sum)
50
+ end
51
+ puts '----------------------'
52
+ end
53
+
54
+ # Summarize and show the aggregate annual budget totals
55
+ def show_annual_total
56
+ calc_quarters_raw(@pouch['Budget']) if @quarters.empty?
57
+ puts format('%-30s %.2f', 'Budget Annual Total:', calc_quarters_raw_total.to_s)
58
+ end
59
+
60
+ # Summarize and show the aggregate annual budget as a percentage of income
61
+ def show_budget_percentage
62
+ calc_quarters_raw(@pouch['Budget']) if @quarters.empty?
63
+ puts format('%-30s %.2f%s',
64
+ 'Budget Income Percent:',
65
+ calc_budget_percentage(
66
+ calc_quarters_raw_total,
67
+ calc_salary(@pouch['Salary']['quantity'], @pouch['Salary']['frequency'])
68
+ ),
69
+ '%')
70
+ end
71
+
72
+ # Summarize and show annual income
73
+ def show_annual_income
74
+ puts format('%-30s %.2f',
75
+ 'Budget Annual Income:',
76
+ calc_salary(@pouch['Salary']['quantity'], @pouch['Salary']['frequency']))
77
+ end
78
+
79
+ # Summarize and show all aggregate components of the quarterly, annual budgets, assets
80
+ def show_budget
81
+ show_quarters
82
+ show_annual_total
83
+ show_annual_income
84
+ show_budget_percentage
85
+ show_assets_total
86
+ show_debts_total
87
+ show_debt_ratio
88
+ end
89
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Financial budget calculation and operations for Bouch
4
+ module BouchCalculate
5
+ # Calculate asset value aggregate amount
6
+ def calc_assets(assets)
7
+ assets.each_value do |value|
8
+ @assets.push(value)
9
+ end
10
+ end
11
+
12
+ # Calculate the percentage of budget of a salary/income
13
+ def calc_budget_percentage(total, salary)
14
+ ((total.to_f / salary.to_f) * 100).round(2)
15
+ end
16
+
17
+ # Calculate debt/liability aggregate amount
18
+ def calc_debts(debts)
19
+ debts.each_value do |value|
20
+ @debts.push(value)
21
+ end
22
+ end
23
+
24
+ # Calculate a debt ratio: total debts divided by total assets
25
+ def calc_debt_ratio(debts, assets)
26
+ (debts.to_f / assets.to_f).round(4)
27
+ end
28
+
29
+ # Calculate a debt ratio percentage
30
+ def calc_debt_ratio_percent(debt_ratio)
31
+ (debt_ratio.to_f * 100).round(2)
32
+ end
33
+
34
+ # Calculate a quarterly repeating budget item amount
35
+ def calc_repeating(item)
36
+ (item.to_f * 3).round(2)
37
+ end
38
+
39
+ # Calculate an annual income based on a weekly frequency salary
40
+ def calc_salary(amount, freq)
41
+ (amount.to_f * (52 / freq)).round(2)
42
+ end
43
+
44
+ # Calculate each financial quarters budget items, including repeating ones
45
+ def calc_quarters_raw(budget)
46
+ budget.each_value do |items|
47
+ @quarters[@quarters.length.to_s] = Array.new
48
+ items.each_value do |value|
49
+ case value
50
+ when Hash
51
+ if value.key?('repeat')
52
+ @quarters[(@quarters.length - 1).to_s].push(calc_repeating(value['cost']))
53
+ end
54
+ else
55
+ @quarters[(@quarters.length - 1).to_s].push(value)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Calculate all financial quarter budget into an aggregate annual budget
62
+ def calc_quarters_raw_total
63
+ @quarters['0'].sum + @quarters['1'].sum + @quarters['2'].sum + @quarters['3'].sum
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bouch'
4
+ require 'bouch/version'
5
+
6
+ class Bouch
7
+ # Parse the command line
8
+ class CLI
9
+ attr_accessor :yaml_file
10
+
11
+ def initialize(file)
12
+ @yaml_file = file
13
+ end
14
+
15
+ def start
16
+ if @yaml_file.eql?(nil)
17
+ puts 'Please supply budget pouch YAML file path and rerun bouch.'
18
+ usage
19
+ elsif File.exist?(@yaml_file)
20
+ budget = Bouch.new(@yaml_file)
21
+ budget.show_budget
22
+ else
23
+ puts "Whoops. The budget pouch file specified: #{@yaml_file} ; does not exist!"
24
+ usage
25
+ end
26
+ end
27
+
28
+ def usage
29
+ puts "<<bouch #{Bouch::VERSION}>>\nUsage: #{File.basename($PROGRAM_NAME)} [YAML_FILE]"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Bouch
4
+ VERSION = '1.1.0'
5
+ end
@@ -0,0 +1,36 @@
1
+ ---
2
+ Budget:
3
+ Q1:
4
+ Rent:
5
+ cost: 1000
6
+ repeat: true
7
+ foo: 20
8
+ bar: 5
9
+ Q2:
10
+ Rent:
11
+ cost: 1000
12
+ repeat: true
13
+ fizz: 20
14
+ buzz: 5
15
+ Q3:
16
+ Rent:
17
+ cost: 1000
18
+ repeat: true
19
+ bubble: 20
20
+ sort: 5
21
+ Q4:
22
+ Rent:
23
+ cost: 1000
24
+ repeat: true
25
+ baz: 20
26
+ qax: 5
27
+ Salary:
28
+ quantity: 1108.00
29
+ frequency: 2
30
+ Assets:
31
+ foo: 500
32
+ bar: 500
33
+ Debts:
34
+ baz: 120
35
+ qax: 300
36
+ ...
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'bouch'
5
+ require 'bouch/calc'
6
+
7
+ describe BouchCalculate do
8
+ before :all do
9
+ @yaml = File.realdirpath('pouch.example.yml')
10
+ @bouch = Bouch.new(@yaml)
11
+ end
12
+
13
+ describe '.calc_assets' do
14
+ it 'pushes asset values to an Array and returns a Hash' do
15
+ expect(@bouch.calc_assets(@bouch.pouch['Assets'])).to be_a_kind_of(Hash)
16
+ end
17
+ it 'creates array of assets greater than zero' do
18
+ # Uncomment line below for Debug
19
+ # puts @bouch.assets.inspect
20
+ expect(@bouch.assets.length).to be > 0
21
+ end
22
+ end
23
+
24
+ describe '.calc_budget_percentage' do
25
+ context 'given a budget total and salary income total' do
26
+ it 'returns a percentage of budget of a salary income' do
27
+ expect(@bouch.calc_budget_percentage(12100.00, 28808.00)).to eq(42.00)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '.calc_debts' do
33
+ it 'pushes debt values to an Array and returns a Hash' do
34
+ expect(@bouch.calc_debts(@bouch.pouch['Debts'])).to be_a_kind_of(Hash)
35
+ end
36
+ it 'creates an array of debts greater than zero' do
37
+ # Uncomment line below for Debug
38
+ # puts @bouch.debts.inspect
39
+ expect(@bouch.debts.length).to be > 0
40
+ end
41
+ end
42
+
43
+ describe '.calc_debt_ratio' do
44
+ context 'given a sum of debts and assets' do
45
+ it 'returns a Float number' do
46
+ expect(@bouch.calc_debt_ratio(420.00, 1000.00)).to be_a_kind_of(Float)
47
+ end
48
+ it 'returns a debt ratio' do
49
+ expect(@bouch.calc_debt_ratio(420.00, 1000.00)).to eq(0.42)
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '.calc_debt_ratio_percent' do
55
+ context 'given a debt ratio' do
56
+ it 'returns a Float number' do
57
+ debt_ratio = @bouch.calc_debt_ratio(420.00, 1000.00)
58
+ # Uncomment line below for Debug
59
+ # puts debt_ratio
60
+ expect(@bouch.calc_debt_ratio_percent(debt_ratio)).to be_a_kind_of(Float)
61
+ end
62
+ it 'returns a debt ratio percentage' do
63
+ debt_ratio = @bouch.calc_debt_ratio(420.00, 1000.00)
64
+ # Uncomment line below for Debug
65
+ # puts debt_ratio
66
+ expect(@bouch.calc_debt_ratio_percent(debt_ratio)).to eq(42.00)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '.calc_repeating' do
72
+ context 'given a numerical cost amount' do
73
+ it 'returns a Float number' do
74
+ expect(@bouch.calc_repeating(1000)).to be_a_kind_of(Float)
75
+ end
76
+ it 'returns a number three times that amount' do
77
+ expect(@bouch.calc_repeating(1000)).to eq(3000.00)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '.calc_salary' do
83
+ context 'given a salary amount and weekly frequency' do
84
+ it 'returns a Float number' do
85
+ expect(@bouch.calc_salary(1108.00, 2)).to be_a_kind_of(Float)
86
+ end
87
+ it 'returns an annual income total' do
88
+ expect(@bouch.calc_salary(1108.00, 2)).to eq(28808.00)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe '.calc_quarters_raw' do
94
+ context 'given a quarterly budget' do
95
+ it 'pushes quarterly budget items to a collection of Arrays and returns a Hash' do
96
+ expect(@bouch.calc_quarters_raw(@bouch.pouch['Budget'])).to be_a_kind_of(Hash)
97
+ end
98
+ it 'creates an array of financial quarter one budget items greater than zero' do
99
+ # Uncomment line below for Debug
100
+ # puts @bouch.quarters['0'].inspect
101
+ expect(@bouch.quarters['0'].length).to be > 0
102
+ end
103
+ it 'creates an array of financial quarter two budget items greater than zero' do
104
+ # Uncomment line below for Debug
105
+ # puts @bouch.quarters['1'].inspect
106
+ expect(@bouch.quarters['1'].length).to be > 0
107
+ end
108
+ it 'creates an array of financial quarter three budget items greater than zero' do
109
+ # Uncomment line below for Debug
110
+ # puts @bouch.quarters['2'].inspect
111
+ expect(@bouch.quarters['2'].length).to be > 0
112
+ end
113
+ it 'creates an array of financial quarter four budget items greater than zero' do
114
+ # Uncomment line below for Debug
115
+ # puts @bouch.quarters['3'].inspect
116
+ expect(@bouch.quarters['3'].length).to be > 0
117
+ end
118
+ end
119
+ end
120
+
121
+ describe '.calc_quarters_raw_total' do
122
+ context 'given arrays of quarterly budget items' do
123
+ it 'returns a Float number' do
124
+ expect(@bouch.calc_quarters_raw_total).to be_a_kind_of(Float)
125
+ end
126
+
127
+ it 'returns a financial quarter aggregate sum' do
128
+ expect(@bouch.calc_quarters_raw_total).to eq(12100.00)
129
+ end
130
+ end
131
+ end
132
+ end