hledger-forecast 3.3.1 → 3.4.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: 0b3fc72db9564b895f8412f6467b3350700b12e7c4cb393ae48d7640ff0766ef
4
- data.tar.gz: 78092306250621f6d6e93df98d600035d85960125d40b2df35bd728f1e38cb73
3
+ metadata.gz: e8f490f679ec154468837d25878ef658fd0225d73cdd30f291679475e247d00f
4
+ data.tar.gz: bcc7247a985c775b2b053c43d25f4e5269ed224ee6f396afdaa4339f6ddf4c52
5
5
  SHA512:
6
- metadata.gz: c07f7f68ff23714ec25f2708405d4700f8f0df800e07571d805c548598d23775e70b947ba94c74e72c2f9b6064fefdcef2a9d2931fb4d863856c7f0ad1ec14b3
7
- data.tar.gz: b00893cc0d8f03f4d7d89c7396d69a8337889a40957b31b572d150eb432fb22a37ca53948106009c33846c49db6499ec90a49a617ab79ce3b29c8b9c20b8b1c2
6
+ metadata.gz: 0cf0966195c657ec869ecf442031c1d9a1e71dca2ebff8544efeb1fda027945a2ed3cbd280241148cfff276fc01fc08878a25f93876ba4c0e71ada81dae67b4c
7
+ data.tar.gz: 24de22a18a04df4041401495776267beeae94e5eb3e3ee95362b81216cf48e64f484d70bcad441751c63dc74d12400cdd601cbac5ed56f7958c266f43933af02
@@ -14,7 +14,7 @@ jobs:
14
14
  ruby-version: ['3.3', '3.4']
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v4
17
+ - uses: actions/checkout@v7
18
18
 
19
19
  - name: Update packages 📦️
20
20
  run: sudo apt-get update
@@ -8,7 +8,7 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
 
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v7
12
12
 
13
13
  - name: Set up Ruby
14
14
  uses: ruby/setup-ruby@v1
@@ -22,7 +22,8 @@ jobs:
22
22
  id: release
23
23
  uses: googleapis/release-please-action@v5
24
24
  with:
25
- release-type: ruby
25
+ config-file: release-please-config.json
26
+ manifest-file: .release-please-manifest.json
26
27
 
27
28
  publish:
28
29
  runs-on: ubuntu-latest
@@ -30,7 +31,7 @@ jobs:
30
31
  if: needs.release.outputs.release_created == 'true'
31
32
 
32
33
  steps:
33
- - uses: actions/checkout@v4
34
+ - uses: actions/checkout@v7
34
35
 
35
36
  - name: Set up Ruby
