hledger-forecast 1.5.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -17
- data/README.md +7 -171
- data/example.journal +14 -14
- data/hledger-forecast.gemspec +1 -1
- data/lib/hledger_forecast/calculator.rb +5 -1
- data/lib/hledger_forecast/cli.rb +5 -18
- data/lib/hledger_forecast/generator.rb +52 -35
- data/lib/hledger_forecast/settings.rb +42 -27
- data/lib/hledger_forecast/summarizer.rb +28 -62
- data/lib/hledger_forecast/summarizer_formatter.rb +4 -4
- data/lib/hledger_forecast/transactions/default.rb +28 -57
- data/lib/hledger_forecast/transactions/trackers.rb +34 -40
- data/lib/hledger_forecast/utilities.rb +14 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +1 -2
- data/spec/cli_spec.rb +3 -12
- data/spec/computed_amounts_spec.rb +11 -22
- data/spec/custom_spec.rb +15 -35
- data/spec/half-yearly_spec.rb +6 -13
- data/spec/monthly_end_date_spec.rb +8 -18
- data/spec/monthly_end_date_transaction_spec.rb +20 -45
- data/spec/monthly_spec.rb +11 -28
- data/spec/once_spec.rb +6 -13
- data/spec/quarterly_spec.rb +5 -12
- data/spec/summarizer_spec.rb +11 -42
- data/spec/track_spec.rb +19 -49
- data/spec/verbose_output_spec.rb +3 -3
- data/spec/yearly_spec.rb +5 -12
- metadata +4 -13
- data/example.yml +0 -98
- data/lib/hledger_forecast/csv_parser.rb +0 -106
- data/spec/csv_and_yml_comparison_spec.rb +0 -32
- data/spec/csv_parser_spec.rb +0 -110
- data/spec/modifier_spec.rb +0 -102
- data/spec/stubs/forecast.yml +0 -19
| @@ -6,67 +6,46 @@ module HledgerForecast | |
| 6 6 | 
             
                end
         | 
| 7 7 |  | 
| 8 8 | 
             
                def summarize(config, cli_options = nil)
         | 
| 9 | 
            -
                  @forecast =  | 
| 9 | 
            +
                  @forecast = CSV.parse(config, headers: true)
         | 
| 10 10 | 
             
                  @settings = Settings.config(@forecast, cli_options)
         | 
| 11 11 |  | 
| 12 | 
            -
                   | 
| 12 | 
            +
                  { output: generate(@forecast), settings: @settings }
         | 
| 13 13 | 
             
                end
         | 
| 14 14 |  | 
| 15 15 | 
             
                private
         | 
| 16 16 |  | 
| 17 17 | 
             
                def generate(forecast)
         | 
| 18 | 
            -
                  output = {}
         | 
| 19 | 
            -
                  forecast.each do |period, blocks|
         | 
| 20 | 
            -
                    next if %w[settings].include?(period)
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                    blocks.each do |block|
         | 
| 23 | 
            -
                      key = if @settings[:roll_up].nil?
         | 
| 24 | 
            -
                              period
         | 
| 25 | 
            -
                            else
         | 
| 26 | 
            -
                              output.length
         | 
| 27 | 
            -
                            end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                      output[key] ||= []
         | 
| 30 | 
            -
                      output[key] << process_block(period, block)
         | 
| 31 | 
            -
                    end
         | 
| 32 | 
            -
                  end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                  output = filter_out(flatten_and_merge(output))
         | 
| 35 | 
            -
                  output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                  output
         | 
| 38 | 
            -
                end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
                def process_block(period, block)
         | 
| 41 18 | 
             
                  output = []
         | 
| 42 19 |  | 
| 43 | 
            -
                   | 
| 44 | 
            -
                     | 
| 45 | 
            -
                     | 
| 46 | 
            -
                    to: block['to'] ? Date.parse(block['to']) : nil,
         | 
| 47 | 
            -
                    type: period,
         | 
| 48 | 
            -
                    frequency: block['frequency'],
         | 
| 49 | 
            -
                    transactions: []
         | 
| 50 | 
            -
                  }
         | 
| 20 | 
            +
                  forecast.each do |row|
         | 
| 21 | 
            +
                    next if row['type'] == 'settings'
         | 
| 22 | 
            +
                    next if row['summary_exclude']
         | 
| 51 23 |  | 
| 52 | 
            -
             | 
| 53 | 
            -
                end
         | 
| 24 | 
            +
                    row['amount'] = Calculator.new.evaluate(Utilities.convert_amount(row['amount']))
         | 
| 54 25 |  | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
                     | 
| 26 | 
            +
                    begin
         | 
| 27 | 
            +
                      annualised_amount = row['roll-up'] ? row['amount'] * row['roll-up'].to_f : row['amount'] * annualise(row['type'])
         | 
| 28 | 
            +
                    rescue StandardError
         | 
| 29 | 
            +
                      puts "\nError: ".bold.red + 'Could not create an annualised ammount. Have you set the roll-up for your custom type transactions?'
         | 
