hledger-forecast 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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]