rvgp 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +23 -0
  4. data/LICENSE +504 -0
  5. data/README.md +223 -0
  6. data/Rakefile +32 -0
  7. data/bin/rvgp +8 -0
  8. data/lib/rvgp/application/config.rb +159 -0
  9. data/lib/rvgp/application/descendant_registry.rb +122 -0
  10. data/lib/rvgp/application/status_output.rb +139 -0
  11. data/lib/rvgp/application.rb +170 -0
  12. data/lib/rvgp/base/command.rb +457 -0
  13. data/lib/rvgp/base/grid.rb +531 -0
  14. data/lib/rvgp/base/reader.rb +29 -0
  15. data/lib/rvgp/base/reconciler.rb +434 -0
  16. data/lib/rvgp/base/validation.rb +261 -0
  17. data/lib/rvgp/commands/cashflow.rb +160 -0
  18. data/lib/rvgp/commands/grid.rb +70 -0
  19. data/lib/rvgp/commands/ireconcile.rb +95 -0
  20. data/lib/rvgp/commands/new_project.rb +296 -0
  21. data/lib/rvgp/commands/plot.rb +41 -0
  22. data/lib/rvgp/commands/publish_gsheets.rb +83 -0
  23. data/lib/rvgp/commands/reconcile.rb +58 -0
  24. data/lib/rvgp/commands/rotate_year.rb +202 -0
  25. data/lib/rvgp/commands/validate_journal.rb +59 -0
  26. data/lib/rvgp/commands/validate_system.rb +44 -0
  27. data/lib/rvgp/commands.rb +160 -0
  28. data/lib/rvgp/dashboard.rb +252 -0
  29. data/lib/rvgp/fakers/fake_feed.rb +245 -0
  30. data/lib/rvgp/fakers/fake_journal.rb +57 -0
  31. data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
  32. data/lib/rvgp/fakers/faker_helpers.rb +25 -0
  33. data/lib/rvgp/gem.rb +80 -0
  34. data/lib/rvgp/journal/commodity.rb +453 -0
  35. data/lib/rvgp/journal/complex_commodity.rb +214 -0
  36. data/lib/rvgp/journal/currency.rb +101 -0
  37. data/lib/rvgp/journal/journal.rb +141 -0
  38. data/lib/rvgp/journal/posting.rb +156 -0
  39. data/lib/rvgp/journal/pricer.rb +267 -0
  40. data/lib/rvgp/journal.rb +24 -0
  41. data/lib/rvgp/plot/gnuplot.rb +478 -0
  42. data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
  43. data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
  44. data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
  45. data/lib/rvgp/plot.rb +293 -0
  46. data/lib/rvgp/pta/hledger.rb +237 -0
  47. data/lib/rvgp/pta/ledger.rb +308 -0
  48. data/lib/rvgp/pta.rb +311 -0
  49. data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
  50. data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
  51. data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
  52. data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
  53. data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
  54. data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
  55. data/lib/rvgp/utilities/grid_query.rb +190 -0
  56. data/lib/rvgp/utilities/yaml.rb +131 -0
  57. data/lib/rvgp/utilities.rb +44 -0
  58. data/lib/rvgp/validations/balance_validation.rb +68 -0
  59. data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
  60. data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
  61. data/lib/rvgp.rb +66 -0
  62. data/resources/README.MD/2022-cashflow-google.png +0 -0
  63. data/resources/README.MD/2022-cashflow.png +0 -0
  64. data/resources/README.MD/all-wealth-growth-google.png +0 -0
  65. data/resources/README.MD/all-wealth-growth.png +0 -0
  66. data/resources/gnuplot/default.yml +80 -0
  67. data/resources/i18n/en.yml +192 -0
  68. data/resources/iso-4217-currencies.json +171 -0
  69. data/resources/skel/Rakefile +5 -0
  70. data/resources/skel/app/grids/cashflow_grid.rb +27 -0
  71. data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
  72. data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
  73. data/resources/skel/app/plots/cashflow.yml +33 -0
  74. data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
  75. data/resources/skel/app/plots/wealth-growth.yml +20 -0
  76. data/resources/skel/config/csv-format-acme-checking.yml +9 -0
  77. data/resources/skel/config/google-secrets.yml +5 -0
  78. data/resources/skel/config/rvgp.yml +0 -0
  79. data/resources/skel/journals/prices.db +0 -0
  80. data/rvgp.gemspec +6 -0
  81. data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
  82. data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
  83. data/test/test_command_base.rb +61 -0
  84. data/test/test_commodity.rb +270 -0
  85. data/test/test_csv_reconciler.rb +60 -0
  86. data/test/test_currency.rb +24 -0
  87. data/test/test_fake_feed.rb +228 -0
  88. data/test/test_fake_journal.rb +98 -0
  89. data/test/test_fake_reconciler.rb +60 -0
  90. data/test/test_journal_parse.rb +545 -0
  91. data/test/test_ledger.rb +102 -0
  92. data/test/test_plot.rb +133 -0
  93. data/test/test_posting.rb +50 -0
  94. data/test/test_pricer.rb +139 -0
  95. data/test/test_pta_adapter.rb +575 -0
  96. data/test/test_utilities.rb +45 -0
  97. metadata +268 -0
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'faker_helpers'
4
+ require_relative '../journal/currency'
5
+
6
+ module RVGP
7
+ module Fakers
8
+ # Contains faker implementations that produce CSV files, for use as a data feed
9
+ class FakeFeed < Faker::Base
10
+ class << self
11
+ include FakerHelpers
12
+
13
+ # This error is thrown when there is a mismatch between two parameter arrays, passed to
14
+ # a function, whose lengths are required to match.
15
+ class ParameterLengthError < StandardError
16
+ # @!visibility private
17
+ MSG_FORMAT = 'Expected %<expected>s elements in %<parameter>s, but found %<found>s'
18
+
19
+ def initialize(parameter, expected, found)
20
+ super format(MSG_FORMAT, expected: expected, parameter: parameter, found: found)
21
+ end
22
+ end
23
+
24
+ # @!visibility private
25
+ DEFAULT_LENGTH_IN_DAYS = 365 / 4
26
+ # @!visibility private
27
+ DEFAULT_POST_COUNT = 300
28
+ # @!visibility private
29
+ FEED_COLUMNS = ['Date', 'Type', 'Description', 'Withdrawal (-)', 'Deposit (+)', 'RunningBalance'].freeze
30
+ # @!visibility private
31
+ DEFAULT_CURRENCY = RVGP::Journal::Currency.from_code_or_symbol('$')
32
+
33
+ # Generates a basic csv feed string, that resembles thos
34
+
35
+ # Generates a basic csv feed string, that resembles those offered by banking institutions
36
+ #
37
+ # @param from [Date] The date to start generated feed from
38
+ # @param to [Date] The date to end generated feed
39
+ # @param income_descriptions [Array] Strings containing the pool of available income descriptions, for use in
40
+ # random selection
41
+ # @param expense_descriptions [Array] Strings containing the pool of available expense descriptions, for use in
42
+ # random selection
43
+ # @param deposit_average [RVGP::Journal::Commodity] The average deposit amount
44
+ # @param deposit_stddev [Float] The stand deviation, on random deposits
45
+ # @param withdrawal_average [RVGP::Journal::Commodity] The average withdrawal amount
46
+ # @param withdrawal_stddev [Float] The stand deviation, on random withdrawals
47
+ # @param starting_balance [RVGP::Journal::Commodity]
48
+ # The balance of the account, before generating the transactions in the feed
49
+ # @param post_count [Numeric] The number of transactions to generate, in this csv feed
50
+ # @param deposit_ratio [Float] The odds ratio, for a given transaction, to be a deposit
51
+ # @param entries [Array] An array of Array's, that are appended to the generated entries (aka 'lines')
52
+ # @return [String] A CSV, containing the generated transactions
53
+ def basic_checking(from: ::Date.today,
54
+ to: from + DEFAULT_LENGTH_IN_DAYS,
55
+ expense_descriptions: nil,
56
+ income_descriptions: nil,
57
+ deposit_average: RVGP::Journal::Commodity.from_symbol_and_amount('$', 6000),
58
+ deposit_stddev: 500.0,
59
+ withdrawal_average: RVGP::Journal::Commodity.from_symbol_and_amount('$', 300),
60
+ withdrawal_stddev: 24.0,
61
+ post_count: DEFAULT_POST_COUNT,
62
+ starting_balance: RVGP::Journal::Commodity.from_symbol_and_amount('$', 5000),
63
+ deposit_ratio: 0.05,
64
+ entries: [])
65
+
66
+ running_balance = starting_balance.dup
67
+
68
+ entry_to_row = lambda do |entry|
69
+ FEED_COLUMNS.map do |column|
70
+ if column == 'RunningBalance'
71
+ deposit = entry['Deposit (+)']
72
+ withdrawal = entry['Withdrawal (-)']
73
+
74
+ running_balance = withdrawal.nil? ? running_balance + deposit : running_balance - withdrawal
75
+ else
76
+ entry[column]
77
+ end
78
+ end
79
+ end
80
+
81
+ # Newest to oldest:
82
+ to_csv do |csv|
83
+ dates = entries_over_date_range from, to, post_count
84
+
85
+ dates.each_with_index do |date, i|
86
+ # If there are any :entries to insert, in this date, do that now:
87
+ entries.each do |entry|
88
+ csv << entry_to_row.call(entry) if entry['Date'] <= date && (i.zero? || entry['Date'] > dates[i - 1])
89
+ end
90
+
91
+ accumulator, mean, stddev, type, description = *(
92
+ if Faker::Boolean.boolean true_ratio: deposit_ratio
93
+ [:+, deposit_average.to_f, deposit_stddev, 'ACH',
94
+ format('%s DIRECT DEP',
95
+ income_descriptions ? income_descriptions.sample : Faker::Company.name.upcase)]
96
+ else
97
+ [:-, withdrawal_average.to_f, withdrawal_stddev, 'VISA',
98
+ expense_descriptions ? expense_descriptions.sample : Faker::Company.name.upcase]
99
+ end)
100
+
101
+ amount = RVGP::Journal::Commodity.from_symbol_and_amount(
102
+ DEFAULT_CURRENCY.symbol,
103
+ Faker::Number.normal(mean: mean, standard_deviation: stddev)
104
+ )
105
+
106
+ running_balance = running_balance.send accumulator, amount
107
+
108
+ amounts = [nil, amount]
109
+ csv << ([date, type, description] + (accumulator == :- ? amounts.reverse : amounts) + [running_balance])
110
+ end
111
+
112
+ # Are there any more entries? If so, sort 'em and push them:
113
+ entries.each { |entry| csv << entry_to_row.call(entry) if entry['Date'] > dates.last }
114
+ end
115
+ end
116
+
117
+ # Generates a basic csv feed string, that resembles those offered by banking institutions. Unlike
118
+ # #basic_checking, this faker supports a set of parameters that will better conform the output to a
119
+ # typical model of commerence for an employee with a paycheck and living expenses. As such, the
120
+ # parameters are a bit different, and suited to plotting aesthetics.
121
+ #
122
+ # @param from [Date] The date to start generated feed from
123
+ # @param to [Date] The date to end generated feed
124
+ # @param income_sources [Array] Strings containing the pool of income companies, to use for growing our assets
125
+ # @param expense_sources [Array] Strings containing the pool of available expense companies, to use for
126
+ # shrinking our assets
127
+ # @param opening_liability_balance [RVGP::Journal::Commodity] The opening balance of the liability account,
128
+ # preceeding month zero
129
+ # @param opening_asset_balance [RVGP::Journal::Commodity] The opening balance of the asset account, preceeding
130
+ # month zero
131
+ # @param liability_sources [Array] Strings containing the pool of available liability sources (aka 'companies')
132
+ # @param liabilities_by_month [Array] An array of RVGP::Journal::Commodity entries, indicatiing the liability
133
+ # balance for a month with offset n, from the from date
134
+ # @param assets_by_month [Array] An array of RVGP::Journal::Commodity entries, indicating the asset
135
+ # balance for a month with offset n, from the from date
136
+ # @return [String] A CSV, containing the generated transactions
137
+ def personal_checking(from: ::Date.today,
138
+ to: from + DEFAULT_LENGTH_IN_DAYS,
139
+ expense_sources: [Faker::Company.name.tr('^a-zA-Z0-9 ', '')],
140
+ income_sources: [Faker::Company.name.tr('^a-zA-Z0-9 ', '')],
141
+ monthly_expenses: {},
142
+ opening_liability_balance: '$ 0.00'.to_commodity,
143
+ opening_asset_balance: '$ 0.00'.to_commodity,
144
+ liability_sources: [Faker::Company.name.tr('^a-zA-Z0-9 ', '')],
145
+ liabilities_by_month: months_in_range(from, to).map.with_index do |_, n|
146
+ RVGP::Journal::Commodity.from_symbol_and_amount('$', 200 + ((n + 1) * 800))
147
+ end,
148
+ assets_by_month: months_in_range(from, to).map.with_index do |_, n|
149
+ RVGP::Journal::Commodity.from_symbol_and_amount('$', 500 * ((n + 1) * 5))
150
+ end)
151
+
152
+ num_months_in_range = ((to.year * 12) + to.month) - ((from.year * 12) + from.month) + 1
153
+
154
+ ['liabilities_by_month', liabilities_by_month.length,
155
+ 'assets_by_month', assets_by_month.length].each_slice(2) do |attr, length|
156
+ raise ParameterLengthError.new(attr, num_months_in_range, length) unless num_months_in_range == length
157
+ end
158
+
159
+ monthly_expenses.each_pair do |company, expenses_by_month|
160
+ unless num_months_in_range == expenses_by_month.length
161
+ attr = format('monthly_expenses: %s', company)
162
+ raise ParameterLengthError.new(attr, num_months_in_range, expenses_by_month.length)
163
+ end
164
+ end
165
+
166
+ liability_balance = opening_liability_balance
167
+ asset_balance = opening_asset_balance
168
+
169
+ # Newest to oldest:
170
+ to_csv do |csv|
171
+ months_in_range(from, to).each_with_index do |first_of_month, i|
172
+ expected_liability = liabilities_by_month[i]
173
+
174
+ # Let's adjust the liability to suit
175
+ csv << if expected_liability > liability_balance
176
+ # We need to borrow some more money:
177
+ deposit = (expected_liability - liability_balance).abs
178
+ asset_balance += deposit
179
+ liability_balance += deposit
180
+ [first_of_month, 'ACH', liability_sources.sample, nil, deposit, asset_balance]
181
+ elsif expected_liability < liability_balance
182
+ # We want to pay off our balance:
183
+ payment = (expected_liability - liability_balance).abs
184
+ asset_balance -= payment
185
+ liability_balance -= payment
186
+ [first_of_month, 'ACH', liability_sources.sample, payment, nil, asset_balance]
187
+ end
188
+
189
+ expected_assets = assets_by_month[i]
190
+
191
+ monthly_expenses.each_pair do |company, expenses_by_month|
192
+ asset_balance -= expenses_by_month[i]
193
+ csv << [first_of_month, 'VISA', company, expenses_by_month[i], nil, asset_balance]
194
+ end
195
+
196
+ # Let's adjust the assets to suit
197
+ if expected_assets > asset_balance
198
+ # We need a paycheck:
199
+
200
+ deposit = expected_assets - asset_balance
201
+ asset_balance += deposit
202
+ csv << [first_of_month, 'ACH', income_sources.sample, nil, deposit, asset_balance]
203
+ elsif expected_assets < asset_balance
204
+ # We need to generate some expenses:
205
+ payment = asset_balance - expected_assets
206
+ asset_balance -= payment
207
+ csv << [first_of_month, 'VISA', expense_sources.sample, payment, nil, asset_balance]
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ private
214
+
215
+ def months_in_range(from, to)
216
+ ret = []
217
+ i = 0
218
+ loop do
219
+ ret << (Date.new(from.year, from.month, 1) >> i)
220
+ i += 1
221
+ break if ret.last.year == to.year && ret.last.month == to.month
222
+ end
223
+
224
+ ret
225
+ end
226
+
227
+ def to_csv(&block)
228
+ converter = lambda do |field|
229
+ case field
230
+ when Date
231
+ field.strftime('%m/%d/%Y')
232
+ when RVGP::Journal::Commodity
233
+ field.to_s(precision: DEFAULT_CURRENCY.minor_unit)
234
+ else
235
+ field
236
+ end
237
+ end
238
+
239
+ CSV.generate force_quotes: true, headers: FEED_COLUMNS, write_headers: true, write_converters: [converter],
240
+ &block
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'faker_helpers'
4
+
5
+ module RVGP
6
+ # This module encapsulate various faker objects that we use (mostly) in our test cases.
7
+ # The new_project command uses some of these classes as well.
8
+ module Fakers
9
+ # Contains faker implementation(s) that produce pta journals
10
+ class FakeJournal < Faker::Base
11
+ class << self
12
+ include FakerHelpers
13
+
14
+ # Generates a basic journal, that credits/debits from a Cash account
15
+ #
16
+ # @param from [Date] The date to start generated postings from
17
+ # @param to [Date] The date to end generated postings
18
+ # @param sum [RVGP::Journal::Commodity]
19
+ # The amount that all postings in the generated journal, will add up to
20
+ # @param post_count [Numeric] The number of postings to generate, in this journal
21
+ # @param postings [Array] An array of RVGP::Journal::Posting objects, for inclusion in the output
22
+ # @return [RVGP::Journal] A fake journal, conforming to the provided params
23
+ def basic_cash(from: ::Date.today,
24
+ to: from + 9,
25
+ sum: '$ 100.00'.to_commodity,
26
+ post_count: 10,
27
+ postings: [])
28
+
29
+ raise StandardError unless sum.is_a?(RVGP::Journal::Commodity)
30
+
31
+ amount_increment = (sum / post_count).floor sum.precision
32
+ running_sum = nil
33
+
34
+ generated_postings = entries_over_date_range(from, to, post_count).map.with_index do |date, i|
35
+ post_amount = i + 1 == post_count ? (sum - running_sum) : amount_increment
36
+
37
+ running_sum = running_sum.nil? ? post_amount : (running_sum + post_amount)
38
+
39
+ simple_posting date, post_amount
40
+ end
41
+ RVGP::Journal.new((postings + generated_postings).sort_by(&:date))
42
+ end
43
+
44
+ private
45
+
46
+ def simple_posting(date, amount)
47
+ transfers = [to_transfer('Expense', commodity: amount), to_transfer('Cash')]
48
+ RVGP::Journal::Posting.new date, Faker::Company.name, transfers: transfers
49
+ end
50
+
51
+ def to_transfer(*args)
52
+ RVGP::Journal::Posting::Transfer.new(*args)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'faker_helpers'
4
+
5
+ module RVGP
6
+ module Fakers
7
+ # Contains faker implementations that produce reconciler yamls
8
+ class FakeReconciler < Faker::Base
9
+ class << self
10
+ include FakerHelpers
11
+
12
+ # @!visibility private
13
+ DEFAULT_FORMAT = <<~FORMAT_TEMPLATE
14
+ csv_headers: true
15
+ reverse_order: true
16
+ default_currency: $
17
+ fields:
18
+ date: !!proc Date.strptime(row['Date'], '%m/%d/%Y')
19
+ amount: !!proc >
20
+ withdrawal, deposit = row[3..4].collect {|a| a.to_commodity unless a.empty?};
21
+ ( deposit ? deposit.invert! : withdrawal ).quantity_as_s
22
+ description: !!proc row['Description']
23
+ FORMAT_TEMPLATE
24
+
25
+ # @!visibility private
26
+ BASIC_CHECKING_FEED = <<~FEED_TEMPLATE
27
+ from: "%<from>s"
28
+ label: "%<label>s"
29
+ format: %<format>s
30
+ input: %<input_path>s
31
+ output: %<output_path>s
32
+ balances:
33
+ # TODO: Transcribe some expected balances, based off bank statements,
34
+ # here, in the form :
35
+ # '2023-01-04': $ 1000.00
36
+ income: %<income>s
37
+ expense: %<expense>s
38
+ FEED_TEMPLATE
39
+
40
+ # Generates a basic reconciler, for use in reconciling a basic_checking feed
41
+ # @param from [String] The from parameter to write into our yaml
42
+ # @param label [String] The label parameter to write into our yaml
43
+ # @param format_path [String] A path to the format yaml, for use in the format parameter of our yaml
44
+ # @param input_path [String] A path to the input feed, for use in the input parameter of our yaml
45
+ # @param output_path [String] A path to the output journal, for use in the output parameter of our yaml
46
+ # @param income [Array] An array of hashes, containing the income rules, to write into our yaml
47
+ # @param expense [Array] An array of hashes, containing the expense rules, to write into our yaml
48
+ # @return [String] A YAML file, containing the generated reconciler
49
+ def basic_checking(from: 'Personal:Assets:AcmeBank:Checking',
50
+ label: nil,
51
+ format_path: nil,
52
+ input_path: nil,
53
+ output_path: nil,
54
+ income: nil,
55
+ expense: nil)
56
+
57
+ raise StandardError if [from, label, input_path, output_path].any?(&:nil?)
58
+
59
+ format = "!!include #{format_path}" if format_path
60
+ format ||= format("\n%s", DEFAULT_FORMAT.gsub(/^/, ' ').chomp)
61
+
62
+ format BASIC_CHECKING_FEED,
63
+ from: from,
64
+ label: label,
65
+ format: format,
66
+ input_path: input_path,
67
+ output_path: output_path,
68
+ income: hashes_to_yaml_array(
69
+ [income, { match: '/.*/', to: 'Personal:Income:Unknown' }].flatten.compact
70
+ ),
71
+ expense: hashes_to_yaml_array(
72
+ [expense, { match: '/.*/', to: 'Personal:Expenses:Unknown' }].flatten.compact
73
+ )
74
+ end
75
+
76
+ private
77
+
78
+ def hashes_to_yaml_array(hashes)
79
+ format("\n%s", hashes.map do |hash|
80
+ hash.each_with_index.map do |pair, i|
81
+ (i.zero? ? ' - ' : ' ') + pair.join(': ')
82
+ end
83
+ end.flatten.join("\n"))
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faker'
4
+
5
+ require_relative '../journal'
6
+ require_relative '../journal/commodity'
7
+
8
+ module RVGP
9
+ module Fakers
10
+ # This module offers code that is shared by a number of our fakers. Mostly,
11
+ # this just keeps those fakers DRY.
12
+ module FakerHelpers
13
+ private
14
+
15
+ # Uniformly distribute the transactions over a date range
16
+ def entries_over_date_range(from, to, count = nil)
17
+ raise StandardError unless [from.is_a?(::Date), to.is_a?(::Date), count.is_a?(Numeric)].all?
18
+
19
+ run_length = ((to - from).to_f + 1) / (count - 1)
20
+
21
+ 1.upto(count).map { |n| n == count ? to : from + (run_length * (n - 1)) }
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/rvgp/gem.rb ADDED
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RVGP
6
+ # This class contains information relating to our Gem configuration, and is
7
+ # used to produce a gemspec.
8
+ class Gem
9
+ VERSION = '0.3.2'
10
+
11
+ # @!visibility private
12
+ GEM_DIR = File.expand_path format('%s/../..', File.dirname(__FILE__))
13
+
14
+ class << self
15
+ # Returns the gem specification
16
+ # @return [Gem::Specification]
17
+ def specification
18
+ ::Gem::Specification.new do |s|
19
+ s.name = 'rvgp'
20
+ s.version = VERSION
21
+ s.required_ruby_version = '>= 3.0.0'
22
+ s.licenses = ['LGPL-2.0']
23
+ s.authors = ['Chris DeRose']
24
+ s.email = 'chris@chrisderose.com'
25
+ s.metadata = {
26
+ 'source_code_uri' => 'https://github.com/brighton36/rvgp',
27
+ 'documentation_uri' => ['https://www.rubydoc.info/gems/rvgp', VERSION].join('/')
28
+ }
29
+
30
+ s.doc_dir 'doc'
31
+
32
+ s.summary = 'A workflow tool to: reconcile bank-downloaded csv\'s into ' \
33
+ 'categorized pta journals. Run finance validations on those ' \
34
+ 'journals. And generate csvs and plots on the output.'
35
+ s.homepage = 'https://github.com/brighton36/rvgp'
36
+
37
+ s.files = files
38
+
39
+ s.executables = ['rvgp']
40
+
41
+ s.add_development_dependency 'minitest', '~> 5.16.0'
42
+ s.add_development_dependency 'yard', '~> 0.9.34'
43
+ s.add_development_dependency 'redcarpet', '~> 3.6.0'
44
+
45
+ s.add_dependency 'open3', '~> 0.1.1'
46
+ s.add_dependency 'shellwords', '~> 0.1.0'
47
+ s.add_dependency 'google-apis-sheets_v4', '~> 0.28.0'
48
+ s.add_dependency 'faker', '~> 3.2.0'
49
+ s.add_dependency 'finance', '~> 2.0.0'
50
+ s.add_dependency 'tty-table', '~> 0.12.0'
51
+ end
52
+ end
53
+
54
+ # This is a git-less alternative to : `git ls-files`.split "\n"
55
+ # @return [Array<String>] the paths of all rvgp development files in this gem.
56
+ def files
57
+ output, exit_code = Open3.capture2(format("find %s -type f -printf '%%P\n'", root))
58
+ raise StandardError, 'find command failed' unless exit_code.success?
59
+
60
+ output.split("\n").reject do |file|
61
+ ignores = ['.git/*'] + File.read(format('%s/.gitignore', GEM_DIR)).split("\n")
62
+ ignores.any? { |glob| File.fnmatch glob, file }
63
+ end
64
+ end
65
+
66
+ # Return all ruby (code) files in this project.
67
+ # @return [Array<String>] the paths of all ruby files in this gem.
68
+ def ruby_files
69
+ files.select { |f| /\A(?:bin.*|Rakefile|.*\.rb)\Z/.match f }
70
+ end
71
+
72
+ # The directory path to the rvgp gem, as calculated from the location of this gem.rb file.
73
+ # @param [String] sub_path If provided, append this path to the output
74
+ # @return [String] The full path to the gem root, plus any subpathing, if appropriate
75
+ def root(sub_path = nil)
76
+ sub_path ? [GEM_DIR, sub_path].join('/') : GEM_DIR
77
+ end
78
+ end
79
+ end
80
+ end