bouch 1.2.0 → 2.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb58a99429b5afdc33cb7f65053580cf742aca9da08f2461861429ceaa75325f
4
- data.tar.gz: e15f9cab5a04bebd6ce4cd6f1aca42943ee82f18a8359911632709572559aabd
3
+ metadata.gz: 3591c24557e25403cacda4cd01cdb2885239a0f5e71908ccdc53d09fec55d229
4
+ data.tar.gz: 81ddaf8e49d0557e5c753c0c85604b35f549723a886fad30f949cac119984eec
5
5
  SHA512:
6
- metadata.gz: 0a396b50771862a7ae3187f8da5ee6bd9e903b0c2e58a02b383541412bcc131c86f1ed0b0b79633e8cc92d711aa460bc7c1e4ac1f399e7050dd26b0505e297ec
7
- data.tar.gz: b17ba90c8d8e9b4b090282c85accc8a6bcdaa3e9b0983c4b4122e9c72e01bdb2c673db304a2caeeb070a4dfebc829ea5a9bba348982703217a135ee68b6b13e5
6
+ metadata.gz: 1238cbc844c7a5767c5d0ed5c15b20592bf15e86a11018f993fa2769e9091ac987fb6af1664c4de4bbd811bd91a9ff15a2e6297a4d3bf5e9427366ab51aaeb05
7
+ data.tar.gz: 791d31224bb22952f710a874ff159cab6da456acd0a15103efe328f7cad06657703f777da1bb877dcf6fa7ba1c50e2568f2c79564bf9db8569ed81770fb9d0e1
data/.gitlab-ci.yml CHANGED
@@ -1,4 +1,6 @@
1
1
  ---
2
+ image: ruby:4.0
3
+
2
4
  rspec:
3
5
  script:
4
6
  - bundle install
@@ -8,4 +10,10 @@ rubocop:
8
10
  script:
9
11
  - bundle install
10
12
  - bundle exec rubocop
13
+
14
+ integration:
15
+ script:
16
+ - bundle install
17
+ - bundle exec bouch example > /tmp/my_budget.yml
18
+ - bundle exec bouch /tmp/my_budget.yml
11
19
  ...
data/.rubocop.yml CHANGED
@@ -3,16 +3,24 @@ AllCops:
3
3
  - 'vendor/**/*'
4
4
  - 'spec/fixtures/**/*'
5
5
  - 'tmp/**/*'
6
- TargetRubyVersion: 2.4
6
+ NewCops: disable
7
+ TargetRubyVersion: 4.0
8
+ SuggestExtensions: false
9
+
10
+ #################### Gemspec #########################
11
+
12
+ Gemspec/RequiredRubyVersion:
13
+ Enabled: false
7
14
 
8
15
  #################### Layout #############################
9
16
  Layout/IndentationWidth:
10
17
  Width: 2
11
18
 
12
- #################### Lint ###############################
13
-
14
- Lint/DeprecatedClassMethods:
15
- Enabled: false
19
+ Layout/LineLength:
20
+ Max: 140
21
+ AllowURI: true
22
+ URISchemes:
23
+ - https
16
24
 
17
25
  #################### Metrics ############################
18
26
 
@@ -31,12 +39,6 @@ Metrics/ClassLength:
31
39
  Metrics/CyclomaticComplexity:
32
40
  Enabled: false
33
41
 
34
- Metrics/LineLength:
35
- Max: 120
36
- AllowURI: true
37
- URISchemes:
38
- - https
39
-
40
42
  Metrics/MethodLength:
41
43
  Enabled: false
42
44
 
@@ -53,9 +55,6 @@ Naming/FileName:
53
55
 
54
56
  #################### Style ###########################
55
57
 
56
- Style/BracesAroundHashParameters:
57
- Enabled: false
58
-
59
58
  Style/CommentAnnotation:
60
59
  Enabled: false
61
60
 
data/.simplecov CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  SimpleCov.start do
2
4
  add_group 'Libraries', 'lib'
3
5
  add_group 'RSpec Unit Tests', 'spec'
data/Gemfile CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- ruby '>=2.4.3'
5
+ ruby '>=3.2.10'
6
6
 
7
7
  gemspec
data/Gemfile.lock CHANGED
@@ -1,48 +1,65 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bouch (1.2.0)
4
+ bouch (2.0.1)
5
+ paint (~> 2.3)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
- ast (2.4.0)
10
- diff-lcs (1.3)
11
- docile (1.3.2)
12
- json (2.3.0)
13
- parallel (1.19.1)
14
- parser (2.7.0.1)
15
- ast (~> 2.4.0)
16
- powerpack (0.1.2)
17
- rainbow (3.0.0)
18
- rake (13.0.1)
19
- rspec (3.9.0)
20
- rspec-core (~> 3.9.0)
21
- rspec-expectations (~> 3.9.0)
22
- rspec-mocks (~> 3.9.0)
23
- rspec-core (3.9.1)
24
- rspec-support (~> 3.9.1)
25
- rspec-expectations (3.9.0)
10
+ ast (2.4.3)
11
+ diff-lcs (1.6.2)
12
+ docile (1.4.1)
13
+ json (2.19.5)
14
+ language_server-protocol (3.17.0.5)
15
+ lint_roller (1.1.0)
16
+ paint (2.3.0)
17
+ parallel (2.1.0)
18
+ parser (3.3.11.1)
19
+ ast (~> 2.4.1)
20
+ racc
21
+ prism (1.9.0)
22
+ racc (1.8.1)
23
+ rainbow (3.1.1)
24
+ rake (13.4.2)
25
+ regexp_parser (2.12.0)
26
+ rspec (3.13.2)
27
+ rspec-core (~> 3.13.0)
28
+ rspec-expectations (~> 3.13.0)
29
+ rspec-mocks (~> 3.13.0)
30
+ rspec-core (3.13.6)
31
+ rspec-support (~> 3.13.0)
32
+ rspec-expectations (3.13.5)
26
33
  diff-lcs (>= 1.2.0, < 2.0)
