rvgp 0.3.2

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.
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