hledger-forecast 0.2.1 → 0.3.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -2
  3. data/README.md +150 -71
  4. data/example.journal +35 -0
  5. data/example.yml +35 -36
  6. data/hledger-forecast.gemspec +5 -4
  7. data/lib/hledger_forecast/cli.rb +35 -93
  8. data/lib/hledger_forecast/generator.rb +233 -90
  9. data/lib/hledger_forecast/summarize.rb +4 -5
  10. data/lib/hledger_forecast/tracker.rb +37 -0
  11. data/lib/hledger_forecast/version.rb +1 -1
  12. data/lib/hledger_forecast.rb +1 -1
  13. data/spec/command_spec.rb +14 -4
  14. data/spec/custom_spec.rb +35 -42
  15. data/spec/half-yearly_spec.rb +22 -6
  16. data/spec/modifier_spec.rb +87 -0
  17. data/spec/monthly_end_date_spec.rb +47 -0
  18. data/spec/monthly_end_date_transaction_spec.rb +32 -0
  19. data/spec/monthly_spec.rb +35 -27
  20. data/spec/once_spec.rb +22 -7
  21. data/spec/quarterly_spec.rb +21 -7
  22. data/spec/stubs/{monthly/forecast_monthly.yml → forecast.yml} +7 -7
  23. data/spec/stubs/transactions_found.journal +24 -0
  24. data/spec/stubs/transactions_found_inverse.journal +24 -0
  25. data/spec/stubs/transactions_not_found.journal +16 -0
  26. data/spec/track_spec.rb +197 -0
  27. data/spec/yearly_spec.rb +21 -7
  28. metadata +32 -74
  29. data/spec/start_date_spec.rb +0 -12
  30. data/spec/stubs/custom/forecast_custom_days.yml +0 -14
  31. data/spec/stubs/custom/forecast_custom_months.yml +0 -14
  32. data/spec/stubs/custom/forecast_custom_weeks.yml +0 -14
  33. data/spec/stubs/custom/forecast_custom_weeks_twice.yml +0 -24
  34. data/spec/stubs/custom/output_custom_days.journal +0 -24
  35. data/spec/stubs/custom/output_custom_months.journal +0 -20
  36. data/spec/stubs/custom/output_custom_weeks.journal +0 -28
  37. data/spec/stubs/custom/output_custom_weeks_twice.journal +0 -44
  38. data/spec/stubs/half-yearly/forecast_half-yearly.yml +0 -10
  39. data/spec/stubs/half-yearly/output_half-yearly.journal +0 -20
  40. data/spec/stubs/modifiers/forecast_modifiers.yml +0 -13
  41. data/spec/stubs/modifiers/output_modifiers.journal +0 -44
  42. data/spec/stubs/monthly/forecast_monthly_enddate.yml +0 -14
  43. data/spec/stubs/monthly/forecast_monthly_enddate_top.yml +0 -14
  44. data/spec/stubs/monthly/forecast_monthly_modifier.yml +0 -11
  45. data/spec/stubs/monthly/output_monthly.journal +0 -44
  46. data/spec/stubs/monthly/output_monthly_enddate.journal +0 -48
  47. data/spec/stubs/monthly/output_monthly_enddate_top.journal +0 -40
  48. data/spec/stubs/monthly/output_monthly_modifier.journal +0 -20
  49. data/spec/stubs/once/forecast_once.yml +0 -10
  50. data/spec/stubs/once/output_once.journal +0 -12
  51. data/spec/stubs/quarterly/forecast_quarterly.yml +0 -10
  52. data/spec/stubs/quarterly/output_quarterly.journal +0 -20
  53. data/spec/stubs/start_date/forecast_startdate.yml +0 -26
  54. data/spec/stubs/start_date/output_startdate.journal +0 -56
  55. data/spec/stubs/transactions.journal +0 -8
  56. data/spec/stubs/yearly/forecast_yearly.yml +0 -10
  57. data/spec/stubs/yearly/output_yearly.journal +0 -16
@@ -2,27 +2,26 @@ module HledgerForecast
2
2
  # The Command Line Interface for the application
3
3
  # Takes user arguments and translates them into actions
4
4
  class Cli
