hledger-forecast 2.0.0 → 3.0.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{test.yml → ci.yml} +18 -10
  3. data/.github/workflows/publish_ruby_gem.yml +24 -0
  4. data/.github/workflows/release.yml +12 -13
  5. data/.mise.toml +2 -0
  6. data/CHANGELOG.md +17 -0
  7. data/Gemfile +1 -0
  8. data/README.md +149 -119
  9. data/example.csv +15 -15
  10. data/example.journal +17 -18
  11. data/hledger-forecast.gemspec +20 -18
  12. data/lib/hledger_forecast/calculator.rb +7 -15
  13. data/lib/hledger_forecast/cli.rb +98 -71
  14. data/lib/hledger_forecast/comparator.rb +12 -11
  15. data/lib/hledger_forecast/forecast.rb +29 -0
  16. data/lib/hledger_forecast/formatter.rb +13 -15
  17. data/lib/hledger_forecast/generator.rb +32 -72
  18. data/lib/hledger_forecast/settings.rb +34 -47
  19. data/lib/hledger_forecast/summarizer.rb +34 -55
  20. data/lib/hledger_forecast/summarizer_formatter.rb +75 -78
  21. data/lib/hledger_forecast/transaction.rb +63 -0
  22. data/lib/hledger_forecast/transactions/default.rb +45 -72
  23. data/lib/hledger_forecast/version.rb +1 -1
  24. data/lib/hledger_forecast.rb +21 -22
  25. data/spec/calculator_spec.rb +45 -0
  26. data/spec/cli_spec.rb +19 -17
  27. data/spec/compare_spec.rb +16 -14
  28. data/spec/computed_amounts_spec.rb +7 -7
  29. data/spec/custom_spec.rb +9 -9
  30. data/spec/formatter_spec.rb +51 -0
  31. data/spec/half-yearly_spec.rb +5 -5
  32. data/spec/monthly_end_date_spec.rb +6 -6
  33. data/spec/monthly_end_date_transaction_spec.rb +10 -10
  34. data/spec/monthly_spec.rb +7 -7
  35. data/spec/once_spec.rb +5 -5
  36. data/spec/quarterly_spec.rb +5 -5
  37. data/spec/settings_spec.rb +101 -0
  38. data/spec/stubs/forecast.csv +4 -4
  39. data/spec/summarizer_spec.rb +28 -33
  40. data/spec/tags_spec.rb +92 -0
  41. data/spec/verbose_output_spec.rb +8 -8
  42. data/spec/yearly_spec.rb +5 -5
  43. metadata +49 -13
  44. data/lib/hledger_forecast/transactions/modifiers.rb +0 -90
  45. data/lib/hledger_forecast/transactions/trackers.rb +0 -88
  46. data/lib/hledger_forecast/utilities.rb +0 -14
  47. data/spec/track_spec.rb +0 -105
@@ -1,56 +1,43 @@
1
1
  module HledgerForecast
2
- # Set the options from a user's config
3
2
  class Settings
4
- def self.config(forecast, cli_options)
5
- settings = {}
6
- settings[:max_amount] = 0
7
- settings[:max_category] = 0
8
-
9
- forecast.each do |row|
10
- if row['type'] != 'settings'
11
- category_length = row['category'].length
12
- settings[:max_category] = category_length if category_length > settings[:max_category]
13
-
14
- amount = if row['amount'].is_a?(Integer) || row['amount'].is_a?(Float)
15
- ((row['amount'] + 3) * 100).to_s
16
- else
17
- row['amount'].to_s
18
- end
19
-
20
- settings[:max_amount] = amount.length if amount.length > settings[:max_amount]
21
- end
22
-
23
- if row['type'] == 'settings'
24
-
25
- settings[:currency] = if row['frequency'] == "currency"
26
- row['account']
27
- else
28
- "USD"
29
- end
30
-
31
- settings[:show_symbol] = if row['frequency'] == "show_symbol"
32
- row['account']
33
- else
34
- true
35
- end
3
+ DEFAULTS = {
4
+ currency: "USD",
5
+ show_symbol: true,
6
+ sign_before_symbol: false,
7
+ thousands_separator: ","
8
+ }.freeze
9
+
10
+ attr_reader(
11
+ :currency,
12
+ :show_symbol,
13
+ :sign_before_symbol,
14
+ :thousands_separator,
15
+ :verbose,
16
+ :roll_up
17
+ )
18
+
19
+ def self.parse(settings_rows, cli_options = nil)
20
+ new(settings_rows, cli_options)
21
+ end
36
22
 
