hledger-forecast 3.0.0 → 3.2.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: 33ae1c8b9a296f2420b87963e12ccc77a6a4b18cb5e9e977ed6e17f3d83b3c03
4
- data.tar.gz: f4a4e1fa8df7a5d982036349903ce6a38b68edc59f2734be1ebc12a071ebf27f
3
+ metadata.gz: 5acfd1bbab287a58bfe5ce7237576535b6b927d034da9e3651a4c240653e0f33
4
+ data.tar.gz: 23eb8d2646996b422473440b8f90797215a7390f4d6e2862b470bd361b6d55d5
5
5
  SHA512:
6
- metadata.gz: 1942ba18912517ceb70a6f2e4639632f5bccb6c98945d88509405c046e41c59631bebb099f15c89cc630ebca270360d5efd2c9eaea9fe7449eeb9be2fc5d9dee
7
- data.tar.gz: d717115be6e73cf7277f29e01d4f465a63f1e03c88271a16ecd9df20600430b0d7483dc5f9a4c1831796a71413fb8bb2df9adb9f9dd5b3fd4c583c7e3e79a5fa
6
+ metadata.gz: 62a027f56e0faf7641769b13928ed61c91e3851fe6b1623df00efd233ec244be03c57d7b94ea2605f01580ba18e0a916807a59c6ef848118d9b80fa9a3e8be28
7
+ data.tar.gz: 470b89a138eff0a2f5da24d4a4fe31ffd7e9584646094665020d19a40a9303877073bfcbc95eb35eae1498149fbfa96b899bd44c5ee4eab4225f26acec9b2e11
@@ -13,9 +13,36 @@ jobs:
13
13
  contents: write
14
14
  pull-requests: write
15
15
 
16
+ outputs:
17
+ release_created: ${{ steps.release.outputs.release_created }}
18
+
16
19
  steps:
17
- - name: Release 📦
20
+ - name: Release
21
+ id: release
18
22
  uses: googleapis/release-please-action@v4
19
23
  with:
20
24
  release-type: ruby
21
25
 
