rvgp 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/rvgp/plot.rb
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'plot/gnuplot'
|
4
|
+
|
5
|
+
module RVGP
|
6
|
+
# This class assembles grids into a series of plots, given a plot specification
|
7
|
+
# yaml. Once a grid is assembled, it's dispatched to a driver ({GoogleDrive} or {Gnuplot})
|
8
|
+
# for rendering.
|
9
|
+
# Here's an example plot specification, included in the default project build as
|
10
|
+
# {https://github.com/brighton36/rvgp/blob/main/resources/skel/app/plots/wealth-growth.yml wealth-growth.yml}, as
|
11
|
+
# created by the new_project command:
|
12
|
+
# ```
|
13
|
+
# title: "Wealth Growth (%{year})"
|
14
|
+
# glob: "%{year}-wealth-growth.csv"
|
15
|
+
# grid_hacks:
|
16
|
+
# store_cell: !!proc >
|
17
|
+
# (cell) ? cell.to_f.abs : nil
|
18
|
+
# google:
|
19
|
+
# chart_type: area
|
20
|
+
# axis:
|
21
|
+
# left: "Amount"
|
22
|
+
# bottom: "Date"
|
23
|
+
# gnuplot:
|
24
|
+
# chart_type: area
|
25
|
+
# domain: monthly
|
26
|
+
# axis:
|
27
|
+
# left: "Amount"
|
28
|
+
# bottom: "Date"
|
29
|
+
# additional_lines: |+
|
30
|
+
# set xtics scale 0 rotate by 45 offset -1.4,-1.4
|
31
|
+
# set key title ' '
|
32
|
+
# set style fill transparent solid 0.7 border
|
33
|
+
# ```
|
34
|
+
#
|
35
|
+
# The yaml file is required to have :title and :glob parameters. Additionally,
|
36
|
+
# the following parameter groups are supported: :grid_hacks, :gnuplot, and :google.
|
37
|
+
#
|
38
|
+
# The :gnuplot section of this file, is merged with the contents of
|
39
|
+
# {https://github.com/brighton36/rvgp/blob/main/resources/gnuplot/default.yml default.yml}, and passed to the {RVGP::Plot::Gnuplot}
|
40
|
+
# constructor. See the {RVGP::Plot::Gnuplot::Plot#initialize} method for more details on what parameters are supported
|
41
|
+
# in this section. NOTE: Depending on the kind of chart being specified, some initialize options are specific to the
|
42
|
+
# chart being built, and those options will be documented in the constructor for that specific chart. ie:
|
43
|
+
# {RVGP::Plot::Gnuplot::AreaChart#initialize} or {RVGP::Plot::Gnuplot::ColumnAndLineChart#initialize}.
|
44
|
+
#
|
45
|
+
# The :google section of this file, is provided to {RVGP::Plot::GoogleDrive::Sheet#initialize}. See this method for
|
46
|
+
# details on supported options.
|
47
|
+
#
|
48
|
+
# The :grid_hacks section of this file, contains miscellaneous hacks to the dataset. These include: :keystone,
|
49
|
+
# :store_cell, :select_rows, :sort_rows_by, :sort_columns_by, :truncate_rows, :switch_rows_columns, and
|
50
|
+
# :truncate_columns. These options are documented in: {RVGP::Utilities::GridQuery#initialize} and
|
51
|
+
# {RVGP::Utilities::GridQuery#to_grid}.
|
52
|
+
#
|
53
|
+
# @attr_reader [String] path A path to the location of the input yaml, as provided to #initialize
|
54
|
+
# @attr_reader [RVGP::Utilities::Yaml] yaml The yaml object, containing the parameters of this plot
|
55
|
+
# @attr_reader [String] glob A string containing wildcards, used to match input grids in the filesystem. This
|
56
|
+
# parameter is expected to be found inside the yaml[:glob], and will generally look
|
57
|
+
# something like: "%\\{year}-wealth-growth.csv" or
|
58
|
+
# "%\\{year}-property-incomes-%\\{property}.csv".
|
59
|
+
# The variables which are supported, include 'the year' of a plot, as well as whatever
|
60
|
+
# variables are defined in a plot's glob_variants ('property', as was the case
|
61
|
+
# above.) glob_variants are output by grids, and detected in the filenames those grids
|
62
|
+
# produce by the {RVGP::Plot.glob_variants} method.
|
63
|
+
class Plot
|
64
|
+
attr_reader :path, :yaml, :glob
|
65
|
+
|
66
|
+
# The required keys, expected to exist in the plot yaml
|
67
|
+
REQUIRED_FIELDS = %i[glob title].freeze
|
68
|
+
|
69
|
+
# The path to rvgp's 'default' include file search path. Any '!!include' directives encountered in the plot yaml,
|
70
|
+
# will search this location for targets.
|
71
|
+
GNUPLOT_RESOURCES_PATH = [RVGP::Gem.root, '/resources/gnuplot'].join
|
72
|
+
|
73
|
+
# This exception is raised when a provided yaml file, is missing required
|
74
|
+
# attributes.
|
75
|
+
class MissingYamlAttribute < StandardError
|
76
|
+
# @!visibility private
|
77
|
+
MSG_FORMAT = 'Missing one or more required fields in %<path>s: %<fields>s'
|
78
|
+
|
79
|
+
def initialize(path, fields)
|
80
|
+
super format(MSG_FORMAT, path: path, fields: fields)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# This exception is raised when a provided yaml file, stipulates an invalid
|
85
|
+
# {glob} attribute
|
86
|
+
class InvalidYamlGlob < StandardError
|
87
|
+
# @!visibility private
|
88
|
+
MSG_FORMAT = 'Plot file %<path>s is missing a required \'year\' parameter in glob'
|
89
|
+
|
90
|
+
def initialize(path)
|
91
|
+
super format(MSG_FORMAT, path: path)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Create a plot, from a specification yaml
|
96
|
+
# @param [String] path The path to a specification yaml
|
97
|
+
def initialize(path)
|
98
|
+
@path = path
|
99
|
+
|
100
|
+
@yaml = RVGP::Utilities::Yaml.new path, [RVGP.app.config.project_path, GNUPLOT_RESOURCES_PATH]
|
101
|
+
|
102
|
+
missing_attrs = REQUIRED_FIELDS.reject { |f| yaml.key? f }
|
103
|
+
raise MissingYamlAttribute, yaml.path, missing_attrs unless missing_attrs.empty?
|
104
|
+
|
105
|
+
@glob = yaml[:glob] if yaml.key? :glob
|
106
|
+
raise InvalidYamlGlob, yaml.path unless /%\{year\}/.match glob
|
107
|
+
|
108
|
+
grids_corpus = Dir[RVGP.app.config.build_path('grids/*')]
|
109
|
+
|
110
|
+
@variants ||= self.class.glob_variants(glob, grids_corpus) +
|
111
|
+
self.class.glob_variants(glob, grids_corpus, year: 'all')
|
112
|
+
|
113
|
+
@title = yaml[:title] if yaml.key? :title
|
114
|
+
end
|
115
|
+
|
116
|
+
# In the case that a name is provided, limit the return to the variant of the provided :name.
|
117
|
+
# If no name is provided, all variants in this plot are returned. Variants are determined
|
118
|
+
# by the yaml parameter :glob, as applied to the grids found in the build/grids/* path.
|
119
|
+
# @param [String] name (nil) Limit the return to this variant, if set
|
120
|
+
# @return [Hash<Symbol, Object>] The hash will return name, :pairs, and :files keys,
|
121
|
+
# that contain the variant details.
|
122
|
+
def variants(name = nil)
|
123
|
+
name ? @variants.find { |v| v[:name] == name } : @variants
|
124
|
+
end
|
125
|
+
|
126
|
+
# This method returns only the :files parameter, of the {#variants} return.
|
127
|
+
# @param [String] variant_name (nil) Limit the return to this variant, if set
|
128
|
+
# @return [Array<String>] An array of grid paths
|
129
|
+
def variant_files(variant_name)
|
130
|
+
variants(variant_name)[:files]
|
131
|
+
end
|
132
|
+
|
133
|
+
# The plot title, of the given variant
|
134
|
+
# @param [String] variant_name The :name of the variant you're looking to title
|
135
|
+
# @return [String] The title of the plot
|
136
|
+
def title(variant_name)
|
137
|
+
@title % variants(variant_name)[:pairs]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Generate an output file path, for the given variant. Typically this is a .csv grid, in the build/grids
|
141
|
+
# subdirectory of your project folder.
|
142
|
+
# @param [String] name The :name of the variant you're looking for
|
143
|
+
# @param [String] ext The file extension you wish to append, to the return
|
144
|
+
# @return [String] The path to the output file, of the plot
|
145
|
+
def output_file(name, ext)
|
146
|
+
RVGP.app.config.build_path format('plots/%<name>s.%<ext>s', name: name, ext: ext)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Generate and return, a plot grid, for the given variant.
|
150
|
+
# @param [String] variant_name The :name of the variant you're looking for
|
151
|
+
# @return [Array<Array<Object>>] The grid, as an array of arrays.
|
152
|
+
def grid(variant_name)
|
153
|
+
@grid ||= {}
|
154
|
+
@grid[variant_name] ||= begin
|
155
|
+
gopts = {}
|
156
|
+
rvopts = {
|
157
|
+
store_cell: if grid_hacks.key?(:store_cell)
|
158
|
+
->(cell) { grid_hacks[:store_cell].call cell: cell }
|
159
|
+
else
|
160
|
+
->(cell) { cell ? cell.to_f : nil }
|
161
|
+
end
|
162
|
+
}
|
163
|
+
|
164
|
+
# Grid Reader Options:
|
165
|
+
rvopts[:keystone] = grid_hacks[:keystone] if grid_hacks.key? :keystone
|
166
|
+
|
167
|
+
if grid_hacks.key? :select_rows
|
168
|
+
rvopts[:select_rows] = ->(name, data) { grid_hacks[:select_rows].call name: name, data: data }
|
169
|
+
end
|
170
|
+
|
171
|
+
# to_grid Options
|
172
|
+
gopts[:truncate_rows] = grid_hacks[:truncate_rows].to_i if grid_hacks.key? :truncate_rows
|
173
|
+
gopts[:truncate_columns] = grid_hacks[:truncate_columns].to_i if grid_hacks.key? :truncate_columns
|
174
|
+
gopts[:switch_rows_columns] = grid_hacks[:switch_rows_columns] if grid_hacks.key? :switch_rows_columns
|
175
|
+
gopts[:sort_rows_by] = ->(row) { grid_hacks[:sort_rows_by].call row: row } if grid_hacks.key? :sort_rows_by
|
176
|
+
|
177
|
+
if grid_hacks.key? :sort_columns_by
|
178
|
+
gopts[:sort_cols_by] = ->(column) { grid_hacks[:sort_columns_by].call column: column }
|
179
|
+
end
|
180
|
+
|
181
|
+
RVGP::Utilities::GridQuery.new(variant_files(variant_name), rvopts).to_grid(gopts)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Return the column titles, on the plot for a given variant
|
186
|
+
# @param [String] variant_name The :name of the variant you're looking for
|
187
|
+
# @return [Array<String>] An array of strings, representing the column titles
|
188
|
+
def column_titles(variant_name)
|
189
|
+
grid(variant_name)[0]
|
190
|
+
end
|
191
|
+
|
192
|
+
# Return the portion of the grid, containing series labels, and their data.
|
193
|
+
# @param [String] variant_name The :name of the variant you're looking for
|
194
|
+
# @return [Array<Array<Object>>] The portion of the grid, that contains series data
|
195
|
+
def series(variant_name)
|
196
|
+
grid(variant_name)[1..]
|
197
|
+
end
|
198
|
+
|
199
|
+
# Return the google plot options, from the yaml of this plot.
|
200
|
+
# @return [Hash] The contents of the google: section of this plot's yml
|
201
|
+
def google_options
|
202
|
+
@google_options = yaml[:google] if yaml.key? :google
|
203
|
+
end
|
204
|
+
|
205
|
+
# Return the gnuplot object, for a given variant
|
206
|
+
# @param [String] name The :name of the variant you're looking for
|
207
|
+
# @return [RVGP::Plot::Gnuplot::Plot] The gnuplot
|
208
|
+
def gnuplot(name)
|
209
|
+
@gnuplots ||= {}
|
210
|
+
@gnuplots[name] ||= RVGP::Plot::Gnuplot::Plot.new title(name), grid(name), gnuplot_options
|
211
|
+
end
|
212
|
+
|
213
|
+
# Return the rendered gnuplot code, for a given variant
|
214
|
+
# @param [String] name The :name of the variant you're looking for
|
215
|
+
# @return [String] The gnuplot code that represents this variant
|
216
|
+
def script(name)
|
217
|
+
gnuplot(name).script
|
218
|
+
end
|
219
|
+
|
220
|
+
# Execute the rendered gnuplot code, for a given variant. Typically, this opens a gnuplot
|
221
|
+
# window.
|
222
|
+
# @param [String] name The :name of the variant you're looking for
|
223
|
+
# @return [void]
|
224
|
+
def show(name)
|
225
|
+
gnuplot(name).execute!
|
226
|
+
end
|
227
|
+
|
228
|
+
# Write the gnuplot code, for a given variant, to the :output_file
|
229
|
+
# @param [String] name The :name of the variant you're looking for
|
230
|
+
# @return [void]
|
231
|
+
def write!(name)
|
232
|
+
File.write output_file(name, 'gpi'), gnuplot(name).script
|
233
|
+
end
|
234
|
+
|
235
|
+
# This returns what plot variants are possible, given a glob, when matched against the
|
236
|
+
# provided file names.
|
237
|
+
# If pair_values contains key: value combinations, then, any of the returned
|
238
|
+
# variants will be sorted under the key:value provided . (Its really just meant
|
239
|
+
# for year: 'all', atm..)
|
240
|
+
# @param [String] glob A string that matches 'variables' in the form of \\{variablename} specifiers
|
241
|
+
# @param [Array<String>] corpus An array of file paths. The paths are matched against the glob, and
|
242
|
+
# separated based on the variables found, in their names.
|
243
|
+
# @return [Array<Hash<Symbol,Object>>] An array of Hashes, containing :name, :pairs, and :files components
|
244
|
+
def self.glob_variants(glob, corpus, pair_values = {})
|
245
|
+
variant_names = glob.scan(/%\{([^ }]+)/).flatten.map(&:to_sym)
|
246
|
+
|
247
|
+
glob_vars = variant_names.to_h { |key| [key, '(.+)'] }
|
248
|
+
variant_matcher = Regexp.new format(glob, glob_vars)
|
249
|
+
|
250
|
+
corpus.each_with_object([]) do |file, ret|
|
251
|
+
matches = variant_matcher.match File.basename(file)
|
252
|
+
|
253
|
+
if matches
|
254
|
+
pairs = variant_names.map.with_index do |key, i|
|
255
|
+
[key, pair_values.key?(key.to_sym) ? pair_values[key] : matches[i + 1]]
|
256
|
+
end.to_h
|
257
|
+
|
258
|
+
pair_i = ret.find_index { |variant| variant[:pairs] == pairs }
|
259
|
+
if pair_i
|
260
|
+
ret[pair_i][:files] << file
|
261
|
+
else
|
262
|
+
ret << { name: File.basename(glob % pairs, '.*'),
|
263
|
+
pairs: pairs,
|
264
|
+
files: [file] }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
ret
|
269
|
+
end.compact
|
270
|
+
end
|
271
|
+
|
272
|
+
# Return all the plot objects, initialized from the yaml files in the plot_directory_path
|
273
|
+
# @param [String] plot_directory_path A path to search, for (plot) yml files
|
274
|
+
# @return [Array<RVGP::Plot>] An array of the plots, available in the provided directory
|
275
|
+
def self.all(plot_directory_path)
|
276
|
+
Dir.glob(format('%s/*.yml', plot_directory_path)).map { |path| new path }
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
def grid_hacks
|
282
|
+
@grid_hacks = yaml.key?(:grid_hacks) ? yaml[:grid_hacks] : {}
|
283
|
+
end
|
284
|
+
|
285
|
+
def gnuplot_options
|
286
|
+
@gnuplot_options ||= begin
|
287
|
+
gnuplot_options = yaml[:gnuplot] || {}
|
288
|
+
template = RVGP::Utilities::Yaml.new(format('%s/default.yml', GNUPLOT_RESOURCES_PATH))
|
289
|
+
gnuplot_options.merge(template: template)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require_relative '../journal/pricer'
|
7
|
+
require_relative '../pta'
|
8
|
+
|
9
|
+
module RVGP
|
10
|
+
class Pta
|
11
|
+
# A plain text accounting adapter implementation, for the 'ledger' pta command.
|
12
|
+
# This class conforms the ledger query, and output, interfaces in a ruby-like
|
13
|
+
# syntax, and with structured ruby objects as outputs.
|
14
|
+
#
|
15
|
+
# For a more detailed example of these queries in action, take a look at the
|
16
|
+
# {https://github.com/brighton36/rra/blob/main/test/test_pta_adapter.rb test/test_pta_adapter.rb}
|
17
|
+
class HLedger < RVGP::Pta
|
18
|
+
# @!visibility private
|
19
|
+
BIN_PATH = '/usr/bin/hledger'
|
20
|
+
|
21
|
+
# This module contains intermediary parsing objects, used to represent the output of hledger in
|
22
|
+
# a structured and hierarchial format.
|
23
|
+
module Output
|
24
|
+
# This is a base class from which RVGP::Pta::HLedger's outputs inherit. This class mostly just provides
|
25
|
+
# helpers for dealing with the json output that hledger produces.
|
26
|
+
# @attr_reader [Json] json a parsed representation of the output from hledger
|
27
|
+
# @attr_reader [RVGP::Journal::Pricer] pricer A price exchanger, to use for any currency exchange lookups
|
28
|
+
class JsonBase
|
29
|
+
attr_reader :json, :pricer
|
30
|
+
|
31
|
+
# Declare the class, and initialize with the relevant options
|
32
|
+
# @param [String] json The json that was produced by hledger, to construct this object
|
33
|
+
# @param [Hash] options Additional options
|
34
|
+
# @option options [RVGP::Journal::Pricer] :pricer see {RVGP::Pta::Ledger::Output::XmlBase#pricer}
|
35
|
+
def initialize(json, options)
|
36
|
+
@pricer = options[:pricer] || RVGP::Journal::Pricer.new
|
37
|
+
@json = JSON.parse json, symbolize_names: true
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def commodity_from_json(json)
|
43
|
+
symbol = json[:acommodity]
|
44
|
+
raise RVGP::Pta::AssertionError unless json.key? :aquantity
|
45
|
+
|
46
|
+
currency = RVGP::Journal::Currency.from_code_or_symbol symbol
|
47
|
+
# TODO: It seems like HLedger defaults to 10 digits. Probably
|
48
|
+
# we should shrink these numbers down to the currency specifier...
|
49
|
+
RVGP::Journal::Commodity.new symbol,
|
50
|
+
currency ? currency.alphabetic_code : symbol,
|
51
|
+
json[:aquantity][:decimalMantissa],
|
52
|
+
json[:aquantity][:decimalPlaces]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# An json parser, to structure the output of balance queries to hledger. This object exists, as
|
57
|
+
# a return value, from the {RVGP::Pta::HLedger#balance} method
|
58
|
+
# @attr_reader [RVGP::Pta::BalanceAccount] accounts The accounts, and their components, that were
|
59
|
+
# returned by hledger.
|
60
|
+
# @attr_reader [Array<RVGP::Journal::Commodity>] summary_amounts The sum amounts, at the end of the account
|
61
|
+
# output.
|
62
|
+
class Balance < JsonBase
|
63
|
+
attr_reader :accounts, :summary_amounts
|
64
|
+
|
65
|
+
# Declare the registry, and initialize with the relevant options
|
66
|
+
# @param [String] json see {RVGP::Pta::HLedger::Output::JsonBase#initialize}
|
67
|
+
# @param [Hash] options see {RVGP::Pta::HLedger::Output::JsonBase#initialize}
|
68
|
+
def initialize(json, options = {})
|
69
|
+
super json, options
|
70
|
+
|
71
|
+
raise RVGP::Pta::AssertionError unless @json.length == 2
|
72
|
+
|
73
|
+
@accounts = @json[0].collect do |json_account|
|
74
|
+
# I'm not sure why there are two identical entries here, for fullname
|
75
|
+
raise RVGP::Pta::AssertionError unless json_account[0] == json_account[1]
|
76
|
+
|
77
|
+
RVGP::Pta::BalanceAccount.new(json_account[0],
|
78
|
+
json_account[3].collect { |l| commodity_from_json l })
|
79
|
+
end
|
80
|
+
|
81
|
+
@summary_amounts = @json[1].collect { |json_amount| commodity_from_json json_amount }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# An json parser, to structure the output of register queries to ledger. This object exists, as
|
86
|
+
# a return value, from the {RVGP::Pta::HLedger#register} method
|
87
|
+
# @attr_reader [RVGP::Pta::RegisterTransaction] transactions The transactions, and their components, that were
|
88
|
+
# returned by ledger.
|
89
|
+
class Register < JsonBase
|
90
|
+
attr_reader :transactions
|
91
|
+
|
92
|
+
# Declare the registry, and initialize with the relevant options
|
93
|
+
# @param [String] json see {RVGP::Pta::HLedger::Output::JsonBase#initialize}
|
94
|
+
# @param [Hash] options see {RVGP::Pta::HLedger::Output::JsonBase#initialize}
|
95
|
+
def initialize(json, options = {})
|
96
|
+
super json, options
|
97
|
+
|
98
|
+
@transactions = @json.each_with_object([]) do |row, sum|
|
99
|
+
row[0] ? (sum << [row]) : (sum.last << row)
|
100
|
+
end
|
101
|
+
|
102
|
+
@transactions.map! do |postings|
|
103
|
+
date = Date.strptime postings[0][0], '%Y-%m-%d'
|
104
|
+
|
105
|
+
RVGP::Pta::RegisterTransaction.new(
|
106
|
+
date,
|
107
|
+
postings[0][2], # Payee
|
108
|
+
(postings.map do |posting|
|
109
|
+
amounts, totals = [posting[3][:pamount], posting[4]].map do |pamounts|
|
110
|
+
pamounts.map { |pamount| commodity_from_json pamount }
|
111
|
+
end
|
112
|
+
|
113
|
+
RVGP::Pta::RegisterPosting.new(
|
114
|
+
posting[3][:paccount],
|
115
|
+
amounts,
|
116
|
+
totals,
|
117
|
+
posting[3][:ptags].to_h do |pair|
|
118
|
+
[pair.first, pair.last.empty? ? true : pair.last]
|
119
|
+
end,
|
120
|
+
pricer: pricer,
|
121
|
+
# This sets our date to the end -of the month, if this is a
|
122
|
+
# monthly query
|
123
|
+
price_date: options[:monthly] ? Date.new(date.year, date.month, -1) : date
|
124
|
+
)
|
125
|
+
end)
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return the tags that were found, given the specified journal path, and filters.
|
133
|
+
#
|
134
|
+
# The behavior between hledger and ledger are rather different here. Ledger has a slightly different
|
135
|
+
# featureset than HLedger, regarding tags. As such, while the return format is the same between implementations.
|
136
|
+
# The results for a given query won't be identical between pta implementations. Mostly, these results differ
|
137
|
+
# when a \\{values: true} option is supplied. In that case, ledger will return tags in a series of keys and
|
138
|
+
# values, separated by a colon, one per line. hledger, in that case, will only return the tag values themselves,
|
139
|
+
# without denotating their key.
|
140
|
+
#
|
141
|
+
# This method will simply parse the output of hledger, and return that.
|
142
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
143
|
+
# details
|
144
|
+
# @return [Array<String>] An array of the lines returned by hledger, split into strings. In most cases, this
|
145
|
+
# could also be described as simply 'an array of the filtered tags'.
|
146
|
+
def tags(*args)
|
147
|
+
args, opts = args_and_opts(*args)
|
148
|
+
command('tags', *args, opts).split("\n")
|
149
|
+
end
|
150
|
+
|
151
|
+
# Return the files that were encountered, when parsing the provided arguments.
|
152
|
+
# The output of this method should be identical, regardless of the Pta Adapter that resolves the request.
|
153
|
+
#
|
154
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
155
|
+
# details
|
156
|
+
# @return [Array<String>] An array of paths that were referenced when fetching data in provided arguments.
|
157
|
+
def files(*args)
|
158
|
+
args, opts = args_and_opts(*args)
|
159
|
+
# TODO: This should get its own error class...
|
160
|
+
raise StandardError, "Unexpected argument(s) : #{args.inspect}" unless args.empty?
|
161
|
+
|
162
|
+
command('files', opts).split("\n")
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns the newest transaction, retured in set of transactions filtered with the provided arguments.
|
166
|
+
# This method is mostly a wrapper around {#register}, which a return of the .last element in its set.
|
167
|
+
# The only reason this method here is to ensure parity with the {RVGP::Pta::Ledger} class, which, exists
|
168
|
+
# because an accelerated query is offered by that pta implementation. This method may produce
|
169
|
+
# counterintutive results, if you override the sort: option.
|
170
|
+
#
|
171
|
+
# NOTE: For almost any case you think you want to use this method, {#newest_transaction_date} is probably
|
172
|
+
# what you want, as that function has an accelerated implementation provided by hledger.
|
173
|
+
#
|
174
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
175
|
+
# details.
|
176
|
+
# @return [RVGP::Pta::RegisterTransaction] The newest transaction in the set
|
177
|
+
def newest_transaction(*args)
|
178
|
+
register(*args)&.transactions&.last
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns the oldest transaction, retured in set of transactions filtered with the provided arguments.
|
182
|
+
# This method is mostly a wrapper around {RVGP::Pta::HLedger#register}, which a return of the .last element in its
|
183
|
+
# set. The only reason this method here is to ensure parity with the {RVGP::Pta::Ledger} class, which, exists
|
184
|
+
# because an accelerated query is offered by that pta implementation. This method may produce
|
185
|
+
# counterintutive results, if you override the sort: option.
|
186
|
+
#
|
187
|
+
# NOTE: There is almost certainly, no a good reason to be using this method. Perhaps in the future,
|
188
|
+
# hledger will offer an equivalent to ledger's --head and --tail options, at which time this method would
|
189
|
+
# make sense.
|
190
|
+
#
|
191
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
192
|
+
# details.
|
193
|
+
# @return [RVGP::Pta::RegisterTransaction] The oldest transaction in the set
|
194
|
+
def oldest_transaction(*args)
|
195
|
+
register(*args)&.transactions&.first
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns the value of the 'Last transaction' key, of the #{RVGP::Pta#stats} method. This method is a fast query
|
199
|
+
# to resolve.
|
200
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
201
|
+
# details.
|
202
|
+
# @return [Date] The date of the newest transaction found in your files.
|
203
|
+
def newest_transaction_date(*args)
|
204
|
+
Date.strptime stats(*args)['Last transaction'], '%Y-%m-%d'
|
205
|
+
end
|
206
|
+
|
207
|
+
# Run the 'hledger balance' command, and return it's output.
|
208
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
209
|
+
# details.
|
210
|
+
# @return [RVGP::Pta::HLedger::Output::Balance] A parsed, hierarchial, representation of the output
|
211
|
+
def balance(*args)
|
212
|
+
args, opts = args_and_opts(*args)
|
213
|
+
RVGP::Pta::HLedger::Output::Balance.new command('balance', *args, { 'output-format': 'json' }.merge(opts))
|
214
|
+
end
|
215
|
+
|
216
|
+
# Run the 'hledger register' command, and return it's output.
|
217
|
+
#
|
218
|
+
# This method also supports the following options, for additional handling:
|
219
|
+
# - **:pricer** (RVGP::Journal::Pricer) - If provided, this option will use the specified pricer object when
|
220
|
+
# calculating exchange rates.
|
221
|
+
#
|
222
|
+
# @param [Array<Object>] args Arguments and options, passed to the pta command. See {RVGP::Pta#args_and_opts} for
|
223
|
+
# details.
|
224
|
+
# @return [RVGP::Pta::HLedger::Output::Register] A parsed, hierarchial, representation of the output
|
225
|
+
def register(*args)
|
226
|
+
args, opts = args_and_opts(*args)
|
227
|
+
|
228
|
+
pricer = opts.delete :pricer
|
229
|
+
|
230
|
+
# TODO: Provide and Test translate_meta_accounts here
|
231
|
+
RVGP::Pta::HLedger::Output::Register.new command('register', *args, { 'output-format': 'json' }.merge(opts)),
|
232
|
+
monthly: (opts[:monthly] == true),
|
233
|
+
pricer: pricer
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|