5
-
6
- def self.run(subcommand, options)
7
- case subcommand
5
+ def self.run(command, options)
6
+ case command
8
7
  when 'generate'
9
8
  generate(options)
10
9
  when 'summarize'
11
10
  summarize(options)
12
11
  else
13
- puts "Unknown command: #{subcommand}"
12
+ puts "Unknown command: #{command}"
14
13
  exit(1)
15
14
  end
16
15
  end
17
16
 
18
17
  def self.parse_commands(args = ARGV, _stdin = $stdin)
19
- subcommand = nil
18
+ command = nil
20
19
  options = {}
21
20
 
22
21
  global = OptionParser.new do |opts|
23
- opts.banner = "Usage: hledger-forecast [subcommand] [options]"
22
+ opts.banner = "Usage: hledger-forecast [command] [options]"
24
23
  opts.separator ""
25
- opts.separator "Subcommands:"
24
+ opts.separator "Commands:"
26
25
  opts.separator " generate Generate the forecast file"
27
26
  opts.separator " summarize Summarize the forecast file and output to the terminal"
28
27
  opts.separator ""
@@ -33,7 +32,7 @@ module HledgerForecast
33
32
  exit
34
33
  end
35
34
 
36
- opts.on_tail("-v", "--version", "Show version") do
35
+ opts.on_tail("-v", "--version", "Show the installed version") do
37
36
  puts VERSION
38
37
  exit
39
38
  end
@@ -41,63 +40,58 @@ module HledgerForecast
41
40
 
42
41
  begin
43
42
  global.order!(args)
44
- subcommand = args.shift || 'generate'
43
+ command = args.shift || 'generate'
45
44
  rescue OptionParser::InvalidOption => e
46
45
  puts e
47
46
  puts global
48
47
  exit(1)
49
48
  end
50
49
 
51
- case subcommand
50
+ case command
52
51
  when 'generate'
53
52
  options = parse_generate_options(args)
54
53
  when 'summarize'
55
54
  options = parse_summarize_options(args)
56
55
  else
57
- puts "Unknown subcommand: #{subcommand}"
56
+ puts "Unknown command: #{command}"
58
57
  puts global
59
58
  exit(1)
60
59
  end
61
60
 
62
- return subcommand, options
61
+ return command, options
63
62
  end
64
63
 
65
64
  def self.parse_generate_options(args)
66
65
  options = {}
67
66
 
68
67
  OptionParser.new do |opts|
69
- opts.banner = "Usage: Hledger-Forecast generate [options]"
68
+ opts.banner = "Usage: hledger-forecast generate [options]"
70
69
  opts.separator ""
71
70
 
72
- opts.on("-t", "--transaction FILE",
73
- "The base TRANSACTIONS file to extend from") do |file|
74
- options[:transactions_file] = file if file && !file.empty?
75
- end
76
-
77
71
  opts.on("-f", "--forecast FILE",
78
- "The FORECAST yaml file to generate from") do |file|
72
+ "The path to the FORECAST yaml file to generate from") do |file|
79
73
  options[:forecast_file] = file
80
74
  options[:output_file] ||= file.sub(/\.yml$/, '.journal')
81
75
  end
82
76
 
83
77
  opts.on("-o", "--output-file FILE",
84
- "The OUTPUT file to create") do |file|
78
+ "The path to the OUTPUT file to create") do |file|
85
79
  options[:output_file] = file
86
80
  end
87
81
 
88
- opts.on("-s", "--start-date DATE",
89
- "The date to start generating from (yyyy-mm-dd)") do |a|
90
- options[:start_date] = a
82
+ opts.on("-t", "--transaction FILE",
83
+ "The path to the TRANSACTION journal file") do |file|
84
+ options[:transaction_file] = file
91
85
  end
92
86
 
93
- opts.on("-e", "--end-date DATE",
94
- "The date to start generating to (yyyy-mm-dd)") do |a|
95
- options[:end_date] = a
87
+ opts.on("--force",
88
+ "Force an overwrite of the output file") do
89
+ options[:force] = true
96
90
  end
97
91
 
98
- opts.on("--force",
99
- "Force an overwrite of the output file") do |a|
100
- options[:force] = a
92
+ opts.on("--no-track",
93
+ "Don't track any transactions") do
94
+ options[:no_track] = true
101
95
  end