36
37
  uses: ruby/setup-ruby@v1
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "3.4.0"
3
+ }
data/AGENTS.md CHANGED
@@ -25,3 +25,5 @@ In non-verbose mode, `Generator.build_groups` (lib/hledger_forecast/generator.rb
25
25
  ## version.rb
26
26
 
27
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.
28
+
29
+ The bump comes from the release PR, driven by `release-please-config.json`. Two settings there are load-bearing: `version-file` points at `version.rb`, and `package-name` is the gem name the `Gemfile.lock` updater matches on. Without them release-please writes only `CHANGELOG.md`, and because the gemspec reads `HledgerForecast::VERSION`, the publish job then pushes the previous version and RubyGems rejects it as a duplicate. Do not add a `release-type` input to `.github/workflows/release.yml` - it switches the action to standalone mode, which ignores the config file entirely.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.4.0](https://github.com/olimorris/hledger-forecast/compare/v3.3.1...v3.4.0) (2026-09-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * better summarisation ([#28](https://github.com/olimorris/hledger-forecast/issues/28)) ([619291d](https://github.com/olimorris/hledger-forecast/commit/619291d61bcb0aeb442edff4dd4bd382295629ab))
9
+
3
10
  ## [3.3.1](https://github.com/olimorris/hledger-forecast/compare/v3.3.0...v3.3.1) (2026-07-28)
4
11
 
5
12
 
data/README.md CHANGED
@@ -107,9 +107,9 @@ This will generate a forecast up to the end of Feb 2027, showing asset balances
107
107
 
108
108
  > **Tip:** If you use `hledger-ui`, the `--verbose` flag is worth using. It keeps each transaction as its own entry in the journal, making descriptions much easier to read in the UI.
109
109
 
110
- ### Summarize
110
+ ### Summarise
111
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
+ It can be useful to summarise the entirety of your forecast into a profit and loss statement, rolling up periodic transactions accordingly. This is a great way of being able to see your monthly or yearly profit/loss. .
113
113
 
114
114
  hledger-forecast summarize -f my_forecast.csv
115
115
 
@@ -120,8 +120,45 @@ As your forecast grows, it's useful to see the totals at a glance. Think of this
120
120
  [yearly], [half-yearly], [quarterly], [monthly], [weekly], [daily]
121
121
  -t, --tags TAGS Only include transactions with given tags (comma-separated)
122
122
  -v, --verbose Show additional information in the summary
123
+ --from DATE Only include transactions still running from a given DATE,
124
+ e.g. "Sep 2027", "2027-09" or "01/09/2027"
125
+ --exclude-once Exclude one-off transactions from the summary
126
+ -e, --export FILE Also write the summary to a CSV FILE
127
+ --force Force an overwrite of the exported file
123
128
  -h, --help Show this help message
124
129
 
130
+ **Scoping** - one-off purchases can distort the overall summary, since a `once` row is counted at its full value alongside your recurring commitments. They can be excluded with `--exclude-once`:
131
+
132
+ ```bash
133
+ hledger-forecast summarize -f forecast.csv --exclude-once
134
+ ```
135
+
136
+ You can use the `--from` flag to enable questions like _"what does my P&L look like at this point in the future?"_. Anything that has finished by then - a mortgage with a `to` date in 2025, a one-off purchase in 2023 - is left out:
137
+
138
+ ```bash
139
+ hledger-forecast summarize -f forecast.csv --from="Sep 2027"
140
+ ```
141
+
142
+ You can also scope for specific tags:
143
+
144
+ ```bash
145
+ hledger-forecast summarize -f forecast.csv --tags=fixed
146
+ ```
147
+
148
+ The above would only show transactions that have been permanently tagged as `fixed` in the CSV file.
149
+
150
+ ```bash
151
+ hledger-forecast summarize -f forecast.csv --tags=-periodic
152
+ ```
153
+
154
+ Whereas the above would show everything _except_ transactions tagged as `periodic`.
155
+
156
+ **Exporting** - `--export` writes the summary to a CSV file as well as printing the table:
157
+
158
+ ```bash
159
+ hledger-forecast summarize -f forecast.csv --export summary.csv
160
+ ```
161
+
125
162
  ### Compare
126
163
 
127
164
  A core part of managing personal finances is comparing what you _expected_ to happen with what _actually_ happened. The `compare` command makes this easy:
@@ -22,6 +22,17 @@ module HledgerForecast
22
22
  date >> @calc.evaluate(offset_expr).to_i
23
23
  end
24
24
 
25
+ # Parses a user-supplied date such as "Sep 2027", "2027-09" or "01/09/2027"
26
+ def self.parse_date(value)
27
+ value = value.to_s.strip
28
+ value = "#{value}-01" if value.match?(/\A\d{4}-\d{1,2}\z/)
29
+ value = "#{value}-01-01" if value.match?(/\A\d{4}\z/)
30
+
31
+ Date.parse(value)
32
+ rescue Date::Error
33
+ raise ArgumentError, "invalid date '#{value}'"
34
+ end
35
+
25
36
  def self.evaluate_date(from, to)
26
37
  return (from >> to) - 1 if to.is_a?(Numeric)
27
38
  return Date.parse(to) unless to.start_with?("=") || to.start_with?("+")
@@ -185,15 +185,39 @@ module HledgerForecast
185
185
  options[:verbose] = true
186
186
  end
187
187
 
188
- # opts.on("--from DATE",
189
- # "Include transactions that start FROM a given DATE [yyyy-mm-dd]") do |from|
190
- # options[:from] = from
191
- # end
192
- #
193
- # opts.on("--to DATE",
194
- # "Include transactions that run TO a given DATE [yyyy-mm-dd]") do |to|
195
- # options[:to] = to
196
- # end
188
+ opts
189
+ .on(
190
+ "--from DATE",
191
+ "Only include transactions still running from a given DATE,",
192
+ "e.g. \"Sep 2027\", \"2027-09\" or \"01/09/2027\""
193
+ ) do |from|
194
+ options[:from] = Calculator.parse_date(from)
195
+ rescue ArgumentError => e
196
+ puts("\nError: ".bold.red + e.message)
197
+ exit(1)
198
+ end
199
+
200
+ opts.on(
201
+ "--exclude-once",
202
+ "Exclude one-off transactions from the summary"
203
+ ) do
204
+ options[:exclude_once] = true
205
+ end
206
+
207
+ opts.on(
208
+ "-e",
209
+ "--export FILE",
210
+ "Also write the summary to a CSV FILE"
211
+ ) do |file|
212
+ options[:export] = file
213
+ end
214
+
215
+ opts.on(
216
+ "--force",
217
+ "Force an overwrite of the exported file"
218
+ ) do
219
+ options[:force] = true
220
+ end
197
221
 
198
222
  # opts.on("-s", "--scenario \"NAMES\"",
199
223
  # "Include transactions from given scenarios, e.g.:",
@@ -261,22 +285,7 @@ module HledgerForecast
261
285
  exit(1)
262
286
  end
263
287
 
264
- output_file = options[:output_file]
265
-
266
- if File.exist?(output_file) && !options[:force]
267
- print("\nFile '#{output_file}' already exists. Overwrite? (y/n): ")
268
- overwrite = gets.chomp.downcase
269
-
270
- if overwrite == "y"
271
- File.write(output_file, transactions)
272
- puts("\nSuccess: ".bold.green + "File '#{output_file}' has been overwritten.")
273
- else
274
- puts("\nInfo: ".bold.blue + "Operation aborted. File '#{output_file}' was not overwritten.")
275
- end
276
- else
277
- File.write(output_file, transactions)
278
- puts("\nSuccess: ".bold.green + "File '#{output_file}' has been created")
279
- end
288
+ write_file(options[:output_file], transactions, options[:force])
280
289
  end
281
290
 
282
291
  def self.summarize(options)
@@ -291,6 +300,27 @@ module HledgerForecast
291
300
  end
292
301
 
293
302
  puts(SummarizerFormatter.format(summarizer[:output], summarizer[:settings]))
303
+
304
+ return unless options[:export]
305
+
306
+ csv = SummarizerExporter.export(summarizer[:output], summarizer[:settings])
307
+ write_file(options[:export], csv, options[:force])
308
+ end
309
+
310
+ private_class_method def self.write_file(path, contents, force)
311
+ if File.exist?(path) && !force
312
+ print("\nFile '#{path}' already exists. Overwrite? (y/n): ")
313
+
314
+ unless gets.chomp.downcase == "y"
315
+ return puts("\nInfo: ".bold.blue + "Operation aborted. File '#{path}' was not overwritten.")
316
+ end
317
+
318
+ File.write(path, contents)
319
+ return puts("\nSuccess: ".bold.green + "File '#{path}' has been overwritten.")
320
+ end
321
+
322
+ File.write(path, contents)
323
+ puts("\nSuccess: ".bold.green + "File '#{path}' has been created")
294
324
  end
295
325
 
296
326
  def self.compare(options)
@@ -13,7 +13,9 @@ module HledgerForecast
13
13
  :sign_before_symbol,
14
14
  :thousands_separator,
15
15
  :verbose,
16
- :roll_up
16
+ :roll_up,
17
+ :from,
18
+ :exclude_once
17
19
  )
