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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +23 -0
- data/LICENSE +504 -0
- data/README.md +223 -0
- data/Rakefile +32 -0
- data/bin/rvgp +8 -0
- data/lib/rvgp/application/config.rb +159 -0
- data/lib/rvgp/application/descendant_registry.rb +122 -0
- data/lib/rvgp/application/status_output.rb +139 -0
- data/lib/rvgp/application.rb +170 -0
- data/lib/rvgp/base/command.rb +457 -0
- data/lib/rvgp/base/grid.rb +531 -0
- data/lib/rvgp/base/reader.rb +29 -0
- data/lib/rvgp/base/reconciler.rb +434 -0
- data/lib/rvgp/base/validation.rb +261 -0
- data/lib/rvgp/commands/cashflow.rb +160 -0
- data/lib/rvgp/commands/grid.rb +70 -0
- data/lib/rvgp/commands/ireconcile.rb +95 -0
- data/lib/rvgp/commands/new_project.rb +296 -0
- data/lib/rvgp/commands/plot.rb +41 -0
- data/lib/rvgp/commands/publish_gsheets.rb +83 -0
- data/lib/rvgp/commands/reconcile.rb +58 -0
- data/lib/rvgp/commands/rotate_year.rb +202 -0
- data/lib/rvgp/commands/validate_journal.rb +59 -0
- data/lib/rvgp/commands/validate_system.rb +44 -0
- data/lib/rvgp/commands.rb +160 -0
- data/lib/rvgp/dashboard.rb +252 -0
- data/lib/rvgp/fakers/fake_feed.rb +245 -0
- data/lib/rvgp/fakers/fake_journal.rb +57 -0
- data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
- data/lib/rvgp/fakers/faker_helpers.rb +25 -0
- data/lib/rvgp/gem.rb +80 -0
- data/lib/rvgp/journal/commodity.rb +453 -0
- data/lib/rvgp/journal/complex_commodity.rb +214 -0
- data/lib/rvgp/journal/currency.rb +101 -0
- data/lib/rvgp/journal/journal.rb +141 -0
- data/lib/rvgp/journal/posting.rb +156 -0
- data/lib/rvgp/journal/pricer.rb +267 -0
- data/lib/rvgp/journal.rb +24 -0
- data/lib/rvgp/plot/gnuplot.rb +478 -0
- data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
- data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
- data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
- data/lib/rvgp/plot.rb +293 -0
- data/lib/rvgp/pta/hledger.rb +237 -0
- data/lib/rvgp/pta/ledger.rb +308 -0
- data/lib/rvgp/pta.rb +311 -0
- data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
- data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
- data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
- data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
- data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
- data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
- data/lib/rvgp/utilities/grid_query.rb +190 -0
- data/lib/rvgp/utilities/yaml.rb +131 -0
- data/lib/rvgp/utilities.rb +44 -0
- data/lib/rvgp/validations/balance_validation.rb +68 -0
- data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
- data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
- data/lib/rvgp.rb +66 -0
- data/resources/README.MD/2022-cashflow-google.png +0 -0
- data/resources/README.MD/2022-cashflow.png +0 -0
- data/resources/README.MD/all-wealth-growth-google.png +0 -0
- data/resources/README.MD/all-wealth-growth.png +0 -0
- data/resources/gnuplot/default.yml +80 -0
- data/resources/i18n/en.yml +192 -0
- data/resources/iso-4217-currencies.json +171 -0
- data/resources/skel/Rakefile +5 -0
- data/resources/skel/app/grids/cashflow_grid.rb +27 -0
- data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
- data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
- data/resources/skel/app/plots/cashflow.yml +33 -0
- data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
- data/resources/skel/app/plots/wealth-growth.yml +20 -0
- data/resources/skel/config/csv-format-acme-checking.yml +9 -0
- data/resources/skel/config/google-secrets.yml +5 -0
- data/resources/skel/config/rvgp.yml +0 -0
- data/resources/skel/journals/prices.db +0 -0
- data/rvgp.gemspec +6 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
- data/test/test_command_base.rb +61 -0
- data/test/test_commodity.rb +270 -0
- data/test/test_csv_reconciler.rb +60 -0
- data/test/test_currency.rb +24 -0
- data/test/test_fake_feed.rb +228 -0
- data/test/test_fake_journal.rb +98 -0
- data/test/test_fake_reconciler.rb +60 -0
- data/test/test_journal_parse.rb +545 -0
- data/test/test_ledger.rb +102 -0
- data/test/test_plot.rb +133 -0
- data/test/test_posting.rb +50 -0
- data/test/test_pricer.rb +139 -0
- data/test/test_pta_adapter.rb +575 -0
- data/test/test_utilities.rb +45 -0
- metadata +268 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require_relative '../application/descendant_registry'
|
|
5
|
+
require_relative '../utilities'
|
|
6
|
+
|
|
7
|
+
module RVGP
|
|
8
|
+
# This module largely exists as a folder, in which to group Parent classes, that are used throughout the project.
|
|
9
|
+
# There's nothing else interesting happening here in this module, other than its use as as namespace.
|
|
10
|
+
module Base
|
|
11
|
+
# This is the base class implementation, for your application-defined grids. This class offers the bulk of
|
|
12
|
+
# functionality that your grids will use. The goal of a grid, is to compute csv files, in the project's build/grids
|
|
13
|
+
# directory. Sometimes, these grids will simply be an assemblage of pta queries. Other times, these grids won't
|
|
14
|
+
# involve any pta queries at all, and may instead contain projections and statistics computed elsewhere.
|
|
15
|
+
#
|
|
16
|
+
# Users are expected to inherit from this class, in their grid bulding implementations, inside ruby classes defined
|
|
17
|
+
# in your project's app/grids directory. This class offers helpers for working with pta_adapters (particularly for
|
|
18
|
+
# use with by-month queries). Additionally, this class offers code to detect and produce annual, and otherwise
|
|
19
|
+
# arbitary(see {RVGP::Base::Grid::HasMultipleSheets}) segmentation of grids.
|
|
20
|
+
#
|
|
21
|
+
# The function and purpose of grids, in your project, is as follows:
|
|
22
|
+
# - Store a state of our data in the project's build, and thus its git history.
|
|
23
|
+
# - Provide the data used by a subsequent {RVGP::Plot}.
|
|
24
|
+
# - Provide the data used by a subsequent {RVGP::Utilities::GridQuery}.
|
|
25
|
+
#
|
|
26
|
+
# Each instance of Grid, in your build is expected to represent a segment of the data. Typically this segment will
|
|
27
|
+
# be as simple as a date range (either a specific year, or 'all dates'). However, the included
|
|
28
|
+
# {RVGP::Base::Grid::HasMultipleSheets} module, allows you to add additional arbitrary segments (perhaps a segment
|
|
29
|
+
# for each value of a tag), that may be used to produce additional grids in your build, on top of the dated
|
|
30
|
+
# segments.
|
|
31
|
+
#
|
|
32
|
+
# ## Example
|
|
33
|
+
# Perhaps the easiest way to understand what this class does, is to look at one of the sample grids produced by
|
|
34
|
+
# the new_project command. Here's the contents of an app/grids/wealth_growth_grid.rb, that you can use in your
|
|
35
|
+
# projects:
|
|
36
|
+
# class WealthGrowthGrid < RVGP::Base::Grid
|
|
37
|
+
# grid 'wealth_growth', 'Generate Wealth Growth Grids', 'Wealth Growth by month (%s)',
|
|
38
|
+
# output_path_template: '%s-wealth-growth'
|
|
39
|
+
#
|
|
40
|
+
# def sheet_header
|
|
41
|
+
# %w[Date Assets Liabilities]
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# def sheet_body
|
|
45
|
+
# assets, liabilities = *%w[Assets Liabilities].map { |acct| monthly_totals acct, accrue_before_begin: true }
|
|
46
|
+
#
|
|
47
|
+
# months_through(starting_at, ending_at).map do |month|
|
|
48
|
+
# [month.strftime('%m-%y'), assets[month], liabilities[month]]
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# This WealthGrowthGrid, depending on your data, will output a series of grids in your build directory, such as the
|
|
54
|
+
# following:
|
|
55
|
+
# - build/grids/2018-wealth-growth.csv
|
|
56
|
+
# - build/grids/2019-wealth-growth.csv
|
|
57
|
+
# - build/grids/2020-wealth-growth.csv
|
|
58
|
+
# - build/grids/2021-wealth-growth.csv
|
|
59
|
+
# - build/grids/2022-wealth-growth.csv
|
|
60
|
+
# - build/grids/2023-wealth-growth.csv
|
|
61
|
+
#
|
|
62
|
+
# And, inside each of this files, will be a csv similar to:
|
|
63
|
+
# ```
|
|
64
|
+
# Date,Assets,Liabilities
|
|
65
|
+
# 01-23,89418.01,-4357.45
|
|
66
|
+
# 02-23,89708.53,-3731.10
|
|
67
|
+
# 03-23,89899.81,-3150.35
|
|
68
|
+
# 04-23,89991.36,-2616.21
|
|
69
|
+
# 05-23,89982.94,-2129.60
|
|
70
|
+
# 06-23,89874.60,-1691.37
|
|
71
|
+
# 07-23,89666.59,-1302.28
|
|
72
|
+
# 08-23,89359.43,-963.00
|
|
73
|
+
# 09-23,88953.92,-674.13
|
|
74
|
+
# 10-23,88451.01,-436.16
|
|
75
|
+
# ```
|
|
76
|
+
#
|
|
77
|
+
# @attr_reader [Date] starting_at The first day in this instance of the grid
|
|
78
|
+
# @attr_reader [Date] ending_at The last day in this instance of the grid
|
|
79
|
+
# @attr_reader [Integer] year The year segmentation, for an instance of this Grid. This value is pulled from the
|
|
80
|
+
# year of :ending_at.
|
|
81
|
+
class Grid
|
|
82
|
+
include RVGP::Application::DescendantRegistry
|
|
83
|
+
include RVGP::Pta::AvailabilityHelper
|
|
84
|
+
include RVGP::Utilities
|
|
85
|
+
|
|
86
|
+
register_descendants RVGP, :grids, accessors: {
|
|
87
|
+
task_names: lambda { |registry|
|
|
88
|
+
registry.names.map do |name|
|
|
89
|
+
RVGP.app.config.grid_years.map do |year|
|
|
90
|
+
format 'grid:%<year>d-%<name>s', year: year, name: name.tr('_', '-')
|
|
91
|
+
end
|
|
92
|
+
end.flatten
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
attr_reader :starting_at, :ending_at, :year
|
|
97
|
+
|
|
98
|
+
# Create a Grid, given the following date segment
|
|
99
|
+
# @param [Date] starting_at See {RVGP::Base::Grid#starting_at}.
|
|
100
|
+
# @param [Date] ending_at See {RVGP::Base::Grid#ending_at}.
|
|
101
|
+
def initialize(starting_at, ending_at)
|
|
102
|
+
# NOTE: It seems that with monthly queries, the ending date works a bit
|
|
103
|
+
# differently. It's not necessariy to add one to the day here. If you do,
|
|
104
|
+
# you get the whole month of January, in the next year added to the output.
|
|
105
|
+
@year = starting_at.year
|
|
106
|
+
@starting_at = starting_at
|
|
107
|
+
@ending_at = ending_at
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Write the computed grid, to its default build path
|
|
111
|
+
# @return [void]
|
|
112
|
+
def to_file!
|
|
113
|
+
write! self.class.output_path(year), to_table
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Return the computed grid, in a parsed form, before it's serialized to a string.
|
|
118
|
+
# @return [Array[Array<String>]] Each row is an array, itself composed of an array of cells.
|
|
119
|
+
def to_table
|
|
120
|
+
[sheet_header] + sheet_body
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# @!visibility public
|
|
126
|
+
# The provided args are passed to {RVGP::Pta::AvailabilityHelper#pta}'s '#register. The total amounts returned by
|
|
127
|
+
# this query are reduced by account, then month. This means that the return value is a Hash, whose keys correspond
|
|
128
|
+
# to each of the accounts that were encounted. The values for each of those keys, is itself a Hash indexed by
|
|
129
|
+
# month, whose value is the total amount returned, for that month.
|
|
130
|
+
#
|
|
131
|
+
# In addition to the options supported by pta.register, the following options are supported:
|
|
132
|
+
# - **accrue_before_begin** [Boolean] - This flag will create a pta-adapter independent query, to accrue balances
|
|
133
|
+
# before the start date of the returned set. This is useful if you want to (say) output the current year's
|
|
134
|
+
# total's, but, you want to start with the ending balance of the prior year, as opposed to '0'.
|
|
135
|
+
# - **initial** [Hash] - defaults to ({}). This is the value we begin to map values from. Typically we want to
|
|
136
|
+
# start that process from nil, this allows us to decorate the starting point.
|
|
137
|
+
# - **in_code** [String] - defaults to ('$') . This value, expected to be a commodity code, is ultimately passed
|
|
138
|
+
# to {RVGP::Pta::RegisterPosting#total_in}
|
|
139
|
+
# @param [Array<Object>] args See {RVGP::Pta::HLedger#register}, {RVGP::Pta::Ledger#register} for details
|
|
140
|
+
# @return [Hash<String,Hash<Date,RVGP::Journal::Commodity>>] all totals, indexed by month. Months are indexed by
|
|
141
|
+
# account.
|
|
142
|
+
def monthly_totals_by_account(*args)
|
|
143
|
+
reduce_monthly_by_account(*args, :total_in)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @!visibility public
|
|
147
|
+
# The provided args are passed to {RVGP::Pta::AvailabilityHelper#pta}'s #register. The amounts returned by this
|
|
148
|
+
# query are reduced by account, then month. This means that the return value is a Hash, whose keys correspond to
|
|
149
|
+
# each of the accounts that were encounted. The values for each of those keys, is itself a Hash indexed by month,
|
|
150
|
+
# whose value is the amount amount returned, for that month.
|
|
151
|
+
#
|
|
152
|
+
# In addition to the options supported by pta.register, the following options are supported:
|
|
153
|
+
# - **accrue_before_begin** [Boolean] - This flag will create a pta-adapter independent query, to accrue balances
|
|
154
|
+
# before the start date of the returned set. This is useful if you want to (say) output the current year's
|
|
155
|
+
# amount's, but, you want to start with the ending balance of the prior year, as opposed to '0'.
|
|
156
|
+
# - **initial** [Hash] - defaults to ({}). This is the value we begin to map values from. Typically we want to
|
|
157
|
+
# start that process from nil, this allows us to decorate the starting point.
|
|
158
|
+
# - **in_code** [String] - defaults to ('$') . This value, expected to be a commodity code, is ultimately passed
|
|
159
|
+
# to {RVGP::Pta::RegisterPosting#amount_in}
|
|
160
|
+
# @param [Array<Object>] args See {RVGP::Pta::HLedger#register}, {RVGP::Pta::Ledger#register} for details
|
|
161
|
+
# @return [Hash<String,Hash<Date,RVGP::Journal::Commodity>>] all amounts, indexed by month. Months are indexed by
|
|
162
|
+
# account.
|
|
163
|
+
def monthly_amounts_by_account(*args)
|
|
164
|
+
reduce_monthly_by_account(*args, :amount_in)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @!visibility public
|
|
168
|
+
# The provided args are passed to {RVGP::Pta::AvailabilityHelper#pta}'s #register. The total amounts returned by
|
|
169
|
+
# this query are reduced by month. This means that the return value is a Hash, indexed by month (in the form of a
|
|
170
|
+
# Date class) whose value is itself a Commodity, which indicates the total for that month.
|
|
171
|
+
#
|
|
172
|
+
# In addition to the options supported by pta.register, the following options are supported:
|
|
173
|
+
# - **accrue_before_begin** [Boolean] - This flag will create a pta-adapter independent query, to accrue balances
|
|
174
|
+
# before the start date of the returned set. This is useful if you want to (say) output the current year's
|
|
175
|
+
# amount's, but, you want to start with the ending balance of the prior year, as opposed to '0'.
|
|
176
|
+
# - **initial** [Hash] - defaults to ({}). This is the value we begin to map values from. Typically we want to
|
|
177
|
+
# start that process from nil, this allows us to decorate the starting point.
|
|
178
|
+
# - **in_code** [String] - defaults to ('$') . This value, expected to be a commodity code, is ultimately passed
|
|
179
|
+
# to {RVGP::Pta::RegisterPosting#total_in}
|
|
180
|
+
# @param [Array<Object>] args See {RVGP::Pta::HLedger#register}, {RVGP::Pta::Ledger#register} for details
|
|
181
|
+
# @return [Hash<Date,RVGP::Journal::Commodity>] all amounts, indexed by month. Months are indexed by
|
|
182
|
+
# account.
|
|
183
|
+
def monthly_totals(*args)
|
|
184
|
+
reduce_monthly(*args, :total_in)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @!visibility public
|
|
188
|
+
# The provided args are passed to {RVGP::Pta::AvailabilityHelper#pta}'s #register. The amounts returned by this
|
|
189
|
+
# query are reduced by month. This means that the return value is a Hash, indexed by month (in the form of a Date
|
|
190
|
+
# class) whose value is itself a Commodity, which indicates the amount for that month.
|
|
191
|
+
#
|
|
192
|
+
# In addition to the options supported by pta.register, the following options are supported:
|
|
193
|
+
# - **accrue_before_begin** [Boolean] - This flag will create a pta-adapter independent query, to accrue balances
|
|
194
|
+
# before the start date of the returned set. This is useful if you want to (say) output the current year's
|
|
195
|
+
# amount's, but, you want to start with the ending balance of the prior year, as opposed to '0'.
|
|
196
|
+
# - **initial** [Hash] - defaults to ({}). This is the value we begin to map values from. Typically we want to
|
|
197
|
+
# start that process from nil, this allows us to decorate the starting point.
|
|
198
|
+
# - **in_code** [String] - defaults to ('$') . This value, expected to be a commodity code, is ultimately passed
|
|
199
|
+
# to {RVGP::Pta::RegisterPosting#amount_in}
|
|
200
|
+
# @param [Array<Object>] args See {RVGP::Pta::HLedger#register}, {RVGP::Pta::Ledger#register} for details
|
|
201
|
+
# @return [Hash<Date,RVGP::Journal::Commodity>] all amounts, indexed by month. Months are indexed by
|
|
202
|
+
# account.
|
|
203
|
+
def monthly_amounts(*args)
|
|
204
|
+
reduce_monthly(*args, :amount_in)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def reduce_monthly_by_account(*args, posting_method)
|
|
208
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
209
|
+
|
|
210
|
+
in_code = opts[:in_code] || '$'
|
|
211
|
+
|
|
212
|
+
reduce_postings_by_month(*args, opts) do |sum, date, posting|
|
|
213
|
+
next sum if posting.account.nil? || posting.account.is_a?(Symbol)
|
|
214
|
+
|
|
215
|
+
amount_in_code = posting.send posting_method, in_code
|
|
216
|
+
if amount_in_code
|
|
217
|
+
sum[posting.account] ||= {}
|
|
218
|
+
if sum[posting.account].key? date
|
|
219
|
+
sum[posting.account][date] += amount_in_code
|
|
220
|
+
else
|
|
221
|
+
sum[posting.account][date] = amount_in_code
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
sum
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def reduce_monthly(*args, posting_method)
|
|
229
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
230
|
+
|
|
231
|
+
in_code = opts[:in_code] || '$'
|
|
232
|
+
|
|
233
|
+
opts[:ledger_opts] ||= {}
|
|
234
|
+
opts[:ledger_opts][:collapse] ||= true
|
|
235
|
+
|
|
236
|
+
opts[:hledger_args] ||= []
|
|
237
|
+
opts[:hledger_args] << 'depth:0' unless (args + opts[:hledger_args]).any? { |arg| /^depth:\d+$/.match arg }
|
|
238
|
+
|
|
239
|
+
reduce_postings_by_month(*args, opts) do |sum, date, posting|
|
|
240
|
+
amount_in_code = posting.send(posting_method, in_code)
|
|
241
|
+
if amount_in_code
|
|
242
|
+
if sum[date]
|
|
243
|
+
sum[date] += amount_in_code
|
|
244
|
+
else
|
|
245
|
+
sum[date] = amount_in_code
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
sum
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# This method keeps our grids DRY. It accrues a sum for each posting, on a
|
|
253
|
+
# monthly query
|
|
254
|
+
def reduce_postings_by_month(*args, &block)
|
|
255
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
256
|
+
|
|
257
|
+
initial = opts.delete(:initial) || {}
|
|
258
|
+
|
|
259
|
+
# TODO: I've never been crazy about this name... maybe we can borrow terminology
|
|
260
|
+
# from the hledger help, on what historical is...
|
|
261
|
+
accrue_before_begin = opts.delete :accrue_before_begin
|
|
262
|
+
|
|
263
|
+
opts.merge!({ pricer: RVGP.app.pricer,
|
|
264
|
+
monthly: true,
|
|
265
|
+
empty: false, # This applies to Ledger, and ensures it's results match HLedger's exactly
|
|
266
|
+
# TODO: I don't think I need this file: here
|
|
267
|
+
file: RVGP.app.config.project_journal_path })
|
|
268
|
+
|
|
269
|
+
opts[:hledger_opts] ||= {}
|
|
270
|
+
opts[:ledger_opts] ||= {}
|
|
271
|
+
opts[:hledger_args] ||= []
|
|
272
|
+
|
|
273
|
+
if accrue_before_begin
|
|
274
|
+
opts[:ledger_opts][:display] = format('date>=[%<starting_at>s] and date <=[%<ending_at>s]',
|
|
275
|
+
starting_at: starting_at.strftime('%Y-%m-%d'),
|
|
276
|
+
ending_at: ending_at.strftime('%Y-%m-%d'))
|
|
277
|
+
# TODO: Can we maybe use opts on this?
|
|
278
|
+
opts[:hledger_args] += [format('date:%s-', starting_at.strftime('%Y/%m/%d')),
|
|
279
|
+
format('date:-%s', ending_at.strftime('%Y/%m/%d'))]
|
|
280
|
+
opts[:hledger_opts][:historical] = true
|
|
281
|
+
else
|
|
282
|
+
# NOTE: I'm not entirely sure we want this path. It may be that we should always use the
|
|
283
|
+
# display option....
|
|
284
|
+
opts[:begin] = (opts[:begin] || starting_at).strftime('%Y-%m-%d')
|
|
285
|
+
# It seems that ledger interprets the --end parameter as :<, and hledger
|
|
286
|
+
# interprets it as :<= . So, we add one here, and, this makes the output consistent with
|
|
287
|
+
# hledger, as well as our :display syntax above.
|
|
288
|
+
opts[:ledger_opts][:end] = (ending_at + 1).strftime('%Y-%m-%d')
|
|
289
|
+
opts[:hledger_opts][:end] = ending_at.strftime('%Y-%m-%d')
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
pta.register(*args, opts).transactions.inject(initial) do |ret, tx|
|
|
293
|
+
tx.postings.reduce(ret) do |sum, posting|
|
|
294
|
+
block.call sum, tx.date, posting
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def write!(path, rows)
|
|
300
|
+
CSV.open(path, 'w') do |csv|
|
|
301
|
+
rows.each do |row|
|
|
302
|
+
csv << row.map { |val| val.is_a?(RVGP::Journal::Commodity) ? val.to_s(no_code: true, precision: 2) : val }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# @attr_reader [String] name The name of this grid. Typically, this would be an underscorized version of the class
|
|
308
|
+
# name, without the _grid suffix. This is used to compose the rake task names for each
|
|
309
|
+
# instance of this class.
|
|
310
|
+
# @attr_reader [String] description A description of this grid, for use in a rake task description.
|
|
311
|
+
# @attr_reader [String] output_path_template A string template, for use in building output files. A single '%s'
|
|
312
|
+
# formatter is expected, which, will be substituted with the year of
|
|
313
|
+
# a segment or the string 'all'.
|
|
314
|
+
class << self
|
|
315
|
+
include RVGP::Utilities
|
|
316
|
+
include RVGP::Pta::AvailabilityHelper
|
|
317
|
+
|
|
318
|
+
attr_reader :name, :description, :output_path_template
|
|
319
|
+
|
|
320
|
+
# This helper method is provided for child classes, to easily establish a definition of this grid, that
|
|
321
|
+
# can be used to produce it's instances, and their resulting output.
|
|
322
|
+
# @param [String] name See {RVGP::Base::Grid.name}.
|
|
323
|
+
# @param [String] description See {RVGP::Base::Grid.description}.
|
|
324
|
+
# @param [String] status_name_template A template to use, when composing the build status. A single '%s'
|
|
325
|
+
# formatter is expected, which, will be substituted with the year
|
|
326
|
+
# of a segment or the string 'all'.
|
|
327
|
+
# @param [String] options what options to configure this registry with
|
|
328
|
+
# @option options [String] :output_path_template See {RVGP::Base::Grid.output_path_template}.
|
|
329
|
+
# @return [void]
|
|
330
|
+
def grid(name, description, status_name_template, options = {})
|
|
331
|
+
@name = name
|
|
332
|
+
@description = description
|
|
333
|
+
@status_name_template = status_name_template
|
|
334
|
+
@output_path_template = options[:output_path_template]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# This method returns an array of paths, to the files it produces it's output from. This is used by rake
|
|
338
|
+
# to establish the freshness of our output. We assume that output is deterministic, and based on these
|
|
339
|
+
# inputs.
|
|
340
|
+
# @return [Array<String>] an array of relative paths, to our inputs.
|
|
341
|
+
def dependency_paths
|
|
342
|
+
# NOTE: This is only used right now, in the plot task. So, the cache is fine.
|
|
343
|
+
# But, if we start using this before the journals are built, we're going to
|
|
344
|
+
# need to clear this cache, thereafter. So, maybe we want to take a parameter
|
|
345
|
+
# here, or figure something out then, to prevent problems.
|
|
346
|
+
@dependency_paths ||= pta.files(file: RVGP.app.config.project_journal_path)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Whether this grid's outputs are fresh. This is determined, by examing the mtime's of our #dependency_paths.
|
|
350
|
+
# @return [TrueClass, FalseClass] true, if we're fresh, false if we're stale.
|
|
351
|
+
def uptodate?(year)
|
|
352
|
+
FileUtils.uptodate? output_path(year), dependency_paths
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Given a year, compute the output path for an instance of this grid
|
|
356
|
+
# @param [String,Integer] year The year to which this output is specific. Or, alternatively 'all'.
|
|
357
|
+
# @return [String] relative path to an output file
|
|
358
|
+
def output_path(year)
|
|
359
|
+
raise StandardError, 'Missing output_path_template' unless output_path_template
|
|
360
|
+
|
|
361
|
+
[RVGP.app.config.build_path('grids'), '/', output_path_template % year, '.csv'].join
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Given a year, compute the status label for an instance of this grid
|
|
365
|
+
# @param [String,Integer] year The year to which this status is specific. Or, alternatively 'all'.
|
|
366
|
+
# @return [String] A friendly label, constructed from the :status_name_template
|
|
367
|
+
def status_name(year)
|
|
368
|
+
@status_name_template % year
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# This module can be included into classes descending from RVGP::Base::Grid, in order to add support for multiple
|
|
373
|
+
# sheets, per year. These sheets can be declared using the provided 'has_sheets' class method, like so:
|
|
374
|
+
# ```
|
|
375
|
+
# has_sheets('cashflow') { %w(personal business) }
|
|
376
|
+
# ```
|
|
377
|
+
# This declaration will ensure the creation of "#\\{year}-cashflow-business.csv" and
|
|
378
|
+
# "#\\{year}-cashflow-personal.csv" grids, in the project's build/grids output. This is achieved by providing the
|
|
379
|
+
# sheet name as a parameter to your #sheet_header, and #sheet_body methods. (see the below example)
|
|
380
|
+
#
|
|
381
|
+
# ## Example
|
|
382
|
+
# Here's a simple example of a grid that's segmented both by year, as well as by "property". The property an
|
|
383
|
+
# expense correlates with, is determined by the value of it's property tag (should one exist).
|
|
384
|
+
# This grid will build a separate grid for every property that we've tagged expenses for, with the expenses for
|
|
385
|
+
# that tag, separated by year.
|
|
386
|
+
# ```
|
|
387
|
+
# class PropertyExpensesGrid < RVGP::Base::Grid
|
|
388
|
+
# include HasMultipleSheets
|
|
389
|
+
#
|
|
390
|
+
# grid 'expenses_by_property', 'Generate Property Expense Grids', 'Property Expenses by month (%s)'
|
|
391
|
+
#
|
|
392
|
+
# has_sheets('property-expenses') { |year| pta.tags 'property', values: true, begin: year, end: year + 1 }
|
|
393
|
+
#
|
|
394
|
+
# def sheet_header(property)
|
|
395
|
+
# ['Date'] + sheet_series(property)
|
|
396
|
+
# end
|
|
397
|
+
#
|
|
398
|
+
# def sheet_body(property)
|
|
399
|
+
# months = property_expenses(property).values.map(&:keys).flatten.uniq.sort
|
|
400
|
+
#
|
|
401
|
+
# months_through_dates(months.first, months.last).map do |month|
|
|
402
|
+
# [month.strftime('%m-%y')] + sheet_series(property).map { |col| property_expenses(property)[col][month] }
|
|
403
|
+
# end
|
|
404
|
+
# end
|
|
405
|
+
#
|
|
406
|
+
# private
|
|
407
|
+
#
|
|
408
|
+
# def sheet_series(property)
|
|
409
|
+
# property_expenses(property).keys.sort
|
|
410
|
+
# end
|
|
411
|
+
#
|
|
412
|
+
# def property_expenses(property)
|
|
413
|
+
# @property_expenses ||= {}
|
|
414
|
+
# @property_expenses[property] ||= monthly_amounts_by_account(
|
|
415
|
+
# ledger_args: [format('%%property=%s', property), 'and', 'Expense'],
|
|
416
|
+
# hledger_args: [format('tag:property=%s', property), 'Expense']
|
|
417
|
+
# )
|
|
418
|
+
# end
|
|
419
|
+
# end
|
|
420
|
+
# ```
|
|
421
|
+
#
|
|
422
|
+
# This PropertyExpensesGrid, depending on your data, will output a series of grids in your build directory, such
|
|
423
|
+
# as the following:
|
|
424
|
+
# - build/grids/2018-property-expenses-181_yurakucho.csv
|
|
425
|
+
# - build/grids/2018-property-expenses-101_0021tokyo.csv
|
|
426
|
+
# - build/grids/2019-property-expenses-181_yurakucho.csv
|
|
427
|
+
# - build/grids/2019-property-expenses-101_0021tokyo.csv
|
|
428
|
+
# - build/grids/2020-property-expenses-181_yurakucho.csv
|
|
429
|
+
# - build/grids/2020-property-expenses-101_0021tokyo.csv
|
|
430
|
+
# - build/grids/2021-property-expenses-181_yurakucho.csv
|
|
431
|
+
# - build/grids/2021-property-expenses-101_0021tokyo.csv
|
|
432
|
+
# - build/grids/2022-property-expenses-181_yurakucho.csv
|
|
433
|
+
# - build/grids/2022-property-expenses-101_0021tokyo.csv
|
|
434
|
+
# - build/grids/2023-property-expenses-181_yurakucho.csv
|
|
435
|
+
# - build/grids/2023-property-expenses-101_0021tokyo.csv
|
|
436
|
+
#
|
|
437
|
+
# And, inside each of this files, will be a csv similar to:
|
|
438
|
+
# ```
|
|
439
|
+
# Date,Business:Expenses:Banking:Interest:181Yurakucho,Business:Expenses:Home:Improvement:181Yurakucho[...]
|
|
440
|
+
# 01-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
441
|
+
# 02-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
442
|
+
# 03-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
443
|
+
# 04-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
444
|
+
# 05-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
445
|
+
# 06-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
446
|
+
# 07-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
447
|
+
# 07-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
448
|
+
# 08-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
449
|
+
# 09-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
450
|
+
# 10-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
451
|
+
# 11-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
452
|
+
# 12-23,123.45,678.90,123.45,678.90,123.45,678.90,123.45,678.90,123.45
|
|
453
|
+
# ```
|
|
454
|
+
module HasMultipleSheets
|
|
455
|
+
# Return the computed grid, in a parsed form, before it's serialized to a string.
|
|
456
|
+
# @return [Array[Array<String>]] Each row is an array, itself composed of an array of cells.
|
|
457
|
+
def to_table(sheet)
|
|
458
|
+
[sheet_header(sheet)] + sheet_body(sheet)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Write the computed grid, to its default build path
|
|
462
|
+
# @return [void]
|
|
463
|
+
def to_file!
|
|
464
|
+
self.class.sheets(year).each do |sheet|
|
|
465
|
+
write! self.class.output_path(year, sheet.to_s.downcase), to_table(sheet)
|
|
466
|
+
end
|
|
467
|
+
nil
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# see (RVGP::Base::Grid::HasMultipleSheets.sheets)
|
|
471
|
+
def sheets(year)
|
|
472
|
+
self.class.sheets year
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# @!visibility private
|
|
476
|
+
def self.included(klass)
|
|
477
|
+
klass.extend ClassMethods
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# This module contains the Class methods, that are automatically included,
|
|
481
|
+
# at the time RVGP::Base::Grid::HasMultipleSheets is included into a class.
|
|
482
|
+
module ClassMethods
|
|
483
|
+
# Define what additional sheets, this Grid will handle.
|
|
484
|
+
# @param [String] sheet_output_prefix This is used in constructing the output file, and is expected to be
|
|
485
|
+
# a friendly name, describing the container, under which our multiple
|
|
486
|
+
# sheets exist.
|
|
487
|
+
# @yield [year] Return the sheets, that are available in the given year
|
|
488
|
+
# @yieldparam [Integer] year The year being queried.
|
|
489
|
+
# @yieldreturn [Array<String>] The sheets (aka grids) that we can generate for this year
|
|
490
|
+
# @return [void]
|
|
491
|
+
def has_sheets(sheet_output_prefix, &block) # rubocop:disable Naming/PredicateName
|
|
492
|
+
@has_sheets = block
|
|
493
|
+
@sheet_output_prefix = sheet_output_prefix
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Returns the sheets that are available for the given year. This is calculated using the block provided in
|
|
497
|
+
# #has_sheets
|
|
498
|
+
# @param [Integer] year The year being queried.
|
|
499
|
+
# @return [Array<String>] What sheets (aka grids) are available this year
|
|
500
|
+
def sheets(year)
|
|
501
|
+
@sheets ||= {}
|
|
502
|
+
@sheets[year] ||= @has_sheets.call year
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Returns the sheet_output_prefix, that was set in #has_sheets
|
|
506
|
+
# @return [String] The label for our multiple sheet taxonomy
|
|
507
|
+
def sheet_output_prefix
|
|
508
|
+
@sheet_output_prefix
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# @!visibility private
|
|
512
|
+
def output_path(year, sheet)
|
|
513
|
+
format '%<path>s/%<year>s-%<prefix>s-%<sheet>s.csv',
|
|
514
|
+
path: RVGP.app.config.build_path('grids'),
|
|
515
|
+
year: year,
|
|
516
|
+
prefix: sheet_output_prefix,
|
|
517
|
+
sheet: sheet.to_s.downcase
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Whether this grid's outputs are fresh. This is determined, by examing the mtime's of our #dependency_paths.
|
|
521
|
+
# @return [TrueClass, FalseClass] true, if we're fresh, false if we're stale.
|
|
522
|
+
def uptodate?(year)
|
|
523
|
+
sheets(year).all? do |sheet|
|
|
524
|
+
FileUtils.uptodate? output_path(year, sheet), dependency_paths
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RVGP
|
|
4
|
+
module Base
|
|
5
|
+
# This base class exists as a shorthand, for classes whose public readers are populated via
|
|
6
|
+
# #initialize(). Think of this as an lighter-weight alternative to OpenStruct.
|
|
7
|
+
# @attr_reader [Hash<Symbol,Object>] options This instance's reader attributes, and their values, as a Hash
|
|
8
|
+
class Reader
|
|
9
|
+
# Classes which inherit from this class, can declare their attr_reader in a shorthand format, by way of
|
|
10
|
+
# this method. Attributes declared in this method, will be able to be set in the options passed to
|
|
11
|
+
# their #initialize
|
|
12
|
+
# @param readers [Array<Symbol>] A list of the attr_readers, this class will provide
|
|
13
|
+
def self.readers(*readers)
|
|
14
|
+
attr_reader(*readers)
|
|
15
|
+
attr_reader :options
|
|
16
|
+
|
|
17
|
+
define_method :initialize do |*args|
|
|
18
|
+
readers.each_with_index do |r, i|
|
|
19
|
+
instance_variable_set format('@%s', r).to_sym, args[i]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# If there are more arguments than attr's the last argument is an options
|
|
23
|
+
# hash
|
|
24
|
+
instance_variable_set '@options', args[readers.length].is_a?(Hash) ? args[readers.length] : {}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|