26
+ publish:
27
+ runs-on: ubuntu-latest
28
+ needs: release
29
+ if: needs.release.outputs.release_created == 'true'
30
+
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - name: Set up Ruby
35
+ uses: ruby/setup-ruby@v1
36
+ with:
37
+ ruby-version: "3.3"
38
+
39
+ - name: Publish to RubyGems
40
+ run: |
41
+ mkdir -p $HOME/.gem
42
+ touch $HOME/.gem/credentials
43
+ chmod 0600 $HOME/.gem/credentials
44
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
45
+ gem build *.gemspec
46
+ gem push *.gem
47
+ env:
48
+ GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.2.0](https://github.com/olimorris/hledger-forecast/compare/v3.1.0...v3.2.0) (2026-04-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * can exclude tags in summarizer ([#16](https://github.com/olimorris/hledger-forecast/issues/16)) ([faee83e](https://github.com/olimorris/hledger-forecast/commit/faee83ee8eb554d146177832c307b87f5ecd4616))
9
+ * from date can have calculations ([#18](https://github.com/olimorris/hledger-forecast/issues/18)) ([e62a693](https://github.com/olimorris/hledger-forecast/commit/e62a69385bf6c34f2d446593d5046fe22a1e7f5f))
10
+
11
+ ## [3.1.0](https://github.com/olimorris/hledger-forecast/compare/v3.0.0...v3.1.0) (2026-04-02)
12
+
13
+
14
+ ### Features
15
+
16
+ * dates now support `+` prefix ([#14](https://github.com/olimorris/hledger-forecast/issues/14)) ([16ee70e](https://github.com/olimorris/hledger-forecast/commit/16ee70ec6d3a717f679fddcbf22324f71010e93d))
17
+
3
18
  ## [3.0.0](https://github.com/olimorris/hledger-forecast/compare/v2.0.1...v3.0.0) (2026-03-27)
4
19
 
5
20
 
data/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  <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>
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
  **"Improved", you say?** 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)!**
13
14
 
14
15
  ## :sparkles: Features
@@ -37,7 +38,7 @@ Except, you'd need to work out what 20 months from `2026-01-01` is and do $3000
37
38
  In `hledger-forecast`, you add a single line to your CSV:
38
39
 
39
40
  ```csv
40
- monthly,,Assets:Checking,01/01/2026,=20,New Laptop,Expenses:General Expenses,=3000/20,,,
41
+ monthly,,Assets:Checking,01/01/2026,+20,New Laptop,Expenses:General Expenses,=3000/20,,,
41
42
  ```
42
43
 
43
44
  The tool calculates the amount and the end date for you.
@@ -67,14 +68,14 @@ Assuming you have Ruby and [RubyGems](http://rubygems.org/pages/download) instal
67
68
  ## :rocket: Usage
68
69
 
69
70
  hledger-forecast
70
-
71
+
71
72
  Usage: hledger-forecast [command] [options]
72
-
73
+
73
74
  Commands:
74
75
  generate Generate a forecast from a CSV file
75
76
  summarize Summarize the forecast file and output to the terminal
76
77
  compare Compare and highlight the differences between two CSV files
77
-
78
+
78
79
  Options:
79
80
  -h, --help Show this help message
80
81
  -v, --version Show version
@@ -84,9 +85,9 @@ Assuming you have Ruby and [RubyGems](http://rubygems.org/pages/download) instal
84
85
  Reads your CSV file and creates a journal file ready to use with hledger. See [example.journal](https://github.com/olimorris/hledger-forecast/blob/main/example.journal) for an example of the output.
85
86
 
86
87
  hledger-forecast generate -f my_forecast.csv -o forecast.journal
87
-
88
+
88
89
  Usage: hledger-forecast generate [options]
89
-
90
+
90
91
  -f, --forecast FILE The path to the FORECAST CSV file to generate from
91
92
  -o, --output-file FILE The path to the OUTPUT file to create
92
93
  -t, --tags TAGS Only include transactions with given tags (comma-separated)
@@ -111,9 +112,9 @@ This will generate a forecast up to the end of Feb 2027, showing asset balances
111
112
  As your forecast grows, it's useful to see the totals at a glance. Think of this as your income statement, rolled up to whatever period makes sense.
112
113
 
113
114
  hledger-forecast summarize -f my_forecast.csv
114
-
115
+
115
116
  Usage: hledger-forecast summarize [options]
116
-
117
+
117
118
  -f, --forecast FILE The path to the FORECAST CSV file to summarize
118
119
  -r, --roll-up PERIOD The period to roll-up your forecasts into. One of:
119
120
  [yearly], [half-yearly], [quarterly], [monthly], [weekly], [daily]
@@ -146,8 +147,8 @@ The CSV file should have a header row with these columns:
146
147
  | `type` | string | yes | One of: `monthly`, `quarterly`, `half-yearly`, `yearly`, `once`, `custom` |
147
148
  | `frequency` | string | `custom` only | Repeating frequency, using hledger's [periodic rule syntax](https://hledger.org/dev/hledger.html#periodic-transactions) |
148
149
  | `account` | string | yes | The account the transaction applies to, e.g. `Assets:Bank` |
149
- | `from` | date | yes | Start date, e.g. `01/03/2023` |
150
- | `to` | date | no | End date, e.g. `01/01/2025` |
150
+ | `from` | date | yes | Start date, e.g. `01/03/2023`. Supports `=` prefix for calculated values, e.g. `=01/03/2023+(5*12)` |
151
+ | `to` | date | no | End date, e.g. `01/01/2025`. Supports `+` prefix for calculated values, e.g. `+12` for 12 months |
151
152
  | `description` | string | yes | A description of the transaction |
152
153
  | `category` | string | yes | The category account, e.g. `Expenses:Food` |
153
154
  | `amount` | number | yes | The transaction amount. Supports `=` prefix for calculated values |
@@ -164,7 +165,7 @@ monthly,,Assets:Bank,01/03/2023,01/01/2025,Mortgage,Expenses:Mortgage,2000,,,fix
164
165
  monthly,,Assets:Bank,01/03/2023,,Bills,Expenses:Bills,175,,,fixed|essential
165
166
  monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,500,,,living|essential
166
167
  monthly,,Assets:Bank,01/03/2023,,New Kitchen,Expenses:House,=5000/24,,,living
167
- monthly,,Assets:Bank,01/03/2023,=12,Holiday,Expenses:Holiday,125,,,living
168
+ monthly,,Assets:Bank,01/03/2023,+12,Holiday,Expenses:Holiday,125,,,living
168
169
  monthly,,Assets:Bank,01/03/2023,01/03/2025,Rainy day fund,Assets:Savings,300,,,saving
169
170
  monthly,,Assets:Pension,01/01/2024,,Pension draw down,Income:Pension,-500,,,fixed|essential
170
171
  quarterly,,Assets:Bank,01/04/2023,,Quarterly bonus,Income:Bonus,-1000,,,fixed
@@ -188,13 +189,29 @@ monthly,,Assets:Bank,01/03/2023,,New Kitchen,Expenses:House,=5000/24,,
188
189
 
189
190
  ### Calculated dates
190
191
 
191
- The `to` column also supports calculated values. Use `=` followed by a number to mean "N months from the `from` date":
192
+ Both `from` and `to` support calculated values.
193
+
194
+ **`from` column** — prefix with `=` to compute a start date. The formula is `=BASE_DATE+OFFSET` where the offset is evaluated as months:
192
195
 
193
196
  ```csv
194
- monthly,,Assets:Bank,01/03/2023,=12,Holiday,Expenses:Holiday,125,,
197
+ monthly,,Assets:Bank,=01/09/2025+(5*12),,Salary,Income:Salary,-3500,,
195
198
  ```
196
199
 
197
- That sets the end date to 12 months after `01/03/2023`.
200
+ That sets the start date to 60 months (5 years) after `01/09/2025`, giving `01/09/2030`.
201
+
202
+ **`to` column** — use `+` followed by a number to mean "N months from the `from` date":
203
+
204
+ ```csv
205
+ monthly,,Assets:Bank,01/03/2026,+12,Holiday,Expenses:Holiday,125,,
206
+ ```
207
+
208
+ That sets the end date to 12 months after `01/03/2026`. The `=` prefix also supports formulas — useful for longer periods:
209
+
210
+ ```csv
211
+ monthly,,Assets:Bank,01/03/2026,=12*5,Mortgage,Expenses:Mortgage,2000,,
212
+ ```
213
+
214
+ That sets the end date to 5 years (60 months) after `01/03/2026`.
198
215
 
199
216
  ### Tags
200
217
 
@@ -228,6 +245,9 @@ hledger-forecast summarize -f forecast.csv --tags=essential
228
245
 
229
246
  # Multiple tags use OR logic — matches any
230
247
  hledger-forecast summarize -f forecast.csv --tags=fixed,living
248
+
249
+ # Can also exclude tags with a `-` prefix
250
+ hledger-forecast summarize -f forecast.csv --tags=fixed,-essential
231
251
  ```
232
252
 
233
253
  **Querying in hledger** - because the tags are native hledger format, you can query them directly:
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/example.csv CHANGED
@@ -10,7 +10,7 @@ monthly,,Assets:Pension,01/01/2024,,Pension draw down,Income:Pension,-500,,,fixe
10
10
  quarterly,,Assets:Bank,01/04/2023,,Quarterly bonus,Income:Bonus,-1000,,,fixed
11
11
  half-yearly,,Assets:Bank,01/04/2023,,Top up holiday funds,Expenses:Holiday,500,,,living
12
12
  yearly,,Assets:Bank,01/04/2023,,Annual bonus,Income:Bonus,-2000,,,fixed
13
- once,,Assets:Bank,05/03/2023,,Refund for that damn laptop,Expenses:Shopping,-3000,TRUE,
13
+ once,,Assets:Bank,05/03/2023,,Refund for that damn laptop,Expenses:Shopping,-3000,,,
14
14
  custom,every 2 weeks,Assets:Bank,01/03/2023,,Hair and beauty,Expenses:Personal Care,80,26,,living
15
15
  custom,every 5 weeks,Assets:Bank,01/03/2023,,Misc expenses,Expenses:General Expenses,30,10.4,,living
16
16
  settings,currency,USD,,,,,,,,
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.name = "hledger-forecast"
9
9
  s.version = HledgerForecast::VERSION
10
10
  s.authors = ["Oli Morris"]
11
- s.summary = "An extended wrapper around hledger's forecasting functionality"
12
- s.description = "Use a CSV file for improved forecasting with hledger"
11
+ s.summary = "Generate hledger journal entries and income statements from a CSV forecast"
12
+ s.description = "Define recurring transactions in a CSV file with support for formulas, calculated dates, and tags. Generates hledger periodic transaction journals and provides income statement summaries with savings rates."
13
13
  s.email = "olimorris@users.noreply.github.com"
14
14
  s.homepage = "https://github.com/olimorris/hledger-forecast"
15
15
  s.license = "MIT"
@@ -8,8 +8,20 @@ module HledgerForecast
8
8
  @calc.evaluate(amount.slice(1..-1))
9
9
  end
10
10
 
11
+ def self.evaluate_from_date(value)
12
+ value = value.to_s
13
+ return Date.parse(value) unless value.start_with?("=")
14
+
15
+ date_str, offset_expr = value[1..].split("+", 2)
16
+ date = Date.parse(date_str)
17
+ return date unless offset_expr
18
+
19
+ date >> @calc.evaluate(offset_expr).to_i
20
+ end
21
+
11
22
  def self.evaluate_date(from, to)
12
- return Date.parse(to) unless to.start_with?("=")
23
+ return (from >> to) - 1 if to.is_a?(Numeric)
24
+ return Date.parse(to) unless to.start_with?("=") || to.start_with?("+")
13
25
 
14
26
  (from >> @calc.evaluate(to.slice(1..-1))) - 1
15
27
  end
@@ -24,13 +24,13 @@ module HledgerForecast
24
24
  keyword_init: true
25
25
  ) do
26
26
  def self.from_row(row)
27
- from = Date.parse(row[:from].to_s)
27
+ from = Calculator.evaluate_from_date(row[:from])
28
28
  new(
29
29
  type: row[:type],
30
30
  frequency: row[:frequency],
31
31
  account: row[:account],
32
32
  from: from,
33
- to: row[:to] ? Calculator.evaluate_date(from, row[:to].to_s) : nil,
33
+ to: row[:to] ? Calculator.evaluate_date(from, row[:to]) : nil,
34
34
  description: row[:description],
35
35
  category: row[:category],
36
36
  amount: Calculator.evaluate(row[:amount]),
@@ -42,7 +42,14 @@ module HledgerForecast
42
42
 
43
43
  def matches_tags?(filter_tags)
44
44
  return true if filter_tags.nil? || filter_tags.empty?
45
- (tags & filter_tags).any?
45
+
46
+ exclude_tags = filter_tags.select { |t| t.start_with?("-") }.map { |t| t[1..] }
47
+ include_tags = filter_tags.reject { |t| t.start_with?("-") }
48
+
49
+ return false if exclude_tags.any? && (tags & exclude_tags).any?
50
+ return (tags & include_tags).any? if include_tags.any?
51
+
52
+ true
46
53
  end
47
54
 
48
55
  def annualised_amount
@@ -1,3 +1,3 @@
1
1
  module HledgerForecast
2
- VERSION = "3.0.0"
2
+ VERSION = "3.2.0"
3
3
  end
@@ -23,6 +23,24 @@ RSpec.describe HledgerForecast::Calculator do
23
23
  end
24
24
  end
25
25
 
26
+ describe ".evaluate_from_date" do
27
+ it "parses a plain date string" do
28
+ expect(described_class.evaluate_from_date("01/09/2022")).to(eq(Date.parse("2022-09-01")))
29
+ end
30
+
31
+ it "evaluates a date with a month offset" do
32
+ expect(described_class.evaluate_from_date("=01/09/2022+6")).to(eq(Date.parse("2023-03-01")))
33
+ end
34
+
35
+ it "evaluates a date with a multiplied offset" do
36
+ expect(described_class.evaluate_from_date("=01/09/2022+(5*12)")).to(eq(Date.parse("2027-09-01")))
37
+ end
38
+
39
+ it "evaluates a formula with no offset" do
40
+ expect(described_class.evaluate_from_date("=01/09/2022")).to(eq(Date.parse("2022-09-01")))
41
+ end
42
+ end
43
+
26
44
  describe ".evaluate_date" do
27
45
  let(:from) { Date.parse("2023-03-01") }
28
46
 
@@ -41,5 +59,13 @@ RSpec.describe HledgerForecast::Calculator do
41
59
  it "handles a 24-month offset" do
42
60
  expect(described_class.evaluate_date(from, "=24")).to(eq(Date.parse("2025-02-28")))
43
61
  end
62
+
63
+ it "handles dates with a +" do
64
+ expect(described_class.evaluate_date(from, "+24")).to(eq(Date.parse("2025-02-28")))
65
+ end
66
+
67
+ it "handles a numeric offset (CSV numeric converter coerces +N to integer)" do
68
+ expect(described_class.evaluate_date(from, 24)).to(eq(Date.parse("2025-02-28")))
69
+ end
44
70
  end
45
71
  end
data/spec/tags_spec.rb CHANGED
@@ -72,6 +72,32 @@ RSpec.describe "tags" do
72
72
  expect(descriptions).to(eq(["Food", "Netflix"]))
73
73
  end
74
74
 
75
+ it "excludes transactions by tag with - prefix" do
76
+ result = HledgerForecast::Summarizer.summarize(config, {tags: ["-fixed"]})
77
+ descriptions = result[:output].map { |r| r[:description] }
78
+
79
+ expect(descriptions).to(eq(["Food", "Netflix"]))
80
+ end
81
+
82
+ it "excludes tags in generator output" do
83
+ expected = <<~JOURNAL
84
+ ~ monthly from 2023-03-01 * Food, Netflix
85
+ Expenses:Food £500.00; living:, essential:
86
+ Expenses:Subscriptions £15.00 ; living:
87
+ Assets:Bank
88
+
89
+ JOURNAL
90
+
91
+ expect(HledgerForecast::Generator.generate(config, {tags: ["-fixed"]})).to(eq(expected))
92
+ end
93
+
94
+ it "combines include and exclude tags" do
95
+ result = HledgerForecast::Summarizer.summarize(config, {tags: ["essential", "-fixed"]})
96
+ descriptions = result[:output].map { |r| r[:description] }
97
+
98
+ expect(descriptions).to(eq(["Food"]))
99
+ end
100
+
75
101
  it "raises an error when --tags is used on a CSV without a tag column" do
76
102
  csv_without_tag_column = <<~CSV
77
103
  type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude
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.0.0
4
+ version: 3.2.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: 2026-03-27 00:00:00.000000000 Z
11
+ date: 2026-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: abbrev
@@ -122,7 +122,9 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '3.12'
125
- description: Use a CSV file for improved forecasting with hledger
125
+ description: Define recurring transactions in a CSV file with support for formulas,
126
+ calculated dates, and tags. Generates hledger periodic transaction journals and
127
+ provides income statement summaries with savings rates.
126
128
  email: olimorris@users.noreply.github.com
127
129
  executables:
128
130
  - hledger-forecast
@@ -130,7 +132,6 @@ extensions: []
130
132
  extra_rdoc_files: []
131
133
  files:
132
134
  - ".github/workflows/ci.yml"
133
- - ".github/workflows/publish_ruby_gem.yml"
134
135
  - ".github/workflows/release.yml"
135
136
  - ".gitignore"
136
137
  - ".mise.toml"
@@ -139,6 +140,7 @@ files:
139
140
  - Gemfile
140
141
  - LICENSE
141
142
  - README.md
143
+ - Rakefile
142
144
  - bin/hledger-forecast
143
145
  - example.csv
144
146
  - example.journal
@@ -198,10 +200,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
200
  - !ruby/object:Gem::Version
199
201
  version: '0'
200
202
  requirements: []
201
- rubygems_version: 3.4.20
203
+ rubygems_version: 3.5.22
202
204
  signing_key:
203
205
  specification_version: 4
204
- summary: An extended wrapper around hledger's forecasting functionality
206
+ summary: Generate hledger journal entries and income statements from a CSV forecast
205
207
  test_files:
206
208
  - spec/calculator_spec.rb
207
209
  - spec/cli_spec.rb
@@ -1,24 +0,0 @@
1
- name: Publish Ruby Gem
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
- workflow_dispatch:
8
-
9
- jobs:
10
- release:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - uses: actions/checkout@v4
15
- - name: Publish Gem 💎️
16
- run: |
17
- mkdir -p $HOME/.gem
18
- touch $HOME/.gem/credentials
19
- chmod 0600 $HOME/.gem/credentials
20
- printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
21
- gem build *.gemspec
22
- gem push *.gem
23
- env:
24
- GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"