102
96
 
103
97
  opts.on_tail("-h", "--help", "Show this help message") do
@@ -109,18 +103,6 @@ module HledgerForecast
109
103
  options[:forecast_file] = "forecast.yml" unless options[:forecast_file]
110
104
  options[:output_file] = "forecast.journal" unless options[:output_file]
111
105
 
112
- today = Date.today
113
-
114
- unless options[:start_date]
115
- options[:default_dates] = true
116
- options[:start_date] =
117
- Date.new(today.year, today.month, 1).next_month.to_s
118
- end
119
- unless options[:end_date]
120
- options[:default_dates] = true
121
- options[:end_date] = Date.new(today.year + 3, 12, 31).to_s
122
- end
123
-
124
106
  options
125
107
  end
126
108
 
@@ -128,11 +110,11 @@ module HledgerForecast
128
110
  options = {}
129
111
 
130
112
  OptionParser.new do |opts|
131
- opts.banner = "Usage: Hledger-Forecast summarize [options]"
113
+ opts.banner = "Usage: hledger-forecast summarize [options]"
132
114
  opts.separator ""
133
115
 
134
116
  opts.on("-f", "--forecast FILE",
135
- "The FORECAST yaml file to summarize") do |file|
117
+ "The path to the FORECAST yaml file to summarize") do |file|
136
118
  options[:forecast_file] = file
137
119
  end
138
120
 
@@ -146,30 +128,29 @@ module HledgerForecast
146
128
  end
147
129
 
148
130
  def self.generate(options)
149
- end_date = options[:end_date]
150
- start_date = options[:start_date]
151
131
  forecast = File.read(options[:forecast_file])
152
- transactions = options[:transactions_file] ? File.read(options[:transactions_file]) : nil
153
-
154
- # Generate the forecast
155
- puts "[Using default dates: #{start_date} to #{end_date}]" if options[:default_dates]
156
132
 
157
- transactions = Generator.generate(transactions, forecast, start_date, end_date)
133
+ begin
134
+ transactions = Generator.generate(forecast, options)
135
+ rescue StandardError => e
136
+ puts "An error occurred while generating transactions: #{e.message}"
137
+ exit(1)
138
+ end
158
139
 
159
140
  output_file = options[:output_file]
160
141
  if File.exist?(output_file) && !options[:force]
161
- print "File '#{output_file}' already exists. Overwrite? (y/n): "
142
+ print "\nFile '#{output_file}' already exists. Overwrite? (y/n): "
162
143
  overwrite = gets.chomp.downcase
163
144
 
164
145
  if overwrite == 'y'
165
146
  File.write(output_file, transactions)
166
- puts "File '#{output_file}' has been overwritten."
147
+ puts "\nSuccess: ".bold.green + "File '#{output_file}' has been overwritten."
167
148
  else
168
- puts "Operation aborted. File '#{output_file}' was not overwritten."
149
+ puts "\nInfo: ".bold.blue + "Operation aborted. File '#{output_file}' was not overwritten."
169
150
  end
170
151
  else
171
152
  File.write(output_file, transactions)
172
- puts "File '#{output_file}' has been created."
153
+ puts "\nSuccess: ".bold.green + "File '#{output_file}' has been created"
173
154
  end
174
155
  end
175
156
 
@@ -177,44 +158,5 @@ module HledgerForecast
177
158
  forecast = File.read(options[:forecast_file])
178
159
  puts Summarize.generate(forecast)
179
160
  end