27
- rspec-support (~> 3.9.0)
28
- rspec-mocks (3.9.0)
34
+ rspec-support (~> 3.13.0)
35
+ rspec-mocks (3.13.8)
29
36
  diff-lcs (>= 1.2.0, < 2.0)
30
- rspec-support (~> 3.9.0)
31
- rspec-support (3.9.2)
32
- rubocop (0.56.0)
33
- parallel (~> 1.10)
34
- parser (>= 2.5)
35
- powerpack (~> 0.1)
37
+ rspec-support (~> 3.13.0)
38
+ rspec-support (3.13.7)
39
+ rubocop (1.86.2)
40
+ json (~> 2.3)
41
+ language_server-protocol (~> 3.17.0.2)
42
+ lint_roller (~> 1.1.0)
43
+ parallel (>= 1.10)
44
+ parser (>= 3.3.0.2)
36
45
  rainbow (>= 2.2.2, < 4.0)
46
+ regexp_parser (>= 2.9.3, < 3.0)
47
+ rubocop-ast (>= 1.49.0, < 2.0)
37
48
  ruby-progressbar (~> 1.7)
38
- unicode-display_width (~> 1.0, >= 1.0.1)
39
- ruby-progressbar (1.10.1)
40
- simplecov (0.17.1)
49
+ unicode-display_width (>= 2.4.0, < 4.0)
50
+ rubocop-ast (1.49.1)
51
+ parser (>= 3.3.7.2)
52
+ prism (~> 1.7)
53
+ ruby-progressbar (1.13.0)
54
+ simplecov (0.22.0)
41
55
  docile (~> 1.1)
42
- json (>= 1.8, < 3)
43
- simplecov-html (~> 0.10.0)
44
- simplecov-html (0.10.2)
45
- unicode-display_width (1.6.0)
56
+ simplecov-html (~> 0.11)
57
+ simplecov_json_formatter (~> 0.1)
58
+ simplecov-html (0.13.2)
59
+ simplecov_json_formatter (0.1.4)
60
+ unicode-display_width (3.2.0)
61
+ unicode-emoji (~> 4.1)
62
+ unicode-emoji (4.2.0)
46
63
 
47
64
  PLATFORMS
48
65
  ruby
@@ -52,11 +69,11 @@ DEPENDENCIES
52
69
  bundler
53
70
  rake
54
71
  rspec
55
- rubocop (<= 0.56.0)
72
+ rubocop (~> 1.72)
56
73
  simplecov
57
74
 
58
75
  RUBY VERSION
59
- ruby 2.5.0p0
76
+ ruby 4.0.1
60
77
 
61
78
  BUNDLED WITH
