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