180
-
181
- # def self.run(args)
182
- # end_date = args[:end_date]
183
- # start_date = args[:start_date]
184
- # forecast = File.read(args[:forecast_file])
185
- # transactions = args[:transactions_file] ? File.read(args[:transactions_file]) : nil
186
- #
187
- # # Output the summary
188
- # return HledgerForecast::Summarize.generate(forecast) if args[:summarize]
189
- #
190
- # # Generate the forecast
191
- # unless args[:skip]
192
- #
193
- # puts "[Using default dates: #{start_date} to #{end_date}]" if args[:default_dates]
194
- #
195
- # transactions = Generator.generate(transactions, forecast, start_date, end_date)
196
- #
197
- # output_file = args[:output_file]
198
- # if File.exist?(output_file) && !args[:force]
199
- # print "File '#{output_file}' already exists. Overwrite? (y/n): "
200
- # overwrite = gets.chomp.downcase
201
- #
202
- # if overwrite == 'y'
203
- # File.write(output_file, transactions)
204
- # puts "File '#{output_file}' has been overwritten."
205
- # else
206
- # puts "Operation aborted. File '#{output_file}' was not overwritten."
207
- # end
208
- # else
209
- # File.write(output_file, transactions)
210
- # puts "File '#{output_file}' has been created."
211
- # end
212
- # end
213
- #
214
- # # Check for missing transactions
215
- # return unless args[:check] && args[:transactions_file] && args[:forecast_file]
216
- #
217
- # HledgerForecast::Checker.check(args)
218
- # end
219
161
  end
220
162
  end
@@ -1,130 +1,273 @@
1
1
  module HledgerForecast
2
- # Generates journal entries based on a YAML forecast file.
3
- # on forecast data and optional existing transactions.
2
+ # Generates periodic transactions from a YAML file
4
3
  class Generator
5
4
  class << self
6
- attr_accessor :settings
5
+ attr_accessor :options, :modified, :tracked
7
6
  end
8
7
 
9
- self.settings = {}
8
+ self.options = {}
9
+ self.modified = {}
10
+ self.tracked = {}
10
11
 
