hledger-forecast 1.4.0 → 1.5.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/README.md +25 -15
 - data/lib/hledger_forecast/cli.rb +79 -8
 - data/lib/hledger_forecast/comparator.rb +80 -0
 - 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
 - metadata +12 -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
         
     | 
| 
         @@ -91,9 +92,7 @@ If you use the `hledger-ui` tool, it may be helpful to use the `--verbose` flag. 
     | 
|
| 
       91 
92 
     | 
    
         | 
| 
       92 
93 
     | 
    
         
             
            ### Summarize command
         
     | 
| 
       93 
94 
     | 
    
         | 
| 
       94 
     | 
    
         
            -
            As your  
     | 
| 
       95 
     | 
    
         
            -
            Furthermore, being able to see your monthly profit and loss statement _if_ you were to purchase that new item may
         
     | 
| 
       96 
     | 
    
         
            -
            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.
         
     | 
| 
       97 
96 
     | 
    
         | 
| 
       98 
97 
     | 
    
         
             
                hledger-forecast summarize -f my_forecast.csv
         
     | 
| 
       99 
98 
     | 
    
         | 
| 
         @@ -107,6 +106,18 @@ The available options are: 
     | 
|
| 
       107 
106 
     | 
    
         
             
                -v, --verbose                    Show additional information in the summary
         
     | 
| 
       108 
107 
     | 
    
         
             
                -h, --help                       Show this help message
         
     | 
| 
       109 
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 
     | 
    
         
            +
             
     | 
| 
       110 
121 
     | 
    
         
             
            ## :gear: Creating your forecast
         
     | 
| 
       111 
122 
     | 
    
         | 
| 
       112 
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.
         
     | 
| 
         @@ -375,4 +386,3 @@ The app will use a hledger query to determine if the combination of category and 
     | 
|
| 
       375 
386 
     | 
    
         
             
            ## :pencil2: Contributing
         
     | 
| 
       376 
387 
     | 
    
         | 
| 
       377 
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.
         
     | 
| 
       378 
     | 
    
         
            -
             
     | 
    
        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 
     | 
    
         | 
| 
         @@ -110,11 +120,24 @@ module HledgerForecast 
     | 
|
| 
       110 
120 
     | 
    
         
             
                      puts opts
         
     | 
| 
       111 
121 
     | 
    
         
             
                      exit
         
     | 
| 
       112 
122 
     | 
    
         
             
                    end
         
     | 
| 
       113 
     | 
    
         
            -
                  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
         
     | 
| 
       114 
137 
     | 
    
         | 
| 
       115 
     | 
    
         
            -
                  options[:forecast_file]  
     | 
| 
       116 
     | 
    
         
            -
                  options[:file_type]  
     | 
| 
       117 
     | 
    
         
            -
                  options[:output_file]  
     | 
| 
      
 138 
     | 
    
         
            +
                  options[:forecast_file] ||= "forecast.csv"
         
     | 
| 
      
 139 
     | 
    
         
            +
                  options[:file_type] ||= "csv"
         
     | 
| 
      
 140 
     | 
    
         
            +
                  options[:output_file] ||= "forecast.journal"
         
     | 
| 
       118 
141 
     | 
    
         | 
| 
       119 
142 
     | 
    
         
             
                  options
         
     | 
| 
       120 
143 
     | 
    
         
             
                end
         
     | 
| 
         @@ -122,7 +145,7 @@ module HledgerForecast 
     | 
|
| 
       122 
145 
     | 
    
         
             
                def self.parse_summarize_options(args)
         
     | 
| 
       123 
146 
     | 
    
         
             
                  options = {}
         
     | 
| 
       124 
147 
     | 
    
         | 
| 
       125 
     | 
    
         
            -
                  OptionParser.new do |opts|
         
     | 
| 
      
 148 
     | 
    
         
            +
                  global = OptionParser.new do |opts|
         
     | 