18
20
 
19
21
  def self.parse(settings_rows, cli_options = nil)
@@ -38,6 +40,8 @@ module HledgerForecast
38
40
  DEFAULTS[:thousands_separator]
39
41
  @verbose = opts[:verbose] || false
40
42
  @roll_up = opts[:roll_up]
43
+ @from = opts[:from]
44
+ @exclude_once = opts[:exclude_once] || false
41
45
  end
42
46
  end
43
47
  end
@@ -8,6 +8,12 @@ module HledgerForecast
8
8
  forecast = Forecast.parse(csv_string, cli_options)
9
9
  transactions = forecast.transactions.reject(&:summary_exclude?)
10
10
 
11
+ transactions = transactions.reject(&:once?) if forecast.settings.exclude_once
12
+
13
+ if (from = forecast.settings.from)
14
+ transactions = transactions.select { |t| t.active_on_or_after?(from) }
15
+ end
16
+
11
17
  if cli_options&.dig(:tags)
12
18
  raise "The --tags option requires a 'tag' column in the forecast CSV" unless forecast.has_tags_column?
13
19
  transactions = transactions.select { |t| t.matches_tags?(cli_options[:tags]) }
@@ -0,0 +1,31 @@
1
+ module HledgerForecast
2
+ # Renders the summary as CSV, mirroring the forecast file's columns and
3
+ # appending the amounts the summarizer calculated
4
+ class SummarizerExporter
5
+ HEADERS = %i[type frequency account from to description category amount annualised_amount].freeze
6
+ MONEY_FIELDS = %i[amount annualised_amount rolled_up_amount].freeze
7
+
8
+ def self.export(output, settings)
9
+ new.export(output, settings)
10
+ end
11
+
12
+ def export(output, settings)
13
+ headers = settings.roll_up ? HEADERS + [:rolled_up_amount] : HEADERS
14
+
15
+ CSV.generate do |csv|
16
+ csv << headers
17
+ output.each { |row| csv << headers.map { |h| format_value(h, row[h]) } }
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def format_value(key, value)
24
+ return nil if value.nil?
25
+ return value.iso8601 if value.is_a?(Date)
26
+ return value.to_f.round(2) if MONEY_FIELDS.include?(key)
27
+
28
+ value
29
+ end
30
+ end
31
+ end
@@ -25,6 +25,7 @@ module HledgerForecast
25
25
 
