hledger-forecast 1.3.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +28 -15
- data/lib/hledger_forecast/cli.rb +84 -8
- data/lib/hledger_forecast/comparator.rb +80 -0
- data/lib/hledger_forecast/transactions/default.rb +43 -18
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +2 -0
- data/spec/{command_spec.rb → cli_spec.rb} +28 -0
- data/spec/compare_spec.rb +54 -0
- data/spec/stubs/output1.csv +2 -0
- data/spec/stubs/output2.csv +2 -0
- data/spec/verbose_output_spec.rb +27 -0
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f769e017bc9ad46ea7d7571f74c64286e9c2415d0696e40f3301438a9038514
|
4
|
+
data.tar.gz: 52f69b30edfbc0c9d5430b88d25687c9c311d6c6aa4b1e5fd3b2815b3602d240
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
- :
|
21
|
-
- :
|
22
|
-
- :
|
23
|
-
- :
|
24
|
-
- :
|
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
|
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="
|
32
|
+
<img src="https://github.com/olimorris/hledger-forecast/assets/9512444/430503b5-f447-4972-b122-b48f8628aff9" alt="hledger-Forecast" />
|
32
33
|
|
33
|
-
**
|
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
|
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
|
@@ -70,6 +71,7 @@ The available options are:
|
|
70
71
|
-f, --forecast FILE The path to the FORECAST CSV/YML file to generate from
|
71
72
|
-o, --output-file FILE The path to the OUTPUT file to create
|
72
73
|
-t, --transaction FILE The path to the TRANSACTION journal file
|
74
|
+
-v, --verbose Don't group transactions by type in the output file
|
73
75
|
--force Force an overwrite of the output file
|
74
76
|
--no-track Don't track any transactions
|
75
77
|
-h, --help Show this help message
|
@@ -86,11 +88,11 @@ To work with hledger, include the forecast file and use the `--forecast` flag:
|
|
86
88
|
|
87
89
|
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. Forecasting in hledger can be complicated so be sure to refer to the [documentation](https://hledger.org/dev/hledger.html) or start a [discussion](https://github.com/olimorris/hledger-forecast/discussions/new?category=q-a).
|
88
90
|
|
91
|
+
If you use the `hledger-ui` tool, it may be helpful to use the `--verbose` flag. This ensures that transactions are not grouped together in the forecast journal file, making descriptions much easier to read.
|
92
|
+
|
89
93
|
### Summarize command
|
90
94
|
|
91
|
-
As your
|
92
|
-
Furthermore, being able to see your monthly profit and loss statement _if_ you were to purchase that new item may
|
93
|
-
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.
|
94
96
|
|
95
97
|
hledger-forecast summarize -f my_forecast.csv
|
96
98
|
|
@@ -104,6 +106,18 @@ The available options are:
|
|
104
106
|
-v, --verbose Show additional information in the summary
|
105
107
|
-h, --help Show this help message
|
106
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
|
+
|
107
121
|
## :gear: Creating your forecast
|
108
122
|
|
109
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.
|
@@ -372,4 +386,3 @@ The app will use a hledger query to determine if the combination of category and
|
|
372
386
|
## :pencil2: Contributing
|
373
387
|
|
374
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.
|
375
|
-
|
data/lib/hledger_forecast/cli.rb
CHANGED
@@ -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
|
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
|
|
@@ -91,6 +101,11 @@ module HledgerForecast
|
|
91
101
|
options[:transaction_file] = file
|
92
102
|
end
|
93
103
|
|
104
|
+
opts.on("-v", "--verbose",
|
105
|
+
"Don't group transactions by type in the output file") do
|
106
|
+
options[:verbose] = true
|
107
|
+
end
|
108
|
+
|
94
109
|
opts.on("--force",
|
95
110
|
"Force an overwrite of the output file") do
|
96
111
|
options[:force] = true
|
@@ -105,11 +120,24 @@ module HledgerForecast
|
|
105
120
|
puts opts
|
106
121
|
exit
|
107
122
|
end
|
108
|
-
end
|
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
|
109
137
|
|
110
|
-
options[:forecast_file]
|
111
|
-
options[:file_type]
|
112
|
-
options[:output_file]
|
138
|
+
options[:forecast_file] ||= "forecast.csv"
|
139
|
+
options[:file_type] ||= "csv"
|
140
|
+
options[:output_file] ||= "forecast.journal"
|
113
141
|
|
114
142
|
options
|
115
143
|
end
|
@@ -117,7 +145,7 @@ module HledgerForecast
|
|
117
145
|
def self.parse_summarize_options(args)
|
118
146
|
options = {}
|
119
147
|
|
120
|
-
OptionParser.new do |opts|
|
148
|
+
global = OptionParser.new do |opts|
|
121
149
|
opts.banner = "Usage: hledger-forecast summarize [options]"
|
122
150
|
opts.separator ""
|
123
151
|
|
@@ -163,7 +191,47 @@ module HledgerForecast
|
|
163
191
|
puts opts
|
164
192
|
exit
|
165
193
|
end
|
166
|
-
end
|
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]
|
167
235
|
|
168
236
|
options
|
169
237
|
end
|
@@ -205,5 +273,13 @@ module HledgerForecast
|
|
205
273
|
|
206
274
|
puts SummarizerFormatter.format(summarizer[:output], summarizer[:settings])
|
207
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
|
208
284
|
end
|
209
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
|
@@ -13,11 +13,7 @@ module HledgerForecast
|
|
13
13
|
def generate
|
14
14
|
data.each_value do |blocks|
|
15
15
|
blocks.each do |block|
|
16
|
-
|
17
|
-
process_custom_block(block)
|
18
|
-
else
|
19
|
-
process_block(block)
|
20
|
-
end
|
16
|
+
process_block(block)
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
@@ -34,30 +30,59 @@ module HledgerForecast
|
|
34
30
|
@output = []
|
35
31
|
end
|
36
32
|
|
37
|
-
def
|
33
|
+
def process_block(block)
|
38
34
|
block[:transactions].each do |to, transactions|
|
39
35
|
to = get_header(block[:to], to)
|
40
36
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
37
|
+
if block[:type] == "custom"
|
38
|
+
process_custom_transactions(block, to, transactions)
|
39
|
+
else
|
40
|
+
process_standard_transactions(block, to, transactions)
|
45
41
|
end
|
46
42
|
end
|
47
43
|
end
|
48
44
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
block[:descriptions] = get_descriptions(transactions)
|
45
|
+
def process_custom_transactions(block, to, transactions)
|
46
|
+
transactions.each do |t|
|
47
|
+
frequency = get_periodic_rules(block[:type], t[:frequency])
|
53
48
|
|
54
|
-
|
49
|
+
header = build_header(block, to, frequency, t[:description])
|
50
|
+
footer = build_footer(block)
|
51
|
+
output << build_transaction(header, [t], footer)
|
52
|
+
end
|
53
|
+
end
|
55
54
|
|
56
|
-
|
57
|
-
|
55
|
+
def process_standard_transactions(block, to, transactions)
|
56
|
+
if @options[:verbose]
|
57
|
+
transactions.map do |t|
|
58
|
+
# Skip transactions that have been marked as tracked
|
59
|
+
next if t[:track]
|
58
60
|
|
59
|
-
|
61
|
+
frequency = get_periodic_rules(block[:type], block[:frequency])
|
62
|
+
header = build_header(block, to, frequency, t[:description])
|
63
|
+
footer = build_footer(block)
|
64
|
+
output << build_transaction(header, [t], footer)
|
65
|
+
end
|
66
|
+
return
|
60
67
|
end
|
68
|
+
|
69
|
+
block[:descriptions] = get_descriptions(transactions)
|
70
|
+
frequency = get_periodic_rules(block[:type], block[:frequency])
|
71
|
+
header = build_header(block, to, frequency, block[:descriptions])
|
72
|
+
footer = build_footer(block)
|
73
|
+
output << build_transaction(header, transactions, footer)
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_header(block, to, frequency, description)
|
77
|
+
"#{frequency} #{block[:from]}#{to} * #{description}\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_footer(block)
|
81
|
+
" #{block[:account]}\n\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_transaction(header, transactions, footer)
|
85
|
+
{ header: header, transactions: write_transactions(transactions), footer: footer }
|
61
86
|
end
|
62
87
|
|
63
88
|
def get_header(block, transaction)
|
data/lib/hledger_forecast.rb
CHANGED
@@ -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,27 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
output = <<~JOURNAL
|
4
|
+
~ monthly from 2023-03-01 * Mortgage
|
5
|
+
Expenses:Mortgage £2,000.55; Mortgage
|
6
|
+
Assets:Bank
|
7
|
+
|
8
|
+
~ monthly from 2023-03-01 * Food
|
9
|
+
Expenses:Food £100.00 ; Food
|
10
|
+
Assets:Bank
|
11
|
+
|
12
|
+
~ monthly from 2023-03-01 * Savings
|
13
|
+
Assets:Bank £-1,000.00; Savings
|
14
|
+
Assets:Savings
|
15
|
+
|
16
|
+
JOURNAL
|
17
|
+
|
18
|
+
RSpec.describe 'verbose command' do
|
19
|
+
it 'does not group similar type transactions together in the output' do
|
20
|
+
generated_journal = './test_output.journal'
|
21
|
+
File.delete(generated_journal) if File.exist?(generated_journal)
|
22
|
+
|
23
|
+
system("./bin/hledger-forecast generate -f ./spec/stubs/forecast.csv -o ./test_output.journal --verbose --force")
|
24
|
+
|
25
|
+
expect(File.read(generated_journal)).to eq(output)
|
26
|
+
end
|
27
|
+
end
|
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
|
+
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-
|
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/
|
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,11 +142,14 @@ 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
|
146
150
|
- spec/summarizer_spec.rb
|
147
151
|
- spec/track_spec.rb
|
152
|
+
- spec/verbose_output_spec.rb
|
148
153
|
- spec/yearly_spec.rb
|
149
154
|
homepage: https://github.com/olimorris/hledger-forecast
|
150
155
|
licenses:
|
@@ -165,12 +170,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
170
|
- !ruby/object:Gem::Version
|
166
171
|
version: '0'
|
167
172
|
requirements: []
|
168
|
-
rubygems_version: 3.4.
|
173
|
+
rubygems_version: 3.4.19
|
169
174
|
signing_key:
|
170
175
|
specification_version: 4
|
171
176
|
summary: An extended wrapper around hledger's forecasting functionality
|
172
177
|
test_files:
|
173
|
-
- spec/
|
178
|
+
- spec/cli_spec.rb
|
179
|
+
- spec/compare_spec.rb
|
174
180
|
- spec/computed_amounts_spec.rb
|
175
181
|
- spec/csv_and_yml_comparison_spec.rb
|
176
182
|
- spec/csv_parser_spec.rb
|
@@ -184,9 +190,12 @@ test_files:
|
|
184
190
|
- spec/quarterly_spec.rb
|
185
191
|
- spec/stubs/forecast.csv
|
186
192
|
- spec/stubs/forecast.yml
|
193
|
+
- spec/stubs/output1.csv
|
194
|
+
- spec/stubs/output2.csv
|
187
195
|
- spec/stubs/transactions_found.journal
|
188
196
|
- spec/stubs/transactions_found_inverse.journal
|
189
197
|
- spec/stubs/transactions_not_found.journal
|
190
198
|
- spec/summarizer_spec.rb
|
191
199
|
- spec/track_spec.rb
|
200
|
+
- spec/verbose_output_spec.rb
|
192
201
|
- spec/yearly_spec.rb
|