62
- 1.17.3
79
+ 4.0.10
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Bouch
2
- [![Gem Version](https://badge.fury.io/rb/bouch.svg)](https://badge.fury.io/rb/bouch) [![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)
2
+ [![Gem Version](https://badge.fury.io/rb/bouch.svg)](https://badge.fury.io/rb/bouch) [![pipeline status](https://gitlab.com/ssofos/bouch/badges/master/pipeline.svg)](https://gitlab.com/ssofos/bouch/commits/master) [![ruby-forthebadge](https://forthebadge.com/badges/made-with-ruby.svg)](https://forthebadge.com/badges/made-with-ruby.svg)
3
3
 
4
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
5
 
@@ -23,35 +23,67 @@ gem 'bouch'
23
23
 
24
24
  Bouch takes a simple YAML file as its primary data input. This will be referred to as a budget pouch file.
25
25
 
26
- There is an example budget pouch file called **pouch.example.yml**.
26
+ Generate an example budget pouch file and run it:
27
27
 
28
- Use this budget pouch file and execute the following command on your favorite terminal emulator's CLI:
28
+ ```
29
+ bouch example > my_budget.yml
30
+ bouch my_budget.yml
31
+ ```
32
+
33
+ ### Generate an Example Pouch File
34
+
35
+ You can generate an example budget pouch YAML file to get started:
29
36
 
30
37
  ```
31
- bouch pouch.example.yml
38
+ bouch example > my_budget.yml
39
+ ```
40
+
41
+ Edit `my_budget.yml` with your own data, then run:
42
+
32
43
  ```
44
+ bouch my_budget.yml
45
+ ```
46
+
47
+ ### CLI Flags
48
+
49
+ ```
50
+ bouch [YAML_FILE] # Calculate and display budget
51
+ bouch example # Print an example budget pouch YAML
52
+ bouch --version / -v # Print version
53
+ bouch --help / -h # Print usage help
54
+ ```
55
+
56
+ ### Example Output
33
57
 
34
- You should see the example budget summary output:
58
+ You should see the example budget summary output (with colorized sections):
35
59
 
36
60
  ```
37
- ----------------------
61
+ ──────────────────────────────────────────
62
+ BUDGET
63
+ ──────────────────────────────────────────
38
64
  Quarter 1: 3025.00
39
65
  Quarter 2: 3025.00
40
66
  Quarter 3: 3025.00
41
67
  Quarter 4: 3025.00
42
- ----------------------
43
68
  Budget Annual Total: 12100.00
69
+ ──────────────────────────────────────────
70
+ INCOME
71
+ ──────────────────────────────────────────
44
72
  Budget Annual Income: 28808.00
45
73
  Budget Income Percent: 42.00%
46
- ----------------------
74
+ ──────────────────────────────────────────
75
+ ASSETS
76
+ ──────────────────────────────────────────
47
77
  Assets Total: 500.00
48
78
  Equity Total: 500.00
49
79
  Net Worth: 1000.00
50
- ----------------------
80
+ ──────────────────────────────────────────
81
+ DEBTS
82
+ ──────────────────────────────────────────
51
83
  Debt Total: 420.00
52
84
  Debt Ratio: 0.4200
53
85
  Debt Ratio Percent: 42.00%
54
- ----------------------
86
+ ──────────────────────────────────────────
55
87
  ```
56
88
 
57
89
  ### Pouch Schema
@@ -65,9 +97,9 @@ Budget pouch files are written in Ruby friendly YAML and currently use the follo
65
97
  * Primary hash keys:
66
98
  * **Budget**
67
99
  * **Salary**
68
- * **Assets**
69
- * **Equity**
70
- * **Debts**
100
+ * **Assets** (optional)
101
+ * **Equity** (optional)
102
+ * **Debts** (optional)
71
103
  * **Budget** nested keys and values:
72
104
  * **Q1**
73
105
  * foo
data/Rakefile CHANGED
@@ -8,3 +8,8 @@ RSpec::Core::RakeTask.new
8
8
  RuboCop::RakeTask.new
9
9
 
10
10
  task default: %i[spec rubocop]
11
+
12
+ desc 'Sign in to RubyGems.org'
13
+ task :signin do
14
+ sh 'gem signin'
15
+ end
data/bouch.gemspec CHANGED
@@ -5,7 +5,6 @@ require 'bouch/version'
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = 'bouch'
8
- s.date = '2018-06-09'
9
8
  s.version = Bouch::VERSION
10
9
  s.authors = ['Shane R. Sofos']
11
10
  s.email = ['ssofos@gmail.com']
@@ -13,21 +12,22 @@ Gem::Specification.new do |s|
13
12
  s.license = 'GPL-3.0'
14
13
  s.description = 'The Budget Pouch. Fast annual budget projections.'
15
14
  s.summary =
16
- 'A simple tool to calculate and project your ' +
17
- 'annual personal budget based on fiscal quarters ' +
15
+ 'A simple tool to calculate and project your ' \
16
+ 'annual personal budget based on fiscal quarters ' \
18
17
  'expenditures, income, assets, and debts.'
19
18
 
20
- s.files = %x(git ls-files).split($INPUT_RECORD_SEPARATOR)
21
- s.bindir = ['bin']
19
+ s.files = %x(git ls-files).split("\n")
20
+ s.bindir = 'bin'
22
21
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
- s.test_files = s.files.grep(%r{^(test|spec|features)/})
24
22
  s.require_paths = ['lib']
25
23
 
24
+ s.add_dependency 'paint', '~> 2.3'
25
+
26
26
  s.add_development_dependency 'bundler'
27
27
  s.add_development_dependency 'rake'
28
28
  s.add_development_dependency 'rspec'
29
- s.add_development_dependency 'rubocop', '<=0.56.0'
29
+ s.add_development_dependency 'rubocop', '~> 1.72'
30
30
  s.add_development_dependency 'simplecov'
31
31
 
32
- s.required_ruby_version = '>= 2.4.0'
32
+ s.required_ruby_version = '>= 3.2'
33
33
  end
data/lib/bouch/calc.rb CHANGED
@@ -4,31 +4,35 @@
4
4
  module BouchCalculate
5
5
  # Calculate asset value aggregate amount
6
6
  def calc_assets(assets, type = 'assets')
7
+ return if assets.nil?
8
+
7
9
  assets.each_value do |value|
8
10
  case type
9
11
  when 'assets'
10
- @assets.push(value)
12
+ @assets << value
11
13
  when 'equity'
12
- @equity.push(value)
14
+ @equity << value
13
15
  end
14
16
  end
15
17
  end
16
18
 
17
19
  # Calculate the percentage of budget of a salary/income
18
20
  def calc_budget_percentage(total, salary)
19
- ((total.to_f / salary.to_f) * 100).round(2)
21
+ ((total.to_f / salary) * 100).round(2)
20
22
  end
21
23
 
22
24
  # Calculate debt/liability aggregate amount
23
25
  def calc_debts(debts)
26
+ return if debts.nil?
27
+
24
28
  debts.each_value do |value|
25
- @debts.push(value)
29
+ @debts << value
26
30
  end
27
31
  end
28
32
 
29
33
  # Calculate a debt ratio: total debts divided by total assets
30
34
  def calc_debt_ratio(debts, assets)
31
- (debts.to_f / assets.to_f).round(4)
35
+ (debts.to_f / assets).round(4)
32
36
  end
33
37
 
34
38
  # Calculate a debt ratio percentage
@@ -36,7 +40,7 @@ module BouchCalculate
36
40
  (debt_ratio.to_f * 100).round(2)
37
41
  end
38
42
 
39
- # Calcuate a Net Worth sum
43
+ # Calculate a Net Worth sum
40
44
  def calc_net_worth(assets = [], equity = [])
41
45
  @net_worth = (assets + equity).sum.to_f.round(2)
42
46
  end
@@ -48,21 +52,22 @@ module BouchCalculate
48
52
 
49
53
  # Calculate an annual income based on a weekly frequency salary
50
54
  def calc_salary(amount, freq)
51
- (amount.to_f * (52 / freq)).round(2)
55
+ (amount.to_f * (52.0 / freq)).round(2)
52
56
  end
53
57
 
54
58
  # Calculate each financial quarters budget items, including repeating ones
55
59
  def calc_quarters_raw(budget)
56
60
  budget.each_value do |items|
57
- @quarters[@quarters.length.to_s] = Array.new
61
+ idx = @quarters.length
62
+ @quarters[idx] = []
58
63
  items.each_value do |value|
59
64
  case value
60
65
  when Hash
61
66
  if value.key?('repeat')
62
- @quarters[(@quarters.length - 1).to_s].push(calc_repeating(value['cost']))
67
+ @quarters[idx] << calc_repeating(value['cost'])
63
68
  end
64
69
  else
65
- @quarters[(@quarters.length - 1).to_s].push(value)
70
+ @quarters[idx] << value
66
71
  end
67
72
  end
68
73
  end
@@ -70,6 +75,6 @@ module BouchCalculate
70
75
 
71
76
  # Calculate all financial quarter budget into an aggregate annual budget
72
77
  def calc_quarters_raw_total
73
- @quarters['0'].sum + @quarters['1'].sum + @quarters['2'].sum + @quarters['3'].sum
78
+ @quarters[0].sum + @quarters[1].sum + @quarters[2].sum + @quarters[3].sum
74
79
  end
75
80
  end
data/lib/bouch/cli.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'bouch'
4
4
  require 'bouch/version'
5
+ require 'bouch/example'
6
+ require 'paint'
5
7
 
6
8
  class Bouch
7
9
  # Parse the command line
@@ -13,20 +15,33 @@ class Bouch
13
15
  end
14
16
 
15
17
  def start
16
- if @yaml_file.eql?(nil)
17
- puts 'Please supply budget pouch YAML file path and rerun bouch.'
18
+ if @yaml_file.nil? || %w[--help -h].include?(@yaml_file)
18
19
  usage
20
+ elsif %w[--version -v].include?(@yaml_file)
21
+ puts Paint["bouch #{Bouch::VERSION}", :magenta, :bold]
22
+ elsif @yaml_file == 'example'
23
+ puts Bouch::EXAMPLE_POUCH
19
24
  elsif File.exist?(@yaml_file)
20
- budget = Bouch.new(@yaml_file)
21
- budget.show_budget
25
+ begin
26
+ budget = Bouch.new(@yaml_file)
27
+ budget.show_budget
28
+ rescue ArgumentError, Psych::SyntaxError => e
29
+ puts Paint["Whoops. Failed to load the budget pouch file: #{e.message}", :red]
30
+ usage
31
+ exit 1
32
+ end
22
33
  else
23
- puts "Whoops. The budget pouch file specified: #{@yaml_file} ; does not exist!"
34
+ puts Paint["Whoops. The budget pouch file specified: #{@yaml_file} ; does not exist!", :red]
24
35
  usage
36
+ exit 1
25
37
  end
26
38
  end
27
39
 
28
40
  def usage
29
- puts "<<bouch #{Bouch::VERSION}>>\nUsage: #{File.basename($PROGRAM_NAME)} [YAML_FILE]"
41
+ puts Paint["<<bouch #{Bouch::VERSION}>>", :magenta, :bold]
42
+ puts "Usage: #{Paint[File.basename($PROGRAM_NAME), :green]} #{Paint['[YAML_FILE]', :faint]}"
43
+ puts " #{Paint[File.basename($PROGRAM_NAME), :green]} #{Paint['example', :magenta]} # Print an example budget pouch YAML"
44
+ puts " #{Paint[File.basename($PROGRAM_NAME), :green]} #{Paint['--version', :magenta]} # Print version"
30
45
  end
31
46
  end
32
47
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Bouch
4
+ EXAMPLE_POUCH = <<~YAML
5
+ ---
6
+ Budget:
7
+ Q1:
8
+ Rent:
9
+ cost: 1000
10
+ repeat: true
11
+ foo: 20
12
+ bar: 5
13
+ Q2:
14
+ Rent:
15
+ cost: 1000
16
+ repeat: true
17
+ fizz: 20
18
+ buzz: 5
19
+ Q3:
20
+ Rent:
21
+ cost: 1000
22
+ repeat: true
23
+ bubble: 20
24
+ sort: 5
25
+ Q4:
26
+ Rent:
27
+ cost: 1000
28
+ repeat: true
29
+ baz: 20
30
+ qax: 5
31
+ Salary:
32
+ quantity: 1108.00
33
+ frequency: 2
34
+ Assets:
35
+ foo: 500
36
+ Equity:
37
+ bar: 500
38
+ Debts:
39
+ baz: 120
40
+ qax: 300
41
+ ...
42
+ YAML
43
+ end
data/lib/bouch/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Bouch
4
- VERSION = '1.2.0'
4
+ VERSION = '2.0.1'
5
5
  end
data/lib/bouch.rb CHANGED
@@ -1,35 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require 'paint'
4
5
  require 'bouch/calc'
5
6
 
6
7
  # Calculate and create simple financial budgets
7
8
  # by parsing a single YAML file as input
8
9
  class Bouch
9
10
  attr_accessor :assets, :debts, :equity, :net_worth, :pouch, :quarters
11
+
10
12
  include BouchCalculate
11
13
 
14
+ SEPARATOR = Paint["\u2500" * 42, :faint]
15
+
12
16
  def initialize(file)
13
- @assets = Array.new
14
- @debts = Array.new
15
- @equity = Array.new
17
+ @assets = []
18
+ @debts = []
19
+ @equity = []
16
20
  @net_worth = nil
17
- @pouch = YAML.safe_load(IO.read(file))
18
- @quarters = Hash.new
21
+ @pouch = YAML.safe_load(File.read(file))
22
+ raise ArgumentError, 'Invalid or empty YAML file' unless @pouch.is_a?(Hash)
23
+
24
+ @quarters = {}
19
25
  end
20
26
 
21
27
  # Summarize and show aggregate asset totals
22
28
  def show_assets_total
23
29
  calc_assets(@pouch['Assets'], 'assets') if @assets.empty?
24
- puts '----------------------'
25
- puts format('%-30s %.2f', 'Assets Total:', @assets.sum)
30
+ label = Paint[format('%-30s', 'Assets Total:'), :green]
31
+ value = Paint[format('%.2f', @assets.sum), :green, :bright]
32
+ puts "#{label} #{value}"
26
33
  end
27
34
 
28
35
  # Summarize and show aggregate liabilities totals
29
36
  def show_debts_total
30
37
  calc_debts(@pouch['Debts']) if @debts.empty?
31
- puts '----------------------'
32
- puts format('%-30s %.2f', 'Debt Total:', @debts.sum)
38
+ label = Paint[format('%-30s', 'Debt Total:'), :red]
39
+ value = Paint[format('%.2f', @debts.sum), :red, :bright]
40
+ puts "#{label} #{value}"
33
41
  end
34
42
 
35
43
  # Summarize and show debt ratio
@@ -38,70 +46,97 @@ class Bouch
38
46
  calc_assets(@pouch['Equity'], 'equity') if @equity.empty?
39
47
  calc_net_worth(@assets, @equity) if @net_worth.nil?
40
48
  calc_debts(@pouch['Debts']) if @debts.empty?
41
- puts format('%-30s %.4f', 'Debt Ratio:', calc_debt_ratio(@debts.sum, @net_worth))
42
- puts format('%-30s %.2f%s', 'Debt Ratio Percent:',
43
- calc_debt_ratio_percent(calc_debt_ratio(@debts.sum, @net_worth)),
44
- '%')
45
- puts '----------------------'
49
+ ratio = calc_debt_ratio(@debts.sum, @net_worth)
50
+ label1 = Paint[format('%-30s', 'Debt Ratio:'), :red]
51
+ value1 = Paint[format('%.4f', ratio), :red, :bright]
52
+ puts "#{label1} #{value1}"
53
+ label2 = Paint[format('%-30s', 'Debt Ratio Percent:'), :red]
54
+ value2 = Paint[format('%.2f%s', calc_debt_ratio_percent(ratio), '%'), :red, :bright]
55
+ puts "#{label2} #{value2}"
46
56
  end
47
57
 
48
58
  # Summarize and show aggregate equity totals
49
59
  def show_equity_total
50
60
  calc_assets(@pouch['Equity'], 'equity') if @equity.empty?
51
- puts format('%-30s %.2f', 'Equity Total:', @equity.sum)
61
+ label = Paint[format('%-30s', 'Equity Total:'), :green]
62
+ value = Paint[format('%.2f', @equity.sum), :green, :bright]
63
+ puts "#{label} #{value}"
52
64
  end
53
65
 
54
66
  # Summarize and show Net Worth
55
67
  def show_net_worth
56
68
  calc_net_worth(@assets, @equity) if @net_worth.nil?
57
- puts format('%-30s %.2f', 'Net Worth:', @net_worth)
69
+ label = Paint[format('%-30s', 'Net Worth:'), :green]
70
+ value = Paint[format('%.2f', @net_worth), :green, :bright]
71
+ puts "#{label} #{value}"
58
72
  end
59
73
 
60
74
  # Summarize and show all financial quarter budgets
61
75
  def show_quarters
62
76
  calc_quarters_raw(@pouch['Budget']) if @quarters.empty?
63
- puts '----------------------'
77
+ puts SEPARATOR
78
+ puts Paint['BUDGET', :magenta, :bold]
79
+ puts SEPARATOR
64
80
  4.times do |n|
65
- puts format('%-30s %.2f', 'Quarter ' + (n + 1).to_s + ':', @quarters[n.to_s].sum)
81
+ label = Paint[format('%-30s', "Quarter #{n + 1}:"), :magenta]
82
+ value = Paint[format('%.2f', @quarters[n].sum), :magenta, :bright]
83
+ puts "#{label} #{value}"
66
84
  end
67
- puts '----------------------'
68
85
  end
69
86
 
70
87
  # Summarize and show the aggregate annual budget totals
71
88
  def show_annual_total
72
89
  calc_quarters_raw(@pouch['Budget']) if @quarters.empty?
73
- puts format('%-30s %.2f', 'Budget Annual Total:', calc_quarters_raw_total.to_s)
90
+ label = Paint[format('%-30s', 'Budget Annual Total:'), :magenta]
91
+ value = Paint[format('%.2f', calc_quarters_raw_total), :magenta, :bright]
92
+ puts "#{label} #{value}"
74
93
  end
75
94
 
76
95
  # Summarize and show the aggregate annual budget as a percentage of income
77
96
  def show_budget_percentage
78
97
  calc_quarters_raw(@pouch['Budget']) if @quarters.empty?
79
- puts format('%-30s %.2f%s',
80
- 'Budget Income Percent:',
81
- calc_budget_percentage(
82
- calc_quarters_raw_total,
83
- calc_salary(@pouch['Salary']['quantity'], @pouch['Salary']['frequency'])
84
- ),
85
- '%')
98
+ label = Paint[format('%-30s', 'Budget Income Percent:'), :magenta]
99
+ value = Paint[format('%.2f%s',
100
+ calc_budget_percentage(
101
+ calc_quarters_raw_total,
102
+ calc_salary(@pouch['Salary']['quantity'], @pouch['Salary']['frequency'])
103
+ ),
104
+ '%'), :magenta, :bright]
105
+ puts "#{label} #{value}"
86
106
  end
87
107
 
88
108
  # Summarize and show annual income
89
109
  def show_annual_income
90
- puts format('%-30s %.2f',
91
- 'Budget Annual Income:',
92
- calc_salary(@pouch['Salary']['quantity'], @pouch['Salary']['frequency']))
110
+ label = Paint[format('%-30s', 'Budget Annual Income:'), :magenta]
111
+ value = Paint[format('%.2f',
112
+ calc_salary(@pouch['Salary']['quantity'], @pouch['Salary']['frequency'])), :magenta, :bright]
113
+ puts "#{label} #{value}"
93
114
  end
94
115
 
95
116
  # Summarize and show all aggregate components of the quarterly, annual budgets, assets
96
117
  def show_budget
97
118
  show_quarters
98
119
  show_annual_total
120
+ puts SEPARATOR
121
+ puts Paint['INCOME', :magenta, :bold]
122
+ puts SEPARATOR
99
123
  show_annual_income
100
124
  show_budget_percentage
101
- show_assets_total
102
- show_equity_total
103
- show_net_worth
104
- show_debts_total
105
- show_debt_ratio
125
+ if @pouch['Assets'] || @pouch['Equity']
126
+ puts SEPARATOR
127
+ puts Paint['ASSETS', :green, :bold]
128
+ puts SEPARATOR
129
+ show_assets_total if @pouch['Assets']
130
+ show_equity_total if @pouch['Equity']
131
+ show_net_worth
132
+ end
133
+ if @pouch['Debts']
134
+ puts SEPARATOR
135
+ puts Paint['DEBTS', :red, :bold]
136
+ puts SEPARATOR
137
+ show_debts_total
138
+ show_debt_ratio if @net_worth
139
+ end
140
+ puts SEPARATOR
106
141
  end
107
142
  end
@@ -6,8 +6,12 @@ require 'bouch/calc'
6
6
 
7
7
  describe BouchCalculate do
8
8
  before :all do
9
- @yaml = File.realdirpath('pouch.example.yml')
10
- @bouch = Bouch.new(@yaml)
9
+ @tempfile = create_example_pouch_tempfile
10
+ @bouch = Bouch.new(@tempfile.path)
11
+ end
12
+
13
+ after :all do
14
+ @tempfile.unlink
11
15
  end
12
16
 
13
17
  describe '.calc_assets' do
@@ -15,8 +19,6 @@ describe BouchCalculate do
15
19
  expect(@bouch.calc_assets(@bouch.pouch['Assets'])).to be_a_kind_of(Hash)
16
20
  end
17
21
  it 'creates array of assets greater than zero' do
18
- # Uncomment line below for Debug
19
- # puts @bouch.assets.inspect
20
22
  expect(@bouch.assets.length).to be > 0
21
23
  end
22
24
  end
@@ -34,8 +36,6 @@ describe BouchCalculate do
34
36
  expect(@bouch.calc_debts(@bouch.pouch['Debts'])).to be_a_kind_of(Hash)
35
37
  end
36
38
  it 'creates an array of debts greater than zero' do
37
- # Uncomment line below for Debug
38
- # puts @bouch.debts.inspect
39
39
  expect(@bouch.debts.length).to be > 0
40
40
  end
41
41
  end
@@ -55,14 +55,10 @@ describe BouchCalculate do
55
55
  context 'given a debt ratio' do
56
56
  it 'returns a Float number' do
57
57
  debt_ratio = @bouch.calc_debt_ratio(420.00, 1000.00)
58
- # Uncomment line below for Debug
59
- # puts debt_ratio
60
58
  expect(@bouch.calc_debt_ratio_percent(debt_ratio)).to be_a_kind_of(Float)
61
59
  end
62
60
  it 'returns a debt ratio percentage' do
63
61
  debt_ratio = @bouch.calc_debt_ratio(420.00, 1000.00)
64
- # Uncomment line below for Debug
65
- # puts debt_ratio
66
62
  expect(@bouch.calc_debt_ratio_percent(debt_ratio)).to eq(42.00)
67
63
  end
68
64
  end
@@ -107,24 +103,16 @@ describe BouchCalculate do
107
103
  expect(@bouch.calc_quarters_raw(@bouch.pouch['Budget'])).to be_a_kind_of(Hash)
108
104
  end
109
105
  it 'creates an array of financial quarter one budget items greater than zero' do
110
- # Uncomment line below for Debug
111
- # puts @bouch.quarters['0'].inspect
112
- expect(@bouch.quarters['0'].length).to be > 0
106
+ expect(@bouch.quarters[0].length).to be > 0
113
107
  end
114
108
  it 'creates an array of financial quarter two budget items greater than zero' do
115
- # Uncomment line below for Debug
116
- # puts @bouch.quarters['1'].inspect
117
- expect(@bouch.quarters['1'].length).to be > 0
109
+ expect(@bouch.quarters[1].length).to be > 0
118
110
  end
119
111
  it 'creates an array of financial quarter three budget items greater than zero' do
120
- # Uncomment line below for Debug
121
- # puts @bouch.quarters['2'].inspect
122
- expect(@bouch.quarters['2'].length).to be > 0
112
+ expect(@bouch.quarters[2].length).to be > 0
123
113
  end
124
114
  it 'creates an array of financial quarter four budget items greater than zero' do
125
- # Uncomment line below for Debug
126
- # puts @bouch.quarters['3'].inspect
127
- expect(@bouch.quarters['3'].length).to be > 0
115
+ expect(@bouch.quarters[3].length).to be > 0
128
116
  end
129
117
  end
130
118
  end
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'stringio'
4
+ require 'yaml'
3
5
  require 'bouch/cli'
4
6
 
5
7
  describe 'Bouch::CLI' do
6
8
  before :all do
7
- @cli = Bouch::CLI.new('pouch.example.yml')
9
+ @tempfile = create_example_pouch_tempfile
10
+ @cli = Bouch::CLI.new(@tempfile.path)
11
+ end
12
+
13
+ after :all do
14
+ @tempfile.unlink
8
15
  end
9
16
 
10
17
  it 'is a Bouch::CLI object' do
@@ -25,17 +32,86 @@ describe 'Bouch::CLI' do
25
32
  end
26
33
  end
27
34
 
35
+ context 'when --help flag is supplied' do
36
+ it 'outputs usage help message' do
37
+ @cli.yaml_file = '--help'
38
+ expect { @cli.start }.to output(/Usage:/).to_stdout
39
+ end
40
+ end
41
+
42
+ context 'when -h flag is supplied' do
43
+ it 'outputs usage help message' do
44
+ @cli.yaml_file = '-h'
45
+ expect { @cli.start }.to output(/Usage:/).to_stdout
46
+ end
47
+ end
48
+
49
+ context 'when --version flag is supplied' do
50
+ it 'outputs the version' do
51
+ @cli.yaml_file = '--version'
52
+ expect { @cli.start }.to output(/bouch #{Bouch::VERSION}/).to_stdout
53
+ end
54
+ end
55
+
56
+ context 'when -v flag is supplied' do
57
+ it 'outputs the version' do
58
+ @cli.yaml_file = '-v'
59
+ expect { @cli.start }.to output(/bouch #{Bouch::VERSION}/).to_stdout
60
+ end
61
+ end
62
+
63
+ context 'when example subcommand is supplied' do
64
+ it 'outputs valid YAML with expected top-level keys' do
65
+ @cli.yaml_file = 'example'
66
+ output = capture_stdout { @cli.start }
67
+ parsed = YAML.safe_load(output)
68
+ expect(parsed).to have_key('Budget')
69
+ expect(parsed).to have_key('Salary')
70
+ expect(parsed).to have_key('Assets')
71
+ expect(parsed).to have_key('Equity')
72
+ expect(parsed).to have_key('Debts')
73
+ end
74
+ end
75
+
28
76
  context 'when an existent file is supplied as the first CLI argument' do
29
77
  it 'calculates a budget from a budget pouch file and outputs it' do
30
- @cli.yaml_file = 'pouch.example.yml'
78
+ @cli.yaml_file = @tempfile.path
31
79
  expect { @cli.start }.to output.to_stdout
32
80
  end
33
81
  end
34
82
 
35
83
  context 'when a non-existent file is supplied as the first CLI argument' do
36
- it 'ouputs a warning and usage help messsages' do
84
+ it 'outputs a warning and usage help messages and exits' do
37
85
  @cli.yaml_file = 'foobar.yml'
38
- expect { @cli.start }.to output.to_stdout
86
+ expect { expect { @cli.start }.to output.to_stdout }.to raise_error(SystemExit)
87
+ end
88
+ end
89
+
90
+ context 'when an empty YAML file is supplied' do
91
+ it 'outputs a friendly error message and exits' do
92
+ empty_yaml = Tempfile.new(['empty', '.yml'])
93
+ begin
94
+ @cli.yaml_file = empty_yaml.path
95
+ expect do
96
+ expect { @cli.start }.to output(/Whoops. Failed to load the budget pouch file: Invalid or empty YAML file/).to_stdout
97
+ end.to raise_error(SystemExit)
98
+ ensure
99
+ empty_yaml.unlink
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'when a malformed YAML file is supplied' do
105
+ it 'outputs a friendly error message and exits' do
106
+ malformed_yaml = Tempfile.new(['malformed', '.yml'])
107
+ malformed_yaml.write("Budget:\n - Q1: :")
108
+ malformed_yaml.close
109
+ begin
110
+ @cli.yaml_file = malformed_yaml.path
111
+ expect { expect { @cli.start }.to output(/Whoops. Failed to load the budget pouch file/).to_stdout }.to raise_error(SystemExit)
112
+ ensure
113
+ malformed_yaml.unlink
114
+ end
39
115
  end
40
116
  end
41
117
  end
@@ -44,5 +120,18 @@ describe 'Bouch::CLI' do
44
120
  it 'outputs a bouch usage help message' do
45
121
  expect { @cli.usage }.to output.to_stdout
46
122
  end
123
+
124
+ it 'includes example subcommand in usage' do
125
+ expect { @cli.usage }.to output(/example/).to_stdout
126
+ end
47
127
  end
48
128
  end
129
+
130
+ def capture_stdout
131
+ original_stdout = $stdout
132
+ $stdout = StringIO.new
133
+ yield
134
+ $stdout.string
135
+ ensure
136
+ $stdout = original_stdout
137
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require 'bouch/example'
4
5
 
5
6
  describe 'Pouch YAML Schema' do
6
7
  before :all do
7
- @yaml = File.realdirpath('pouch.example.yml')
8
- @pouch = YAML.safe_load(IO.read(@yaml))
8
+ @pouch = YAML.safe_load(Bouch::EXAMPLE_POUCH)
9
9
  end
10
10
 
11
11
  it 'has four major Hash keys' do
data/spec/bouch_spec.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
3
4
  require 'bouch'
4
5
 
5
6
  describe Bouch do
6
7
  before :all do
7
- @yaml = File.realdirpath('pouch.example.yml')
8
- @bouch = Bouch.new(@yaml)
8
+ @tempfile = create_example_pouch_tempfile
9
+ @bouch = Bouch.new(@tempfile.path)
10
+ end
11
+
12
+ after :all do
13
+ @tempfile.unlink
9
14
  end
10
15
 
11
16
  it 'is a Bouch object' do
@@ -38,7 +43,7 @@ describe Bouch do
38
43
 
39
44
  describe '#pouch' do
40
45
  it 'loads the budget pouch YAML file' do
41
- expect(@bouch.pouch).to eq(YAML.safe_load(IO.read(@yaml)))
46
+ expect(@bouch.pouch).to eq(YAML.safe_load(Bouch::EXAMPLE_POUCH))
42
47
  end
43
48
  end
44
49
 
@@ -49,7 +54,7 @@ describe Bouch do
49
54
  end
50
55
 
51
56
  describe '.show_assets_total' do
52
- it 'outputs financial budget assests sum' do
57
+ it 'outputs financial budget assets sum' do
53
58
  expect { @bouch.show_assets_total }.to output.to_stdout
54
59
  end
55
60
  end
@@ -107,4 +112,59 @@ describe Bouch do
107
112
  expect { @bouch.show_budget }.to output.to_stdout
108
113
  end
109
114
  end
115
+
116
+ describe 'missing YAML sections' do
117
+ before :all do
118
+ minimal = <<~YAML
119
+ ---
120
+ Budget:
121
+ Q1:
122
+ foo: 100
123
+ Q2:
124
+ bar: 200
125
+ Q3:
126
+ baz: 300
127
+ Q4:
128
+ qax: 400
129
+ Salary:
130
+ quantity: 1000
131
+ frequency: 2
132
+ ...
133
+ YAML
134
+ @minimal_yaml = Tempfile.new(['pouch', '.yml'])
135
+ @minimal_yaml.write(minimal)
136
+ @minimal_yaml.close
137
+ @minimal_bouch = Bouch.new(@minimal_yaml.path)
138
+ end
139
+
140
+ after :all do
141
+ @minimal_yaml.unlink
142
+ end
143
+
144
+ it 'does not crash when Assets, Equity, and Debts are missing' do
145
+ expect { @minimal_bouch.show_budget }.to output.to_stdout
146
+ end
147
+ end
148
+
149
+ describe 'invalid or empty YAML' do
150
+ it 'raises ArgumentError for an empty file' do
151
+ empty_yaml = Tempfile.new(['empty', '.yml'])
152
+ begin
153
+ expect { Bouch.new(empty_yaml.path) }.to raise_error(ArgumentError, /Invalid or empty YAML file/)
154
+ ensure
155
+ empty_yaml.unlink
156
+ end
157
+ end
158
+
159
+ it 'raises ArgumentError for a file that is not a Hash' do
160
+ not_a_hash = Tempfile.new(['list', '.yml'])
161
+ not_a_hash.write("- item1\n- item2")
162
+ not_a_hash.close
163
+ begin
164
+ expect { Bouch.new(not_a_hash.path) }.to raise_error(ArgumentError, /Invalid or empty YAML file/)
165
+ ensure
166
+ not_a_hash.unlink
167
+ end
168
+ end
169
+ end
110
170
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'simplecov'
4
+ require 'tempfile'
5
+ require 'bouch/example'
4
6
 
5
7
  # This file was generated by the `rspec --init` command. Conventionally, all
6
8
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
@@ -79,3 +81,11 @@ RSpec.configure do |config|
79
81
  # as the one that triggered the failure.
80
82
  Kernel.srand config.seed
81
83
  end
84
+
85
+ # Write the embedded example pouch to a tempfile for use across specs
86
+ def create_example_pouch_tempfile
87
+ tempfile = Tempfile.new(['pouch', '.yml'])
88
+ tempfile.write(Bouch::EXAMPLE_POUCH)
89
+ tempfile.close
90
+ tempfile
91
+ end
metadata CHANGED
@@ -1,16 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bouch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane R. Sofos
8
- autorequire:
9
- bindir:
10
- - bin
8
+ bindir: bin
11
9
  cert_chain: []
12
- date: 2018-06-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
13
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: paint
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.3'
14
26
  - !ruby/object:Gem::Dependency
15
27
  name: bundler
16
28
  requirement: !ruby/object:Gem::Requirement
@@ -57,16 +69,16 @@ dependencies:
57
69
  name: rubocop
58
70
  requirement: !ruby/object:Gem::Requirement
59
71
  requirements:
60
- - - "<="
72
+ - - "~>"
61
73
  - !ruby/object:Gem::Version
62
- version: 0.56.0
74
+ version: '1.72'
63
75
  type: :development
64
76
  prerelease: false
65
77
  version_requirements: !ruby/object:Gem::Requirement
66
78
  requirements:
67
- - - "<="
79
+ - - "~>"
68
80
  - !ruby/object:Gem::Version
69
- version: 0.56.0
81
+ version: '1.72'
70
82
  - !ruby/object:Gem::Dependency
71
83
  name: simplecov
72
84
  requirement: !ruby/object:Gem::Requirement
@@ -104,8 +116,8 @@ files:
104
116
  - lib/bouch.rb
105
117
  - lib/bouch/calc.rb
106
118
  - lib/bouch/cli.rb
119
+ - lib/bouch/example.rb
107
120
  - lib/bouch/version.rb
108
- - pouch.example.yml
109
121
  - spec/bouch_calc_spec.rb
110
122
  - spec/bouch_cli_spec.rb
111
123
  - spec/bouch_pouch_yaml_spec.rb
@@ -116,7 +128,6 @@ homepage: https://gitlab.com/ssofos/bouch
116
128
  licenses:
117
129
  - GPL-3.0
118
130
  metadata: {}
119
- post_install_message:
120
131
  rdoc_options: []
121
132
  require_paths:
122
133
  - lib
@@ -124,22 +135,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
135
  requirements:
125
136
  - - ">="
126
137
  - !ruby/object:Gem::Version
127
- version: 2.4.0
138
+ version: '3.2'
128
139
  required_rubygems_version: !ruby/object:Gem::Requirement
129
140
  requirements:
130
141
  - - ">="
131
142
  - !ruby/object:Gem::Version
132
143
  version: '0'
133
144
  requirements: []
134
- rubygems_version: 3.0.3
135
- signing_key:
145
+ rubygems_version: 4.0.10
136
146
  specification_version: 4
137
147
  summary: A simple tool to calculate and project your annual personal budget based
138
148
  on fiscal quarters expenditures, income, assets, and debts.
139
- test_files:
140
- - spec/bouch_calc_spec.rb
141
- - spec/bouch_cli_spec.rb
142
- - spec/bouch_pouch_yaml_spec.rb
143
- - spec/bouch_spec.rb
144
- - spec/bouch_version_spec.rb
145
- - spec/spec_helper.rb
149
+ test_files: []
data/pouch.example.yml DELETED
@@ -1,37 +0,0 @@
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
- Equity:
33
- bar: 500
34
- Debts:
35
- baz: 120
36
- qax: 300
37
- ...