26
26
  def init_table
27
27
  title = "FORECAST SUMMARY"
28
+ title += " FROM #{@settings.from.strftime("%b %Y").upcase}" if @settings.from
28
29
  title += " (#{@settings.roll_up.upcase} ROLL UP)" if @settings.roll_up
29
30
 
30
31
  @table.add_row([{value: title.bold, colspan: 3, alignment: :center}])
@@ -67,6 +67,15 @@ module HledgerForecast
67
67
 
68
68
  def summary_exclude? = !!summary_exclude
69
69
 
70
+ def once? = type == "once"
71
+
72
+ # A `once` transaction has no TO date but only ever occurs on its FROM date
73
+ def ends_on = to || (once? ? from : nil)
74
+
75
+ def active_on_or_after?(date)
76
+ ends_on.nil? || ends_on >= date
77
+ end
78
+
70
79
  def self.validate_required_fields!(row)
71
80
  %i[type account from category amount].each do |field|
72
81
  next unless row[field].nil? || row[field].to_s.strip.empty?
@@ -1,3 +1,3 @@
1
1
  module HledgerForecast
2
- VERSION = "3.3.1"
2
+ VERSION = "3.4.0"
3
3
  end
@@ -21,6 +21,7 @@ require_relative "hledger_forecast/formatter"
21
21
  require_relative "hledger_forecast/generator"
22
22
  require_relative "hledger_forecast/summarizer"
23
23
  require_relative "hledger_forecast/summarizer_formatter"
24
+ require_relative "hledger_forecast/summarizer_exporter"
24
25
  require_relative "hledger_forecast/comparator"
25
26
  require_relative "hledger_forecast/cli"
26
27
  require_relative "hledger_forecast/version"
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "include-component-in-tag": false,
4
+ "packages": {
5
+ ".": {
6
+ "release-type": "ruby",
7
+ "package-name": "hledger-forecast",
8
+ "version-file": "lib/hledger_forecast/version.rb"
9
+ }
10
+ }
11
+ }
@@ -69,3 +69,27 @@ RSpec.describe HledgerForecast::Calculator do
69
69
  end
70
70
  end
71
71
  end
72
+
73
+ RSpec.describe HledgerForecast::Calculator do
74
+ describe ".parse_date" do
75
+ it "parses a month and year" do
76
+ expect(described_class.parse_date("Sep 2027")).to(eq(Date.new(2027, 9, 1)))
77
+ end
78
+
79
+ it "parses an ISO year-month" do
80
+ expect(described_class.parse_date("2027-09")).to(eq(Date.new(2027, 9, 1)))
81
+ end
82
+
83
+ it "parses a bare year" do
84
+ expect(described_class.parse_date("2027")).to(eq(Date.new(2027, 1, 1)))
85
+ end
86
+
87
+ it "parses a day-first date" do
88
+ expect(described_class.parse_date("15/09/2027")).to(eq(Date.new(2027, 9, 15)))
89
+ end
90
+
91
+ it "raises on an unparseable value" do
92
+ expect { described_class.parse_date("nonsense") }.to(raise_error(ArgumentError, /invalid date/))
93
+ end
94
+ end
95
+ end
data/spec/cli_spec.rb CHANGED
@@ -38,6 +38,39 @@ RSpec.describe "command" do
38
38
  expect(File.read(generated_journal)).to(eq(output))
39
39
  end
40
40
 
