hledger-forecast 0.1.7 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 119f25363a44375cf21a44614306b714f9beacc8e6c87fd0cd8b527a601d039d
4
- data.tar.gz: 7bc635c9122980d9c4f2d321b2572b39113f9b6db409d913fe0fc7468dabeb6e
3
+ metadata.gz: f6dc9ef51b0f8d7927eb2b798d66257b740fad83be62d86c8207a5982aac752f
4
+ data.tar.gz: aec51b551655e2927de40237c64db32cfd4f4ebc40c855cd2bf97601e27112e2
5
5
  SHA512:
6
- metadata.gz: 6da28ae5b49a274894c089b0fad8adac80544beee0d1ff435cbd30c435c429a65ddec379d5e73efc09e0f6ad5eef6a30316d05a769a34b7a5ee902e13c58973d
7
- data.tar.gz: 8acb5c8ab9e229eacc914e6cb2c341eb5fc57bb4e1f70d44e60904e5030de745e2e9357036c1f6fb5f7a56e4d56d592ea0da145ef4c508153512d2b6cdcbb938
6
+ metadata.gz: bf00818fbbb03977e633c1e4c4eba8dd3596a6150e87117aa04e2d4b0df94c83593bef598200fa524a5d7c1777ce0be4bec2c0c4f4f0f8c9cc362732a3c8a1dc
7
+ data.tar.gz: 7a093260f8624ba0a27ead898fcda52926a00cfa12ed9f3bf67e020eb8d3df0cdfe0aa498164f0d07975f7c39efac9f0c39dd282fdd14e6684328d94b3ebbac1
data/Gemfile CHANGED
@@ -4,5 +4,6 @@ source "https://rubygems.org"
4
4
 
5
5
  gem 'colorize'
6
6
  gem 'money'
7
+ gem 'terminal-table'
7
8
 
8
9
  gemspec
data/README.md CHANGED
@@ -2,18 +2,17 @@
2
2
 
3
3
  [![Tests](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml/badge.svg)](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml)
4
4
 
