hledger-forecast 1.4.0 → 1.5.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.
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