41
+ it "uses the CLI to export a summary to a CSV file" do
42
+ exported_csv = "./test_summary.csv"
43
+ File.delete(exported_csv) if File.exist?(exported_csv)
44
+
45
+ system("./bin/hledger-forecast summarize -f ./spec/stubs/forecast.csv --export #{exported_csv} > /dev/null")
46
+
47
+ expect(File.read(exported_csv)).to(
48
+ eq(
49
+ <<~CSV
50
+ type,frequency,account,from,to,description,category,amount,annualised_amount
51
+ monthly,,Assets:Bank,2023-03-01,,Mortgage,Expenses:Mortgage,2000.55,24006.6
52
+ monthly,,Assets:Bank,2023-03-01,,Food,Expenses:Food,100.0,1200.0
53
+ monthly,,Assets:Savings,2023-03-01,,Savings,Assets:Bank,-1000.0,-12000.0
54
+ CSV
55
+ )
56
+ )
57
+ ensure
58
+ File.delete(exported_csv) if File.exist?(exported_csv)
59
+ end
60
+
61
+ it "does not overwrite an existing export without confirmation" do
62
+ exported_csv = "./test_summary.csv"
63
+ File.write(exported_csv, "untouched")
64
+
65
+ system(
66
+ "echo n | ./bin/hledger-forecast summarize -f ./spec/stubs/forecast.csv --export #{exported_csv} > /dev/null"
67
+ )
68
+
69
+ expect(File.read(exported_csv)).to(eq("untouched"))
70
+ ensure
71
+ File.delete(exported_csv) if File.exist?(exported_csv)
72
+ end
73
+
41
74
  it "uses the CLI to compare two CSV files" do