| 30 | 
            +
                      exit
         | 
| 31 | 
            +
                    end
         | 
| 58 32 |  | 
| 59 | 
            -
                    output | 
| 60 | 
            -
                       | 
| 61 | 
            -
                       | 
| 62 | 
            -
                       | 
| 63 | 
            -
                       | 
| 64 | 
            -
                       | 
| 65 | 
            -
                       | 
| 66 | 
            -
                       | 
| 33 | 
            +
                    output << {
         | 
| 34 | 
            +
                      account: row['account'],
         | 
| 35 | 
            +
                      from: Date.parse(row['from']),
         | 
| 36 | 
            +
                      to: row['to'] ? Calculator.new.evaluate_date(Date.parse(row['from']), row['to']) : nil,
         | 
| 37 | 
            +
                      type: row['type'],
         | 
| 38 | 
            +
                      frequency: row['frequency'],
         | 
| 39 | 
            +
                      category: row['category'],
         | 
| 40 | 
            +
                      description: row['description'],
         | 
| 41 | 
            +
                      amount: row['amount'],
         | 
| 42 | 
            +
                      annualised_amount: annualised_amount.to_f,
         | 
| 43 | 
            +
                      exclude: row['summary_exclude']
         | 
| 67 44 | 
             
                    }
         | 
| 68 45 | 
             
                  end
         | 
| 69 46 |  | 
| 47 | 
            +
                  output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
         | 
| 48 | 
            +
             | 
| 70 49 | 
             
                  output
         | 
| 71 50 | 
             
                end
         | 
| 72 51 |  | 
| @@ -84,22 +63,9 @@ module HledgerForecast | |
| 84 63 | 
             
                  annualise[period]
         | 
| 85 64 | 
             
                end
         | 
| 86 65 |  | 
| 87 | 
            -
                def  | 
| 88 | 
            -
                   | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
                def flatten_and_merge(blocks)
         | 
| 92 | 
            -
                  blocks.values.flatten.flat_map do |block|
         | 
| 93 | 
            -
                    block[:transactions].map do |transaction|
         | 
| 94 | 
            -
                      block.slice(:account, :from, :to, :type, :frequency).merge(transaction)
         | 
| 95 | 
            -
                    end
         | 
| 96 | 
            -
                  end
         | 
| 97 | 
            -
                end
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                def calculate_rolled_up_amount(data)
         | 
| 100 | 
            -
                  data.map do |item|
         | 
| 101 | 
            -
                    item[:rolled_up_amount] = item[:annualised_amount] / annualise(@settings[:roll_up])
         | 
| 102 | 
            -
                    item
         | 
| 66 | 
            +
                def calculate_rolled_up_amount(forecast)
         | 
| 67 | 
            +
                  forecast.each do |row|
         | 
| 68 | 
            +
                    row[:rolled_up_amount] = row[:annualised_amount] / annualise(@settings[:roll_up])
         | 
| 103 69 | 
             
                  end
         | 
| 104 70 | 
             
                end
         | 
| 105 71 | 
             
              end
         | 
| @@ -41,7 +41,7 @@ module HledgerForecast | |
| 41 41 | 
             
                    @table.add_row([{ value: type.capitalize.bold, colspan: 3, alignment: :center }])
         | 
| 42 42 | 
             
                    total = 0
         | 
| 43 43 | 
             
                    items.each do |item|
         | 
| 44 | 
            -
                      total += item[:amount]
         | 
| 44 | 
            +
                      total += item[:amount].to_f
         | 
| 45 45 |  | 
| 46 46 | 
             
                      if @settings[:verbose]
         | 