37
- settings[:sign_before_symbol] = if row['frequency'] == "sign_before_symbol"
38
- row['account']
39
- else
40
- false
41
- end
23
+ def verbose? = @verbose
42
24
 
43
- settings[:thousands_separator] = if row['frequency'] == "thousands_separator"
44
- row['account']
45
- else
46
- ","
47
- end
48
- end
25
+ private
49
26
 
50
- settings.merge!(cli_options) if cli_options
51
- end
27
+ def initialize(settings_rows, cli_options)
28
+ overrides = settings_rows.each_with_object({}) { |row, h| h[row[:frequency]] = row[:account] }
29
+ opts = cli_options || {}
52
30
 
53
- settings
31
+ @currency = opts[:currency] || overrides["currency"] || DEFAULTS[:currency]
32
+ @show_symbol = opts[:show_symbol] || overrides["show_symbol"] || DEFAULTS[:show_symbol]
33
+ @sign_before_symbol = opts[:sign_before_symbol] ||
34
+ overrides["sign_before_symbol"] ||
35
+ DEFAULTS[:sign_before_symbol]
36
+ @thousands_separator = opts[:thousands_separator] ||
37
+ overrides["thousands_separator"] ||
38
+ DEFAULTS[:thousands_separator]
39
+ @verbose = opts[:verbose] || false
40
+ @roll_up = opts[:roll_up]
54
41
  end
55
42
  end
56
43
  end
@@ -1,72 +1,51 @@
1
1
  module HledgerForecast
2
- # Summarise a forecast yaml file and output it to the CLI
3
2
  class Summarizer
4
- def self.summarize(config, cli_options)
5
- new.summarize(config, cli_options)
3
+ def self.summarize(csv_string, cli_options = nil)
4
+ new.summarize(csv_string, cli_options)
6
5
  end
7
6
 
8
- def summarize(config, cli_options = nil)
9
- @forecast = CSV.parse(config, headers: true)
10
- @settings = Settings.config(@forecast, cli_options)
7
+ def summarize(csv_string, cli_options = nil)
8
+ forecast = Forecast.parse(csv_string, cli_options)
9
+ transactions = forecast.transactions.reject(&:summary_exclude?)
11
10
 
12
- { output: generate(@forecast), settings: @settings }
13
- end
14
-
15
- private
16
-
17
- def generate(forecast)
18
- output = []
11
+ if cli_options&.dig(:tags)
12
+ raise "The --tags option requires a 'tag' column in the forecast CSV" unless forecast.has_tags_column?
13
+ transactions = transactions.select { |t| t.matches_tags?(cli_options[:tags]) }
14
+ end
19
15
 
20
- forecast.each do |row|
21
- next if row['type'] == 'settings'
22
- next if row['summary_exclude']
16
+ output = transactions.map { |t| build_summary_row(t) }
17
+ output = apply_roll_up(output, forecast.settings.roll_up) if forecast.settings.roll_up
23
18
 
24
- row['amount'] = Utilities.convert_amount(row['amount'])
19
+ {output: output, settings: forecast.settings}
20
+ end
25
21
 
26
- begin
27
- annualised_amount = row['roll-up'] ? row['amount'] * row['roll-up'].to_f : row['amount'] * annualise(row['type'])
28
- rescue StandardError
29
- puts "\nError: ".bold.red + 'Could not create an annualised ammount. Have you set the roll-up for your custom type transactions?'
30
- exit
31
- end
22
+ private
32
23
 
33
- output << {
34
- account: row['account'],
35
- from: Date.parse(row['from']),
36
- to: row['to'] ? Calculator.new.evaluate_date(Date.parse(row['from']), row['to']) : nil,
37
- type: row['type'],
38
- frequency: row['frequency'],
39
- category: row['category'],
40
- description: row['description'],
41
- amount: row['amount'],
42
- annualised_amount: annualised_amount.to_f,
43
- exclude: row['summary_exclude']
44
- }
24
+ def build_summary_row(transaction)
25
+ annualised = begin
26
+ transaction.annualised_amount
27
+ rescue KeyError => e
28
+ puts("\nError: ".bold.red + e.message)
29
+ exit
45
30
  end
46
31
 