42
75
  expected_output = strip_ansi_codes(
43
76
  <<~OUTPUT
@@ -0,0 +1,62 @@
1
+ require_relative "../lib/hledger_forecast"
2
+
3
+ EXPORT_CONFIG = <<~CSV
4
+ type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude
5
+ monthly,,Assets:Bank,01/03/2023,01/03/2025,Mortgage,Expenses:Mortgage,2000.555,,
6
+ monthly,,Assets:Bank,01/03/2023,,"Food, drink",Expenses:Food,100,,
7
+ custom,every 2 weeks,Assets:Bank,01/05/2023,,Hair,Expenses:Personal Care,80,26,
8
+ settings,currency,GBP,,,,,,,,
9
+ CSV
10
+
11
+ RSpec.describe HledgerForecast::SummarizerExporter do
12
+ def exported(options = {})
13
+ summary = HledgerForecast::Summarizer.summarize(EXPORT_CONFIG, options)
14
+ CSV.parse(described_class.export(summary[:output], summary[:settings]), headers: true)
15
+ end
16
+
17
+ it "writes a header row mirroring the forecast columns" do
18
+ expect(exported.headers).to(
19
+ eq(%w[type frequency account from to description category amount annualised_amount])
20
+ )
21
+ end
22
+
23
+ it "writes a row per transaction" do
24
+ expect(exported.length).to(eq(3))
25
+ end
26
+
27
+ it "formats dates as ISO 8601" do
28
+ expect(exported[0]["from"]).to(eq("2023-03-01"))
29
+ expect(exported[0]["to"]).to(eq("2025-03-01"))
30
+ end
31
+
32
+ it "leaves an open-ended TO date blank" do
33
+ expect(exported[1]["to"]).to(be_nil)
34
+ end
35
+
36
+ it "rounds amounts to two decimal places" do
37
+ expect(exported[0]["amount"]).to(eq("2000.56"))
38
+ end
39
+
40
+ it "writes amounts without currency formatting" do
41
+ expect(exported[0]["annualised_amount"]).to(eq("24006.66"))
42
+ end
43
+
44
+ it "quotes fields containing a comma" do
45
+ expect(exported[1]["description"]).to(eq("Food, drink"))
46
+ end
47
+
48
+ it "adds a rolled_up_amount column when rolling up" do
49
+ rolled = exported({roll_up: "monthly"})
50
+
51
+ expect(rolled.headers).to(include("rolled_up_amount"))
52
+ expect(rolled[2]["rolled_up_amount"]).to(eq(((80.0 * 26) / 12).round(2).to_s))
53
+ end
54
+
55
+ it "omits the rolled_up_amount column without a roll-up" do
56
+ expect(exported.headers).not_to(include("rolled_up_amount"))
57
+ end
58
+
59
+ it "only exports rows that survived the filters" do
60
+ expect(exported({from: Date.new(2026, 1, 1)}).map { |r| r["description"] }).to(eq(["Food, drink", "Hair"]))
61
+ end
62
+ end
@@ -48,3 +48,46 @@ RSpec.describe HledgerForecast::Summarizer do
48
48
  end
49
49
  end
50
50
  end
51
+
52
+ FILTER_CONFIG = <<~CSV
53
+ type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude
54
+ monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,100,,
55
+ monthly,,Assets:Bank,01/03/2023,01/01/2025,Mortgage,Expenses:Mortgage,2000,,
56
+ yearly,,Assets:Bank,01/04/2023,01/06/2028,Insurance,Expenses:Insurance,500,,
57
+ once,,Assets:Bank,05/03/2023,,Laptop refund,Expenses:Shopping,-3000,,
58
+ once,,Assets:Bank,05/03/2029,,New car,Expenses:Car,10000,,
59
+ CSV
60
+
61
+ RSpec.describe HledgerForecast::Summarizer do
62
+ def descriptions(options)
63
+ described_class.summarize(FILTER_CONFIG, options)[:output].map { |r| r[:description] }
64
+ end
65
+
66
+ describe "#summarize with exclude_once" do
67
+ it "removes one-off transactions" do
68
+ expect(descriptions({exclude_once: true})).to(eq(["Food", "Mortgage", "Insurance"]))
69
+ end
70
+
71
+ it "keeps them by default" do
72
+ expect(descriptions({})).to(include("Laptop refund", "New car"))
73
+ end
74
+ end
75
+
76
+ describe "#summarize with from" do
77
+ it "keeps transactions that are still running" do
78
+ expect(descriptions({from: Date.new(2027, 9, 1)})).to(eq(["Food", "Insurance", "New car"]))
79
+ end
80
+
81
+ it "drops a one-off that has already happened" do
82
+ expect(descriptions({from: Date.new(2023, 4, 1)})).not_to(include("Laptop refund"))
83
+ end
84
+
85
+ it "drops a transaction that ends before the date" do
86
+ expect(descriptions({from: Date.new(2028, 7, 1)})).to(eq(["Food", "New car"]))
87
+ end
88
+
89
+ it "combines with exclude_once" do
90
+ expect(descriptions({from: Date.new(2027, 9, 1), exclude_once: true})).to(eq(["Food", "Insurance"]))
91
+ end
92
+ end
93
+ 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: 3.3.1
4
+ version: 3.4.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-07-28 00:00:00.000000000 Z
11
+ date: 2026-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: abbrev
@@ -136,6 +136,7 @@ files:
136
136
  - ".github/workflows/release.yml"
137
137
  - ".gitignore"
138
138
  - ".mise.toml"
139
+ - ".release-please-manifest.json"
139
140
  - ".rubocop.yml"
140
141
  - AGENTS.md
141
142
  - CHANGELOG.md
@@ -156,10 +157,12 @@ files:
156
157
  - lib/hledger_forecast/generator.rb
157
158
  - lib/hledger_forecast/settings.rb
158
159
  - lib/hledger_forecast/summarizer.rb
160
+ - lib/hledger_forecast/summarizer_exporter.rb
159
161
  - lib/hledger_forecast/summarizer_formatter.rb
160
162
  - lib/hledger_forecast/transaction.rb
161
163
  - lib/hledger_forecast/transactions/default.rb
162
164
  - lib/hledger_forecast/version.rb
165
+ - release-please-config.json
163
166
  - spec/calculator_spec.rb
164
167
  - spec/cli_spec.rb
165
168
  - spec/compare_spec.rb
@@ -179,6 +182,7 @@ files:
179
182
  - spec/stubs/transactions_found.journal
180
183
  - spec/stubs/transactions_found_inverse.journal
181
184
  - spec/stubs/transactions_not_found.journal
185
+ - spec/summarizer_exporter_spec.rb
182
186
  - spec/summarizer_spec.rb
183
187
  - spec/tags_spec.rb
184
188
  - spec/verbose_output_spec.rb
@@ -226,6 +230,7 @@ test_files:
226
230
  - spec/stubs/transactions_found.journal
227
231
  - spec/stubs/transactions_found_inverse.journal
228
232
  - spec/stubs/transactions_not_found.journal
233
+ - spec/summarizer_exporter_spec.rb
229
234
  - spec/summarizer_spec.rb
230
235
  - spec/tags_spec.rb
231
236
  - spec/verbose_output_spec.rb