hledger-forecast 1.4.0 → 1.5.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: b5ba12a84909e9d92ac3a9b1b5d977b661131fe6673f8a1c6e73f9ec57d790e2
4
- data.tar.gz: 8f7f23fde8f640912eacb23982e198d67bfe83671c43eba6357a1cdde9a87528
3
+ metadata.gz: 0f769e017bc9ad46ea7d7571f74c64286e9c2415d0696e40f3301438a9038514
4
+ data.tar.gz: 52f69b30edfbc0c9d5430b88d25687c9c311d6c6aa4b1e5fd3b2815b3602d240
5
5
  SHA512:
6
- metadata.gz: 33c532dc9e25ff41c968a1ada59379dbc56a9a0587e2abc4f07acdd10387c88b6f834ec6025dccb44e5ab5f4b3446341e09b947275e540857884728e84b5392a
7
- data.tar.gz: 918e5be4d90e1bd52a75436ce0ded33a72e49dd29e9d0a112067eddf2b86693a8ac6c21bd027ed2148d28819e5af432e594a063ca28fdcbe14bceeaee335d283
6
+ metadata.gz: 2721eb985469db575b66172a94ee670e96711437da7b94f27da53301beed9f385b46c19121c9d365ce01fb0423ea5ea728ce511296c29adc26fc2051ff33e794
7
+ data.tar.gz: e41363954be45ca668dd73cc1a816b47a94419be53cde4fccdfc5a88a55601a2166e5f6eeb6532d4673c3bfe204020122165b71fabcca07a9dbf7cf0f1f80271
data/README.md CHANGED
@@ -11,30 +11,30 @@
11
11
  <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>
12
12
  </p>
13
13
 
