hledger-forecast 0.2.1 → 0.4.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -2
  3. data/README.md +169 -71
  4. data/example.journal +40 -0
  5. data/example.yml +38 -36
  6. data/hledger-forecast.gemspec +6 -4
  7. data/lib/hledger_forecast/cli.rb +35 -93
  8. data/lib/hledger_forecast/generator.rb +242 -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 +2 -1
  13. data/spec/command_spec.rb +14 -4
  14. data/spec/computed_amounts_spec.rb +35 -0
  15. data/spec/custom_spec.rb +35 -42
  16. data/spec/half-yearly_spec.rb +22 -6
  17. data/spec/modifier_spec.rb +87 -0
  18. data/spec/monthly_end_date_spec.rb +47 -0
  19. data/spec/monthly_end_date_transaction_spec.rb +32 -0
  20. data/spec/monthly_spec.rb +35 -27
  21. data/spec/once_spec.rb +22 -7
  22. data/spec/quarterly_spec.rb +21 -7
  23. data/spec/stubs/{monthly/forecast_monthly.yml → forecast.yml} +7 -7
  24. data/spec/stubs/transactions_found.journal +24 -0
  25. data/spec/stubs/transactions_found_inverse.journal +24 -0
  26. data/spec/stubs/transactions_not_found.journal +16 -0
  27. data/spec/track_spec.rb +197 -0
  28. data/spec/yearly_spec.rb +21 -7
  29. metadata +48 -74
  30. data/spec/start_date_spec.rb +0 -12
  31. data/spec/stubs/custom/forecast_custom_days.yml +0 -14
  32. data/spec/stubs/custom/forecast_custom_months.yml +0 -14
  33. data/spec/stubs/custom/forecast_custom_weeks.yml +0 -14
  34. data/spec/stubs/custom/forecast_custom_weeks_twice.yml +0 -24
  35. data/spec/stubs/custom/output_custom_days.journal +0 -24
  36. data/spec/stubs/custom/output_custom_months.journal +0 -20
  37. data/spec/stubs/custom/output_custom_weeks.journal +0 -28
  38. data/spec/stubs/custom/output_custom_weeks_twice.journal +0 -44
  39. data/spec/stubs/half-yearly/forecast_half-yearly.yml +0 -10
  40. data/spec/stubs/half-yearly/output_half-yearly.journal +0 -20
  41. data/spec/stubs/modifiers/forecast_modifiers.yml +0 -13
  42. data/spec/stubs/modifiers/output_modifiers.journal +0 -44
  43. data/spec/stubs/monthly/forecast_monthly_enddate.yml +0 -14
  44. data/spec/stubs/monthly/forecast_monthly_enddate_top.yml +0 -14
  45. data/spec/stubs/monthly/forecast_monthly_modifier.yml +0 -11
  46. data/spec/stubs/monthly/output_monthly.journal +0 -44
  47. data/spec/stubs/monthly/output_monthly_enddate.journal +0 -48
  48. data/spec/stubs/monthly/output_monthly_enddate_top.journal +0 -40
  49. data/spec/stubs/monthly/output_monthly_modifier.journal +0 -20
  50. data/spec/stubs/once/forecast_once.yml +0 -10
  51. data/spec/stubs/once/output_once.journal +0 -12
  52. data/spec/stubs/quarterly/forecast_quarterly.yml +0 -10
  53. data/spec/stubs/quarterly/output_quarterly.journal +0 -20
  54. data/spec/stubs/start_date/forecast_startdate.yml +0 -26
  55. data/spec/stubs/start_date/output_startdate.journal +0 -56
  56. data/spec/stubs/transactions.journal +0 -8
  57. data/spec/stubs/yearly/forecast_yearly.yml +0 -10
  58. 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,282 @@
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 :calculator, :options, :modified, :tracked
7
6
  end
8
7
 
9
- self.settings = {}
8
+ self.calculator = {}
9
+ self.options = {}
10
+ self.modified = {}
11
+ self.tracked = {}
10
12
 
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
13
+ def self.set_options(forecast_data)
14
+ @options[:max_amount] = get_max_field_size(forecast_data, 'amount') + 1 # +1 for the negatives
15
+ @options[:max_category] = get_max_field_size(forecast_data, 'category')
17
16
 
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")
17
+ @options[:currency] = Money::Currency.new(forecast_data.fetch('settings', {}).fetch('currency', 'USD'))
18
+ @options[:show_symbol] = forecast_data.fetch('settings', {}).fetch('show_symbol', true)
19
+ # @options[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', false)
20
+ @options[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
22
21
  end
23
22
 
24
- def self.format_transaction(transaction)
25
- formatted_transaction = transaction.clone
23
+ def self.generate(yaml_file, options = nil)
24
+ forecast_data = YAML.safe_load(yaml_file)
25
+
26
+ set_options(forecast_data)
27
+
28
+ @calculator = Dentaku::Calculator.new
29
+
30
+ output = ""
31
+
32
+ # Generate regular transactions
33
+ forecast_data.each do |period, forecasts|
34
+ if period == 'custom'
35
+ output += custom_transaction(forecasts)
36
+ else
37
+ frequency = convert_period_to_frequency(period)
38
+ next unless frequency
26
39
 
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
- )
40
+ forecasts.each do |forecast|
41
+ account = forecast['account']
42
+ from = Date.parse(forecast['from'])
43
+ to = forecast['to'] ? Date.parse(forecast['to']) : nil
44
+ transactions = forecast['transactions']
33
45
 