5
- > **Warning**: This is still in the early stages of development and the API is likely to change
6
-
7
- Uses a YAML file to generate monthly, quarterly, yearly and one-off transactions for better forecasting in [Hledger](https://github.com/simonmichael/hledger).
5
+ Uses a YAML file to generate periodic transactions for better forecasting in [Hledger](https://github.com/simonmichael/hledger).
8
6
 
9
7
  See the [rationale](#brain-rationale) section for why this gem may be useful to you.
10
8
 
11
9
  ## :sparkles: Features
12
10
 
13
11
  - :book: Uses a simple YAML config file to generate periodic transactions
14
- - :date: Specify start and end dates for forecasts
12
+ - :date: Generate forecasts between specified start and end dates
15
13
  - :heavy_dollar_sign: Full currency support (uses the [RubyMoney](https://github.com/RubyMoney/money) gem)
16
14
  - :computer: Simple and easy to use CLI
15
+ - :chart_with_upwards_trend: Summarize your forecasts by period and category and output to the CLI
17
16
 
18
17
  ## :package: Installation
19
18
 
@@ -23,10 +22,12 @@ Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) instal
23
22
 
24
23
  ## :rocket: Usage
25
24
 
26
- Simply run:
25
+ Run:
27
26
 
28
27
  hledger-forecast
29
28
 
29
+ > **Note**: This assumes that a `forecast.yml` exists in the current working directory
30
+
30
31
  Running `hledger-forecast -h` shows the available options:
31
32
 
32
33
  Usage: Hledger-Forecast [options]
@@ -41,18 +42,28 @@ Running `hledger-forecast -h` shows the available options:
41
42
  -h, --help Show this message
42
43
  --version Show version
43
44
 
44
- To then include in Hledger:
45
+ Another example of a common command:
46
+
47
+ hledger-forecast -f my_forecast.yml -s 2023-05-01 -e 2024-12-31
48
+
49
+ This will generate an output file (`my_forecast.journal`) from the forecast file between the two date ranges.
50
+
51
+ ### Using with Hledger
52
+
53
+ To use the outputs in Hledger:
45
54
 
46
- hledger -f transactions.journal -f forecast.journal
55
+ hledger -f transactions.journal -f my_forecast.journal
47
56
 
48
57
  where:
49
58
 
50
59
  - `transactions.journal` might be your bank transactions (your "_actuals_")
51
- - `forecast.journal` is the generated forecast file
60
+ - `my_forecast.journal` is the generated forecast file
52
61
 
53
- ### A simple config file
62
+ ## :gear: Configuration
54
63
 
55
- > **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for all of the options
64
+ ### The YAML file
65
+
66
+ > **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for an example of a complex config file
56
67
 
57
68
  Firstly, create a `yml` file which will contain the transactions you'd like to forecast:
58
69
 
@@ -68,6 +79,9 @@ monthly:
68
79
  - amount: 500
69
80
  category: "[Expenses:Food]"
70
81
  description: Food
82
+
83
+ settings:
84
+ currency: GBP
71
85
  ```
72
86
 
73
87
  Let's examine what's going on in this config file:
@@ -77,50 +91,98 @@ Let's examine what's going on in this config file:
77
91
  - Notice we're also using [virtual postings](https://hledger.org/1.29/hledger.html#virtual-postings) (designated by the brackets). This makes it easy to filter them out with the `-R` or `--real` option in Hledger
78
92
  - We also have not specified a currency; the default (`USD`) will be used
79
93
 
80
- ### Extending the config file
94
+ ### Periods
81
95
 
82
- #### Periods
96
+ Besides monthly recurring transactions, the app also supports the following periods:
83
97
 
84
- If you'd like to add quarterly, half-yearly, yearly or one-off transactions, use the following keys:
98
+ - `quarterly` - For transactions every _3 months_ from the given start date
99
+ - `half-yearly` - For transactions every _6 months_ from the given start date
100
+ - `yearly` - Generate transactions _once a year_ from the given start date
101
+ - `once` - Generate _one-time_ transactions on a specified date
102
+ - `custom` - Generate transactions every _n days/weeks/months_
85
103
 
86
- - `quarterly`
87
- - `half-yearly`
88
- - `yearly`
89
- - `once`
104
+ #### Custom period
90
105
 
91
- The structure of the config file remains exactly the same.
106
+ A custom period allows you to specify a given number of days, weeks or months for a transaction to repeat within. These can be included in the config file as follows:
92
107
 
93
- > **Note**: A quarterly transaction will repeat for every 3 months from the start date
108
+ ```yaml
109
+ custom:
110
+ - description: Fortnightly hair and beauty spend
111
+ recurrence:
112
+ period: weeks
113
+ quantity: 2
114
+ account: "[Assets:Bank]"
115
+ start: "2023-03-01"
116
+ transactions:
117
+ - amount: 80
118
+ category: "[Expenses:Personal Care]"
119
+ description: Hair and beauty
120
+ ```
121
+
122
+ Where `quantity` is an integer and `period` is one of:
123
+
124
+ - days
125
+ - weeks
126
+ - months
94
127
 
95
- #### Currency
128
+ ### Dates
96
129
 
97
- To specify a currency:
130
+ The core of any solid forecast is predicting the correct periods that costs will fall into. When running the app from the CLI, you can specify specific dates to generate transactions over (see the [usage](#rocket-usage) section).
131
+
132
+ You can further control the dates at a period/top-level as well as at a transaction level:
133
+
134
+ #### Top level
135
+
136
+ In the example below, all transactions in the `monthly` block will be constrained by the end date:
98
137
 
99
138
  ```yaml
100
- # forecast.yml
101
- settings:
102
- currency: GBP
139
+ monthly:
140
+ - account: "[Assets:Bank]"
141
+ start: "2023-03-01"
142
+ end: "2025-01-01"
143
+ transactions:
144
+ # details omitted for brevity
103
145
  ```
104
146
 
105
- #### Additional settings
147
+ #### Transaction level
106
148
 
107
- Additional settings in the config file:
149
+ In the example below, only the single transaction will be constrained by the end date and controlled via an additional start date:
150
+
151
+ ```yaml
152
+ monthly:
153
+ - account: "[Assets:Bank]"
154
+ start: "2023-03-01"
155
+ transactions:
156
+ - amount: 2000
157
+ category: "[Expenses:Mortgage]"
158
+ description: Mortgage
159
+ start: "2023-05-01"
160
+ end: "2025-01-01"
161
+ ```
162
+
163
+ The addition of the `start` key means that while the block will start on 2023-03-01, the transaction for the mortgage won't start until `2023-05-01`.
164
+
165
+ ### Additional settings
166
+
167
+ Additional settings in the config file to consider:
108
168
 
109
169
  ```yaml
110
- # forecast.yml
111
170
  settings:
171
+ currency: GBP # Specify the currency to use
112
172
  show_symbol: true # Show the currency symbol?
113
173
  sign_before_symbol: true # Show the negative sign before the symbol?
114
174
  thousands_separator: true # Separate thousands with a comma?
115
175
  ```
116
176
 
117
- ### Summarizing the config file
177
+ ## :rainbow: Helpers
178
+
179
+ ### Summarizing the forecast file
118
180
 
119
181
  As your config file grows, it can be helpful to sum up the total amounts and output them in the CLI. This can be achieved by:
120
182
 
121
- hledger-forecast -f forecast.yml --summarize
183
+ hledger-forecast -f my_forecast.yml --summarize
122
184
 
123
- where `forecast.yml` is the config file to sum up.
185
+ where `my_forecast.yml` is the config file to sum up.
124
186
 
125
187
  ## :brain: Rationale
126
188
 
@@ -128,8 +190,8 @@ Firstly, I've come to realise from reading countless blog and Reddit posts on [p
128
190
 
129
191
  My days working in financial modelling have meant that a big macro-enabled spreadsheet was my go-to tool. Growing tired with the manual approach of importing transactions, heavily manipulating them, watching Excel become increasingly slower lead me to PTA. It's been a wonderful discovery.
130
192
 
131
- One of the aspects of my previous approach to personal finance that I liked was the monthly recap of my performance and the looking ahead to the future. Am I still on track to hit my year-end savings goal? Am I still on track to hit my savings goal for 12, 24 months time? It was at this point in my shift to PTA that I hit a wall.
193
+ One of the aspects of my previous approach to personal finance that I liked was the monthly recap of my performance and the looking ahead to the future. Am I still on track to hit my year-end savings goal given my future commitments? Am I still on track to hit my savings goal in 12 and 24 months time? It was at this point in my shift to PTA that I found it difficult to answer those questions with Hledger.
132
194
 
133
- While Hledger provides support for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions, these are computed virtually at runtime. If I notice a big difference in my forecasted year-end balance compared to what I'm expecting, I want to investigate and start reconcilling. Computed transactions make this difficult and tiresome.
195
+ While there is support for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions in Hledger, these are computed virtually at runtime. If I notice a big difference in my forecasted year-end balance compared to what I'm expecting, I want to investigate and start reconcilling. Computed transactions make this nigh on impossible to unpick. Also, I get a lot of value out of running different forecast scenarios and seeing the impact. For example, _"what's my savings balance looking like in 3 years time if I get the kitchen remodelled?"_.
134
196
 
135
- With this gem, my aim was to make it easy for users to change a config file, re-run a CLI command and be able to open a text file and see the changes. No guesswork. No surprises.
197
+ With this gem, my aim was to make it easy for users to change their config file, regenerate the forecast and open a journal file and see the transactions. Or, use multiple forecast files for different scenarios and pass them in turn to Hledger to observe the impact.
data/example.yml CHANGED
@@ -1,17 +1,24 @@
1
1
  monthly:
2
2
  - account: "[Assets:Bank]"
3
- date: "2023-03-01"
3
+ start: "2023-03-01"
4
4
  transactions:
5
- - amount: 1000.00
6
- category: "[Expenses:Mortgage]"
7
- description: Mortgage
5
+ - amount: -100
6
+ category: "[Income:Bonus]"
7
+ description: Bonus
8
+ - amount: -2000
9
+ category: "[Income:Salary]"
10
+ description: Salary
8
11
  - amount: 500.00
9
12
  category: "[Expenses:Food]"
10
13
  description: Food
14
+ - amount: 1000.00
15
+ category: "[Expenses:Mortgage]"
16
+ description: Mortgage
17
+ end: "2024-01-01"
11
18
 
12
19
  quarterly:
13
20
  - account: "[Assets:Bank]"
14
- date: "2023-04-01"
21
+ start: "2023-04-01"
15
22
  transactions:
16
23
  - amount: -1000.00
17
24
  category: "[Income:Bonus]"
@@ -19,7 +26,7 @@ quarterly:
19
26
 
20
27
  half-yearly:
21
28
  - account: "[Assets:Bank]"
22
- date: "2023-04-01"
29
+ start: "2023-04-01"
23
30
  transactions:
24
31
  - amount: 500
25
32
  category: "[Expenses:Holiday]"
@@ -27,7 +34,7 @@ half-yearly:
27
34
 
28
35
  yearly:
29
36
  - account: "[Assets:Bank]"
30
- date: "2023-04-01"
37
+ start: "2023-04-01"
31
38
  transactions:
32
39
  - amount: -2000.00
33
40
  category: "[Income:Bonus]"
@@ -35,12 +42,24 @@ yearly:
35
42
 
36
43
  once:
37
44
  - account: "[Assets:Bank]"
38
- date: "2024-01-01"
45
+ start: "2024-01-01"
39
46
  transactions:
40
47
  - amount: 5000.00
41
48
  category: "[Expenses:Car]"
42
49
  description: Forecast new car cost
43
50
 
51
+ custom:
52
+ - description: Fuel every 5 days
53
+ recurrence:
54
+ period: days
55
+ quantity: 5
56
+ account: "[Assets:Bank]"
57
+ start: "2023-03-01"
58
+ transactions:
59
+ - amount: 150
60
+ category: "[Expenses:Car:Fuel]"
61
+ description: Car fuel
62
+
44
63
  settings:
45
64
  currency: GBP
46
65
  show_symbol: true
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.add_dependency "highline", "~> 2.1.0"
16
16
  s.add_dependency "money", "~> 6.16.0"
17
17
  s.add_dependency "colorize", "~> 0.8.1"
18
+ s.add_dependency "terminal-table", "~> 3.0.2"
18
19
  s.add_development_dependency 'rspec', '~> 3.12'
19
20
 
20
21
  s.files = `git ls-files`.split("\n")
@@ -1,7 +1,6 @@
1
1
  module HledgerForecast
2
2
  # Generates journal entries based on a YAML forecast file.
3
3
  # on forecast data and optional existing transactions.
4
- #
5
4
  class Generator
6
5
  class << self
7
6
  attr_accessor :settings
@@ -26,15 +25,46 @@ module HledgerForecast
26
25
  formatted_transaction = transaction.clone
27
26
 
28
27
  formatted_transaction['amount'] =
29
- Money.from_cents(formatted_transaction['amount'].to_i * 100, @settings[:currency]).format(
28
+ Money.from_cents(formatted_transaction['amount'].to_f * 100, @settings[:currency]).format(
30
29
  symbol: @settings[:show_symbol],
31
30
  sign_before_symbol: @settings[:sign_before_symbol],
32
- thousands_separator: @settings[:thousands_separator] ? ',' : nil,
31
+ thousands_separator: @settings[:thousands_separator] ? ',' : nil
33
32
  )
34
33
 
35
34
  formatted_transaction
36
35
  end
37
36
 
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']
44
+
45
+ next if end_date && date > end_date
46
+
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
55
+
56
+ if date_matches
57
+ forecast['transactions'].each do |transaction|
58
+ end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
59
+
60
+ next unless end_date.nil? || date <= end_date
61
+
62
+ write_transactions(output, date, account, format_transaction(transaction))
63
+ end
64
+ end
65
+ end
66
+ end
67
+
38
68
  def self.process_forecast(output_file, forecast_data, type, date)
39
69
  forecast_data[type]&.each do |forecast|
40
70
  start_date = Date.parse(forecast['start'])
@@ -58,9 +88,12 @@ module HledgerForecast
58
88
 
59
89
  if date_matches
60
90
  forecast['transactions'].each do |transaction|
61
- end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
91
+ transaction_start_date = transaction['start'] ? Date.parse(transaction['start']) : nil
92
+ transaction_end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
62
93
 
63
- next unless end_date.nil? || date <= end_date
94
+ if (transaction_start_date && date < transaction_start_date) || (transaction_end_date && date > transaction_end_date)
95
+ next
96
+ end
64
97
 
65
98
  write_transactions(output_file, date, account, format_transaction(transaction))
66
99
  end
@@ -86,6 +119,7 @@ module HledgerForecast
86
119
  process_forecast(output, forecast_data, 'half-yearly', date)
87
120
  process_forecast(output, forecast_data, 'yearly', date)
88
121
  process_forecast(output, forecast_data, 'once', date)
122
+ process_custom(output, forecast_data, date)
89
123
 
90
124
  date = date.next_day
91
125
  end
@@ -10,6 +10,7 @@ module HledgerForecast
10
10
  opts.on("-f", "--forecast FILE",
11
11
  "The FORECAST yaml file to generate from") do |file|
12
12
  options[:forecast_file] = file
13
+ options[:output_file] ||= file.sub(/\.yml$/, '.journal')
13
14
  end
14
15
 
15
16
  opts.on("-t", "--transaction FILE",
@@ -1,68 +1,135 @@
1
1
  module HledgerForecast
2
2
  # Summarise a forecast YAML file and output it to the CLI
3
3
  class Summarize
4
+ @table = nil
5
+ @generator = nil
6
+
7
+ def self.init_table
8
+ table = Terminal::Table.new
9
+
10
+ table.add_row([{ value: 'FORECAST SUMMARY'.bold, colspan: 3, alignment: :center }])
11
+ table.add_separator
12
+
13
+ @table = table
14
+ end
15
+
16
+ def self.init_generator(forecast_data)
17
+ generator = HledgerForecast::Generator
18
+ generator.configure_settings(forecast_data)
19
+
20
+ @generator = generator
21
+ end
22
+
4
23
  def self.sum_transactions(forecast_data, period)
5
- category_totals = Hash.new(0)
24
+ category_total = Hash.new(0)
6
25
  forecast_data[period]&.each do |entry|
7
26
  entry['transactions'].each do |transaction|
8
- category_totals[transaction['category']] += transaction['amount']
27
+ category_total[transaction['category']] += transaction['amount']
9
28
  end
10
29
  end
11
- category_totals
30
+
31
+ category_total
12
32
  end
13
33
 
14
- def self.print_category_totals(period, category_totals, generator)
15
- puts "#{period.capitalize}:"
16
- period_total = 0
34
+ def self.sum_custom_transactions(forecast_data)
35
+ category_total = Hash.new(0)
36
+ custom_periods = []
37
+
38
+ forecast_data['custom']&.each do |entry|
39
+ period_data = {}
40
+ period_data[:quantity] = entry['recurrence']['quantity']
41
+ period_data[:period] = entry['recurrence']['period']
42
+ period_data[:category] = entry['transactions'].first['category']
43
+ period_data[:amount] = entry['transactions'].first['amount']
44
+
45
+ entry['transactions'].each do |transaction|
46
+ category_total[transaction['category']] += transaction['amount']
47
+ end
17
48
 
18
- category_totals.each do |category, amount|
19
- formatted_amount = generator.format_transaction({ 'amount' => amount })['amount']
20
- formatted_amount = amount.to_i < 0 ? formatted_amount.red : formatted_amount.green
21
- puts " #{category.ljust(40)}#{formatted_amount}"
22
- period_total += amount
49
+ custom_periods << period_data
23
50
  end
24
51
 
25
- formatted_period_total = generator.format_transaction({ 'amount' => period_total })['amount']
26
- formatted_period_total = period_total.to_i < 0 ? formatted_period_total.red : formatted_period_total.green
27
- puts " TOTAL".ljust(42) + formatted_period_total
52
+ { totals: category_total, periods: custom_periods }
28
53
  end
29
54
 
30
- def self.sum_all_periods(forecast_data)
31
- periods = %w[monthly quarterly half-yearly yearly once]
32
- total = {}
33
- grand_total = 0
55
+ def self.format_amount(amount)
56
+ formatted_amount = @generator.format_transaction({ 'amount' => amount })['amount']
57
+ amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
58
+ end
59
+
60
+ def self.add_rows_to_table(row_data, period_total, custom: false)
61
+ if custom
62
+ row_data[:periods].each do |period|
63
+ @table.add_row [{ value: period[:category], alignment: :left },
64
+ { value: "every #{period[:quantity]} #{period[:period]}", alignment: :right },
65
+ { value: format_amount(period[:amount]), alignment: :right }]
66
+
67
+ period_total += period[:amount]
68
+ end
69
+ else
70
+ row_data.each do |category, amount|
71
+ @table.add_row [{ value: category, colspan: 2, alignment: :left },
72
+ { value: format_amount(amount), alignment: :right }]
34
73
 
35
- periods.each do |period|
36
- total[period] = sum_transactions(forecast_data, period)
37
- grand_total += total[period]
74
+ period_total += amount
75
+ end
38
76
  end
39
77
 
40
- total['total'] = grand_total
41
- total
78
+ period_total
42
79
  end
43
80
 
44
- def self.generate(forecast)
45
- forecast_data = YAML.safe_load(forecast)
81
+ def self.add_categories_to_table(categories, forecast_data)
82
+ first_period = true
83
+ categories.each do |period, total|
84
+ category_total = total.reject { |_, amount| amount == 0 }
85
+ next if category_total.empty?
86
+
87
+ sorted_category_total = sort_transactions(category_total)
88
+
89
+ @table.add_separator unless first_period
90
+ @table.add_row([{ value: period.capitalize.bold, colspan: 3, alignment: :center }])
46
91
 
47
- category_totals_by_period = {}
48
- %w[monthly quarterly half-yearly yearly once].each do |period|
49
- category_totals_by_period[period] = sum_transactions(forecast_data, period)
92
+ period_total = 0
93
+ period_total += if period == 'custom'
94
+ add_rows_to_table(sum_custom_transactions(forecast_data), period_total, custom: true)
95
+ else
96
+ add_rows_to_table(sorted_category_total, period_total)
97
+ end
98
+
99
+ format_total("#{period.capitalize} TOTAL", period_total)
100
+ first_period = false
50
101
  end
102
+ end
51
103
 
52
- grand_total = category_totals_by_period.values.map(&:values).flatten.sum
104
+ def self.sort_transactions(category_total)
105
+ negatives = category_total.select { |_, amount| amount < 0 }.sort_by { |_, amount| amount }
106
+ positives = category_total.select { |_, amount| amount > 0 }.sort_by { |_, amount| -amount }
53
107
 
54
- generator = HledgerForecast::Generator
55
- generator.configure_settings(forecast_data)
108
+ negatives.concat(positives).to_h
109
+ end
110
+
111
+ def self.format_total(text, total)
112
+ @table.add_row [{ value: text.bold, colspan: 2, alignment: :left },
113
+ { value: format_amount(total).bold, alignment: :right }]
114
+ end
56
115
 
57
- puts
58
- category_totals_by_period.each do |period, category_totals|
59
- print_category_totals(period, category_totals, generator)
60
- puts
116
+ def self.generate(forecast)
117
+ forecast_data = YAML.safe_load(forecast)
118
+
119
+ init_table
120
+ init_generator(forecast_data)
121
+
122
+ category_totals = {}
123
+ %w[monthly quarterly half-yearly yearly once custom].each do |period|
124
+ category_totals[period] = sum_transactions(forecast_data, period)
61
125
  end
62
126
 
63
- formatted_grand_total = generator.format_transaction({ 'amount' => grand_total })['amount']
64
- formatted_grand_total = grand_total.to_i < 0 ? formatted_grand_total.red : formatted_grand_total.green
65
- puts "TOTAL:".ljust(42) + formatted_grand_total
127
+ add_categories_to_table(category_totals, forecast_data)
128
+
129
+ @table.add_separator
130
+ format_total("TOTAL", category_totals.values.map(&:values).flatten.sum)
131
+
132
+ puts @table
66
133
  end
67
134
  end
68
135
  end
@@ -1,3 +1,3 @@
1
1
  module HledgerForecast
2
- VERSION = "0.1.7"
2
+ VERSION = "0.1.9"
3
3
  end
@@ -5,6 +5,7 @@ require 'date'
5
5
  require 'highline'
6
6
  require 'money'
7
7
  require 'optparse'
8
+ require 'terminal-table'
8
9
  require 'yaml'
9
10
 
10
11
  Money.locale_backend = nil
@@ -0,0 +1,47 @@
1
+ require_relative '../lib/hledger_forecast'
2
+
3
+ RSpec.describe 'generate' do
4
+ it 'generates a forecast with correct CUSTOM DAILY transactions' do
5
+ transactions = File.read('spec/stubs/transactions.journal')
6
+ forecast = File.read('spec/stubs/custom/forecast_custom_days.yml')
7
+
8
+ generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
9
+ '2023-03-10')
10
+
11
+ expected_output = File.read('spec/stubs/custom/output_custom_days.journal')
12
+ expect(generated_journal).to eq(expected_output)
13
+ end
14
+
15
+ it 'generates a forecast with correct CUSTOM WEEKlY transactions' do
16
+ transactions = File.read('spec/stubs/transactions.journal')
17
+ forecast = File.read('spec/stubs/custom/forecast_custom_weeks.yml')
18
+
19
+ generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
20
+ '2023-04-30')
21
+
22
+ expected_output = File.read('spec/stubs/custom/output_custom_weeks.journal')
23
+ expect(generated_journal).to eq(expected_output)
24
+ end
25
+
26
+ it 'generates a forecast with MULTIPLE correct CUSTOM WEEKlY transactions' do
27
+ transactions = File.read('spec/stubs/transactions.journal')
28
+ forecast = File.read('spec/stubs/custom/forecast_custom_weeks_twice.yml')
29
+
30
+ generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
31
+ '2023-03-30')
32
+
33
+ expected_output = File.read('spec/stubs/custom/output_custom_weeks_twice.journal')
34
+ expect(generated_journal).to eq(expected_output)
35
+ end
36
+
37
+ it 'generates a forecast with correct CUSTOM MONTHLY transactions' do
38
+ transactions = File.read('spec/stubs/transactions.journal')
39
+ forecast = File.read('spec/stubs/custom/forecast_custom_months.yml')
40
+
41
+ generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
42
+ '2024-02-28')
43
+
44
+ expected_output = File.read('spec/stubs/custom/output_custom_months.journal')
45
+ expect(generated_journal).to eq(expected_output)
46
+ end
47
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../lib/hledger_forecast'
2
+ RSpec.describe 'generate' do
3
+ it 'generates a forecast with correct MONTHLY transactions that have a START DATE' do
4
+ transactions = File.read('spec/stubs/transactions.journal')
5
+ forecast = File.read('spec/stubs/start_date/forecast_startdate.yml')
6
+
7
+ generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2023-08-30')
8
+
9
+ expected_output = File.read('spec/stubs/start_date/output_startdate.journal')
10
+ expect(generated_journal).to eq(expected_output)
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ custom:
2
+ - description: Repeat every 3 days
3
+ recurrence:
4
+ period: days
5
+ quantity: 3
6
+ account: "[Assets:Bank]"
7
+ start: "2023-03-01"
8
+ transactions:
9
+ - amount: 80
10
+ category: "[Expenses:Personal Care]"
11
+ description: Hair and beauty
12
+
13
+ settings:
14
+ currency: GBP
@@ -0,0 +1,14 @@
1
+ custom:
2
+ - description: Repeat every 5 months
3
+ recurrence:
4
+ period: months
5
+ quantity: 5
6
+ account: "[Assets:Bank]"
7
+ start: "2023-03-01"
8
+ transactions:
9
+ - amount: 100
10
+ category: "[Expenses:Personal Care]"
11
+ description: Hair and beauty
12
+
13
+ settings:
14
+ currency: GBP
@@ -0,0 +1,14 @@
1
+ custom:
2
+ - description: Repeat every 2 weeks
3
+ recurrence:
4
+ period: weeks
5
+ quantity: 2
6
+ account: "[Assets:Bank]"
7
+ start: "2023-03-01"
8
+ transactions:
9
+ - amount: 75
10
+ category: "[Expenses:Personal Care]"
11
+ description: Hair and beauty
12
+
13
+ settings:
14
+ currency: GBP
@@ -0,0 +1,24 @@
1
+ custom:
2
+ - description: Beauty expenses every 2 weeks
3
+ recurrence:
4
+ period: weeks
5
+ quantity: 2
6
+ account: "[Assets:Bank]"
7
+ start: "2023-03-01"
8
+ transactions:
9
+ - amount: 75
10
+ category: "[Expenses:Personal Care]"
11
+ description: Hair and beauty
12
+ - description: Car fuel every 5 days
13
+ recurrence:
14
+ period: days
15
+ quantity: 5
16
+ account: "[Assets:Bank]"
17
+ start: "2023-03-01"
18
+ transactions:
19
+ - amount: 150
20
+ category: "[Expenses:Car:Fuel]"
21
+ description: Car Fuel
22
+
23
+ settings:
24
+ currency: GBP
@@ -0,0 +1,24 @@
1
+ 2023-02-01 * Opening balance
2
+ Assets:Bank £1,000.00
3
+ Equity:Opening balance
4
+
5
+ 2023-02-05 * Mortgage payment
6
+ Expenses:Mortgage £1,500.00
7
+ Assets:Bank
8
+
9
+ 2023-03-01 * Hair and beauty
10
+ [Expenses:Personal Care] £80.00
11
+ [Assets:Bank]
12
+
13
+ 2023-03-04 * Hair and beauty
14
+ [Expenses:Personal Care] £80.00
15
+ [Assets:Bank]
16
+
17
+ 2023-03-07 * Hair and beauty
18
+ [Expenses:Personal Care] £80.00
19
+ [Assets:Bank]
20
+
21
+ 2023-03-10 * Hair and beauty
22
+ [Expenses:Personal Care] £80.00
23
+ [Assets:Bank]
24
+
@@ -0,0 +1,20 @@
1
+ 2023-02-01 * Opening balance
2
+ Assets:Bank £1,000.00
3
+ Equity:Opening balance
4
+
5
+ 2023-02-05 * Mortgage payment
6
+ Expenses:Mortgage £1,500.00
7
+ Assets:Bank
8
+
9
+ 2023-03-01 * Hair and beauty
10
+ [Expenses:Personal Care] £100.00
11
+ [Assets:Bank]
12
+
13
+ 2023-08-01 * Hair and beauty
14
+ [Expenses:Personal Care] £100.00
15
+ [Assets:Bank]
16
+
17
+ 2024-01-01 * Hair and beauty
18
+ [Expenses:Personal Care] £100.00
19
+ [Assets:Bank]
20
+
@@ -0,0 +1,28 @@
1
+ 2023-02-01 * Opening balance
2
+ Assets:Bank £1,000.00
3
+ Equity:Opening balance
4
+
5
+ 2023-02-05 * Mortgage payment
6
+ Expenses:Mortgage £1,500.00
7
+ Assets:Bank
8
+
9
+ 2023-03-01 * Hair and beauty
10
+ [Expenses:Personal Care] £75.00
11
+ [Assets:Bank]
12
+
13
+ 2023-03-15 * Hair and beauty
14
+ [Expenses:Personal Care] £75.00
15
+ [Assets:Bank]
16
+
17
+ 2023-03-29 * Hair and beauty
18
+ [Expenses:Personal Care] £75.00
19
+ [Assets:Bank]
20
+
21
+ 2023-04-12 * Hair and beauty
22
+ [Expenses:Personal Care] £75.00
23
+ [Assets:Bank]
24
+
25
+ 2023-04-26 * Hair and beauty
26
+ [Expenses:Personal Care] £75.00
27
+ [Assets:Bank]
28
+
@@ -0,0 +1,44 @@
1
+ 2023-02-01 * Opening balance
2
+ Assets:Bank £1,000.00
3
+ Equity:Opening balance
4
+
5
+ 2023-02-05 * Mortgage payment
6
+ Expenses:Mortgage £1,500.00
7
+ Assets:Bank
8
+
9
+ 2023-03-01 * Hair and beauty
10
+ [Expenses:Personal Care] £75.00
11
+ [Assets:Bank]
12
+
13
+ 2023-03-01 * Car Fuel
14
+ [Expenses:Car:Fuel] £150.00
15
+ [Assets:Bank]
16
+
17
+ 2023-03-06 * Car Fuel
18
+ [Expenses:Car:Fuel] £150.00
19
+ [Assets:Bank]
20
+
21
+ 2023-03-11 * Car Fuel
22
+ [Expenses:Car:Fuel] £150.00
23
+ [Assets:Bank]
24
+
25
+ 2023-03-15 * Hair and beauty
26
+ [Expenses:Personal Care] £75.00
27
+ [Assets:Bank]
28
+
29
+ 2023-03-16 * Car Fuel
30
+ [Expenses:Car:Fuel] £150.00
31
+ [Assets:Bank]
32
+
33
+ 2023-03-21 * Car Fuel
34
+ [Expenses:Car:Fuel] £150.00
35
+ [Assets:Bank]
36
+
37
+ 2023-03-26 * Car Fuel
38
+ [Expenses:Car:Fuel] £150.00
39
+ [Assets:Bank]
40
+
41
+ 2023-03-29 * Hair and beauty
42
+ [Expenses:Personal Care] £75.00
43
+ [Assets:Bank]
44
+
@@ -0,0 +1,13 @@
1
+ monthly:
2
+ - account: "[Assets:Bank]"
3
+ start: "2023-03-01"
4
+ transactions:
5
+ - amount: 2000
6
+ category: "[Expenses:Mortgage]"
7
+ description: Mortgage
8
+ modifier:
9
+ amount: 2.0
10
+ period: yearly
11
+
12
+ settings:
13
+ currency: GBP
@@ -0,0 +1,44 @@
1
+ 2023-02-01 * Opening balance
2
+ Assets:Bank £1,000.00
3
+ Equity:Opening balance
4
+
5
+ 2023-02-05 * Mortgage payment
6
+ Expenses:Mortgage £1,500.00
7
+ Assets:Bank
8
+
9
+ 2023-03-01 * Mortgage
10
+ [Expenses:Mortgage] £2,000.00
11
+ [Assets:Bank]
12
+
13
+ 2023-03-01 * Food
14
+ [Expenses:Food] £100.00
15
+ [Assets:Bank]
16
+
17
+ 2023-03-01 * Savings
18
+ [Assets:Bank] -£1,000.00
19
+ [Assets:Savings]
20
+
21
+ 2023-04-01 * Mortgage
22
+ [Expenses:Mortgage] £2,000.00
23
+ [Assets:Bank]
24
+
25
+ 2023-04-01 * Food
26
+ [Expenses:Food] £100.00
27
+ [Assets:Bank]
28
+
29
+ 2023-04-01 * Savings
30
+ [Assets:Bank] -£1,000.00
31
+ [Assets:Savings]
32
+
33
+ 2023-05-01 * Mortgage
34
+ [Expenses:Mortgage] £2,000.00
35
+ [Assets:Bank]
36
+
37
+ 2023-05-01 * Food
38
+ [Expenses:Food] £100.00
39
+ [Assets:Bank]
40
+
41
+ 2023-05-01 * Savings
42
+ [Assets:Bank] -£1,000.00
43
+ [Assets:Savings]
44
+
@@ -2,12 +2,18 @@ monthly:
2
2
  - account: "[Assets:Bank]"
3
3
  start: "2023-03-01"
4
4
  transactions:
5
- - amount: 2000
5
+ - amount: 2000.55
6
6
  category: "[Expenses:Mortgage]"
7
7
  description: Mortgage
8
8
  - amount: 100
9
9
  category: "[Expenses:Food]"
10
10
  description: Food
11
+ - account: "[Assets:Savings]"
12
+ start: "2023-03-01"
13
+ transactions:
14
+ - amount: -1000
15
+ category: "[Assets:Bank]"
16
+ description: Savings
11
17
 
12
18
  settings:
13
19
  currency: GBP
@@ -7,26 +7,38 @@
7
7
  Assets:Bank
8
8
 
9
9
  2023-03-01 * Mortgage
10
- [Expenses:Mortgage] £2,000.00
10
+ [Expenses:Mortgage] £2,000.55
11
11
  [Assets:Bank]
12
12
 
13
13
  2023-03-01 * Food
14
14
  [Expenses:Food] £100.00
15
15
  [Assets:Bank]
16
16
 
17
+ 2023-03-01 * Savings
18
+ [Assets:Bank] -£1,000.00
19
+ [Assets:Savings]
20
+
17
21
  2023-04-01 * Mortgage
18
- [Expenses:Mortgage] £2,000.00
22
+ [Expenses:Mortgage] £2,000.55
19
23
  [Assets:Bank]
20
24
 
21
25
  2023-04-01 * Food
22
26
  [Expenses:Food] £100.00
23
27
  [Assets:Bank]
24
28
 
29
+ 2023-04-01 * Savings
30
+ [Assets:Bank] -£1,000.00
31
+ [Assets:Savings]
32
+
25
33
  2023-05-01 * Mortgage
26
- [Expenses:Mortgage] £2,000.00
34
+ [Expenses:Mortgage] £2,000.55
27
35
  [Assets:Bank]
28
36
 
29
37
  2023-05-01 * Food
30
38
  [Expenses:Food] £100.00
31
39
  [Assets:Bank]
32
40
 
41
+ 2023-05-01 * Savings
42
+ [Assets:Bank] -£1,000.00
43
+ [Assets:Savings]
44
+
@@ -0,0 +1,26 @@
1
+ settings:
2
+ currency: GBP
3
+
4
+ monthly:
5
+ - start: "2023-03-01"
6
+ account: "[Assets:Bank]"
7
+ transactions:
8
+ - description: Monthly Mortgage
9
+ start: "2023-06-01"
10
+ category: "[Expenses:Mortgage]"
11
+ amount: 2000
12
+ - description: Monthly Food
13
+ category: "[Expenses:Food]"
14
+ amount: 100
15
+
16
+ quarterly:
17
+ - start: "2023-04-01"
18
+ account: "[Assets:Bank]"
19
+ transactions:
20
+ - description: Quarterly Mortgage
21
+ start: "2023-07-01"
22
+ category: "[Expenses:Mortgage]"
23
+ amount: 1000
24
+ - description: Quarterly Food
25
+ category: "[Expenses:Food]"
26
+ amount: 50
@@ -0,0 +1,56 @@
1
+ 2023-02-01 * Opening balance
2
+ Assets:Bank £1,000.00
3
+ Equity:Opening balance
4
+
5
+ 2023-02-05 * Mortgage payment
6
+ Expenses:Mortgage £1,500.00
7
+ Assets:Bank
8
+
9
+ 2023-03-01 * Monthly Food
10
+ [Expenses:Food] £100.00
11
+ [Assets:Bank]
12
+
13
+ 2023-04-01 * Monthly Food
14
+ [Expenses:Food] £100.00
15
+ [Assets:Bank]
16
+
17
+ 2023-04-01 * Quarterly Food
18
+ [Expenses:Food] £50.00
19
+ [Assets:Bank]
20
+
21
+ 2023-05-01 * Monthly Food
22
+ [Expenses:Food] £100.00
23
+ [Assets:Bank]
24
+
25
+ 2023-06-01 * Monthly Mortgage
26
+ [Expenses:Mortgage] £2,000.00
27
+ [Assets:Bank]
28
+
29
+ 2023-06-01 * Monthly Food
30
+ [Expenses:Food] £100.00
31
+ [Assets:Bank]
32
+
33
+ 2023-07-01 * Monthly Mortgage
34
+ [Expenses:Mortgage] £2,000.00
35
+ [Assets:Bank]
36
+
37
+ 2023-07-01 * Monthly Food
38
+ [Expenses:Food] £100.00
39
+ [Assets:Bank]
40
+
41
+ 2023-07-01 * Quarterly Mortgage
42
+ [Expenses:Mortgage] £1,000.00
43
+ [Assets:Bank]
44
+
45
+ 2023-07-01 * Quarterly Food
46
+ [Expenses:Food] £50.00
47
+ [Assets:Bank]
48
+
49
+ 2023-08-01 * Monthly Mortgage
50
+ [Expenses:Mortgage] £2,000.00
51
+ [Assets:Bank]
52
+
53
+ 2023-08-01 * Monthly Food
54
+ [Expenses:Food] £100.00
55
+ [Assets:Bank]
56
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hledger-forecast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oli Morris
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-15 00:00:00.000000000 Z
11
+ date: 2023-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.8.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: terminal-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.2
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rspec
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -90,12 +104,24 @@ files:
90
104
  - lib/hledger_forecast/summarize.rb
91
105
  - lib/hledger_forecast/version.rb
92
106
  - spec/command_spec.rb
107
+ - spec/custom_spec.rb
93
108
  - spec/half-yearly_spec.rb
94
109
  - spec/monthly_spec.rb
95
110
  - spec/once_spec.rb
96
111
  - spec/quarterly_spec.rb
112
+ - spec/start_date_spec.rb
113
+ - spec/stubs/custom/forecast_custom_days.yml
114
+ - spec/stubs/custom/forecast_custom_months.yml
115
+ - spec/stubs/custom/forecast_custom_weeks.yml
116
+ - spec/stubs/custom/forecast_custom_weeks_twice.yml
117
+ - spec/stubs/custom/output_custom_days.journal
118
+ - spec/stubs/custom/output_custom_months.journal
119
+ - spec/stubs/custom/output_custom_weeks.journal
120
+ - spec/stubs/custom/output_custom_weeks_twice.journal
97
121
  - spec/stubs/half-yearly/forecast_half-yearly.yml
98
122
  - spec/stubs/half-yearly/output_half-yearly.journal
123
+ - spec/stubs/modifiers/forecast_modifiers.yml
124
+ - spec/stubs/modifiers/output_modifiers.journal
99
125
  - spec/stubs/monthly/forecast_monthly.yml
100
126
  - spec/stubs/monthly/forecast_monthly_enddate.yml
101
127
  - spec/stubs/monthly/forecast_monthly_enddate_top.yml
@@ -108,6 +134,8 @@ files:
108
134
  - spec/stubs/once/output_once.journal
109
135
  - spec/stubs/quarterly/forecast_quarterly.yml
110
136
  - spec/stubs/quarterly/output_quarterly.journal
137
+ - spec/stubs/start_date/forecast_startdate.yml
138
+ - spec/stubs/start_date/output_startdate.journal
111
139
  - spec/stubs/transactions.journal
112
140
  - spec/stubs/yearly/forecast_yearly.yml
113
141
  - spec/stubs/yearly/output_yearly.journal
@@ -131,18 +159,30 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
159
  - !ruby/object:Gem::Version
132
160
  version: '0'
133
161
  requirements: []
134
- rubygems_version: 3.4.10
162
+ rubygems_version: 3.4.12
135
163
  signing_key:
136
164
  specification_version: 4
137
165
  summary: Utility to generate forecasts in Hledger
138
166
  test_files:
139
167
  - spec/command_spec.rb
168
+ - spec/custom_spec.rb
140
169
  - spec/half-yearly_spec.rb
141
170
  - spec/monthly_spec.rb
142
171
  - spec/once_spec.rb
143
172
  - spec/quarterly_spec.rb
173
+ - spec/start_date_spec.rb
174
+ - spec/stubs/custom/forecast_custom_days.yml
175
+ - spec/stubs/custom/forecast_custom_months.yml
176
+ - spec/stubs/custom/forecast_custom_weeks.yml
177
+ - spec/stubs/custom/forecast_custom_weeks_twice.yml
178
+ - spec/stubs/custom/output_custom_days.journal
179
+ - spec/stubs/custom/output_custom_months.journal
180
+ - spec/stubs/custom/output_custom_weeks.journal
181
+ - spec/stubs/custom/output_custom_weeks_twice.journal
144
182
  - spec/stubs/half-yearly/forecast_half-yearly.yml
145
183
  - spec/stubs/half-yearly/output_half-yearly.journal
184
+ - spec/stubs/modifiers/forecast_modifiers.yml
185
+ - spec/stubs/modifiers/output_modifiers.journal
146
186
  - spec/stubs/monthly/forecast_monthly.yml
147
187
  - spec/stubs/monthly/forecast_monthly_enddate.yml
148
188
  - spec/stubs/monthly/forecast_monthly_enddate_top.yml
@@ -155,6 +195,8 @@ test_files:
155
195
  - spec/stubs/once/output_once.journal
156
196
  - spec/stubs/quarterly/forecast_quarterly.yml
157
197
  - spec/stubs/quarterly/output_quarterly.journal
198
+ - spec/stubs/start_date/forecast_startdate.yml
199
+ - spec/stubs/start_date/output_startdate.journal
158
200
  - spec/stubs/transactions.journal
159
201
  - spec/stubs/yearly/forecast_yearly.yml
160
202
  - spec/stubs/yearly/output_yearly.journal