| 
       126 
149 
     | 
    
         
             
                    opts.banner = "Usage: hledger-forecast summarize [options]"
         
     | 
| 
       127 
150 
     | 
    
         
             
                    opts.separator ""
         
     | 
| 
       128 
151 
     | 
    
         | 
| 
         @@ -168,7 +191,47 @@ module HledgerForecast 
     | 
|
| 
       168 
191 
     | 
    
         
             
                      puts opts
         
     | 
| 
       169 
192 
     | 
    
         
             
                      exit
         
     | 
| 
       170 
193 
     | 
    
         
             
                    end
         
     | 
| 
       171 
     | 
    
         
            -
                  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]
         
     | 
| 
       172 
235 
     | 
    
         | 
| 
       173 
236 
     | 
    
         
             
                  options
         
     | 
| 
       174 
237 
     | 
    
         
             
                end
         
     | 
| 
         @@ -210,5 +273,13 @@ module HledgerForecast 
     | 
|
| 
       210 
273 
     | 
    
         | 
| 
       211 
274 
     | 
    
         
             
                  puts SummarizerFormatter.format(summarizer[:output], summarizer[:settings])
         
     | 
| 
       212 
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
         
     | 
| 
       213 
284 
     | 
    
         
             
              end
         
     | 
| 
       214 
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
         
     | 
    
        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
         
     | 
    
        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,6 +142,8 @@ 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
         
     | 
| 
         @@ -166,12 +170,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       166 
170 
     | 
    
         
             
                - !ruby/object:Gem::Version
         
     | 
| 
       167 
171 
     | 
    
         
             
                  version: '0'
         
     | 
| 
       168 
172 
     | 
    
         
             
            requirements: []
         
     | 
| 
       169 
     | 
    
         
            -
            rubygems_version: 3.4. 
     | 
| 
      
 173 
     | 
    
         
            +
            rubygems_version: 3.4.19
         
     | 
| 
       170 
174 
     | 
    
         
             
            signing_key:
         
     | 
| 
       171 
175 
     | 
    
         
             
            specification_version: 4
         
     | 
| 
       172 
176 
     | 
    
         
             
            summary: An extended wrapper around hledger's forecasting functionality
         
     | 
| 
       173 
177 
     | 
    
         
             
            test_files:
         
     | 
| 
       174 
     | 
    
         
            -
            - spec/ 
     | 
| 
      
 178 
     | 
    
         
            +
            - spec/cli_spec.rb
         
     | 
| 
      
 179 
     | 
    
         
            +
            - spec/compare_spec.rb
         
     | 
| 
       175 
180 
     | 
    
         
             
            - spec/computed_amounts_spec.rb
         
     | 
| 
       176 
181 
     | 
    
         
             
            - spec/csv_and_yml_comparison_spec.rb
         
     | 
| 
       177 
182 
     | 
    
         
             
            - spec/csv_parser_spec.rb
         
     | 
| 
         @@ -185,6 +190,8 @@ test_files: 
     | 
|
| 
       185 
190 
     | 
    
         
             
            - spec/quarterly_spec.rb
         
     | 
| 
       186 
191 
     | 
    
         
             
            - spec/stubs/forecast.csv
         
     | 
| 
       187 
192 
     | 
    
         
             
            - spec/stubs/forecast.yml
         
     | 
| 
      
 193 
     | 
    
         
            +
            - spec/stubs/output1.csv
         
     | 
| 
      
 194 
     | 
    
         
            +
            - spec/stubs/output2.csv
         
     | 
| 
       188 
195 
     | 
    
         
             
            - spec/stubs/transactions_found.journal
         
     | 
| 
       189 
196 
     | 
    
         
             
            - spec/stubs/transactions_found_inverse.journal
         
     | 
| 
       190 
197 
     | 
    
         
             
            - spec/stubs/transactions_not_found.journal
         
     |