| 47 47 | 
             
                        @table.add_row [{ value: item[:category], alignment: :left },
         | 
| @@ -63,7 +63,7 @@ module HledgerForecast | |
| 63 63 | 
             
                def sort(data)
         | 
| 64 64 | 
             
                  data.each do |type, items|
         | 
| 65 65 | 
             
                    data[type] = items.sort_by do |item|
         | 
| 66 | 
            -
                      value = item[:amount]
         | 
| 66 | 
            +
                      value = item[:amount].to_f
         | 
| 67 67 | 
             
                      [value >= 0 ? 1 : 0, value >= 0 ? -value : value]
         | 
| 68 68 | 
             
                    end
         | 
| 69 69 | 
             
                  end
         | 
| @@ -114,11 +114,11 @@ module HledgerForecast | |
| 114 114 |  | 
| 115 115 | 
             
                def add_total_row_to_table(data, row_to_sum)
         | 
| 116 116 | 
             
                  total = data.reduce(0) do |sum, item|
         | 
| 117 | 
            -
                    sum + item[row_to_sum]
         | 
| 117 | 
            +
                    sum + item[row_to_sum].to_f
         | 
| 118 118 | 
             
                  end
         | 
| 119 119 |  | 
| 120 120 | 
             
                  income = data.reduce(0) do |sum, item|
         | 
| 121 | 
            -
                    sum += item[row_to_sum] if item[row_to_sum] < 0
         | 
| 121 | 
            +
                    sum += item[row_to_sum].to_f if item[row_to_sum].to_f < 0
         | 
| 122 122 | 
             
                    sum
         | 
| 123 123 | 
             
                  end
         | 
| 124 124 |  | 
| @@ -6,15 +6,15 @@ module HledgerForecast | |
| 6 6 | 
             
                #    Expenses:Groceries    $250.00 ;  Food expenses
         | 
| 7 7 | 
             
                #    Assets:Checking
         | 
| 8 8 | 
             
                class Default
         | 
| 9 | 
            -
                  def self.generate( | 
| 10 | 
            -
                    new( | 
| 9 | 
            +
                  def self.generate(forecast, settings)
         | 
| 10 | 
            +
                    new(forecast, settings).generate
         | 
| 11 11 | 
             
                  end
         | 
| 12 12 |  | 
| 13 13 | 
             
                  def generate
         | 
| 14 | 
            -
                     | 
| 15 | 
            -
                       | 
| 16 | 
            -
             | 
| 17 | 
            -
                       | 
| 14 | 
            +
                    forecast.each do |row|
         | 
| 15 | 
            +
                      next if row[:type] == "settings"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                      process_transactions(row)
         | 
| 18 18 | 
             
                    end
         | 
| 19 19 |  | 
| 20 20 | 
             
                    output
         | 
| @@ -22,59 +22,34 @@ module HledgerForecast | |
| 22 22 |  | 
| 23 23 | 
             
                  private
         | 
| 24 24 |  | 
| 25 | 
            -
                  attr_reader : | 
| 25 | 
            +
                  attr_reader :forecast, :settings, :output
         | 
| 26 26 |  | 
| 27 | 
            -
                  def initialize( | 
| 28 | 
            -
                    @ | 
| 29 | 
            -
                    @ | 
| 27 | 
            +
                  def initialize(forecast, settings)
         | 
| 28 | 
            +
                    @forecast = forecast
         | 
| 29 | 
            +
                    @settings = settings
         | 
| 30 30 | 
             
                    @output = []
         | 
| 31 31 | 
             
                  end
         | 
| 32 32 |  | 
| 33 | 
            -
                  def  | 
| 34 | 
            -
                     | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
                      if block[:type] == "custom"
         | 
| 38 | 
            -
                        process_custom_transactions(block, to, transactions)
         | 
| 39 | 
            -
                      else
         | 
| 40 | 
            -
                        process_standard_transactions(block, to, transactions)
         | 
| 41 | 
            -
                      end
         | 
| 42 | 
            -
                    end
         | 
| 43 | 
            -
                  end
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                  def process_custom_transactions(block, to, transactions)
         | 
| 46 | 
            -
                    transactions.each do |t|
         | 
| 47 | 
            -
                      frequency = get_periodic_rules(block[:type], t[:frequency])
         | 
| 33 | 
            +
                  def process_transactions(row)
         | 
| 34 | 
            +
                    to = build_to_header(row[:to])
         | 
| 35 | 
            +
                    frequency = get_periodic_rules(row[:type], row[:frequency])
         | 
| 48 36 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
                       | 
| 51 | 
            -
                       | 
| 37 | 
            +
                    if @settings[:verbose]
         | 
| 38 | 
            +
                      description = row[:description]
         | 
| 39 | 
            +
                      transactions = [row]
         | 
| 40 | 
            +
                    else
         | 
| 41 | 
            +
                      description = get_descriptions(row[:transactions])
         | 
| 42 | 
            +
                      transactions = row[:transactions]
         | 
| 52 43 | 
             
                    end
         | 
| 53 | 
            -
                  end
         | 
| 54 44 |  | 
| 55 | 
            -
             | 
| 56 | 
            -
                     | 
| 57 | 
            -
                      transactions.map do |t|
         | 
| 58 | 
            -
                        # Skip transactions that have been marked as tracked
         | 
| 59 | 
            -
                        next if t[:track]
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                        frequency = get_periodic_rules(block[:type], block[:frequency])
         | 
| 62 | 
            -
                        header = build_header(block, to, frequency, t[:description])
         | 
| 63 | 
            -
                        footer = build_footer(block)
         | 
| 64 | 
            -
                        output << build_transaction(header, [t], footer)
         | 
| 65 | 
            -
                      end
         | 
| 66 | 
            -
                      return
         | 
| 67 | 
            -
                    end
         | 
| 45 | 
            +
                    header = build_header(row, frequency, to, description)
         | 
| 46 | 
            +
                    footer = build_footer(row)
         | 
| 68 47 |  | 
| 69 | 
            -
                    block[:descriptions] = get_descriptions(transactions)
         | 
| 70 | 
            -
                    frequency = get_periodic_rules(block[:type], block[:frequency])
         | 
| 71 | 
            -
                    header = build_header(block, to, frequency, block[:descriptions])
         | 
| 72 | 
            -
                    footer = build_footer(block)
         | 
| 73 48 | 
             
                    output << build_transaction(header, transactions, footer)
         | 
| 74 49 | 
             
                  end
         | 
| 75 50 |  | 
| 76 | 
            -
                  def build_header( | 
| 77 | 
            -
                    "#{frequency} #{ | 
| 51 | 
            +
                  def build_header(row, frequency, to, descriptions)
         | 
| 52 | 
            +
                    "#{frequency} #{row[:from]}#{to}  * #{descriptions}\n"
         | 
| 78 53 | 
             
                  end
         | 
| 79 54 |  | 
| 80 55 | 
             
                  def build_footer(block)
         | 
| @@ -85,11 +60,8 @@ module HledgerForecast | |
| 85 60 | 
             
                    { header: header, transactions: write_transactions(transactions), footer: footer }
         | 
| 86 61 | 
             
                  end
         | 
| 87 62 |  | 
| 88 | 
            -
                  def  | 
| 89 | 
            -
                    return " to #{ | 
| 90 | 
            -
                    return " to #{block}" if block
         | 
| 91 | 
            -
             | 
| 92 | 
            -
                    return nil
         | 
| 63 | 
            +
                  def build_to_header(to)
         | 
| 64 | 
            +
                    return " to #{to}" if to
         | 
| 93 65 | 
             
                  end
         | 
| 94 66 |  | 
| 95 67 | 
             
                  def get_descriptions(transactions)
         | 
| @@ -110,7 +82,6 @@ module HledgerForecast | |
| 110 82 | 
             
                      'yearly' => '~ yearly from',
         | 
| 111 83 | 
             
                      'custom' => "~ #{frequency} from"
         | 
| 112 84 | 
             
                    }
         | 
| 113 | 
            -
             | 
| 114 85 | 
             
                    map[type]
         | 
| 115 86 | 
             
                  end
         | 
| 116 87 |  | 
| @@ -119,11 +90,11 @@ module HledgerForecast | |
| 119 90 | 
             
                      # Skip transactions that have been marked as tracked
         | 
| 120 91 | 
             
                      next if t[:track]
         | 
| 121 92 |  | 
| 122 | 
            -
                      t[:amount] = t[:amount].to_s.ljust( | 
| 123 | 
            -
                      t[:category] = t[:category].ljust( | 
| 93 | 
            +
                      t[:amount] = t[:amount].to_s.ljust(@settings[:max_amount] + 5)
         | 
| 94 | 
            +
                      t[:category] = t[:category].to_s.ljust(@settings[:max_category])
         | 
| 124 95 |  | 
| 125 96 | 
             
                      "    #{t[:category]}    #{t[:amount]};  #{t[:description]}\n"
         | 
| 126 | 
            -
                    end
         | 
| 97 | 
            +
                    end.compact
         | 
| 127 98 | 
             
                  end
         | 
| 128 99 | 
             
                end
         | 
| 129 100 | 
             
              end
         | 
| @@ -9,84 +9,78 @@ module HledgerForecast | |
| 9 9 | 
             
                #    Expenses:Groceries    $250.00 ;  Food expenses
         | 
| 10 10 | 
             
                #    Assets:Checking
         | 
| 11 11 | 
             
                class Trackers
         | 
| 12 | 
            -
                  def self.generate( | 
| 13 | 
            -
                    new( | 
| 12 | 
            +
                  def self.generate(forecast, options)
         | 
| 13 | 
            +
                    new(forecast, options).generate
         | 
| 14 14 | 
             
                  end
         | 
| 15 15 |  | 
| 16 16 | 
             
                  def generate
         | 
| 17 | 
            -
                    return  | 
| 17 | 
            +
                    return if @options[:no_track]
         | 
| 18 | 
            +
                    return nil unless tracked?(forecast)
         | 
| 18 19 |  | 
| 19 | 
            -
                     | 
| 20 | 
            -
                       | 
| 21 | 
            -
                        process_tracked(block)
         | 
| 22 | 
            -
                      end
         | 
| 20 | 
            +
                    forecast.each do |row|
         | 
| 21 | 
            +
                      process_tracked(row)
         | 
| 23 22 | 
             
                    end
         | 
| 24 23 |  | 
| 25 24 | 
             
                    output
         | 
| 26 25 | 
             
                  end
         | 
| 27 26 |  | 
| 28 | 
            -
                  def self.track?( | 
| 27 | 
            +
                  def self.track?(row, options)
         | 
| 29 28 | 
             
                    now = Date.today
         | 
| 30 | 
            -
                     | 
| 31 | 
            -
                                                                                        data['from'], now, options)
         | 
| 29 | 
            +
                    row['track'] && Date.parse(row['from']) <= now && !exists?(row, now, options)
         | 
| 32 30 | 
             
                  end
         | 
| 33 31 |  | 
| 34 | 
            -
                  def self.exists?( | 
| 35 | 
            -
             | 
| 36 | 
            -
                    if !options[:transaction_file]
         | 
| 32 | 
            +
                  def self.exists?(row, now, options)
         | 
| 33 | 
            +
                    unless options[:transaction_file]
         | 
| 37 34 | 
             
                      puts "\nWarning: ".bold.yellow + "For tracked transactions, please specify a file with the `-t` flag"
         | 
| 38 35 | 
             
                      puts "ERROR: ".bold.red + "Tracked transactions ignored for now"
         | 
| 39 36 | 
             
                      return
         | 
| 40 37 | 
             
                    end
         | 
| 41 38 |  | 
| 42 39 | 
             
                    # Format the money
         | 
| 43 | 
            -
                    amount = Formatter.format_money( | 
| 44 | 
            -
                    inverse_amount = Formatter.format_money( | 
| 40 | 
            +
                    amount = Formatter.format_money(row['amount'], options)
         | 
| 41 | 
            +
                    inverse_amount = Formatter.format_money(row['amount'] * -1, options)
         | 
| 45 42 |  | 
| 46 | 
            -
                     | 
| 43 | 
            +
                    from = Date.parse(row['from'])
         | 
| 44 | 
            +
                    category = row['category'].gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
         | 
| 47 45 |  | 
| 48 46 | 
             
                    # We run two commands and check to see if category +/- amount or account +/- amount exists
         | 
| 49 | 
            -
                    command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{ | 
| 50 | 
            -
                    command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{ | 
| 47 | 
            +
                    command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{now}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{category} (#{amount}|#{inverse_amount})")
         | 
| 48 | 
            +
                    command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{now}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{row['account']} (#{amount}|#{inverse_amount})")
         | 
| 51 49 |  | 
| 52 50 | 
             
                    system(command1) || system(command2)
         | 
| 53 51 | 
             
                  end
         | 
| 54 52 |  | 
| 55 53 | 
             
                  private
         | 
| 56 54 |  | 
| 57 | 
            -
                  attr_reader : | 
| 55 | 
            +
                  attr_reader :forecast, :options, :output
         | 
| 58 56 |  | 
| 59 | 
            -
                  def initialize( | 
| 60 | 
            -
                    @ | 
| 57 | 
            +
                  def initialize(forecast, options)
         | 
| 58 | 
            +
                    @forecast = forecast
         | 
| 61 59 | 
             
                    @options = options
         | 
| 62 60 | 
             
                    @output = []
         | 
| 63 61 | 
             
                  end
         | 
| 64 62 |  | 
| 65 | 
            -
                  def tracked?( | 
| 66 | 
            -
                     | 
| 67 | 
            -
                       | 
| 68 | 
            -
                        block[:transactions].any? do |_, transactions|
         | 
| 69 | 
            -
                          transactions.any? { |t| t[:track] }
         | 
| 70 | 
            -
                        end
         | 
| 71 | 
            -
                      end
         | 
| 63 | 
            +
                  def tracked?(forecast)
         | 
| 64 | 
            +
                    forecast.any? do |row|
         | 
| 65 | 
            +
                      return true if row[:track] == true
         | 
| 72 66 | 
             
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    return false
         | 
| 73 69 | 
             
                  end
         | 
| 74 70 |  | 
| 75 | 
            -
                  def process_tracked( | 
| 76 | 
            -
                     | 
| 77 | 
            -
                       | 
| 78 | 
            -
                        next unless t[:track]
         | 
| 71 | 
            +
                  def process_tracked(row)
         | 
| 72 | 
            +
                    row[:transactions].each do |t|
         | 
| 73 | 
            +
                      next if t[:track] == false
         | 
| 79 74 |  | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 75 | 
            +
                      category = t[:category].ljust(options[:max_category])
         | 
| 76 | 
            +
                      amount = t[:amount].to_s.ljust(options[:max_amount])
         | 
| 82 77 |  | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 78 | 
            +
                      header = "~ #{Date.new(Date.today.year, Date.today.month,
         | 
| 79 | 
            +
                                             1).next_month}  * [TRACKED] #{t[:description]}\n"
         | 
| 80 | 
            +
                      transactions = "    #{category}    #{amount};  #{t[:description]}\n"
         | 
| 81 | 
            +
                      footer = "    #{row[:account]}\n\n"
         | 
| 87 82 |  | 
| 88 | 
            -
             | 
| 89 | 
            -
                      end
         | 
| 83 | 
            +
                      output << { header: header, transactions: [transactions], footer: footer }
         | 
| 90 84 | 
             
                    end
         | 
| 91 85 | 
             
                  end
         | 
| 92 86 | 
             
                end
         | 
| @@ -0,0 +1,14 @@ | |
| 1 | 
            +
            module HledgerForecast
         | 
| 2 | 
            +
              class Utilities
         | 
| 3 | 
            +
                def self.convert_amount(amount)
         | 
| 4 | 
            +
                  case amount
         | 
| 5 | 
            +
                  when /^-?\d+\.\d+$/ # Detects floating-point numbers (including negatives)
         | 
| 6 | 
            +
                    amount.to_f
         | 
| 7 | 
            +
                  when /^-?\d+$/ # Detects integers (including negatives)
         | 
| 8 | 
            +
                    amount.to_i
         | 
| 9 | 
            +
                  else
         | 
| 10 | 
            +
                    amount
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
            end
         | 
    
        data/lib/hledger_forecast.rb
    CHANGED
    
    | @@ -8,7 +8,6 @@ require 'highline' | |
| 8 8 | 
             
            require 'money'
         | 
| 9 9 | 
             
            require 'optparse'
         | 
| 10 10 | 
             
            require 'terminal-table'
         | 
| 11 | 
            -
            require 'yaml'
         | 
| 12 11 |  | 
| 13 12 | 
             
            Money.locale_backend = nil
         | 
| 14 13 | 
             
            Money.rounding_mode = BigDecimal::ROUND_HALF_UP
         | 
| @@ -17,12 +16,12 @@ Money.default_currency = 'USD' | |
| 17 16 | 
             
            require_relative 'hledger_forecast/calculator'
         | 
| 18 17 | 
             
            require_relative 'hledger_forecast/cli'
         | 
| 19 18 | 
             
            require_relative 'hledger_forecast/comparator'
         | 
| 20 | 
            -
            require_relative 'hledger_forecast/csv_parser'
         | 
| 21 19 | 
             
            require_relative 'hledger_forecast/formatter'
         | 
| 22 20 | 
             
            require_relative 'hledger_forecast/generator'
         | 
| 23 21 | 
             
            require_relative 'hledger_forecast/settings'
         | 
| 24 22 | 
             
            require_relative 'hledger_forecast/summarizer'
         | 
| 25 23 | 
             
            require_relative 'hledger_forecast/summarizer_formatter'
         | 
| 24 | 
            +
            require_relative 'hledger_forecast/utilities'
         | 
| 26 25 | 
             
            require_relative 'hledger_forecast/version'
         | 
| 27 26 |  | 
| 28 27 | 
             
            require_relative 'hledger_forecast/transactions/default'
         | 
    
        data/spec/cli_spec.rb
    CHANGED
    
    | @@ -2,12 +2,12 @@ require_relative '../lib/hledger_forecast' | |
| 2 2 |  | 
| 3 3 | 
             
            output = <<~JOURNAL
         | 
| 4 4 | 
             
              ~ monthly from 2023-03-01  * Mortgage, Food
         | 
| 5 | 
            -
                  Expenses:Mortgage    £2,000.55;  Mortgage
         | 
| 6 | 
            -
                  Expenses:Food        £100.00 | 
| 5 | 
            +
                  Expenses:Mortgage    £2,000.55   ;  Mortgage
         | 
| 6 | 
            +
                  Expenses:Food        £100.00     ;  Food
         | 
| 7 7 | 
             
                  Assets:Bank
         | 
| 8 8 |  | 
| 9 9 | 
             
              ~ monthly from 2023-03-01  * Savings
         | 
| 10 | 
            -
                  Assets:Bank          £-1,000.00;  Savings
         | 
| 10 | 
            +
                  Assets:Bank          £-1,000.00  ;  Savings
         | 
| 11 11 | 
             
                  Assets:Savings
         | 
| 12 12 |  | 
| 13 13 | 
             
            JOURNAL
         | 
| @@ -26,15 +26,6 @@ ensure | |
| 26 26 | 
             
            end
         | 
| 27 27 |  | 
| 28 28 | 
             
            RSpec.describe 'command' do
         | 
| 29 | 
            -
              it 'uses the CLI to generate an output' do
         | 
| 30 | 
            -
                generated_journal = './test_output.journal'
         | 
| 31 | 
            -
                File.delete(generated_journal) if File.exist?(generated_journal)
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                system("./bin/hledger-forecast generate -f ./spec/stubs/forecast.yml -o ./test_output.journal --force")
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                expect(File.read(generated_journal)).to eq(output)
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
             | 
| 38 29 | 
             
              it 'uses the CLI to generate an output with a CSV config file' do
         | 
| 39 30 | 
             
                generated_journal = './test_output.journal'
         | 
| 40 31 | 
             
                File.delete(generated_journal) if File.exist?(generated_journal)
         | 
| @@ -1,29 +1,18 @@ | |
| 1 1 | 
             
            require_relative '../lib/hledger_forecast'
         | 
| 2 2 |  | 
| 3 | 
            -
            config = <<~ | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
              monthly:
         | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
                  transactions:
         | 
| 11 | 
            -
                    - amount: "=5000/24"
         | 
| 12 | 
            -
                      category: "Expenses:House"
         | 
| 13 | 
            -
                      description: New Kitchen
         | 
| 14 | 
            -
                    - amount: "=25*4.3"
         | 
| 15 | 
            -
                      category: "Expenses:Food"
         | 
| 16 | 
            -
                      description: Monthly food shop
         | 
| 17 | 
            -
                    - amount: "=(102.50+3.25)/2"
         | 
| 18 | 
            -
                      category: "Expenses:Food"
         | 
| 19 | 
            -
                      description: Random food
         | 
| 20 | 
            -
            YAML
         | 
| 3 | 
            +
            config = <<~CSV
         | 
| 4 | 
            +
              type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
         | 
| 5 | 
            +
              monthly,,Liabilities:Amex,01/05/2023,,New kitchen,Expenses:House,=5000/24,,,
         | 
| 6 | 
            +
              monthly,,Liabilities:Amex,01/05/2023,,Monthly food shop,Expenses:Food,=25*4.3,,,
         | 
| 7 | 
            +
              monthly,,Liabilities:Amex,01/05/2023,,Random food,Expenses:Food,=(102.50+3.25)/2,,,
         | 
| 8 | 
            +
              settings,currency,GBP,,,,,,,,
         | 
| 9 | 
            +
            CSV
         | 
| 21 10 |  | 
| 22 11 | 
             
            output = <<~JOURNAL
         | 
| 23 | 
            -
              ~ monthly from 2023-05-01  * New  | 
| 24 | 
            -
                  Expenses:House    £208.33 | 
| 25 | 
            -
                  Expenses:Food     £107.50 | 
| 26 | 
            -
                  Expenses:Food     £52.88 | 
| 12 | 
            +
              ~ monthly from 2023-05-01  * New kitchen, Monthly food shop, Random food
         | 
| 13 | 
            +
                  Expenses:House    £208.33              ;  New kitchen
         | 
| 14 | 
            +
                  Expenses:Food     £107.50              ;  Monthly food shop
         | 
| 15 | 
            +
                  Expenses:Food     £52.88               ;  Random food
         | 
| 27 16 | 
             
                  Liabilities:Amex
         | 
| 28 17 |  | 
| 29 18 | 
             
            JOURNAL
         | 
    
        data/spec/custom_spec.rb
    CHANGED
    
    | @@ -1,52 +1,32 @@ | |
| 1 1 | 
             
            require_relative '../lib/hledger_forecast'
         | 
| 2 2 |  | 
| 3 | 
            -
            base_config = <<~ | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
                      category: "[Expenses:Personal Care]"
         | 
| 10 | 
            -
                      description: Hair and beauty
         | 
| 11 | 
            -
                      frequency: "every 2 weeks"
         | 
| 12 | 
            -
                    - amount: 50
         | 
| 13 | 
            -
                      category: "[Expenses:Groceries]"
         | 
| 14 | 
            -
                      description: Gotta feed that stomach
         | 
| 15 | 
            -
                      frequency: "every 5 days"
         | 
| 16 | 
            -
             | 
| 17 | 
            -
              settings:
         | 
| 18 | 
            -
                currency: GBP
         | 
| 19 | 
            -
            YAML
         | 
| 3 | 
            +
            base_config = <<~CSV
         | 
| 4 | 
            +
              type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
         | 
| 5 | 
            +
              custom,every 2 weeks,[Assets:Bank],01/05/2023,,Hair and beauty,[Expenses:Personal Care],80,,,
         | 
| 6 | 
            +
              custom,every 5 days,[Assets:Bank],01/05/2023,,Food,[Expenses:Groceries],50,,,
         | 
| 7 | 
            +
              settings,currency,GBP,,,,,,,,
         | 
| 8 | 
            +
            CSV
         | 
| 20 9 |  | 
| 21 10 | 
             
            base_output = <<~JOURNAL
         | 
| 22 11 | 
             
              ~ every 2 weeks from 2023-05-01  * Hair and beauty
         | 
| 23 | 
            -
                  [Expenses:Personal Care]    £80.00;  Hair and beauty
         | 
| 12 | 
            +
                  [Expenses:Personal Care]    £80.00 ;  Hair and beauty
         | 
| 24 13 | 
             
                  [Assets:Bank]
         | 
| 25 14 |  | 
| 26 | 
            -
              ~ every 5 days from 2023-05-01  *  | 
| 27 | 
            -
                  [Expenses:Groceries]        £50.00;   | 
| 15 | 
            +
              ~ every 5 days from 2023-05-01  * Food
         | 
| 16 | 
            +
                  [Expenses:Groceries]        £50.00 ;  Food
         | 
| 28 17 | 
             
                  [Assets:Bank]
         | 
| 29 18 |  | 
| 30 19 | 
             
            JOURNAL
         | 
| 31 20 |  | 
| 32 | 
            -
            calculated_config = <<~ | 
| 33 | 
            -
               | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
                    - amount: 80
         | 
| 38 | 
            -
                      category: "[Expenses:Personal Care]"
         | 
| 39 | 
            -
                      description: Hair and beauty
         | 
| 40 | 
            -
                      frequency: "every 2 weeks"
         | 
| 41 | 
            -
                      to: "=6"
         | 
| 42 | 
            -
             | 
| 43 | 
            -
              settings:
         | 
| 44 | 
            -
                currency: GBP
         | 
| 45 | 
            -
            YAML
         | 
| 21 | 
            +
            calculated_config = <<~CSV
         | 
| 22 | 
            +
              type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
         | 
| 23 | 
            +
              custom,every 2 weeks,[Assets:Bank],01/05/2023,=6,Hair and beauty,[Expenses:Personal Care],80,,,
         | 
| 24 | 
            +
              settings,currency,GBP,,,,,,,,
         | 
| 25 | 
            +
            CSV
         | 
| 46 26 |  | 
| 47 27 | 
             
            calculated_output = <<~JOURNAL
         | 
| 48 28 | 
             
              ~ every 2 weeks from 2023-05-01 to 2023-10-31  * Hair and beauty
         | 
| 49 | 
            -
                  [Expenses:Personal Care]    £80.00;  Hair and beauty
         | 
| 29 | 
            +
                  [Expenses:Personal Care]    £80.00 ;  Hair and beauty
         | 
| 50 30 | 
             
                  [Assets:Bank]
         | 
| 51 31 |  | 
| 52 32 | 
             
            JOURNAL
         | 
    
        data/spec/half-yearly_spec.rb
    CHANGED
    
    | @@ -1,21 +1,14 @@ | |
| 1 1 | 
             
            require_relative '../lib/hledger_forecast'
         | 
| 2 2 |  | 
| 3 | 
            -
            config = <<~ | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
                - from: "2023-04-01"
         | 
| 9 | 
            -
                  account: "Assets:Bank"
         | 
| 10 | 
            -
                  transactions:
         | 
| 11 | 
            -
                    - description: Holiday
         | 
| 12 | 
            -
                      category: "Expenses:Holiday"
         | 
| 13 | 
            -
                      amount: 500
         | 
| 14 | 
            -
            YAML
         | 
| 3 | 
            +
            config = <<~CSV
         | 
| 4 | 
            +
              type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
         | 
| 5 | 
            +
              half-yearly,,Assets:Bank,01/04/2023,,Holiday,Expenses:Holiday,500,,,
         | 
| 6 | 
            +
              settings,currency,GBP,,,,,,,,
         | 
| 7 | 
            +
            CSV
         | 
| 15 8 |  | 
| 16 9 | 
             
            output = <<~JOURNAL
         | 
| 17 10 | 
             
              ~ every 6 months from 2023-04-01  * Holiday
         | 
| 18 | 
            -
                  Expenses:Holiday    £500.00;  Holiday
         | 
| 11 | 
            +
                  Expenses:Holiday    £500.00 ;  Holiday
         | 
| 19 12 | 
             
                  Assets:Bank
         | 
| 20 13 |  | 
| 21 14 | 
             
            JOURNAL
         | 
| @@ -1,26 +1,16 @@ | |
| 1 1 | 
             
            require_relative '../lib/hledger_forecast'
         | 
| 2 2 |  | 
| 3 | 
            -
            config = <<~ | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
               | 
| 8 | 
            -
             | 
| 9 | 
            -
                  to: "2023-06-01"
         | 
| 10 | 
            -
                  account: "Assets:Bank"
         | 
| 11 | 
            -
                  transactions:
         | 
| 12 | 
            -
                    - description: Mortgage
         | 
| 13 | 
            -
                      category: "Expenses:Mortgage"
         | 
| 14 | 
            -
                      amount: 2000.00
         | 
| 15 | 
            -
                    - description: Food
         | 
| 16 | 
            -
                      category: "Expenses:Food"
         | 
| 17 | 
            -
                      amount: 100.00
         | 
| 18 | 
            -
            YAML
         | 
| 3 | 
            +
            config = <<~CSV
         | 
| 4 | 
            +
              type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
         | 
| 5 | 
            +
              monthly,,Assets:Bank,01/03/2023,01/06/2023,Mortgage,Expenses:Mortgage,2000.00,,,
         | 
| 6 | 
            +
              monthly,,Assets:Bank,01/03/2023,01/06/2023,Food,Expenses:Food,100.00,,,
         | 
| 7 | 
            +
              settings,currency,GBP,,,,,,,,
         | 
| 8 | 
            +
            CSV
         | 
| 19 9 |  | 
| 20 10 | 
             
            output = <<~JOURNAL
         | 
| 21 11 | 
             
              ~ monthly from 2023-03-01 to 2023-06-01  * Mortgage, Food
         | 
| 22 | 
            -
                  Expenses:Mortgage    £2,000.00;  Mortgage
         | 
| 23 | 
            -
                  Expenses:Food        £100.00 | 
| 12 | 
            +
                  Expenses:Mortgage    £2,000.00   ;  Mortgage
         | 
| 13 | 
            +
                  Expenses:Food        £100.00     ;  Food
         | 
| 24 14 | 
             
                  Assets:Bank
         | 
| 25 15 |  | 
| 26 16 | 
             
            JOURNAL
         |