rvgp 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +23 -0
  4. data/LICENSE +504 -0
  5. data/README.md +223 -0
  6. data/Rakefile +32 -0
  7. data/bin/rvgp +8 -0
  8. data/lib/rvgp/application/config.rb +159 -0
  9. data/lib/rvgp/application/descendant_registry.rb +122 -0
  10. data/lib/rvgp/application/status_output.rb +139 -0
  11. data/lib/rvgp/application.rb +170 -0
  12. data/lib/rvgp/base/command.rb +457 -0
  13. data/lib/rvgp/base/grid.rb +531 -0
  14. data/lib/rvgp/base/reader.rb +29 -0
  15. data/lib/rvgp/base/reconciler.rb +434 -0
  16. data/lib/rvgp/base/validation.rb +261 -0
  17. data/lib/rvgp/commands/cashflow.rb +160 -0
  18. data/lib/rvgp/commands/grid.rb +70 -0
  19. data/lib/rvgp/commands/ireconcile.rb +95 -0
  20. data/lib/rvgp/commands/new_project.rb +296 -0
  21. data/lib/rvgp/commands/plot.rb +41 -0
  22. data/lib/rvgp/commands/publish_gsheets.rb +83 -0
  23. data/lib/rvgp/commands/reconcile.rb +58 -0
  24. data/lib/rvgp/commands/rotate_year.rb +202 -0
  25. data/lib/rvgp/commands/validate_journal.rb +59 -0
  26. data/lib/rvgp/commands/validate_system.rb +44 -0
  27. data/lib/rvgp/commands.rb +160 -0
  28. data/lib/rvgp/dashboard.rb +252 -0
  29. data/lib/rvgp/fakers/fake_feed.rb +245 -0
  30. data/lib/rvgp/fakers/fake_journal.rb +57 -0
  31. data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
  32. data/lib/rvgp/fakers/faker_helpers.rb +25 -0
  33. data/lib/rvgp/gem.rb +80 -0
  34. data/lib/rvgp/journal/commodity.rb +453 -0
  35. data/lib/rvgp/journal/complex_commodity.rb +214 -0
  36. data/lib/rvgp/journal/currency.rb +101 -0
  37. data/lib/rvgp/journal/journal.rb +141 -0
  38. data/lib/rvgp/journal/posting.rb +156 -0
  39. data/lib/rvgp/journal/pricer.rb +267 -0
  40. data/lib/rvgp/journal.rb +24 -0
  41. data/lib/rvgp/plot/gnuplot.rb +478 -0
  42. data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
  43. data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
  44. data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
  45. data/lib/rvgp/plot.rb +293 -0
  46. data/lib/rvgp/pta/hledger.rb +237 -0
  47. data/lib/rvgp/pta/ledger.rb +308 -0
  48. data/lib/rvgp/pta.rb +311 -0
  49. data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
  50. data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
  51. data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
  52. data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
  53. data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
  54. data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
  55. data/lib/rvgp/utilities/grid_query.rb +190 -0
  56. data/lib/rvgp/utilities/yaml.rb +131 -0
  57. data/lib/rvgp/utilities.rb +44 -0
  58. data/lib/rvgp/validations/balance_validation.rb +68 -0
  59. data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
  60. data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
  61. data/lib/rvgp.rb +66 -0
  62. data/resources/README.MD/2022-cashflow-google.png +0 -0
  63. data/resources/README.MD/2022-cashflow.png +0 -0
  64. data/resources/README.MD/all-wealth-growth-google.png +0 -0
  65. data/resources/README.MD/all-wealth-growth.png +0 -0
  66. data/resources/gnuplot/default.yml +80 -0
  67. data/resources/i18n/en.yml +192 -0
  68. data/resources/iso-4217-currencies.json +171 -0
  69. data/resources/skel/Rakefile +5 -0
  70. data/resources/skel/app/grids/cashflow_grid.rb +27 -0
  71. data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
  72. data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
  73. data/resources/skel/app/plots/cashflow.yml +33 -0
  74. data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
  75. data/resources/skel/app/plots/wealth-growth.yml +20 -0
  76. data/resources/skel/config/csv-format-acme-checking.yml +9 -0
  77. data/resources/skel/config/google-secrets.yml +5 -0
  78. data/resources/skel/config/rvgp.yml +0 -0
  79. data/resources/skel/journals/prices.db +0 -0
  80. data/rvgp.gemspec +6 -0
  81. data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
  82. data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
  83. data/test/test_command_base.rb +61 -0
  84. data/test/test_commodity.rb +270 -0
  85. data/test/test_csv_reconciler.rb +60 -0
  86. data/test/test_currency.rb +24 -0
  87. data/test/test_fake_feed.rb +228 -0
  88. data/test/test_fake_journal.rb +98 -0
  89. data/test/test_fake_reconciler.rb +60 -0
  90. data/test/test_journal_parse.rb +545 -0
  91. data/test/test_ledger.rb +102 -0
  92. data/test/test_plot.rb +133 -0
  93. data/test/test_posting.rb +50 -0
  94. data/test/test_pricer.rb +139 -0
  95. data/test/test_pta_adapter.rb +575 -0
  96. data/test/test_utilities.rb +45 -0
  97. metadata +268 -0
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