14
- **"Improved", you say?** Using a _CSV_ (or _YML_) file, forecasts can be quickly generated into a _journal_ file ready to be fed into [hledger](https://github.com/simonmichael/hledger). **A 15 line [CSV file](https://github.com/olimorris/hledger-forecast/blob/main/example.csv) can generate a 42 line hledger [forecast file](https://github.com/olimorris/hledger-forecast/blob/main/example.journal)!**
14
+ **"Improved", you say?** Using a _CSV_ (or _YML_) file, forecasts can be quickly generated into a _journal_ file ready to be fed into [hledger](https://github.com/simonmichael/hledger). **A 16 line [CSV file](https://github.com/olimorris/hledger-forecast/blob/main/example.csv) can generate a 46 line hledger [forecast file](https://github.com/olimorris/hledger-forecast/blob/main/example.journal)!**
15
15
 
16
16
  Forecasts can also be constrained between dates, inflated by modifiers, tracked until they appear in your bank statements and summarized into your own daily/weekly/monthly/yearly personal forecast income and expenditure statement.
17
17
 
18
18
  ## :sparkles: Features
19
19
 
20
- - :muscle: Uses a simple CSV (or YML) file to generate forecasts which can be used with hledger
21
- - :date: Can smartly track forecasts against your bank statement
22
- - :moneybag: Can automatically apply modifiers such as inflation/deflation to forecasts
23
- - :abacus: Enables the use of maths in your forecasts (for amounts and dates)
24
- - :chart_with_upwards_trend: Display your forecasts as income and expenditure reports (e.g. daily, weekly, monthly)
20
+ - :rocket: Uses a simple CSV (or YML) file to generate forecasts which can be used with hledger
21
+ - :calendar: Can smartly track forecasts against your bank statement
22
+ - :dollar: Can automatically apply modifiers such as inflation/deflation to forecasts
23
+ - :mag: Enables the use of maths in your forecasts (for amounts and dates)
24
+ - :bar_chart: Display your forecasts as income and expenditure reports (e.g. daily, weekly, monthly)
25
+ - :twisted_rightwards_arrows: Compare and display the difference between hledger outputs
25
26
  - :computer: Simple and easy to use CLI
26
27
 
27
28
  ## :camera_flash: Screenshots
28
29
 
29
- **CSV forecast and corresponding journal output**
30
+ **A CSV forecast and the hledger journal it generates**
30
31
 
31
- <img src="https://github.com/olimorris/hledger-forecast/assets/9512444/430503b5-f447-4972-b122-b48f8628aff9" alt="Hledger-Forecast" />
32
+ <img src="https://github.com/olimorris/hledger-forecast/assets/9512444/430503b5-f447-4972-b122-b48f8628aff9" alt="hledger-Forecast" />
32
33
 
33
- **Output from the `summarize` command**
34
+ **The ouput from the `summarize` command**
34
35
 
35
36
  <img src="https://github.com/olimorris/hledger-forecast/assets/9512444/f5017ea2-9606-46ec-8b38-8840dc175e7b" alt="Summarize command" />
36
37
 
37
-
38
38
  ## :package: Installation
39
39
 
40
40
  Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) installed on your system, simply run:
@@ -52,8 +52,9 @@ The available options are:
52
52
  Usage: hledger-forecast [command] [options]
53
53
 
54
54
  Commands:
55
- generate Generate the forecast file
55
+ generate Generate a forecast from a file
56
56
  summarize Summarize the forecast file and output to the terminal
57
+ compare Compare and highlight the differences between two CSV files
57
58
 
58
59
  Options:
59
60
  -h, --help Show this help message
@@ -91,9 +92,7 @@ If you use the `hledger-ui` tool, it may be helpful to use the `--verbose` flag.
91
92
 
92
93
  ### Summarize command
93
94
 
94
- As your configuration file grows, it can be helpful to sum up the total amounts and output them to the CLI.
95
- Furthermore, being able to see your monthly profit and loss statement _if_ you were to purchase that new item may
96
- influence your buying decision. In hledger-forecast, this can be achieved by:
95
+ As your forecast file grows, it can be helpful to sum up the total amounts and output them to the CLI. Think of this command as your own _profit and loss_ summarizer, generating a statement over a period you specify.
97
96
 
98
97
  hledger-forecast summarize -f my_forecast.csv
99
98
 
@@ -107,6 +106,18 @@ The available options are:
107
106
  -v, --verbose Show additional information in the summary
108
107
  -h, --help Show this help message
109
108
 
109
+ ### Compare command
110
+
111
+ A core part of managing your personal finances is the comparison of what you _expected_ to happen versus what _actually_ happened. This can be challenging to accomplish with hledger so to make this easier, the app has a useful `compare` command:
112
+
113
+ hledger-forecast compare [path/to/file1.csv] [path/to/file2.csv]
114
+
115
+ To generate CSV output with hledger, append `-O csv > output.csv` to your desired command.
116
+
117
+ To make it easier to read horizontal output in the terminal, consider the use of a terminal pager like [most](https://en.wikipedia.org/wiki/Most_(Unix)) by appending `| most` to the compare command.
118
+
119
+ > **Note:** The two CSV files being compared must have the same structure
120
+
110
121
  ## :gear: Creating your forecast
111
122
 
112
123
  The app makes it easy to generate a comprehensive _journal_ file with very few lines of code, making it much easier to stay on top of your forecasting from month to month.
@@ -375,4 +386,3 @@ The app will use a hledger query to determine if the combination of category and
375
386
  ## :pencil2: Contributing
376
387
 
377
388
  I am open to any pull requests that fix bugs but would ask that any new functionality is discussed before it could be accepted.
378
-
@@ -8,6 +8,8 @@ module HledgerForecast
8
8
  generate(options)
9
9
  when 'summarize'
10
10
  summarize(options)
11
+ when 'compare'
12
+ compare(options)
11
13
  else
12
14
  puts "Unknown command: #{command}"
13
15
  exit(1)
@@ -22,8 +24,9 @@ module HledgerForecast
22
24
  opts.banner = "Usage: hledger-forecast [command] [options]"
23
25
  opts.separator ""
24
26
  opts.separator "Commands:"
25
- opts.separator " generate Generate the forecast file"
27
+ opts.separator " generate Generate a forecast from a file"
26
28
  opts.separator " summarize Summarize the forecast file and output to the terminal"
29
+ opts.separator " compare Compare and highlight the differences between two CSV files"
27
30
  opts.separator ""
28
31
  opts.separator "Options:"
29
32
 
@@ -38,6 +41,11 @@ module HledgerForecast
38
41
  end
39
42
  end
40
43
 
44
+ if args.empty?
45
+ puts global
46
+ exit(1)
47
+ end
48
+
41
49
  begin
42
50
  global.order!(args)
43
51
  command = args.shift || 'generate'
@@ -52,6 +60,8 @@ module HledgerForecast
52
60
  options = parse_generate_options(args)
53
61
  when 'summarize'
54
62
  options = parse_summarize_options(args)
63
+ when 'compare'
64
+ options = parse_compare_options(args)
55
65
  else
56
66
  puts "Unknown command: #{command}"
57
67
  puts global
@@ -64,7 +74,7 @@ module HledgerForecast
64
74
  def self.parse_generate_options(args)
65
75
  options = {}
66
76
 
67
- OptionParser.new do |opts|
77
+ global = OptionParser.new do |opts|
68
78
  opts.banner = "Usage: hledger-forecast generate [options]"
69
79
  opts.separator ""
70
80
 
@@ -110,11 +120,24 @@ module HledgerForecast
110
120
  puts opts
111
121
  exit
112
122
  end
113
- end.parse!(args)
123
+ end
124
+
125
+ begin
126
+ global.parse!(args)
127
+ rescue OptionParser::InvalidOption => e
128
+ puts e
129
+ puts global
130
+ exit(1)
131
+ end
132
+
133
+ if options.empty?
134
+ puts global
135
+ exit(1)
136
+ end
114
137
 
115
- options[:forecast_file] = "forecast.csv" unless options[:forecast_file]
116
- options[:file_type] = "csv" unless options[:file_type]
117
- options[:output_file] = "forecast.journal" unless options[:output_file]
138
+ options[:forecast_file] ||= "forecast.csv"
139
+ options[:file_type] ||= "csv"
140
+ options[:output_file] ||= "forecast.journal"
118
141
 
119
142
  options
120
143
  end
@@ -122,7 +145,7 @@ module HledgerForecast
122
145
  def self.parse_summarize_options(args)
123
146
  options = {}
124
147
 
125
- OptionParser.new do |opts|
148
+ global = OptionParser.new do |opts|
126
149
  opts.banner = "Usage: hledger-forecast summarize [options]"
127
150
  opts.separator ""
128
151
 
@@ -168,7 +191,47 @@ module HledgerForecast
168
191
  puts opts
169
192
  exit
170
193
  end
171
- end.parse!(args)
194
+ end
195
+
196
+ begin
197
+ global.parse!(args)
198
+ rescue OptionParser::InvalidOption => e
199
+ puts e
200
+ puts global
201
+ exit(1)
202
+ end
203
+
204
+ if options.empty?
205
+ puts global
206
+ exit(1)
207
+ end
208
+
209
+ options
210
+ end
211
+
212
+ def self.parse_compare_options(args)
213
+ options = {}
214
+
215
+ global = OptionParser.new do |opts|
216
+ opts.banner = "Usage: hledger-forecast compare [path/to/file1.csv] [path/to/file2.csv]"
217
+ opts.separator ""
218
+ end
219
+
220
+ begin
221
+ global.parse!(args)
222
+ rescue OptionParser::InvalidOption => e
223
+ puts e
224
+ puts global
225
+ exit(1)
226
+ end
227
+
228
+ if args[0].nil? || args[1].nil?
229
+ puts global
230
+ exit(1)
231
+ end
232
+
233
+ options[:file1] = args[0]
234
+ options[:file2] = args[1]
172
235
 
173
236
  options
174
237
  end
@@ -210,5 +273,13 @@ module HledgerForecast
210
273
 
211
274
  puts SummarizerFormatter.format(summarizer[:output], summarizer[:settings])
212
275
  end
276
+
277
+ def self.compare(options)
278
+ if !File.exist?(options[:file1]) || !File.exist?(options[:file2])
279
+ return puts "\nError: ".bold.red + "One or more of the files could not be found to compare"
280
+ end
281
+
282
+ puts Comparator.compare(options[:file1], options[:file2])
283
+ end
213
284
  end
214
285
  end
@@ -0,0 +1,80 @@
1
+ module HledgerForecast
2
+ # Compare the output of two CSV files
3
+ class Comparator
4
+ def initialize
5
+ @table = Terminal::Table.new
6
+ end
7
+
8
+ def self.compare(file1, file2)
9
+ new.compare(file1, file2)
10
+ end
11
+
12
+ def compare(file1, file2)
13
+ compare_csvs(file1, file2)
14
+ end
15
+
16
+ private
17
+
18
+ def compare_csvs(file1, file2)
19
+ csv1 = CSV.read(file1)
20
+ csv2 = CSV.read(file2)
21
+
22
+ unless csv1.length == csv2.length && csv1[0].length == csv2[0].length
23
+ return puts "\nError: ".bold.red + "The files have different formats and cannot be compared"
24
+ end
25
+
26
+ @table.add_row csv2[0].map(&:bold)
27
+ @table.add_separator
28
+
29
+ generate_diff(csv1, csv2).drop(1).each do |row|
30
+ @table.add_row [row[0].bold] + row[1..]
31
+ end
32
+
33
+ puts @table
34
+ end
35
+
36
+ def header?(row_num)
37
+ row_num == 0
38
+ end
39
+
40
+ def generate_diff(csv1, csv2)
41
+ csv1.each_with_index.map do |row, i|
42
+ row.each_with_index.map do |cell, j|
43
+ if header?(i) || j == 0 # Checking for the first column here
44
+ csv2[i][j]
45
+ else
46
+ difference = parse_money(cell) - parse_money(csv2[i][j])
47
+ format_difference(difference, detect_currency(cell))
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def detect_currency(str)
54
+ # Explicitly check for common currencies first
55
+ return "GBP" if str.include?("£")
56
+ return "EUR" if str.include?("€")
57
+ return "USD" if str.include?("$")
58
+
59
+ Money::Currency.table.each_value do |currency|
60
+ return currency[:iso_code] if str.include?(currency[:symbol])
61
+ end
62
+
63
+ nil
64
+ end
65
+
66
+ def parse_money(value)
67
+ # Remove currency symbols and parse the result as a float, then convert to cents
68
+ cleaned_value = value.gsub(/[^0-9.]/, '').to_f
69
+ cleaned_value.to_i
70
+ end
71
+
72
+ def format_difference(amount, currency)
73
+ formatted_amount = Formatter.format_money(amount, { currency: currency })
74
+
75
+ return formatted_amount if amount == 0
76
+
77
+ amount > 0 ? formatted_amount.green : formatted_amount.red
78
+ end
79
+ end
80
+ end
@@ -1,3 +1,3 @@
1
1
  module HledgerForecast
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -12,9 +12,11 @@ require 'yaml'
12
12
 
13
13
  Money.locale_backend = nil
14
14
  Money.rounding_mode = BigDecimal::ROUND_HALF_UP
15
+ Money.default_currency = 'USD'
15
16
 
16
17
  require_relative 'hledger_forecast/calculator'
17
18
  require_relative 'hledger_forecast/cli'
19
+ require_relative 'hledger_forecast/comparator'
18
20
  require_relative 'hledger_forecast/csv_parser'
19
21
  require_relative 'hledger_forecast/formatter'
20
22
  require_relative 'hledger_forecast/generator'
@@ -12,6 +12,19 @@ output = <<~JOURNAL
12
12
 
13
13
  JOURNAL
14
14
 
15
+ def strip_ansi_codes(str)
16
+ str.gsub(/\e\[([;\d]+)?m/, "")
17
+ end
18
+
19
+ def capture_stdout
20
+ old_stdout = $stdout
21
+ $stdout = StringIO.new
22
+ yield
23
+ $stdout.string
24
+ ensure
25
+ $stdout = old_stdout
26
+ end
27
+
15
28
  RSpec.describe 'command' do
16
29
  it 'uses the CLI to generate an output' do
17
30
  generated_journal = './test_output.journal'
@@ -30,4 +43,19 @@ RSpec.describe 'command' do
30
43
 
31
44
  expect(File.read(generated_journal)).to eq(output)
32
45
  end
46
+
47
+ it 'uses the CLI to compare two CSV files' do
48
+ expected_output = strip_ansi_codes(<<~OUTPUT)
49
+ +---------+---------+---------+
50
+ | account | 2023-07 | 2023-08 |
51
+ +---------+---------+---------+
52
+ | total | £-10.00 | €10.00 |
53
+ +---------+---------+---------+
54
+
55
+ OUTPUT
56
+
57
+ actual_output = `./bin/hledger-forecast compare ./spec/stubs/output1.csv ./spec/stubs/output2.csv`
58
+
59
+ expect(strip_ansi_codes(actual_output)).to eq(expected_output)
60
+ end
33
61
  end
@@ -0,0 +1,54 @@
1
+ require_relative '../lib/hledger_forecast'
2
+ require 'stringio'
3
+
4
+ def strip_ansi_codes(str)
5
+ str.gsub(/\e\[([;\d]+)?m/, "")
6
+ end
7
+
8
+ def capture_stdout
9
+ old_stdout = $stdout
10
+ $stdout = StringIO.new
11
+ yield
12
+ $stdout.string
13
+ ensure
14
+ $stdout = old_stdout
15
+ end
16
+
17
+ RSpec.describe HledgerForecast::Comparator do
18
+ let(:file1_content) do
19
+ <<~CSV
20
+ "account","2023-07","2023-08"
21
+ "total","£100.00","€200.00"
22
+ CSV
23
+ end
24
+
25
+ let(:file2_content) do
26
+ <<~CSV
27
+ "account","2023-07","2023-08"
28
+ "total","£110.00","€190.00"
29
+ CSV
30
+ end
31
+
32
+ let(:file1) { StringIO.new(file1_content) }
33
+ let(:file2) { StringIO.new(file2_content) }
34
+
35
+ before do
36
+ allow(CSV).to receive(:read).with('file1.csv').and_return(CSV.parse(file1.read))
37
+ allow(CSV).to receive(:read).with('file2.csv').and_return(CSV.parse(file2.read))
38
+ end
39
+
40
+ it "compares the contents of two CSV files and outputs the difference" do
41
+ comparator = described_class.new
42
+
43
+ expected_output = strip_ansi_codes(<<~OUTPUT)
44
+ +---------+---------+---------+
45
+ | account | 2023-07 | 2023-08 |
46
+ +---------+---------+---------+
47
+ | total | £-10.00 | €10.00 |
48
+ +---------+---------+---------+
49
+ OUTPUT
50
+
51
+ actual_output = capture_stdout { comparator.compare('file1.csv', 'file2.csv') }
52
+ expect(strip_ansi_codes(actual_output)).to eq(expected_output)
53
+ end
54
+ end
@@ -0,0 +1,2 @@
1
+ "account","2023-07","2023-08"
2
+ "total","£100.00","€200.00"
@@ -0,0 +1,2 @@
1
+ "account","2023-07","2023-08"
2
+ "total","£110.00","€190.00"
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: 1.4.0
4
+ version: 1.5.0
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-06-10 00:00:00.000000000 Z
11
+ date: 2023-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -116,6 +116,7 @@ files:
116
116
  - lib/hledger_forecast.rb
117
117
  - lib/hledger_forecast/calculator.rb
118
118
  - lib/hledger_forecast/cli.rb
119
+ - lib/hledger_forecast/comparator.rb
119
120
  - lib/hledger_forecast/csv_parser.rb
120
121
  - lib/hledger_forecast/formatter.rb
121
122
  - lib/hledger_forecast/generator.rb
@@ -126,7 +127,8 @@ files:
126
127
  - lib/hledger_forecast/transactions/modifiers.rb
127
128
  - lib/hledger_forecast/transactions/trackers.rb
128
129
  - lib/hledger_forecast/version.rb
129
- - spec/command_spec.rb
130
+ - spec/cli_spec.rb
131
+ - spec/compare_spec.rb
130
132
  - spec/computed_amounts_spec.rb
131
133
  - spec/csv_and_yml_comparison_spec.rb
132
134
  - spec/csv_parser_spec.rb
@@ -140,6 +142,8 @@ files:
140
142
  - spec/quarterly_spec.rb
141
143
  - spec/stubs/forecast.csv
142
144
  - spec/stubs/forecast.yml
145
+ - spec/stubs/output1.csv
146
+ - spec/stubs/output2.csv
143
147
  - spec/stubs/transactions_found.journal
144
148
  - spec/stubs/transactions_found_inverse.journal
145
149
  - spec/stubs/transactions_not_found.journal
@@ -166,12 +170,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
170
  - !ruby/object:Gem::Version
167
171
  version: '0'
168
172
  requirements: []
169
- rubygems_version: 3.4.13
173
+ rubygems_version: 3.4.19
170
174
  signing_key:
171
175
  specification_version: 4
172
176
  summary: An extended wrapper around hledger's forecasting functionality
173
177
  test_files:
174
- - spec/command_spec.rb
178
+ - spec/cli_spec.rb
179
+ - spec/compare_spec.rb
175
180
  - spec/computed_amounts_spec.rb
176
181
  - spec/csv_and_yml_comparison_spec.rb
177
182
  - spec/csv_parser_spec.rb
@@ -185,6 +190,8 @@ test_files:
185
190
  - spec/quarterly_spec.rb
186
191
  - spec/stubs/forecast.csv
187
192
  - spec/stubs/forecast.yml
193
+ - spec/stubs/output1.csv
194
+ - spec/stubs/output2.csv
188
195
  - spec/stubs/transactions_found.journal
189
196
  - spec/stubs/transactions_found_inverse.journal
190
197
  - spec/stubs/transactions_not_found.journal