hledger-forecast 0.4.0 → 1.1.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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/README.md +111 -41
- data/example.journal +33 -22
- data/example.yml +40 -26
- data/hledger-forecast.gemspec +1 -0
- data/lib/hledger_forecast/calculator.rb +21 -0
- data/lib/hledger_forecast/cli.rb +27 -2
- data/lib/hledger_forecast/formatter.rb +24 -0
- data/lib/hledger_forecast/generator.rb +41 -258
- data/lib/hledger_forecast/settings.rb +41 -0
- data/lib/hledger_forecast/summarizer.rb +106 -0
- data/lib/hledger_forecast/summarizer_formatter.rb +115 -0
- data/lib/hledger_forecast/transactions/default.rb +88 -0
- data/lib/hledger_forecast/transactions/modifiers.rb +90 -0
- data/lib/hledger_forecast/transactions/trackers.rb +87 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +12 -4
- data/spec/custom_spec.rb +31 -4
- data/spec/modifier_spec.rb +21 -6
- data/spec/monthly_end_date_spec.rb +18 -33
- data/spec/monthly_end_date_transaction_spec.rb +44 -7
- data/spec/monthly_spec.rb +7 -7
- data/spec/summarizer_spec.rb +86 -0
- data/spec/track_spec.rb +5 -67
- metadata +15 -7
- data/lib/hledger_forecast/summarize.rb +0 -134
- data/lib/hledger_forecast/tracker.rb +0 -37
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:
|
4
|
+
version: 1.1.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-05-
|
11
|
+
date: 2023-05-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -113,10 +113,16 @@ files:
|
|
113
113
|
- example.yml
|
114
114
|
- hledger-forecast.gemspec
|
115
115
|
- lib/hledger_forecast.rb
|
116
|
+
- lib/hledger_forecast/calculator.rb
|
116
117
|
- lib/hledger_forecast/cli.rb
|
118
|
+
- lib/hledger_forecast/formatter.rb
|
117
119
|
- lib/hledger_forecast/generator.rb
|
118
|
-
- lib/hledger_forecast/
|
119
|
-
- lib/hledger_forecast/
|
120
|
+
- lib/hledger_forecast/settings.rb
|
121
|
+
- lib/hledger_forecast/summarizer.rb
|
122
|
+
- lib/hledger_forecast/summarizer_formatter.rb
|
123
|
+
- lib/hledger_forecast/transactions/default.rb
|
124
|
+
- lib/hledger_forecast/transactions/modifiers.rb
|
125
|
+
- lib/hledger_forecast/transactions/trackers.rb
|
120
126
|
- lib/hledger_forecast/version.rb
|
121
127
|
- spec/command_spec.rb
|
122
128
|
- spec/computed_amounts_spec.rb
|
@@ -132,6 +138,7 @@ files:
|
|
132
138
|
- spec/stubs/transactions_found.journal
|
133
139
|
- spec/stubs/transactions_found_inverse.journal
|
134
140
|
- spec/stubs/transactions_not_found.journal
|
141
|
+
- spec/summarizer_spec.rb
|
135
142
|
- spec/track_spec.rb
|
136
143
|
- spec/yearly_spec.rb
|
137
144
|
homepage: https://github.com/olimorris/hledger-forecast
|
@@ -144,16 +151,16 @@ require_paths:
|
|
144
151
|
- lib
|
145
152
|
required_ruby_version: !ruby/object:Gem::Requirement
|
146
153
|
requirements:
|
147
|
-
- - "
|
154
|
+
- - "~>"
|
148
155
|
- !ruby/object:Gem::Version
|
149
|
-
version: '0'
|
156
|
+
version: '3.0'
|
150
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
158
|
requirements:
|
152
159
|
- - ">="
|
153
160
|
- !ruby/object:Gem::Version
|
154
161
|
version: '0'
|
155
162
|
requirements: []
|
156
|
-
rubygems_version: 3.
|
163
|
+
rubygems_version: 3.2.3
|
157
164
|
signing_key:
|
158
165
|
specification_version: 4
|
159
166
|
summary: An extended wrapper around hledger's forecasting functionality
|
@@ -172,5 +179,6 @@ test_files:
|
|
172
179
|
- spec/stubs/transactions_found.journal
|
173
180
|
- spec/stubs/transactions_found_inverse.journal
|
174
181
|
- spec/stubs/transactions_not_found.journal
|
182
|
+
- spec/summarizer_spec.rb
|
175
183
|
- spec/track_spec.rb
|
176
184
|
- spec/yearly_spec.rb
|
@@ -1,134 +0,0 @@
|
|
1
|
-
module HledgerForecast
|
2
|
-
# Summarise a forecast YAML file and output it to the CLI
|
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.set_options(forecast_data)
|
19
|
-
|
20
|
-
@generator = generator
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.sum_transactions(forecast_data, period)
|
24
|
-
category_total = Hash.new(0)
|
25
|
-
forecast_data[period]&.each do |entry|
|
26
|
-
entry['transactions'].each do |transaction|
|
27
|
-
category_total[transaction['category']] += transaction['amount']
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
category_total
|
32
|
-
end
|
33
|
-
|
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[:frequency] = entry['frequency']
|
41
|
-
period_data[:category] = entry['transactions'].first['category']
|
42
|
-
period_data[:amount] = entry['transactions'].first['amount']
|
43
|
-
|
44
|
-
entry['transactions'].each do |transaction|
|
45
|
-
category_total[transaction['category']] += transaction['amount']
|
46
|
-
end
|
47
|
-
|
48
|
-
custom_periods << period_data
|
49
|
-
end
|
50
|
-
|
51
|
-
{ totals: category_total, periods: custom_periods }
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.format_amount(amount)
|
55
|
-
formatted_amount = @generator.format_amount(amount)
|
56
|
-
amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.add_rows_to_table(row_data, period_total, custom: false)
|
60
|
-
if custom
|
61
|
-
row_data[:periods].each do |period|
|
62
|
-
@table.add_row [{ value: period[:category], alignment: :left },
|
63
|
-
{ value: period[:frequency], alignment: :right },
|
64
|
-
{ value: format_amount(period[:amount]), alignment: :right }]
|
65
|
-
|
66
|
-
period_total += period[:amount]
|
67
|
-
end
|
68
|
-
else
|
69
|
-
row_data.each do |category, amount|
|
70
|
-
@table.add_row [{ value: category, colspan: 2, alignment: :left },
|
71
|
-
{ value: format_amount(amount), alignment: :right }]
|
72
|
-
|
73
|
-
period_total += amount
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
period_total
|
78
|
-
end
|
79
|
-
|
80
|
-
def self.add_categories_to_table(categories, forecast_data)
|
81
|
-
first_period = true
|
82
|
-
categories.each do |period, total|
|
83
|
-
category_total = total.reject { |_, amount| amount == 0 }
|
84
|
-
next if category_total.empty?
|
85
|
-
|
86
|
-
sorted_category_total = sort_transactions(category_total)
|
87
|
-
|
88
|
-
@table.add_separator unless first_period
|
89
|
-
@table.add_row([{ value: period.capitalize.bold, colspan: 3, alignment: :center }])
|
90
|
-
|
91
|
-
period_total = 0
|
92
|
-
period_total += if period == 'custom'
|
93
|
-
add_rows_to_table(sum_custom_transactions(forecast_data), period_total, custom: true)
|
94
|
-
else
|
95
|
-
add_rows_to_table(sorted_category_total, period_total)
|
96
|
-
end
|
97
|
-
|
98
|
-
format_total("#{period.capitalize} TOTAL", period_total)
|
99
|
-
first_period = false
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.sort_transactions(category_total)
|
104
|
-
negatives = category_total.select { |_, amount| amount < 0 }.sort_by { |_, amount| amount }
|
105
|
-
positives = category_total.select { |_, amount| amount > 0 }.sort_by { |_, amount| -amount }
|
106
|
-
|
107
|
-
negatives.concat(positives).to_h
|
108
|
-
end
|
109
|
-
|
110
|
-
def self.format_total(text, total)
|
111
|
-
@table.add_row [{ value: text.bold, colspan: 2, alignment: :left },
|
112
|
-
{ value: format_amount(total).bold, alignment: :right }]
|
113
|
-
end
|
114
|
-
|
115
|
-
def self.generate(forecast)
|
116
|
-
forecast_data = YAML.safe_load(forecast)
|
117
|
-
|
118
|
-
init_table
|
119
|
-
init_generator(forecast_data)
|
120
|
-
|
121
|
-
category_totals = {}
|
122
|
-
%w[monthly quarterly half-yearly yearly once custom].each do |period|
|
123
|
-
category_totals[period] = sum_transactions(forecast_data, period)
|
124
|
-
end
|
125
|
-
|
126
|
-
add_categories_to_table(category_totals, forecast_data)
|
127
|
-
|
128
|
-
@table.add_separator
|
129
|
-
format_total("TOTAL", category_totals.values.map(&:values).flatten.sum)
|
130
|
-
|
131
|
-
puts @table
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
module HledgerForecast
|
2
|
-
# Checks for the existence of a transaction in a journal file and tracks it
|
3
|
-
class Tracker
|
4
|
-
def self.track(transactions, transaction_file)
|
5
|
-
next_month = Date.new(Date.today.year, Date.today.month, 1).next_month
|
6
|
-
|
7
|
-
transactions.each_with_object({}) do |(key, transaction), updated_transactions|
|
8
|
-
found = transaction_exists?(transaction_file, transaction['from'], Date.today, transaction['account'],
|
9
|
-
transaction['transaction'])
|
10
|
-
updated_transactions[key] = transaction.merge('from' => next_month, 'found' => found)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.latest_date(file)
|
15
|
-
command = %(hledger print --file #{file} | grep '^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}' | awk '{print $1}' | sort -r | head -n 1)
|
16
|
-
|
17
|
-
date_output = `#{command}`
|
18
|
-
date_output.strip
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.transaction_exists?(file, from, to, account, transaction)
|
22
|
-
category = escape_str(transaction['category'])
|
23
|
-
amount = transaction['amount']
|
24
|
-
inverse_amount = transaction['inverse_amount']
|
25
|
-
|
26
|
-
# We run two commands and check to see if category +/- amount or account +/- amount exists
|
27
|
-
command1 = %(hledger print -f #{file} "date:#{from}..#{to}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{category} (#{amount}|#{inverse_amount})")
|
28
|
-
command2 = %(hledger print -f #{file} "date:#{from}..#{to}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{account} (#{amount}|#{inverse_amount})")
|
29
|
-
|
30
|
-
system(command1) || system(command2)
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.escape_str(str)
|
34
|
-
str.gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|