hledger-forecast 3.2.0 → 3.3.1
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/manually_publish.yml +27 -0
- data/.github/workflows/release.yml +2 -1
- data/.gitignore +1 -0
- data/AGENTS.md +27 -0
- data/CHANGELOG.md +14 -0
- data/README.md +3 -3
- data/lib/hledger_forecast/calculator.rb +4 -1
- data/lib/hledger_forecast/cli.rb +6 -1
- data/lib/hledger_forecast/forecast.rb +9 -1
- data/lib/hledger_forecast/generator.rb +1 -1
- data/lib/hledger_forecast/transaction.rb +11 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/spec/cli_spec.rb +4 -1
- data/spec/computed_amounts_spec.rb +4 -1
- data/spec/monthly_end_date_spec.rb +4 -1
- data/spec/monthly_spec.rb +7 -1
- data/spec/tags_spec.rb +23 -5
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b3fc72db9564b895f8412f6467b3350700b12e7c4cb393ae48d7640ff0766ef
|
|
4
|
+
data.tar.gz: 78092306250621f6d6e93df98d600035d85960125d40b2df35bd728f1e38cb73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c07f7f68ff23714ec25f2708405d4700f8f0df800e07571d805c548598d23775e70b947ba94c74e72c2f9b6064fefdcef2a9d2931fb4d863856c7f0ad1ec14b3
|
|
7
|
+
data.tar.gz: b00893cc0d8f03f4d7d89c7396d69a8337889a40957b31b572d150eb432fb22a37ca53948106009c33846c49db6499ec90a49a617ab79ce3b29c8b9c20b8b1c2
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Manually Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch: {}
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
manually_publish:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Set up Ruby
|
|
14
|
+
uses: ruby/setup-ruby@v1
|
|
15
|
+
with:
|
|
16
|
+
ruby-version: "3.3"
|
|
17
|
+
|
|
18
|
+
- name: Publish to RubyGems
|
|
19
|
+
run: |
|
|
20
|
+
mkdir -p $HOME/.gem
|
|
21
|
+
touch $HOME/.gem/credentials
|
|
22
|
+
chmod 0600 $HOME/.gem/credentials
|
|
23
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
24
|
+
gem build *.gemspec
|
|
25
|
+
gem push *.gem
|
|
26
|
+
env:
|
|
27
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
|
@@ -4,6 +4,7 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
6
|
- main
|
|
7
|
+
workflow_dispatch: {}
|
|
7
8
|
|
|
8
9
|
jobs:
|
|
9
10
|
release:
|
|
@@ -19,7 +20,7 @@ jobs:
|
|
|
19
20
|
steps:
|
|
20
21
|
- name: Release
|
|
21
22
|
id: release
|
|
22
|
-
uses: googleapis/release-please-action@
|
|
23
|
+
uses: googleapis/release-please-action@v5
|
|
23
24
|
with:
|
|
24
25
|
release-type: ruby
|
|
25
26
|
|
data/.gitignore
CHANGED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# hledger-forecast
|
|
2
|
+
|
|
3
|
+
This is a RubyGem that enables users to use a CSV file as a forecast. This then feeds into hledger, generating entries into a journal file.
|
|
4
|
+
|
|
5
|
+
## Code formatting
|
|
6
|
+
|
|
7
|
+
After editing any Ruby files, run:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
rubyfmt spec lib -i
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Tests
|
|
14
|
+
|
|
15
|
+
Tests can be run with:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
rspec
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Non-verbose grouping
|
|
22
|
+
|
|
23
|
+
In non-verbose mode, `Generator.build_groups` (lib/hledger_forecast/generator.rb) merges CSV rows into a single hledger periodic transaction only when they share `[type, frequency, from, to, account, category]`. Rows with different categories always produce separate `~` entries, even if they share account/date - hledger treats postings within one transaction as linked, so bundling unrelated line items under one transaction misrepresents them in reports (e.g. `hledger register` shows every posting's account against every row, not just the filtered one). When touching grouping logic, check the effect through `hledger reg`/`register`, not just the raw journal text output.
|
|
24
|
+
|
|
25
|
+
## version.rb
|
|
26
|
+
|
|
27
|
+
`lib/hledger_forecast/version.rb` is bumped by release automation (release-please), not manually. If it shows as changed after running tests/tooling and you didn't intend a release bump, `git checkout` it rather than committing the bump.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.3.1](https://github.com/olimorris/hledger-forecast/compare/v3.3.0...v3.3.1) (2026-07-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* stop grouping transactions by category ([#21](https://github.com/olimorris/hledger-forecast/issues/21)) ([2a718ee](https://github.com/olimorris/hledger-forecast/commit/2a718ee506ea6b7e6058f5321351e959e4267f1c))
|
|
9
|
+
|
|
10
|
+
## [3.3.0](https://github.com/olimorris/hledger-forecast/compare/v3.2.0...v3.3.0) (2026-04-13)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* better errors ([#19](https://github.com/olimorris/hledger-forecast/issues/19)) ([da16ed8](https://github.com/olimorris/hledger-forecast/commit/da16ed8a1fe2398e5af2e0428339b87b25f7691a))
|
|
16
|
+
|
|
3
17
|
## [3.2.0](https://github.com/olimorris/hledger-forecast/compare/v3.1.0...v3.2.0) (2026-04-13)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://github.com/
|
|
2
|
+
<img src="https://github.com/user-attachments/assets/f8582e29-c4f0-429d-9e3c-358fb719ed35" alt="hledger-forecast" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">hledger-forecast</h1>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<a href="https://github.com/olimorris/hledger-forecast/releases"><img src="https://img.shields.io/github/v/release/olimorris/hledger-forecast?style=for-the-badge"</a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Using a _CSV_ 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)!**
|
|
14
14
|
|
|
15
15
|
## :sparkles: Features
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
Forecasting is essential to how I plan and budget with hledger. It enables me to know my financial position in 1 month, 10 months or even 100 months from now.
|
|
27
27
|
|
|
28
|
-
But forecasting in hledger is verbose and
|
|
28
|
+
But forecasting in hledger is verbose and manual. Consider this scenario: you purchase a new $3,000 laptop on a 0% finance deal and spread it over 20 months. In hledger, this would be accounted for with:
|
|
29
29
|
|
|
30
30
|
```ledger
|
|
31
31
|
~ monthly from 2026-01-01 to 2027-08-31 * New Laptop
|
|
@@ -5,7 +5,10 @@ module HledgerForecast
|
|
|
5
5
|
def self.evaluate(amount)
|
|
6
6
|
return amount.to_f unless amount.is_a?(String)
|
|
7
7
|
|
|
8
|
-
@calc.evaluate(amount.slice(1..-1))
|
|
8
|
+
result = @calc.evaluate(amount.slice(1..-1))
|
|
9
|
+
raise ArgumentError, "invalid amount '#{amount}'" if result.nil?
|
|
10
|
+
|
|
11
|
+
result
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
def self.evaluate_from_date(value)
|
data/lib/hledger_forecast/cli.rb
CHANGED
|
@@ -283,7 +283,12 @@ module HledgerForecast
|
|
|
283
283
|
config = File.read(options[:forecast_file])
|
|
284
284
|
config = HledgerForecast::CSVParser.parse(config) if options[:file_type] == "csv"
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
begin
|
|
287
|
+
summarizer = Summarizer.summarize(config, options)
|
|
288
|
+
rescue StandardError => e
|
|
289
|
+
puts("An error occurred while summarizing transactions: #{e.message}")
|
|
290
|
+
exit(1)
|
|
291
|
+
end
|
|
287
292
|
|
|
288
293
|
puts(SummarizerFormatter.format(summarizer[:output], summarizer[:settings]))
|
|
289
294
|
end
|
|
@@ -11,7 +11,15 @@ module HledgerForecast
|
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
settings = Settings.parse(rows.select { |r| r[:type] == "settings" }, cli_options)
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
transactions = []
|
|
16
|
+
rows.each_with_index do |r, i|
|
|
17
|
+
next if r[:type] == "settings"
|
|
18
|
+
|
|
19
|
+
transactions << Transaction.from_row(r)
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
raise "CSV row #{i + 2}: #{e.message}"
|
|
22
|
+
end
|
|
15
23
|
|
|
16
24
|
new(transactions, settings, rows.headers.include?(:tag))
|
|
17
25
|
end
|
|
@@ -26,7 +26,7 @@ module HledgerForecast
|
|
|
26
26
|
end
|
|
27
27
|
else
|
|
28
28
|
transactions
|
|
29
|
-
.group_by { |t| [t.type, t.frequency, t.from, t.to, t.account] }
|
|
29
|
+
.group_by { |t| [t.type, t.frequency, t.from, t.to, t.account, t.category] }
|
|
30
30
|
.map do |(type, frequency, from, to, account), txns|
|
|
31
31
|
TransactionGroup.new(
|
|
32
32
|
type: type,
|
|
@@ -24,6 +24,8 @@ module HledgerForecast
|
|
|
24
24
|
keyword_init: true
|
|
25
25
|
) do
|
|
26
26
|
def self.from_row(row)
|
|
27
|
+
validate_required_fields!(row)
|
|
28
|
+
|
|
27
29
|
from = Calculator.evaluate_from_date(row[:from])
|
|
28
30
|
new(
|
|
29
31
|
type: row[:type],
|
|
@@ -64,6 +66,15 @@ module HledgerForecast
|
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
def summary_exclude? = !!summary_exclude
|
|
69
|
+
|
|
70
|
+
def self.validate_required_fields!(row)
|
|
71
|
+
%i[type account from category amount].each do |field|
|
|
72
|
+
next unless row[field].nil? || row[field].to_s.strip.empty?
|
|
73
|
+
|
|
74
|
+
hint = row[:description] ? " (description: '#{row[:description]}')" : ""
|
|
75
|
+
raise ArgumentError, "missing required field '#{field}'#{hint}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
67
78
|
end
|
|
68
79
|
|
|
69
80
|
TransactionGroup = Struct.new(:type, :frequency, :account, :from, :to, :transactions, keyword_init: true)
|
data/spec/cli_spec.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require_relative "../lib/hledger_forecast"
|
|
2
2
|
|
|
3
3
|
output = <<~JOURNAL
|
|
4
|
-
~ monthly from 2023-03-01 * Mortgage
|
|
4
|
+
~ monthly from 2023-03-01 * Mortgage
|
|
5
5
|
Expenses:Mortgage £2,000.55
|
|
6
|
+
Assets:Bank
|
|
7
|
+
|
|
8
|
+
~ monthly from 2023-03-01 * Food
|
|
6
9
|
Expenses:Food £100.00
|
|
7
10
|
Assets:Bank
|
|
8
11
|
|
|
@@ -9,8 +9,11 @@ config = <<~CSV
|
|
|
9
9
|
CSV
|
|
10
10
|
|
|
11
11
|
output = <<~JOURNAL
|
|
12
|
-
~ monthly from 2023-05-01 * New kitchen
|
|
12
|
+
~ monthly from 2023-05-01 * New kitchen
|
|
13
13
|
Expenses:House £208.33
|
|
14
|
+
Liabilities:Amex
|
|
15
|
+
|
|
16
|
+
~ monthly from 2023-05-01 * Monthly food shop, Random food
|
|
14
17
|
Expenses:Food £107.50
|
|
15
18
|
Expenses:Food £52.88
|
|
16
19
|
Liabilities:Amex
|
|
@@ -8,8 +8,11 @@ config = <<~CSV
|
|
|
8
8
|
CSV
|
|
9
9
|
|
|
10
10
|
output = <<~JOURNAL
|
|
11
|
-
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage
|
|
11
|
+
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage
|
|
12
12
|
Expenses:Mortgage £2,000.00
|
|
13
|
+
Assets:Bank
|
|
14
|
+
|
|
15
|
+
~ monthly from 2023-03-01 to 2023-06-01 * Food
|
|
13
16
|
Expenses:Food £100.00
|
|
14
17
|
Assets:Bank
|
|
15
18
|
|
data/spec/monthly_spec.rb
CHANGED
|
@@ -9,9 +9,15 @@ config = <<~CSV
|
|
|
9
9
|
CSV
|
|
10
10
|
|
|
11
11
|
output = <<~JOURNAL
|
|
12
|
-
~ monthly from 2023-03-01 * Bills
|
|
12
|
+
~ monthly from 2023-03-01 * Bills
|
|
13
13
|
Expenses:Bills £175.00
|
|
14
|
+
Assets:Bank
|
|
15
|
+
|
|
16
|
+
~ monthly from 2023-03-01 * Food
|
|
14
17
|
Expenses:Food £500.00
|
|
18
|
+
Assets:Bank
|
|
19
|
+
|
|
20
|
+
~ monthly from 2023-03-01 * Savings
|
|
15
21
|
Assets:Savings £-1,000.00
|
|
16
22
|
Assets:Bank
|
|
17
23
|
|
data/spec/tags_spec.rb
CHANGED
|
@@ -18,9 +18,15 @@ CSV
|
|
|
18
18
|
RSpec.describe "tags" do
|
|
19
19
|
it "outputs hledger tags in posting comments" do
|
|
20
20
|
expected = <<~JOURNAL
|
|
21
|
-
~ monthly from 2023-03-01 * Salary
|
|
21
|
+
~ monthly from 2023-03-01 * Salary
|
|
22
22
|
Income:Salary £-3,500.00; fixed:, essential:
|
|
23
|
+
Assets:Bank
|
|
24
|
+
|
|
25
|
+
~ monthly from 2023-03-01 * Food
|
|
23
26
|
Expenses:Food £500.00 ; living:, essential:
|
|
27
|
+
Assets:Bank
|
|
28
|
+
|
|
29
|
+
~ monthly from 2023-03-01 * Netflix
|
|
24
30
|
Expenses:Subscriptions £15.00 ; living:
|
|
25
31
|
Assets:Bank
|
|
26
32
|
|
|
@@ -31,8 +37,11 @@ RSpec.describe "tags" do
|
|
|
31
37
|
|
|
32
38
|
it "omits comments when no tags are present" do
|
|
33
39
|
expected = <<~JOURNAL
|
|
34
|
-
~ monthly from 2023-03-01 * Salary
|
|
40
|
+
~ monthly from 2023-03-01 * Salary
|
|
35
41
|
Income:Salary £-3,500.00
|
|
42
|
+
Assets:Bank
|
|
43
|
+
|
|
44
|
+
~ monthly from 2023-03-01 * Food
|
|
36
45
|
Expenses:Food £500.00
|
|
37
46
|
Assets:Bank
|
|
38
47
|
|
|
@@ -43,8 +52,11 @@ RSpec.describe "tags" do
|
|
|
43
52
|
|
|
44
53
|
it "filters transactions by a single tag" do
|
|
45
54
|
expected = <<~JOURNAL
|
|
46
|
-
~ monthly from 2023-03-01 * Food
|
|
55
|
+
~ monthly from 2023-03-01 * Food
|
|
47
56
|
Expenses:Food £500.00; living:, essential:
|
|
57
|
+
Assets:Bank
|
|
58
|
+
|
|
59
|
+
~ monthly from 2023-03-01 * Netflix
|
|
48
60
|
Expenses:Subscriptions £15.00 ; living:
|
|
49
61
|
Assets:Bank
|
|
50
62
|
|
|
@@ -55,8 +67,11 @@ RSpec.describe "tags" do
|
|
|
55
67
|
|
|
56
68
|
it "filters transactions by multiple tags (OR logic)" do
|
|
57
69
|
expected = <<~JOURNAL
|
|
58
|
-
~ monthly from 2023-03-01 * Salary
|
|
70
|
+
~ monthly from 2023-03-01 * Salary
|
|
59
71
|
Income:Salary £-3,500.00; fixed:, essential:
|
|
72
|
+
Assets:Bank
|
|
73
|
+
|
|
74
|
+
~ monthly from 2023-03-01 * Food
|
|
60
75
|
Expenses:Food £500.00 ; living:, essential:
|
|
61
76
|
Assets:Bank
|
|
62
77
|
|
|
@@ -81,8 +96,11 @@ RSpec.describe "tags" do
|
|
|
81
96
|
|
|
82
97
|
it "excludes tags in generator output" do
|
|
83
98
|
expected = <<~JOURNAL
|
|
84
|
-
~ monthly from 2023-03-01 * Food
|
|
99
|
+
~ monthly from 2023-03-01 * Food
|
|
85
100
|
Expenses:Food £500.00; living:, essential:
|
|
101
|
+
Assets:Bank
|
|
102
|
+
|
|
103
|
+
~ monthly from 2023-03-01 * Netflix
|
|
86
104
|
Expenses:Subscriptions £15.00 ; living:
|
|
87
105
|
Assets:Bank
|
|
88
106
|
|
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: 3.
|
|
4
|
+
version: 3.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oli Morris
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: abbrev
|
|
@@ -132,10 +132,12 @@ extensions: []
|
|
|
132
132
|
extra_rdoc_files: []
|
|
133
133
|
files:
|
|
134
134
|
- ".github/workflows/ci.yml"
|
|
135
|
+
- ".github/workflows/manually_publish.yml"
|
|
135
136
|
- ".github/workflows/release.yml"
|
|
136
137
|
- ".gitignore"
|
|
137
138
|
- ".mise.toml"
|
|
138
139
|
- ".rubocop.yml"
|
|
140
|
+
- AGENTS.md
|
|
139
141
|
- CHANGELOG.md
|
|
140
142
|
- Gemfile
|
|
141
143
|
- LICENSE
|