47
- output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
48
-
49
- output
50
- end
51
-
52
- def annualise(period)
53
- annualise = {
54
- 'monthly' => 12,
55
- 'quarterly' => 4,
56
- 'half-yearly' => 2,
57
- 'yearly' => 1,
58
- 'once' => 1,
59
- 'daily' => 352,
60
- 'weekly' => 52
32
+ {
33
+ account: transaction.account,
34
+ from: transaction.from,
35
+ to: transaction.to,
36
+ type: transaction.type,
37
+ frequency: transaction.frequency,
38
+ category: transaction.category,
39
+ description: transaction.description,
40
+ amount: transaction.amount,
41
+ annualised_amount: annualised.to_f,
42
+ exclude: transaction.summary_exclude
61
43
  }
62
-
63
- annualise[period]
64
44
  end
65
45
 
66
- def calculate_rolled_up_amount(forecast)
67
- forecast.each do |row|
68
- row[:rolled_up_amount] = row[:annualised_amount] / annualise(@settings[:roll_up])
69
- end
46
+ def apply_roll_up(output, roll_up_period)
47
+ divisor = ANNUAL_MULTIPLIERS.fetch(roll_up_period)
48
+ output.each { |row| row[:rolled_up_amount] = row[:annualised_amount] / divisor }
70
49
  end
71
50
  end
72
51
  end
@@ -1,5 +1,4 @@
1
1
  module HledgerForecast
2
- # Output the summarised forecast to the CLI
3
2
  class SummarizerFormatter
4
3
  def self.format(output, settings)
5
4
  new.format(output, settings)
@@ -11,7 +10,7 @@ module HledgerForecast
11
10
 
12
11
  init_table
13
12
 
14
- if @settings[:roll_up].nil?
13
+ if @settings.roll_up.nil?
15
14
  add_rows_to_table(output)
16
15
  add_total_row_to_table(output, :amount)
17
16
  else
@@ -25,115 +24,113 @@ module HledgerForecast
25
24
  private
26
25
 
27
26
  def init_table
28
- title = 'FORECAST SUMMARY'
29
- title += " (#{@settings[:roll_up].upcase} ROLL UP)" if @settings[:roll_up]
27
+ title = "FORECAST SUMMARY"
28
+ title += " (#{@settings.roll_up.upcase} ROLL UP)" if @settings.roll_up
30
29
 
31
- @table.add_row([{ value: title.bold, colspan: 3, alignment: :center }])
30
+ @table.add_row([{value: title.bold, colspan: 3, alignment: :center}])
32
31
  @table.add_separator
33
32
  end
34
33
 
35
34
  def add_rows_to_table(data)
36
- data = data.group_by { |item| item[:type] }
35
+ data.group_by { |item| item[:type] }.tap { |d| sort_items(d) }.each_with_index do |(type, items), index|
36
+ @table.add_row([{value: type.capitalize.bold, colspan: 3, alignment: :center}])
37
37
 
38
- data = sort(data)
39
-
40
- data.each_with_index do |(type, items), index|
41
- @table.add_row([{ value: type.capitalize.bold, colspan: 3, alignment: :center }])
42
38
  total = 0
43
39
  items.each do |item|
44
40
  total += item[:amount].to_f
45
41
 
46
- if @settings[:verbose]
47
- @table.add_row [{ value: item[:category], alignment: :left },
48
- { value: item[:description], alignment: :left },
49
- { value: format_amount(item[:amount]), alignment: :right }]
42
+ if @settings.verbose
43
+ @table.add_row(
44
+ [
45
+ {value: item[:category], alignment: :left},
46
+ {value: item[:description], alignment: :left},
47
+ {value: format_amount(item[:amount]), alignment: :right}
48
+ ]
49
+ )
50
50
  else
51
- @table.add_row [{ value: item[:category], colspan: 2, alignment: :left },
52
- { value: format_amount(item[:amount]), alignment: :right }]
51
+ @table.add_row(
52
+ [
53
+ {value: item[:category], colspan: 2, alignment: :left},
54
+ {value: format_amount(item[:amount]), alignment: :right}
55
+ ]
56
+ )
53
57
  end
54
58
  end
55
59
 
56
- @table.add_row [{ value: "TOTAL".bold, colspan: 2, alignment: :left },
57
- { value: format_amount(total).bold, alignment: :right }]
60
+ @table.add_row(
61
+ [
62
+ {value: "TOTAL".bold, colspan: 2, alignment: :left},
63
+ {value: format_amount(total).bold, alignment: :right}
64
+ ]
65
+ )
58
66
 
59
67
  @table.add_separator if index != data.size - 1
60
68
  end
61
69
  end
62
70
 
63
- def sort(data)
64
- data.each do |type, items|
65
- data[type] = items.sort_by do |item|
66
- value = item[:amount].to_f
67
- [value >= 0 ? 1 : 0, value >= 0 ? -value : value]
68
- end
69
- end
70
- end
71
-
72
71
  def add_rolled_up_rows_to_table(data)
73
- sum_hash = Hash.new { |h, k| h[k] = { sum: 0, descriptions: [] } }
74
-
75
- data.each do |item|
76
- sum_hash[item[:category]][:sum] += item[:rolled_up_amount]
77
- sum_hash[item[:category]][:descriptions] << item[:description]
72
+ aggregated = data.each_with_object(Hash.new { |h, k| h[k] = {sum: 0, descriptions: []} }) do |item, h|
73
+ h[item[:category]][:sum] += item[:rolled_up_amount]
74
+ h[item[:category]][:descriptions] << item[:description]
78
75
  end
79
76
 
80
- # Convert arrays of descriptions to single strings
81
- sum_hash.each do |_category, values|
82
- values[:descriptions] = values[:descriptions].join(", ")
83
- end
84
-
85
- # Sort the array
86
- sorted_sums = sort_roll_up(sum_hash, :sum)
87
-
88
- if @settings[:verbose]
89
- sorted_sums.each do |hash|
90
- @table.add_row [{ value: hash[:category], colspan: 1, alignment: :left },
91
- { value: hash[:descriptions], colspan: 1, alignment: :left },
92
- { value: format_amount(hash[:sum]), alignment: :right }]
93
- end
94
- else
95
- sorted_sums.each do |hash|
96
- @table.add_row [{ value: hash[:category], colspan: 2, alignment: :left },
97
- { value: format_amount(hash[:sum]), alignment: :right }]
77
+ aggregated.each_value { |v| v[:descriptions] = v[:descriptions].join(", ") }
78
+
79
+ sort_by_amount(aggregated.map { |cat, v| {category: cat, sum: v[:sum], descriptions: v[:descriptions]} })
80
+ .each do |hash|
81
+ if @settings.verbose
82
+ @table.add_row(
83
+ [
84
+ {value: hash[:category], colspan: 1, alignment: :left},
85
+ {value: hash[:descriptions], colspan: 1, alignment: :left},
86
+ {value: format_amount(hash[:sum]), alignment: :right}
87
+ ]
88
+ )
89
+ else
90
+ @table.add_row(
91
+ [
92
+ {value: hash[:category], colspan: 2, alignment: :left},
93
+ {value: format_amount(hash[:sum]), alignment: :right}
94
+ ]
95
+ )
96
+ end
98
97
  end
99
- end
100
98
  end
101
99
 
102
- def sort_roll_up(data, sort_by)
103
- # Convert the hash to an array of hashes
104
- array = data.map do |category, values|
105
- { category: category, sum: values[sort_by], descriptions: values[:descriptions] }
106
- end
100
+ def add_total_row_to_table(data, row_to_sum)
101
+ total = data.sum { |item| item[row_to_sum].to_f }
102
+ income = data.sum { |item| (v = item[row_to_sum].to_f) < 0 ? v : 0 }
103
+ savings = (total / income * 100).round(2)
107
104
 
108
- # Sort the array
109
- array.sort_by do |hash|
110
- value = hash[:sum]
111
- [value >= 0 ? 1 : 0, value >= 0 ? -value : value]
112
- end
105
+ @table.add_separator
106
+ @table.add_row(
107
+ [
108
+ {value: "TOTAL".bold, colspan: 2, alignment: :left},
109
+ {value: format_amount(total).bold, alignment: :right}
110
+ ]
111
+ )
112
+ @table.add_row(
113
+ [
114
+ {value: "as a % of income".italic, colspan: 2, alignment: :left},
115
+ {value: "#{savings}%".italic, alignment: :right}
116
+ ]
117
+ )
113
118
  end
114
119
 
115
- def add_total_row_to_table(data, row_to_sum)
116
- total = data.reduce(0) do |sum, item|
117
- sum + item[row_to_sum].to_f
118
- end
120
+ def sort_items(grouped)
121
+ grouped.transform_values! { |items| sort_by_amount(items, key: :amount) }
122
+ end
119
123
 
120
- income = data.reduce(0) do |sum, item|
121
- sum += item[row_to_sum].to_f if item[row_to_sum].to_f < 0
122
- sum
124
+ def sort_by_amount(collection, key: :sum)
125
+ collection.sort_by do |item|
126
+ value = item[key].to_f
127
+ [value >= 0 ? 1 : 0, value >= 0 ? -value : value]
123
128
  end
124
-
125
- savings = (total / income * 100).to_f.round(2)
126
-
127
- @table.add_separator
128
- @table.add_row [{ value: "TOTAL".bold, colspan: 2, alignment: :left },
129
- { value: format_amount(total).bold, alignment: :right }]
130
- @table.add_row [{ value: "as a % of income".italic, colspan: 2, alignment: :left },
131
- { value: "#{savings}%".italic, alignment: :right }]
132
129
  end
133
130
 
134
131
  def format_amount(amount)
135
- formatted_amount = Formatter.format_money(amount, @settings)
136
- amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
132
+ formatted = Formatter.format_money(amount, @settings)
133
+ amount.to_f.negative? ? formatted.green : formatted.red
137
134
  end
138
135
  end
139
136
  end
@@ -0,0 +1,63 @@
1
+ module HledgerForecast
2
+ ANNUAL_MULTIPLIERS = {
3
+ "monthly" => 12,
4
+ "quarterly" => 4,
5
+ "half-yearly" => 2,
6
+ "yearly" => 1,
7
+ "once" => 1,
8
+ "daily" => 352,
9
+ "weekly" => 52
10
+ }.freeze
11
+
12
+ Transaction = Struct.new(
13
+ :type,
14
+ :frequency,
15
+ :account,
16
+ :from,
17
+ :to,
18
+ :description,
19
+ :category,
20
+ :amount,
21
+ :roll_up,
22
+ :summary_exclude,
23
+ :tags,
24
+ keyword_init: true
25
+ ) do
26
+ def self.from_row(row)
27
+ from = Date.parse(row[:from].to_s)
28
+ new(
29
+ type: row[:type],
30
+ frequency: row[:frequency],
31
+ account: row[:account],
32
+ from: from,
33
+ to: row[:to] ? Calculator.evaluate_date(from, row[:to].to_s) : nil,
34
+ description: row[:description],
35
+ category: row[:category],
36
+ amount: Calculator.evaluate(row[:amount]),
37
+ roll_up: row[:roll_up],
38
+ summary_exclude: row[:summary_exclude],
39
+ tags: row[:tag].to_s.split("|").map(&:strip).reject(&:empty?)
40
+ )
41
+ end
42
+
43
+ def matches_tags?(filter_tags)
44
+ return true if filter_tags.nil? || filter_tags.empty?
45
+ (tags & filter_tags).any?
46
+ end
47
+
48
+ def annualised_amount
49
+ if roll_up
50
+ amount * roll_up
51
+ else
52
+ amount *
53
+ ANNUAL_MULTIPLIERS.fetch(type) {
54
+ raise(KeyError, "Unknown type '#{type}'. Set a roll-up for custom transactions.")
55
+ }
56
+ end
57
+ end
58
+
59
+ def summary_exclude? = !!summary_exclude
60
+ end
61
+
62
+ TransactionGroup = Struct.new(:type, :frequency, :account, :from, :to, :transactions, keyword_init: true)
63
+ end
@@ -1,100 +1,73 @@
1
1
  module HledgerForecast
2
2
  module Transactions
3
- # Generate default hledger transactions
3
+ # Generate hledger periodic transactions from TransactionGroups.
4
4
  # Example output:
5
- # ~ monthly from 2023-05-1 * Food expenses
5
+ # ~ monthly from 2023-05-01 * Food expenses
6
6
  # Expenses:Groceries $250.00 ; Food expenses
7
7
  # Assets:Checking
8
8
  class Default
9
- def self.generate(forecast, settings)
10
- new(forecast, settings).generate
9
+ def self.render(groups, settings)
10
+ new(groups, settings).render
11
11
  end
12
12
 
13
- def generate
14
- forecast.each do |row|
15
- next if row[:type] == "settings"
16
-
17
- process_transactions(row)
18
- end
19
-
20
- output
13
+ def render
14
+ groups.map { |group| render_group(group) }.join.gsub(/\n{2,}/, "\n\n")
21
15
  end
22
16
 
23
17
  private
24
18
 
25
- attr_reader :forecast, :settings, :output
19
+ attr_reader :groups, :settings
26
20
 
27
- def initialize(forecast, settings)
28
- @forecast = forecast
21
+ def initialize(groups, settings)
22
+ @groups = groups
29
23
  @settings = settings
30
- @output = []
24
+ precompute_padding
31
25
  end
32
26
 
33
- def process_transactions(row)
34
- to = build_to_header(row[:to])
35
- frequency = get_periodic_rules(row[:type], row[:frequency])
36
-
37
- if @settings[:verbose]
38
- description = row[:description]
39
- transactions = [row]
40
- else
41
- description = get_descriptions(row[:transactions])
42
- transactions = row[:transactions]
43
- end
44
-
45
- header = build_header(row, frequency, to, description)
46
- footer = build_footer(row)
27
+ def precompute_padding
28
+ all_transactions = groups.flat_map(&:transactions)
29
+ formatted_amounts = all_transactions.map { |t| Formatter.format_money(t.amount, settings) }
47
30
 
48
- output << build_transaction(header, transactions, footer)
31
+ @max_amount = formatted_amounts.map(&:length).max || 0
32
+ @max_category = all_transactions.map { |t| t.category.to_s.length }.max || 0
49
33
  end
50
34
 
51
- def build_header(row, frequency, to, descriptions)
52
- "#{frequency} #{row[:from]}#{to} * #{descriptions}\n"
35
+ def render_group(group)
36
+ render_header(group) + render_postings(group) + " #{group.account}\n\n"
53
37
  end
54
38
 
55
- def build_footer(block)
56
- " #{block[:account]}\n\n"
39
+ def render_header(group)
40
+ to_part = " to #{group.to}" if group.to
41
+ descriptions = group.transactions.map(&:description).join(", ")
42
+ "#{periodic_rule_for(group.type, group.frequency)} #{group.from}#{to_part} * #{descriptions}\n"
57
43
  end
58
44
 
59
- def build_transaction(header, transactions, footer)
60
- { header: header, transactions: write_transactions(transactions), footer: footer }
45
+ def render_postings(group)
46
+ group
47
+ .transactions
48
+ .map do |t|
49
+ amount = Formatter.format_money(t.amount, settings)
50
+ category = t.category.to_s.ljust(@max_category)
51
+
52
+ if t.tags.any?
53
+ tags_str = t.tags.map { |tag| "#{tag}:" }.join(", ")
54
+ " #{category} #{amount.ljust(@max_amount)}; #{tags_str}\n"
55
+ else
56
+ " #{category} #{amount}\n"
57
+ end
58
+ end
59
+ .join
61
60
  end
62
61
 
63
- def build_to_header(to)
64
- return " to #{to}" if to
65
- end
66
-
67
- def get_descriptions(transactions)
68
- transactions.map do |t|
69
- # Skip transactions that have been marked as tracked
70
- next if t[:track]
71
-
72
- t[:description]
73
- end.compact.join(', ')
74
- end
75
-
76
- def get_periodic_rules(type, frequency)
77
- map = {
78
- 'once' => '~',
79
- 'monthly' => '~ monthly from',
80
- 'quarterly' => '~ every 3 months from',
81
- 'half-yearly' => '~ every 6 months from',
82
- 'yearly' => '~ yearly from',
83
- 'custom' => "~ #{frequency} from"
84
- }
85
- map[type]
86
- end
87
-
88
- def write_transactions(transactions)
89
- transactions.map do |t|
90
- # Skip transactions that have been marked as tracked
91
- next if t[:track]
92
-
93
- t[:amount] = t[:amount].to_s.ljust(@settings[:max_amount] + 5)
94
- t[:category] = t[:category].to_s.ljust(@settings[:max_category])
95
-
96
- " #{t[:category]} #{t[:amount]}; #{t[:description]}\n"
97
- end.compact
62
+ def periodic_rule_for(type, frequency)
63
+ {
64
+ "once" => "~",
65
+ "monthly" => "~ monthly from",
66
+ "quarterly" => "~ every 3 months from",
67
+ "half-yearly" => "~ every 6 months from",
68
+ "yearly" => "~ yearly from",
69
+ "custom" => "~ #{frequency} from"
70
+ }.fetch(type)
98
71
  end
99
72
  end
100
73
  end
@@ -1,3 +1,3 @@
1
1
  module HledgerForecast
2
- VERSION = "2.0.0".freeze
2
+ VERSION = "3.0.0"
3
3
  end