34
- formatted_transaction
46
+ output += regular_transaction(frequency, from, to, transactions, account)
47
+ output += ending_transaction(frequency, from, transactions, account)
48
+ end
49
+ end
50
+ end
51
+
52
+ # Generate tracked transactions
53
+ if options && !options[:no_track] && !@tracked.empty?
54
+ if options[:transaction_file]
55
+ output += output_tracked_transaction(Tracker.track(@tracked,
56
+ options[:transaction_file]))
57
+ else
58
+ puts "\nWarning: ".yellow.bold + "You need to specify a transaction file with the `--t` flag for smart transactions to work\n"
59
+ end
60
+ end
61
+
62
+ output += output_modified_transaction(@modified) unless @modified.empty?
63
+
64
+ output
35
65
  end
36
66
 
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']
67
+ def self.regular_transaction(frequency, from, to, transactions, account)
68
+ transactions = transactions.select { |transaction| transaction['to'].nil? }
69
+ return "" if transactions.empty?
44
70
 
45
- next if end_date && date > end_date
71
+ output = ""
46
72
 
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
73
+ transactions.each do |transaction|
74
+ if track_transaction?(transaction, from)
75
+ track_transaction(from, to, account, transaction)
76
+ next
77
+ end
78
+
79
+ modified_transaction(from, to, account, transaction)
55
80
 
56
- if date_matches
57
- forecast['transactions'].each do |transaction|
58
- end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
81
+ output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
82
+ transaction['description'])
83
+ end
59
84
 
60
- next unless end_date.nil? || date <= end_date
85
+ return "" unless output != ""
61
86
 
62
- write_transactions(output, date, account, format_transaction(transaction))
63
- end
87
+ output = if to
88
+ "#{frequency} #{from} to #{to} * #{extract_descriptions(transactions,
89
+ from)}\n" << output
90
+ else
91
+ "#{frequency} #{from} * #{extract_descriptions(transactions, from)}\n" << output
92
+ end
93
+
94
+ output += " #{account}\n\n"
95
+ output
96
+ end
97
+
98
+ def self.ending_transaction(frequency, from, transactions, account)
99
+ output = ""
100
+
101
+ transactions.each do |transaction|
102
+ to = transaction['to'] ? Date.parse(transaction['to']) : nil
103
+ next unless to
104
+
105
+ if track_transaction?(transaction, from)
106
+ track_transaction(from, to, account, transaction)
107
+ next
64
108
  end
109
+
110
+ modified_transaction(from, to, account, transaction)
111
+
112
+ output += "#{frequency} #{from} to #{to} * #{transaction['description']}\n"
113
+ output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
114
+ transaction['description'])
115
+ output += " #{account}\n\n"
65
116
  end
117
+
118
+ output
66
119
  end
67
120
 
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
121
+ def self.custom_transaction(forecasts)
122
+ output = ""
123
+
124
+ forecasts.each do |forecast|
72
125
  account = forecast['account']
126
+ from = Date.parse(forecast['from'])
127
+ to = forecast['to'] ? Date.parse(forecast['to']) : nil
128
+ frequency = forecast['frequency']
129
+ transactions = forecast['transactions']
130
+
131
+ output += "~ #{frequency} from #{from} * #{extract_descriptions(transactions, from)}\n"
132
+
133
+ transactions.each do |transaction|
134
+ to = transaction['to'] ? Date.parse(transaction['to']) : to
73
135
 
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))
136
+ if track_transaction?(transaction, from)
137
+ track_transaction(from, to, account, transaction)
138
+ next
99
139
  end
140
+
141
+ modified_transaction(from, to, account, transaction)
142
+
143
+ output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
144
+ transaction['description'])
100
145
  end
146
+
147
+ output += " #{account}\n\n"
101
148
  end
149
+
150
+ output
102
151
  end
103
152
 
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)
153
+ def self.output_transaction(category, amount, description)
154
+ " #{category.ljust(@options[:max_category])} #{amount.ljust(@options[:max_amount])}; #{description}\n"
155
+ end
108
156
 
109
- configure_settings(forecast_data)
157
+ def self.output_modified_transaction(transactions)
158
+ output = ""
110
159
 