11
- def self.configure_settings(forecast_data)
12
- @settings[:currency] = Money::Currency.new(forecast_data.fetch('settings', {}).fetch('currency', 'USD'))
13
- @settings[:show_symbol] = forecast_data.fetch('settings', {}).fetch('show_symbol', true)
14
- @settings[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', true)
15
- @settings[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
16
- end
12
+ def self.set_options(forecast_data)
13
+ @options[:max_amount] = get_max_field_size(forecast_data, 'amount') + 1 # +1 for the negatives
14
+ @options[:max_category] = get_max_field_size(forecast_data, 'category')
17
15
 
18
- def self.write_transactions(output, date, account, transaction)
19
- output.concat("#{date} * #{transaction['description']}\n")
20
- output.concat(" #{transaction['category']} #{transaction['amount']}\n")
21
- output.concat(" #{account}\n\n")
16
+ @options[:currency] = Money::Currency.new(forecast_data.fetch('settings', {}).fetch('currency', 'USD'))
17
+ @options[:show_symbol] = forecast_data.fetch('settings', {}).fetch('show_symbol', true)
18
+ # @options[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', false)
19
+ @options[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
22
20
  end
23
21
 
24
- def self.format_transaction(transaction)
25
- formatted_transaction = transaction.clone
22
+ def self.generate(yaml_file, options = nil)
23
+ forecast_data = YAML.safe_load(yaml_file)
24
+
25
+ set_options(forecast_data)
26
+
27
+ output = ""
28
+
29
+ # Generate regular transactions
30
+ forecast_data.each do |period, forecasts|
31
+ if period == 'custom'
32
+ output += custom_transaction(forecasts)
33
+ else
34
+ frequency = convert_period_to_frequency(period)
35
+ next unless frequency
36
+
37
+ forecasts.each do |forecast|
38
+ account = forecast['account']
39
+ from = Date.parse(forecast['from'])
40
+ to = forecast['to'] ? Date.parse(forecast['to']) : nil
41
+ transactions = forecast['transactions']
42
+
43
+ output += regular_transaction(frequency, from, to, transactions, account)
44
+ output += ending_transaction(frequency, from, transactions, account)
45
+ end
46
+ end
47
+ end
48
+
49
+ # Generate tracked transactions
50
+ if options && !options[:no_track] && !@tracked.empty?
51
+ if options[:transaction_file]
52
+ output += output_tracked_transaction(Tracker.track(@tracked,
53
+ options[:transaction_file]))
54
+ else
55
+ puts "\nWarning: ".yellow.bold + "You need to specify a transaction file with the `--t` flag for smart transactions to work\n"
56
+ end
57
+ end
26
58
 
27
- formatted_transaction['amount'] =
28
- Money.from_cents(formatted_transaction['amount'].to_f * 100, @settings[:currency]).format(
29
- symbol: @settings[:show_symbol],
30
- sign_before_symbol: @settings[:sign_before_symbol],
31
- thousands_separator: @settings[:thousands_separator] ? ',' : nil
32
- )
59
+ output += output_modified_transaction(@modified) unless @modified.empty?
33
60
 
34
- formatted_transaction
61
+ output
35
62
  end
36
63
 
37
- def self.process_custom(output, forecast_data, date)
38
- forecast_data['custom']&.each do |forecast|
39
- start_date = Date.parse(forecast['start'])
40
- end_date = forecast['end'] ? Date.parse(forecast['end']) : nil
41
- account = forecast['account']
42
- period = forecast['recurrence']['period']
43
- quantity = forecast['recurrence']['quantity']
64
+ def self.regular_transaction(frequency, from, to, transactions, account)
65
+ transactions = transactions.select { |transaction| transaction['to'].nil? }
66
+ return "" if transactions.empty?
44
67
 
45
- next if end_date && date > end_date
68
+ output = ""
46
69
 
47
- date_matches = case period
48
- when 'days'
49
- (date - start_date).to_i % quantity == 0
50
- when 'weeks'
51
- (date - start_date).to_i % (quantity * 7) == 0
52
- when 'months'
53
- ((date.year * 12 + date.month) - (start_date.year * 12 + start_date.month)) % quantity == 0 && date.day == start_date.day
54
- end
70
+ transactions.each do |transaction|
71
+ if track_transaction?(transaction, from)
72
+ track_transaction(from, to, account, transaction)
73
+ next
74
+ end
75
+
76
+ modified_transaction(from, to, account, transaction)
55
77
 
56
- if date_matches
57
- forecast['transactions'].each do |transaction|
58
- end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
78
+ output += output_transaction(transaction['category'], format_amount(transaction['amount']),
79
+ transaction['description'])
80
+ end
59
81
 
60
- next unless end_date.nil? || date <= end_date
82
+ return "" unless output != ""
61
83
 
62
- write_transactions(output, date, account, format_transaction(transaction))
63
- end
84
+ output = if to
85
+ "#{frequency} #{from} to #{to} * #{extract_descriptions(transactions,
86
+ from)}\n" << output
87
+ else
88
+ "#{frequency} #{from} * #{extract_descriptions(transactions, from)}\n" << output
89
+ end
90
+
91
+ output += " #{account}\n\n"
92
+ output
93
+ end
94
+
95
+ def self.ending_transaction(frequency, from, transactions, account)
96
+ output = ""
97
+
98
+ transactions.each do |transaction|
99
+ to = transaction['to'] ? Date.parse(transaction['to']) : nil
100
+ next unless to
101
+
102
+ if track_transaction?(transaction, from)
103
+ track_transaction(from, to, account, transaction)
104
+ next
64
105
  end
106
+
107
+ modified_transaction(from, to, account, transaction)
108
+
109
+ output += "#{frequency} #{from} to #{to} * #{transaction['description']}\n"
110
+ output += output_transaction(transaction['category'], format_amount(transaction['amount']),
111
+ transaction['description'])
112
+ output += " #{account}\n\n"
65
113
  end
114
+
115
+ output
66
116
  end
67
117
 
68
- def self.process_forecast(output_file, forecast_data, type, date)
69
- forecast_data[type]&.each do |forecast|
70
- start_date = Date.parse(forecast['start'])
71
- end_date = forecast['end'] ? Date.parse(forecast['end']) : nil
118
+ def self.custom_transaction(forecasts)
119
+ output = ""
120
+
121
+ forecasts.each do |forecast|
72
122
  account = forecast['account']
123
+ from = Date.parse(forecast['from'])
124
+ to = forecast['to'] ? Date.parse(forecast['to']) : nil
125
+ frequency = forecast['frequency']
126
+ transactions = forecast['transactions']
127
+
128
+ output += "~ #{frequency} from #{from} * #{extract_descriptions(transactions, from)}\n"
73
129
 
74
- next if end_date && date > end_date
75
-
76
- date_matches = case type
77
- when 'monthly'
78
- date.day == start_date.day
79
- when 'quarterly'
80
- date.day == start_date.day && date.month % 3 == start_date.month % 3
81
- when 'half-yearly'
82
- date.day == start_date.day && (date.month - start_date.month) % 6 == 0
83
- when 'yearly'
84
- date.day == start_date.day && date.month == start_date.month
85
- when 'once'
86
- date == start_date
87
- end
88
-
89
- if date_matches
90
- forecast['transactions'].each do |transaction|
91
- transaction_start_date = transaction['start'] ? Date.parse(transaction['start']) : nil
92
- transaction_end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
93
-
94
- if (transaction_start_date && date < transaction_start_date) || (transaction_end_date && date > transaction_end_date)
95
- next
96
- end
97
-
98
- write_transactions(output_file, date, account, format_transaction(transaction))
130
+ transactions.each do |transaction|
131
+ to = transaction['to'] ? Date.parse(transaction['to']) : to
132
+
133
+ if track_transaction?(transaction, from)
134
+ track_transaction(from, to, account, transaction)
135
+ next
99
136
  end
137
+
138
+ modified_transaction(from, to, account, transaction)
139
+
140
+ output += output_transaction(transaction['category'], format_amount(transaction['amount']),
141
+ transaction['description'])
100
142
  end
143
+
144
+ output += " #{account}\n\n"
101
145
  end
146
+
147
+ output
102
148
  end
103
149
 
104
- def self.generate(transactions, forecast, start_date, end_date)
105
- start_date = Date.parse(start_date)
106
- end_date = Date.parse(end_date)
107
- forecast_data = YAML.safe_load(forecast)
150
+ def self.output_transaction(category, amount, description)
151
+ " #{category.ljust(@options[:max_category])} #{amount.ljust(@options[:max_amount])}; #{description}\n"
152
+ end
153
+
154
+ def self.output_modified_transaction(transactions)
155
+ output = ""
108
156
 
109
- configure_settings(forecast_data)
157
+ transactions.each do |_key, transaction|
158
+ date = "date:#{transaction['from']}"
159
+ date += "..#{transaction['to']}" if transaction['to']
110
160
 
111
- output = ''
112
- output.concat(transactions) if transactions
161
+ output += "= #{transaction['category']} #{date}\n"
162
+ output += " #{transaction['category'].ljust(@options[:max_category])} *#{transaction['amount'].to_s.ljust(@options[:max_amount] - 1)}; #{transaction['description']}\n"
163
+ output += " #{transaction['account'].ljust(@options[:max_category])} *#{transaction['amount'] * -1}\n\n"
164
+ end
113
165
 
114
- date = start_date
166
+ output
167
+ end
115
168
 
116
- while date <= end_date
117
- process_forecast(output, forecast_data, 'monthly', date)
118
- process_forecast(output, forecast_data, 'quarterly', date)
119
- process_forecast(output, forecast_data, 'half-yearly', date)
120
- process_forecast(output, forecast_data, 'yearly', date)
121
- process_forecast(output, forecast_data, 'once', date)
122
- process_custom(output, forecast_data, date)
169
+ def self.output_tracked_transaction(transactions)
170
+ output = ""
123
171
 
124
- date = date.next_day
172
+ transactions.each do |_key, transaction|
173
+ next if transaction['found']
174
+
175
+ output += "~ #{transaction['from']} * [TRACKED] #{transaction['transaction']['description']}\n"
176
+ output += " #{transaction['transaction']['category'].ljust(@options[:max_category])} #{transaction['transaction']['amount'].ljust(@options[:max_amount])}; #{transaction['transaction']['description']}\n"
177
+ output += " #{transaction['account']}\n\n"
125
178
  end
126
179
 
127
180
  output
128
181
  end
182
+
183
+ def self.extract_descriptions(transactions, from)
184
+ descriptions = []
185
+
186
+ transactions.each do |transaction|
187
+ next if track_transaction?(transaction, from)
188
+
189
+ description = transaction['description']
190
+ descriptions << description
191
+ end
192
+
193
+ descriptions.join(', ')
194
+ end
195
+
196
+ def self.modified_transaction(from, to, account, transaction)
197
+ return unless transaction['modifiers']
198
+
199
+ transaction['modifiers'].each do |modifier|
200
+ description = transaction['description']
201
+ description += ' - ' + modifier['description'] unless modifier['description'].empty?
202
+
203
+ @modified[@modified.length] = {
204
+ 'account' => account,
205
+ 'amount' => modifier['amount'],
206
+ 'category' => transaction['category'],
207
+ 'description' => description,
208
+ 'from' => modifier['from'] ? Date.parse(modifier['from']) : (from || nil),
209
+ 'to' => modifier['to'] ? Date.parse(modifier['to']) : (to || nil)
210
+ }
211
+ end
212
+ end
213
+
214
+ def self.track_transaction?(transaction, from)
215
+ transaction['track'] && from <= Date.today
216
+ end
217
+
218
+ def self.track_transaction(from, to, account, transaction)
219
+ amount = transaction['amount']
220
+ transaction['amount'] = format_amount(amount)
221
+ transaction['inverse_amount'] = format_amount(amount * -1)
222
+
223
+ @tracked[@tracked.length] = {
224
+ 'account' => account,
225
+ 'from' => from,
226
+ 'to' => to,
227
+ 'transaction' => transaction
228
+ }
229
+ end
230
+
231
+ def self.convert_period_to_frequency(period)
232
+ map = {
233
+ 'once' => '~',
234
+ 'monthly' => '~ monthly from',
235
+ 'quarterly' => '~ every 3 months from',
236
+ 'half-yearly' => '~ every 6 months from',
237
+ 'yearly' => '~ yearly from'
238
+ }
239
+
240
+ map[period]
241
+ end
242
+
243
+ def self.format_amount(amount)
244
+ Money.from_cents(amount.to_f * 100, @options[:currency]).format(
245
+ symbol: @options[:show_symbol],
246
+ sign_before_symbol: @options[:sign_before_symbol],
247
+ thousands_separator: @options[:thousands_separator] ? ',' : nil
248
+ )
249
+ end
250
+
251
+ def self.get_max_field_size(forecast_data, field)
252
+ max_size = 0
253
+
254
+ forecast_data.each do |period, forecasts|
255
+ next if period == 'settings'
256
+
257
+ forecasts.each do |forecast|
258
+ transactions = forecast['transactions']
259
+ transactions.each do |transaction|
260
+ field_value = if transaction[field].is_a?(Integer) || transaction[field].is_a?(Float)
261
+ ((transaction[field] + 3) * 100).to_s
262
+ else
263
+ transaction[field].to_s
264
+ end
265
+ max_size = [max_size, field_value.length].max
266
+ end
267
+ end
268
+ end
269
+
270
+ max_size
271
+ end
129
272
  end
130
273
  end
@@ -15,7 +15,7 @@ module HledgerForecast
15
15
 
16
16
  def self.init_generator(forecast_data)
17
17
  generator = HledgerForecast::Generator
18
- generator.configure_settings(forecast_data)
18
+ generator.set_options(forecast_data)
19
19
 
20
20
  @generator = generator
21
21
  end
@@ -37,8 +37,7 @@ module HledgerForecast
37
37
 
38
38
  forecast_data['custom']&.each do |entry|
39
39
  period_data = {}
40
- period_data[:quantity] = entry['recurrence']['quantity']
41
- period_data[:period] = entry['recurrence']['period']
40
+ period_data[:frequency] = entry['frequency']
42
41
  period_data[:category] = entry['transactions'].first['category']
43
42
  period_data[:amount] = entry['transactions'].first['amount']
44
43
 
@@ -53,7 +52,7 @@ module HledgerForecast
53
52
  end
54
53
 
55
54
  def self.format_amount(amount)
56
- formatted_amount = @generator.format_transaction({ 'amount' => amount })['amount']
55
+ formatted_amount = @generator.format_amount(amount)
57
56
  amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
58
57
  end
59
58
 
@@ -61,7 +60,7 @@ module HledgerForecast
61
60
  if custom
62
61
  row_data[:periods].each do |period|
63
62
  @table.add_row [{ value: period[:category], alignment: :left },
64
- { value: "every #{period[:quantity]} #{period[:period]}", alignment: :right },
63
+ { value: period[:frequency], alignment: :right },
65
64
  { value: format_amount(period[:amount]), alignment: :right }]
66
65
 
67
66
  period_total += period[:amount]