hledger-forecast 0.4.0 → 1.0.0

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: 85bd96722d50a8d5b2c8e8cbb9f973d88832827f74226d3b78d5f511b96265bf
4
- data.tar.gz: b69f6698e5c7294cd2b62c8672887044a416d0ed6624803293acd9246a84fa14
3
+ metadata.gz: 01faf9619f0b906b089956c1b81c16766830be412a678336848a7bbe23f78cb7
4
+ data.tar.gz: c93786627631d0f589250a8c721d8360a9feddf6f9b21954cc49b0b6b206ff29
5
5
  SHA512:
6
- metadata.gz: 2e3c4e2a1ef57546778525c37fa4b53854a10244f3e509c3bd48d8c3fcb49c2edc892c84f221f7980c56687ec4766f5379939ea82a06a06b238a8aa3f1a1ed8b
7
- data.tar.gz: 4fd49fc32b5fd5aa17e24ac166f384352a5cb85a72f57cb3aa774b8837c62c94439412252ba7164641b59c4cec7fc60a218eab1419dad913ea008c30058722d5
6
+ metadata.gz: 218ecb7ea26f4daee36981d345cf4a67052b6c3876f4f2090d608a86a10ba7e07889010fa91ab3618da60bfac98caf2c48bbdda78ab68f0290e744a580f75076
7
+ data.tar.gz: c3940bb307bbba32d374f0161ecaf5ca8bcdba2a1a0cafd0d1e4ea7f13ceea20a781dd0367eb02cd1c01fe73475f23fdcf1e520d3b76a9b7d875729971f221b7
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2']
15
+ ruby-version: ['3.0', '3.1', '3.2']
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v3
data/.gitignore CHANGED
@@ -10,6 +10,7 @@ pkg/*
10
10
  .rvmrc
11
11
  .ruby-version
12
12
  .ruby-gemset
13
+ .tool-versions
13
14
 
14
15
  # Spec artifacts
15
16
  /coverage
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
1
4
  Layout/LineLength:
2
5
  Max: 120
3
6
 
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
- # Hledger-Forecast
1
+ <h1 align="center">Hledger-Forecast</h1>
2
2
 
3
- [![Tests](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml/badge.svg)](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml)
3
+ <p align="center">
4
+ <a href="https://github.com/olimorris/hledger-forecast/stargazers"><img src="https://img.shields.io/github/stars/olimorris/hledger-forecast?color=c678dd&logoColor=e06c75&style=for-the-badge"></a>
5
+ <a href="https://github.com/olimorris/hledger-forecast/issues"><img src="https://img.shields.io/github/issues/olimorris/hledger-forecast?color=%23d19a66&style=for-the-badge"></a>
6
+ <a href="https://github.com/olimorris/hledger-forecast/blob/main/LICENSE"><img src="https://img.shields.io/github/license/olimorris/hledger-forecast?color=%2361afef&style=for-the-badge"></a>
7
+ <a href="https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/olimorris/hledger-forecast/ci.yml?branch=main&label=tests&style=for-the-badge"></a>
8
+ </p>
4
9
 
5
10
  A wrapper which builds on [hledger's](https://github.com/simonmichael/hledger) [forecasting](https://hledger.org/dev/hledger.html#forecasting) capability. Uses a `yaml` config file to generate forecasts whilst adding functionality for future cost rises (e.g. inflation) and the automatic tracking of planned transactions.
6
11
 
@@ -42,7 +47,7 @@ The available options are:
42
47
 
43
48
  ### Generate command
44
49
 
45
- The `hledger-forecast generate` command will generate a forecast _from_ a `yaml` file _to_ a journal file. You can see the output of this command in the [example.journal](https://github.com/olimorris/hledger-forecast/blob/main/example.journal) file.
50
+ The `hledger-forecast generate` command will generate a forecast _from_ a `yaml` file _to_ a journal file. You can see the output of this command in the [example.journal](https://github.com/olimorris/hledger-forecast/blob/main/example.journal) file.
46
51
 
47
52
  The available options are:
48
53
 
@@ -67,7 +72,7 @@ To work with hledger, include the forecast file and use the `--forecast` flag:
67
72
 
68
73
  The command will generate a forecast up to the end of Feb 2024, showing the balance for any asset accounts, overlaying some bank transactions with the forecast journal file. Of course, refer to the [hledger](https://hledger.org/dev/hledger.html) documentation for more information on how to query your finances.
69
74
 
70
- To apply any modifiers, use the `--auto` flag at the end of your command.
75
+ > **Note**: To apply any modifiers, use the `--auto` flag at the end of your command.
71
76
 
72
77
  ### Summarize command
73
78
 
@@ -84,9 +89,9 @@ The available options are:
84
89
 
85
90
  ## :gear: Configuration
86
91
 
87
- ### The YAML file
92
+ ### The yaml file
88
93
 
89
- > **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for an example of a complex config file and its [output](https://github.com/olimorris/hledger-forecast/blob/main/example.journal)
94
+ > **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for an example config and its corresponding [output](https://github.com/olimorris/hledger-forecast/blob/main/example.journal)
90
95
 
91
96
  Firstly, create a `yaml` file which will contain the transactions you'd like to forecast:
92
97
 
@@ -175,6 +180,17 @@ monthly:
175
180
  to: "2025-01-01"
176
181
  ```
177
182
 
183
+ It can also be useful to compute a `to` date by adding on a number of months to the `from` date. Extending the example above:
184
+
185
+ ```yaml
186
+ - amount: 2000
187
+ category: "Expenses:Mortgage"
188
+ description: Mortgage
189
+ to: "=12"
190
+ ```
191
+
192
+ This will take the `to` date to _2024-02-29_. This can be useful if you know a payment is due to end in _n_ months time and don't wish to use one of the many date calculators on the internet.
193
+
178
194
  ### Calculated amounts
179
195
 
180
196
  > **Note**: Calculations will be determined up to two decimal places
@@ -191,7 +207,7 @@ monthly:
191
207
  description: New Kitchen
192
208
  ```
193
209
 
194
- Simply ensure that the amount starts with an `=` sign, is enclosed in quotation marks and uses standard mathematical notations.
210
+ Simply ensure that the amount starts with an `=` sign, is enclosed in quotation marks and uses standard mathematical notations. Of course, it may make sense to restrict this transaction with a `to` date in months, as per the [transaction level dates](#transaction-level) section.
195
211
 
196
212
  ### Tracking transactions
197
213
 
@@ -203,13 +219,13 @@ To mark transactions as available for tracking you may use the `track` option in
203
219
 
204
220
  ```yaml
205
221
  once:
206
- account: "Assets:Bank"
207
- from: "2023-03-05"
208
- transactions:
209
- - amount: 3000
210
- category: "Expenses:Shopping"
211
- description: Refund for that damn laptop
212
- track: true
222
+ account: "Assets:Bank"
223
+ from: "2023-03-05"
224
+ transactions:
225
+ - amount: 3000
226
+ category: "Expenses:Shopping"
227
+ description: Refund for that damn laptop
228
+ track: true
213
229
  ```
214
230
 
215
231
  > **Note**: This feature has been designed to work with one-off transactions only
@@ -220,7 +236,7 @@ To use this feature, ensure you pass a filepath to the `-t` flag, such as:
220
236
 
221
237
  The app will use a hledger query to determine if the combination of category and amount is present in the periods between the `from` key and the current date in the journal file you've specified. If not, then the app will include it as a forecast transaction in the output file.
222
238
 
223
- ### Applying modifiers
239
+ ### Modifiers
224
240
 
225
241
  > **Note**: For modifiers to be included in your hledger reporting, use the `--auto` flag
226
242
 
@@ -228,17 +244,17 @@ Within your forecasts, it can be useful to reflect future increases/decreases in
228
244
 
229
245
  ```yaml
230
246
  monthly:
231
- account: "Assets:Bank"
232
- from: "2023-03-05"
233
- transactions:
234
- - amount: 450
235
- category: "Expenses:Groceries"
236
- description: Food shopping
237
- modifiers:
238
- - amount: 0.02
239
- description: "Inflation"
240
- from: "2024-01-01"
241
- to: "2024-12-31"
247
+ account: "Assets:Bank"
248
+ from: "2023-03-05"
249
+ transactions:
250
+ - amount: 450
251
+ category: "Expenses:Groceries"
252
+ description: Food shopping
253
+ modifiers:
254
+ - amount: 0.02
255
+ description: "Inflation"
256
+ from: "2024-01-01"
257
+ to: "2024-12-31"
242
258
  ```
243
259
 
244
260
  This will generate an [auto-posting](https://hledger.org/dev/hledger.html#auto-postings) in your forecast which will
@@ -259,15 +275,15 @@ modifiers:
259
275
  to: "2025-12-31"
260
276
  ```
261
277
 
262
- ### Additional settings
278
+ ### Additional config settings
263
279
 
264
280
  Additional settings in the config file to consider:
265
281
 
266
282
  ```yaml
267
283
  settings:
268
- currency: GBP # Specify the currency to use
269
- show_symbol: true # Show the currency symbol?
270
- thousands_separator: true # Separate thousands with a comma?
284
+ currency: GBP # Specify the currency to use
285
+ show_symbol: true # Show the currency symbol?
286
+ thousands_separator: true # Separate thousands with a comma?
271
287
  ```
272
288
 
273
289
  ## :camera_flash: Screenshots
@@ -282,7 +298,7 @@ settings:
282
298
 
283
299
  ## :paintbrush: Rationale
284
300
 
285
- Firstly, I've come to realise from reading countless blog and Reddit posts on [plain text accounting](https://plaintextaccounting.org), that everyone does it __completely__ differently! There is _great_ support in hledger for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions. Infact, it's nearly perfect for my needs. My only wishes were to be able to sum up monthly transactions much faster (so I can see my forecasted monthly I&E), apply future cost pressures more easily (such as inflation) and to be able to track and monitor specific transactions.
301
+ Firstly, I've come to realise from reading countless blog and Reddit posts on [plain text accounting](https://plaintextaccounting.org), that everyone does it **completely** differently! There is _great_ support in hledger for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions. Infact, it's nearly perfect for my needs. My only wishes were to be able to sum up monthly transactions much faster (so I can see my forecasted monthly I&E), apply future cost pressures more easily (such as inflation) and to be able to track and monitor specific transactions.
286
302
 
287
303
  Regarding the latter; I may be expecting a material amount of money to leave my account in May (perhaps for a holiday booking). But maybe, that booking ends up leaving in July instead. Whilst I would have accounted for that expense in my forecast, it will be tied to some date in May. So if that transaction doesn't appear in the "actuals" of my May bank statement (which I import into hledger), it won't be included in my forecast at all (as the latest transaction period will be greater than the forecast period). The impact is that my forecasted balance in any future month could be $X better off than reality. Being able to automatically look out for these transactions, and include them if they're not present, is a nice time saver.
288
304
 
@@ -4,6 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'hledger_forecast/version'
5
5
 
6
6
  Gem::Specification.new do |s|
7
+ s.required_ruby_version = '~> 3.0'
7
8
  s.name = 'hledger-forecast'
8
9
  s.version = HledgerForecast::VERSION
9
10
  s.authors = ['Oli Morris']
@@ -0,0 +1,21 @@
1
+ module HledgerForecast
2
+ # Calculate various
3
+ class Calculator
4
+ def initialize
5
+ @calculator = Dentaku::Calculator.new
6
+ end
7
+
8
+ def evaluate(amount)
9
+ return amount unless amount.is_a?(String)
10
+
11
+ @calculator.evaluate(amount.slice(1..-1))
12
+ end
13
+
14
+ def evaluate_date(from, to)
15
+ return to unless to[0] == "="
16
+
17
+ # Subtract a day from the final date
18
+ (from >> @calculator.evaluate(to.slice(1..-1))) - 1
19
+ end
20
+ end
21
+ end
@@ -155,8 +155,8 @@ module HledgerForecast
155
155
  end
156
156
 
157
157
  def self.summarize(options)
158
- forecast = File.read(options[:forecast_file])
159
- puts Summarize.generate(forecast)
158
+ config = File.read(options[:forecast_file])
159
+ puts Summarizer.summarize(config, options)
160
160
  end
161
161
  end
162
162
  end
@@ -0,0 +1,24 @@
1
+ module HledgerForecast
2
+ # Formats various items used throughout the application
3
+ class Formatter
4
+ def self.format_money(amount, settings)
5
+ Money.from_cents(amount.to_f * 100, (settings[:currency]) || 'USD').format(
6
+ symbol: settings[:show_symbol] || true,
7
+ sign_before_symbol: settings[:sign_before_symbol] || false,
8
+ thousands_separator: settings[:thousands_separator] ? ',' : nil
9
+ )
10
+ end
11
+
12
+ def self.output_to_ledger(*compiled_data)
13
+ output = compiled_data.compact.map do |data|
14
+ data.map do |item|
15
+ next unless item[:transactions].any?
16
+
17
+ item[:header] + item[:transactions].join + item[:footer]
18
+ end.join
19
+ end.join("\n")
20
+
21
+ output.gsub(/\n{2,}/, "\n\n")
22
+ end
23
+ end
24
+ end
@@ -1,282 +1,65 @@
1
1
  module HledgerForecast
2
- # Generates periodic transactions from a YAML file
2
+ # Generate forecasts for hledger from a yaml config file
3
3
  class Generator
4
- class << self
5
- attr_accessor :calculator, :options, :modified, :tracked
4
+ def self.generate(config, cli_options = nil)
5
+ new.generate(config, cli_options)
6
6
  end
7
7
 
8
- self.calculator = {}
9
- self.options = {}
10
- self.modified = {}
11
- self.tracked = {}
8
+ def generate(config, cli_options = nil)
9
+ forecast = YAML.safe_load(config)
10
+ @settings = Settings.config(forecast, cli_options)
12
11
 
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')
12
+ output = {}
13
+ forecast.each do |period, blocks|
14
+ next if %w[settings].include?(period)
16
15
 
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)
21
- end
22
-
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
39
-
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']
45
-
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
65
- end
66
-
67
- def self.regular_transaction(frequency, from, to, transactions, account)
68
- transactions = transactions.select { |transaction| transaction['to'].nil? }
69
- return "" if transactions.empty?
70
-
71
- output = ""
72
-
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)
80
-
81
- output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
82
- transaction['description'])
83
- end
84
-
85
- return "" unless output != ""
86
-
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
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"
116
- end
117
-
118
- output
119
- end
120
-
121
- def self.custom_transaction(forecasts)
122
- output = ""
123
-
124
- forecasts.each do |forecast|
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
135
-
136
- if track_transaction?(transaction, from)
137
- track_transaction(from, to, account, transaction)
138
- next
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'])
16
+ blocks.each do |block|
17
+ output[output.length] = process_block(period, block)
145
18
  end
146
-
147
- output += " #{account}\n\n"
148
- end
149
-
150
- output
151
- end
152
-
153
- def self.output_transaction(category, amount, description)
154
- " #{category.ljust(@options[:max_category])} #{amount.ljust(@options[:max_amount])}; #{description}\n"
155
- end
156
-
157
- def self.output_modified_transaction(transactions)
158
- output = ""
159
-
160
- transactions.each do |_key, transaction|
161
- date = "date:#{transaction['from']}"
162
- date += "..#{transaction['to']}" if transaction['to']
163
-
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
19
  end
168
20
 
169
- output
21
+ Formatter.output_to_ledger(
22
+ Transactions::Default.generate(output, @settings),
23
+ Transactions::Trackers.generate(output, @settings),
24
+ Transactions::Modifiers.generate(output, @settings)
25
+ )
170
26
  end
171
27
 
172
- def self.output_tracked_transaction(transactions)
173
- output = ""
174
-
175
- transactions.each do |_key, transaction|
176
- next if transaction['found']
177
-
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"
181
- end
28
+ private
182
29
 
183
- output
184
- end
30
+ def process_block(period, block)
31
+ output = []
185
32
 
186
- def self.extract_descriptions(transactions, from)
187
- descriptions = []
33
+ output << {
34
+ account: block['account'],
35
+ from: Date.parse(block['from']),
36
+ to: block['to'] ? Date.parse(block['to']) : nil,
37
+ type: period,
38
+ frequency: block['frequency'],
39
+ transactions: []
40
+ }
188
41
 
189
- transactions.each do |transaction|
190
- next if track_transaction?(transaction, from)
42
+ output = process_transactions(block, output)
191
43
 
192
- description = transaction['description']
193
- descriptions << description
44
+ output.map do |item|
45
+ transactions = item[:transactions].group_by { |t| t[:to] }
46
+ item.merge(transactions: transactions)
194
47
  end
195
-
196
- descriptions.join(', ')
197
48
  end
198
49
 
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)
50
+ def process_transactions(block, output)
51
+ block['transactions'].each do |t|
52
+ output.last[:transactions] << {
53
+ category: t['category'],
54
+ amount: Formatter.format_money(Calculator.new.evaluate(t['amount']), @settings),
55
+ description: t['description'],
56
+ to: t['to'] ? Calculator.new.evaluate_date(Date.parse(block['from']), t['to']) : nil,
57
+ modifiers: t['modifiers'] ? Transactions::Modifiers.get_modifiers(t, block) : [],
58
+ track: Transactions::Trackers.track?(t, block, @settings) ? true : false
213
59
  }
214
60
  end
215
- end
216
61
 
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
62
+ output
280
63
  end
281
64
  end
282
65
  end
@@ -0,0 +1,41 @@
1
+ module HledgerForecast
2
+ # Set the options from a user's confgi
3
+ class Settings
4
+ def self.config(forecast, cli_options)
5
+ settings = {}
6
+
7
+ settings[:max_amount] = get_max_field_size(forecast, 'amount') + 1 # +1 for the negatives
8
+ settings[:max_category] = get_max_field_size(forecast, 'category')
9
+
10
+ settings[:currency] = Money::Currency.new(forecast.fetch('settings', {}).fetch('currency', 'USD'))
11
+ settings[:show_symbol] = forecast.fetch('settings', {}).fetch('show_symbol', true)
12
+ # settings[:sign_before_symbol] = forecast.fetch('settings', {}).fetch('sign_before_symbol', false)
13
+ settings[:thousands_separator] = forecast.fetch('settings', {}).fetch('thousands_separator', true)
14
+
15
+ settings.merge!(cli_options) if cli_options
16
+
17
+ settings
18
+ end
19
+
20
+ def self.get_max_field_size(block, field)
21
+ max_size = 0
22
+
23
+ block.each do |period, items|
24
+ next if %w[settings].include?(period)
25
+
26
+ items.each do |item|
27
+ item['transactions'].each do |t|
28
+ field_value = if t[field].is_a?(Integer) || t[field].is_a?(Float)
29
+ ((t[field] + 3) * 100).to_s
30
+ else
31
+ t[field].to_s
32
+ end
33
+ max_size = [max_size, field_value.length].max
34
+ end
35
+ end
36
+ end
37
+
38
+ max_size
39
+ end
40
+ end
41
+ end