111
- output = ''
112
- output.concat(transactions) if transactions
160
+ transactions.each do |_key, transaction|
161
+ date = "date:#{transaction['from']}"
162
+ date += "..#{transaction['to']}" if transaction['to']
113
163
 
114
- date = start_date
164
+ output += "= #{transaction['category']} #{date}\n"
165
+ output += " #{transaction['category'].ljust(@options[:max_category])} *#{transaction['amount'].to_s.ljust(@options[:max_amount] - 1)}; #{transaction['description']}\n"
166
+ output += " #{transaction['account'].ljust(@options[:max_category])} *#{transaction['amount'] * -1}\n\n"
167
+ end
168
+
169
+ output
170
+ end
171
+
172
+ def self.output_tracked_transaction(transactions)
173
+ output = ""
115
174
 
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)
175
+ transactions.each do |_key, transaction|
176
+ next if transaction['found']
123
177
 
124
- date = date.next_day
178
+ output += "~ #{transaction['from']} * [TRACKED] #{transaction['transaction']['description']}\n"
179
+ output += " #{transaction['transaction']['category'].ljust(@options[:max_category])} #{transaction['transaction']['amount'].ljust(@options[:max_amount])}; #{transaction['transaction']['description']}\n"
180
+ output += " #{transaction['account']}\n\n"
125
181
  end
126
182
 
127
183
  output
128
184
  end
185
+
186
+ def self.extract_descriptions(transactions, from)
187
+ descriptions = []
188
+
189
+ transactions.each do |transaction|
190
+ next if track_transaction?(transaction, from)
191
+
192
+ description = transaction['description']
193
+ descriptions << description
194
+ end
195
+
196
+ descriptions.join(', ')
197
+ end
198
+
199
+ def self.modified_transaction(from, to, account, transaction)
200
+ return unless transaction['modifiers']
201
+
202
+ transaction['modifiers'].each do |modifier|
203
+ description = transaction['description']
204
+ description += ' - ' + modifier['description'] unless modifier['description'].empty?
205
+
206
+ @modified[@modified.length] = {
207
+ 'account' => account,
208
+ 'amount' => modifier['amount'],
209
+ 'category' => transaction['category'],
210
+ 'description' => description,
211
+ 'from' => modifier['from'] ? Date.parse(modifier['from']) : (from || nil),
212
+ 'to' => modifier['to'] ? Date.parse(modifier['to']) : (to || nil)
213
+ }
214
+ end
215
+ end
216
+
217
+ def self.track_transaction?(transaction, from)
218
+ transaction['track'] && from <= Date.today
219
+ end
220
+
221
+ def self.track_transaction(from, to, account, transaction)
222
+ amount = calculate_amount(transaction['amount'])
223
+ transaction['amount'] = format_amount(amount)
224
+ transaction['inverse_amount'] = format_amount(amount * -1)
225
+
226
+ @tracked[@tracked.length] = {
227
+ 'account' => account,
228
+ 'from' => from,
229
+ 'to' => to,
230
+ 'transaction' => transaction
231
+ }
232
+ end
233
+
234
+ def self.convert_period_to_frequency(period)
235
+ map = {
236
+ 'once' => '~',
237
+ 'monthly' => '~ monthly from',
238
+ 'quarterly' => '~ every 3 months from',
239
+ 'half-yearly' => '~ every 6 months from',
240
+ 'yearly' => '~ yearly from'
241
+ }
242
+
243
+ map[period]
244
+ end
245
+
246
+ def self.calculate_amount(amount)
247
+ return amount unless amount.is_a?(String)
248
+
249
+ @calculator.evaluate(amount.slice(1..-1))
250
+ end
251
+
252
+ def self.format_amount(amount)
253
+ Money.from_cents(amount.to_f * 100, @options[:currency]).format(
254
+ symbol: @options[:show_symbol],
255
+ sign_before_symbol: @options[:sign_before_symbol],
256
+ thousands_separator: @options[:thousands_separator] ? ',' : nil
257
+ )
258
+ end
259
+
260
+ def self.get_max_field_size(forecast_data, field)
261
+ max_size = 0
262
+
263
+ forecast_data.each do |period, forecasts|
264
+ next if period == 'settings'
265
+
266
+ forecasts.each do |forecast|
267
+ transactions = forecast['transactions']
268
+ transactions.each do |transaction|
269
+ field_value = if transaction[field].is_a?(Integer) || transaction[field].is_a?(Float)
270
+ ((transaction[field] + 3) * 100).to_s
271
+ else
272
+ transaction[field].to_s
273
+ end
274
+ max_size = [max_size, field_value.length].max
275
+ end
276
+ end
277
+ end
278
+
279
+ max_size
280
+ end
129
281
  end
130
282
  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]