lucabook 0.2.22 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4755b2574729e35ae914dd92338ed007df2e1158d49c613613cac3b42423c6ee
4
- data.tar.gz: a559a270322897b10b6dae569102f028004221431300debe19f3d0e5ae6432fe
3
+ metadata.gz: ad4b1eba3419508d64bfeade5f776ed2ca0f32313deab25660864aff47767787
4
+ data.tar.gz: 57aef956cbc763dbf4d089d5745c9cc4d6a6ed44dd3ff726b9c80fbc6b29629d
5
5
  SHA512:
6
- metadata.gz: 34b28c364e41ddecf9296ef39130897f03228e0bb0184241e0a7fce4349d41b14ff0ea72318154eb0bfa5e8fdac401783851c61e34e3419620eac6ab12cf6b05
7
- data.tar.gz: 1bbd862143cc37ba7c459a6d3fae832bbdad744b1db6afc3956175fa97660a1decf8a778c6c40c3e276dd35272ed363f5d9e575ce555b73a368cf2ef8173fcbf
6
+ metadata.gz: 317b373f1a31c85c107dfa294ec6df0dcc89f13b8ab10277bb004b3bdceaaf0ec58cfa5c2220a088932965b5b156fd71fef1ce67be67c330857bbcea127de365
7
+ data.tar.gz: e7456d393a4309bafb9a3622c28da10067e1483c80b0bbbd73a05508bb3d9a6742051e2d190870a1f6e39df5ec10b5dd7d1de698faef969a3bc54efa7b46b153
data/exe/luca-book CHANGED
@@ -10,7 +10,8 @@ class LucaCmd
10
10
  if params['config']
11
11
  LucaBook::Import.new(args[0], params['config']).import_csv
12
12
  elsif params['json']
13
- LucaBook::Import.import_json(STDIN.read)
13
+ str = args[0].nil? ? STDIN.read : File.read(args[0])
14
+ LucaBook::Import.import_json(str)
14
15
  else
15
16
  puts 'Usage: luca-book import -c import_config'
16
17
  exit 1
@@ -20,7 +21,11 @@ class LucaCmd
20
21
  def self.list(args, params)
21
22
  args = gen_range(params[:n] || 1) if args.empty?
22
23
  if params['code']
23
- render(LucaBook::List.term(*args, code: params['code']).list_on_code, params)
24
+ if params['headers']
25
+ render(LucaBook::ListByHeader.term(*args, code: params['code'], header: params['headers']).list_by_code, params)
26
+ else
27
+ render(LucaBook::List.term(*args, code: params['code'], recursive: params[:recursive]).list_by_code(params[:recursive]), params)
28
+ end
24
29
  else
25
30
  render(LucaBook::List.term(*args).list_journals, params)
26
31
  end
@@ -29,14 +34,30 @@ class LucaCmd
29
34
  def self.stats(args, params)
30
35
  args = gen_range(params[:n]) if args.empty?
31
36
  if params['code']
32
- render(LucaBook::State.by_code(params['code'], *args), params)
37
+ render(LucaBook::State.by_code(params['code'], *args, recursive: params[:recursive]), params)
33
38
  else
34
39
  render(LucaBook::State.range(*args).stats(params[:level]), params)
35
40
  end
36
41
  end
42
+
43
+ def self.add_header(args, params)
44
+ args = gen_range(params[:n] || 1) if args.empty?
45
+ if params['code']
46
+ LucaBook::List.add_header(*args, code: params['code'], header_key: params[:key], header_val: params[:value])
47
+ else
48
+ puts 'no code specified.'
49
+ end
50
+ end
37
51
  end
38
52
 
39
53
  class Report < LucaCmd
54
+ def self.xbrl(args, params)
55
+ level = params[:level] || 3
56
+ legal = params[:legal] || false
57
+ args = gen_range(params[:n] || 1) if args.empty?
58
+ LucaBook::State.range(*args).render_xbrl(params[:output])
59
+ end
60
+
40
61
  def self.balancesheet(args, params)
41
62
  level = params[:level] || 3
42
63
  legal = params[:legal] || false
@@ -49,6 +70,12 @@ class LucaCmd
49
70
  args = gen_range(params[:n]) if args.empty?
50
71
  render(LucaBook::State.range(*args).pl(level), params)
51
72
  end
73
+
74
+ def self.report_mail(args, params)
75
+ level = params[:level] || 3
76
+ args = gen_range(params[:n] || 12) if args.empty?
77
+ render(LucaBook::State.range(*args).report_mail(level), params)
78
+ end
52
79
  end
53
80
 
54
81
  def self.gen_range(count)
@@ -68,6 +95,12 @@ class LucaCmd
68
95
  puts YAML.dump(dat)
69
96
  end
70
97
  end
98
+
99
+ class Dict < LucaCmd
100
+ def self.update_balance(args, params)
101
+ LucaBook::Dict.generate_balance(*args)
102
+ end
103
+ end
71
104
  end
72
105
 
73
106
  def new_pj(args = nil, params = {})
@@ -91,9 +124,12 @@ when /journals?/, 'j'
91
124
  LucaCmd::Journal.import(args, params)
92
125
  end
93
126
  when 'list'
127
+ params[:recursive] = false
94
128
  OptionParser.new do |opt|
95
129
  opt.banner = 'Usage: luca-book journals list [options] [YYYY M]'
96
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
130
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
131
+ opt.on('-r', '--recursive', 'include subaccounts') { |_v| params[:recursive] = true }
132
+ opt.on('--customer', 'categorize by x-customer header') { |_v| params['headers'] = 'x-customer' }
97
133
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
98
134
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
99
135
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
@@ -101,10 +137,22 @@ when /journals?/, 'j'
101
137
  args = opt.parse!(ARGV)
102
138
  LucaCmd::Journal.list(args, params)
103
139
  end
140
+ when 'set'
141
+ OptionParser.new do |opt|
142
+ opt.banner = 'Usage: luca-book journals set [options] [YYYY M]'
143
+ opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
144
+ opt.on('--header VAL', 'header key') { |v| params[:key] = v }
145
+ opt.on('--val VAL', 'header value') { |v| params[:value] = v }
146
+ opt.on_tail('set header to journals on specified code.')
147
+ args = opt.parse!(ARGV)
148
+ LucaCmd::Journal.add_header(args, params)
149
+ end
104
150
  when 'stats'
151
+ params[:recursive] = false
105
152
  OptionParser.new do |opt|
106
153
  opt.banner = 'Usage: luca-book journals stats [options] [YYYY M]'
107
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
154
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
155
+ opt.on('-r', '--recursive', 'include subaccounts') { |_v| params[:recursive] = true }
108
156
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
109
157
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
110
158
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
@@ -130,6 +178,13 @@ when 'new'
130
178
  when /reports?/, 'r'
131
179
  subcmd = ARGV.shift
132
180
  case subcmd
181
+ when 'xbrl'
182
+ OptionParser.new do |opt|
183
+ opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
184
+ opt.on('-o', '--output VAL', 'output filename') { |v| params[:output] = v }
185
+ args = opt.parse!(ARGV)
186
+ LucaCmd::Report.xbrl(args, params)
187
+ end
133
188
  when 'bs'
134
189
  OptionParser.new do |opt|
135
190
  opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
@@ -150,6 +205,14 @@ when /reports?/, 'r'
150
205
  args = opt.parse!(ARGV)
151
206
  LucaCmd::Report.profitloss(args, params)
152
207
  end
208
+ when 'mail'
209
+ OptionParser.new do |opt|
210
+ opt.banner = 'Usage: luca-book reports mail [options] [YYYY M YYYY M]'
211
+ opt.on('-l', '--level VAL', 'account level') { |v| params[:level] = v.to_i }
212
+ opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
213
+ args = opt.parse!(ARGV)
214
+ LucaCmd::Report.report_mail(args, params)
215
+ end
153
216
  else
154
217
  puts 'Proper subcommand needed.'
155
218
  puts
@@ -158,6 +221,16 @@ when /reports?/, 'r'
158
221
  puts ' pl: show statement of income'
159
222
  exit 1
160
223
  end
224
+ when /balance/
225
+ subcmd = ARGV.shift
226
+ case subcmd
227
+ when 'update'
228
+ OptionParser.new do |opt|
229
+ opt.banner = 'Usage: luca-book balance update YYYY [M]'
230
+ args = opt.parse!(ARGV)
231
+ LucaCmd::Dict.update_balance(args, params)
232
+ end
233
+ end
161
234
  else
162
235
  puts 'Proper subcommand needed.'
163
236
  puts
data/lib/luca_book.rb CHANGED
@@ -5,10 +5,12 @@ require 'luca_record'
5
5
  require 'luca_book/version'
6
6
 
7
7
  module LucaBook
8
+ autoload :Accumulator, 'luca_book/accumulator'
8
9
  autoload :Dict, 'luca_book/dict'
9
10
  autoload :Import, 'luca_book/import'
10
11
  autoload :Journal, 'luca_book/journal'
11
12
  autoload :List, 'luca_book/list'
13
+ autoload :ListByHeader, 'luca_book/list_by_header'
12
14
  autoload :Setup, 'luca_book/setup'
13
15
  autoload :State, 'luca_book/state'
14
16
  autoload :Util, 'luca_book/util'
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_book/util'
4
+ require 'luca_support/config'
5
+
6
+ module LucaBook
7
+ module Accumulator
8
+ def self.included(klass) # :nodoc:
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def accumulate_month(year, month)
14
+ monthly_record, count = net(year, month)
15
+ [total_subaccount(monthly_record), count]
16
+ end
17
+
18
+ # Accumulate Level 2, 3 account.
19
+ #
20
+ def total_subaccount(report)
21
+ {}.tap do |res|
22
+ res['A0'] = sum_matched(report, /^[A][0-9A-Z]{2,}/)
23
+ res['B0'] = sum_matched(report, /^[B][0-9A-Z]{2,}/)
24
+ res['BA'] = res['A0'] - res['B0']
25
+ res['C0'] = sum_matched(report, /^[C][0-9A-Z]{2,}/)
26
+ res['CA'] = res['BA'] - res['C0']
27
+ res['D0'] = sum_matched(report, /^[D][0-9A-Z]{2,}/)
28
+ res['E0'] = sum_matched(report, /^[E][0-9A-Z]{2,}/)
29
+ res['EA'] = res['CA'] + res['D0'] - res['E0']
30
+ res['F0'] = sum_matched(report, /^[F][0-9A-Z]{2,}/)
31
+ res['G0'] = sum_matched(report, /^[G][0-9][0-9A-Z]{1,}/)
32
+ res['GA'] = res['EA'] + res['F0'] - res['G0']
33
+ res['H0'] = sum_matched(report, /^[H][0-9][0-9A-Z]{1,}/)
34
+ res['HA'] = res['GA'] - res['H0']
35
+
36
+ report['9142'] = (report['9142'] || BigDecimal('0')) + res['HA']
37
+ res['9142'] = report['9142']
38
+ res['10'] = sum_matched(report, /^[12][0-9A-Z]{2,}/)
39
+ jp_4v = sum_matched(report, /^[4][V]{2,}/) # deferred assets for JP GAAP
40
+ res['30'] = sum_matched(report, /^[34][0-9A-Z]{2,}/) - jp_4v
41
+ res['4V'] = jp_4v if LucaSupport::CONFIG['country'] == 'jp'
42
+ res['50'] = sum_matched(report, /^[56][0-9A-Z]{2,}/)
43
+ res['70'] = sum_matched(report, /^[78][0-9A-Z]{2,}/)
44
+ res['91'] = sum_matched(report, /^91[0-9A-Z]{1,}/)
45
+ res['8ZZ'] = res['50'] + res['70']
46
+ res['9ZZ'] = sum_matched(report, /^[9][0-9A-Z]{2,}/)
47
+
48
+ res['1'] = res['10'] + res['30']
49
+ res['5'] = res['8ZZ'] + res['9ZZ']
50
+ res['_d'] = report['_d']
51
+
52
+ report.each do |k, v|
53
+ res[k] ||= sum_matched(report, /^#{k}[0-9A-Z]{1,}/) if k.length == 2
54
+ res[k] = v if k.length == 3
55
+ end
56
+
57
+ report.each do |k, v|
58
+ if k.length >= 4
59
+ if res[k[0, 3]]
60
+ res[k[0, 3]] += v
61
+ else
62
+ res[k[0, 3]] = v
63
+ end
64
+ res[k] = v
65
+ end
66
+ end
67
+ res.sort.to_h
68
+ end
69
+ end
70
+
71
+ def sum_matched(report, reg)
72
+ report.select { |k, v| reg.match(k)}.values.sum
73
+ end
74
+
75
+ # for assert purpose
76
+ #
77
+ def gross(start_year, start_month, end_year = nil, end_month = nil, code: nil, date_range: nil, rows: 4, recursive: false)
78
+ if ! date_range.nil?
79
+ raise if date_range.class != Range
80
+ # TODO: date based range search
81
+ end
82
+
83
+ end_year ||= start_year
84
+ end_month ||= start_month
85
+ sum = { debit: {}, credit: {}, debit_count: {}, credit_count: {} }
86
+ idx_memo = []
87
+ term(start_year, start_month, end_year, end_month, code) do |f, _path|
88
+ CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
89
+ .each_with_index do |row, i|
90
+ break if i >= rows
91
+
92
+ case i
93
+ when 0
94
+ idx_memo = row.map(&:to_s)
95
+ next if code && idx_memo.select { |idx| /^#{code}/.match(idx) }.empty?
96
+
97
+ idx_memo.each do |r|
98
+ sum[:debit][r] ||= BigDecimal('0')
99
+ sum[:debit_count][r] ||= 0
100
+ end
101
+ when 1
102
+ next if code && idx_memo.select { |idx| /^#{code}/.match(idx) }.empty?
103
+
104
+ row.each_with_index do |r, j|
105
+ sum[:debit][idx_memo[j]] += BigDecimal(r.to_s)
106
+ sum[:debit_count][idx_memo[j]] += 1
107
+ end
108
+ when 2
109
+ idx_memo = row.map(&:to_s)
110
+ break if code && idx_memo.select { |idx| /^#{code}/.match(idx) }.empty?
111
+
112
+ idx_memo.each do |r|
113
+ sum[:credit][r] ||= BigDecimal('0')
114
+ sum[:credit_count][r] ||= 0
115
+ end
116
+ when 3
117
+ row.each_with_index do |r, j|
118
+ sum[:credit][idx_memo[j]] += BigDecimal(r.to_s)
119
+ sum[:credit_count][idx_memo[j]] += 1
120
+ end
121
+ else
122
+ puts row # for debug
123
+ end
124
+ end
125
+ end
126
+ return sum if code.nil?
127
+
128
+ codes = if recursive
129
+ sum[:debit].keys.concat(sum[:credit].keys).uniq.select { |k| /^#{code}/.match(k) }
130
+ else
131
+ Array(code)
132
+ end
133
+ res = { debit: 0, credit: 0, debit_count: 0, credit_count: 0 }
134
+ codes.each do |code|
135
+ res[:debit] += sum[:debit][code] || BigDecimal('0')
136
+ res[:credit] += sum[:credit][code] || BigDecimal('0')
137
+ res[:debit_count] += sum[:debit_count][code] || 0
138
+ res[:credit_count] += sum[:credit_count][code] || 0
139
+ end
140
+ res
141
+ end
142
+
143
+ # netting vouchers in specified term
144
+ #
145
+ def net(start_year, start_month, end_year = nil, end_month = nil, code: nil, date_range: nil)
146
+ g = gross(start_year, start_month, end_year, end_month, code: code, date_range: date_range)
147
+ idx = (g[:debit].keys + g[:credit].keys).uniq.sort
148
+ count = {}
149
+ diff = {}.tap do |sum|
150
+ idx.each do |code|
151
+ sum[code] = g.dig(:debit, code).nil? ? BigDecimal('0') : LucaBook::Util.calc_diff(g[:debit][code], code)
152
+ sum[code] -= g.dig(:credit, code).nil? ? BigDecimal('0') : LucaBook::Util.calc_diff(g[:credit][code], code)
153
+ count[code] = (g.dig(:debit_count, code) || 0) + (g.dig(:credit_count, code) || 0)
154
+ end
155
+ end
156
+ [diff, count]
157
+ end
158
+ end
159
+ end
160
+ end
@@ -49,7 +49,7 @@ class LucaBookConsole
49
49
  print " " if h[:code].length > 3
50
50
  end
51
51
  puts cnsl_label(h[:label], h[:code])
52
- h[:value].each_slice(6) do |v|
52
+ h[:amount].each_slice(6) do |v|
53
53
  puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
54
54
  end
55
55
  end
@@ -72,13 +72,13 @@ class LucaBookConsole
72
72
  end
73
73
  convert_collection(report).each do |h|
74
74
  if /^[A-Z]/.match(h[:code])
75
- total = [h[:value].inject(:+)] + Array.new(h[:value].length)
75
+ total = [h[:amount].inject(:+)] + Array.new(h[:amount].length)
76
76
  if /[^0]$/.match(h[:code])
77
77
  print " "
78
78
  print " " if h[:code].length > 3
79
79
  end
80
80
  puts cnsl_label(h[:label], h[:code])
81
- h[:value].each_slice(6).with_index(0) do |v, i|
81
+ h[:amount].each_slice(6).with_index(0) do |v, i|
82
82
  puts "#{cnsl_fmt(total[i], 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
83
83
  end
84
84
  end
@@ -98,7 +98,7 @@ class LucaBookConsole
98
98
  end
99
99
  end
100
100
  }.sort.map do |k,v|
101
- {code: k, label: @report.dict.dig(k, :label), value: v}
101
+ {code: k, label: @report.dict.dig(k, :label), amount: v}
102
102
  end
103
103
  end
104
104
 
@@ -1,12 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'luca_support/code'
3
4
  require 'luca_support/config'
4
5
  require 'luca_record/dict'
6
+ require 'luca_record/io'
7
+ require 'luca_book'
5
8
  require 'date'
6
9
  require 'pathname'
7
10
 
8
11
  module LucaBook
9
12
  class Dict < LucaRecord::Dict
13
+ include Accumulator
14
+ include LucaRecord::IO
15
+
16
+ @dirname = 'journals'
17
+ @record_type = 'raw'
10
18
  # Column number settings for CSV/TSV convert
11
19
  #
12
20
  # :label
@@ -15,10 +23,10 @@ module LucaBook
15
23
  # must be specified with label
16
24
  # :debit_label
17
25
  # for double entry data
18
- # * debit_value
26
+ # * debit_amount
19
27
  # :credit_label
20
28
  # for double entry data
21
- # * credit_value
29
+ # * credit_amount
22
30
  # :note
23
31
  # can be the same column as another label
24
32
  #
@@ -41,8 +49,8 @@ module LucaBook
41
49
  end
42
50
  end
43
51
  config[:type] ||= 'invalid'
44
- config[:debit_value] = @config['debit_value'].to_i if @config.dig('debit_value')
45
- config[:credit_value] = @config['credit_value'].to_i if @config.dig('credit_value')
52
+ config[:debit_amount] = @config['debit_amount'].to_i if @config.dig('debit_amount')
53
+ config[:credit_amount] = @config['credit_amount'].to_i if @config.dig('credit_amount')
46
54
  config[:note] = @config['note'] if @config.dig('note')
47
55
  config[:encoding] = @config['encoding'] if @config.dig('encoding')
48
56
 
@@ -84,14 +92,93 @@ module LucaBook
84
92
  nil
85
93
  end
86
94
 
87
- def self.latest_balance
88
- dict_dir = Pathname(LucaSupport::Config::Pjdir) / 'data' / 'balance'
89
- # TODO: search latest balance dictionary
90
- load_tsv_dict(dict_dir / 'start.tsv')
95
+ # Find balance at financial year start by given date.
96
+ # If not found 'start-yyyy-mm-*.tsv', use 'start.tsv' as default.
97
+ #
98
+ def self.latest_balance(date)
99
+ load_tsv_dict(latest_balance_path(date))
100
+ end
101
+
102
+ def self.latest_balance_path(date)
103
+ start_year = date.month >= LucaSupport::CONFIG['fy_start'] ? date.year : date.year - 1
104
+ latest = Date.new(start_year, LucaSupport::CONFIG['fy_start'], 1).prev_month
105
+ dict_dir = Pathname(LucaSupport::PJDIR) / 'data' / 'balance'
106
+ fileglob = %Q(start-#{latest.year}-#{format("%02d", latest.month)}-*)
107
+ path = Dir.glob(fileglob, base: dict_dir)[0] || 'start.tsv'
108
+ dict_dir / path
91
109
  end
92
110
 
93
111
  def self.issue_date(obj)
94
112
  Date.parse(obj.dig('_date', :label))
95
113
  end
114
+
115
+ def self.generate_balance(year, month = nil)
116
+ start_date = Date.new((year.to_i - 1), LucaSupport::CONFIG['fy_start'], 1)
117
+ month ||= LucaSupport::CONFIG['fy_start'] - 1
118
+ end_date = Date.new(year.to_i, month, -1)
119
+ labels = load('base.tsv')
120
+ bs = load_balance(start_date, end_date)
121
+ fy_digest = checksum(start_date, end_date)
122
+ current_ref = gitref
123
+ csv = CSV.generate(String.new, col_sep: "\t", headers: false) do |f|
124
+ f << ['code', 'label', 'balance']
125
+ f << ['_date', end_date]
126
+ f << ['_digest', fy_digest]
127
+ f << ['_gitref', current_ref] if current_ref
128
+ bs.each do |code, balance|
129
+ next if LucaSupport::Code.readable(balance) == 0
130
+
131
+ f << [code, labels.dig(code, :label), LucaSupport::Code.readable(balance)]
132
+ end
133
+ end
134
+ dict_dir = Pathname(LucaSupport::PJDIR) / 'data' / 'balance'
135
+ filepath = dict_dir / "start-#{end_date.to_s}.tsv"
136
+
137
+ File.open(filepath, 'w') { |f| f.write csv }
138
+ end
139
+
140
+ def self.load_balance(start_date, end_date)
141
+ base = latest_balance(start_date).each_with_object({}) do |(k, v), h|
142
+ h[k] = BigDecimal(v[:balance].to_s) if v[:balance]
143
+ end
144
+
145
+ search_range = term_by_month(start_date, end_date)
146
+ bs = search_range.each_with_object(base) do |date, h|
147
+ net(date.year, date.month)[0].each do |code, amount|
148
+ next if /^[^1-9]/.match(code)
149
+
150
+ h[code] ||= BigDecimal('0')
151
+ h[code] += amount
152
+ end
153
+ end
154
+ bs['9142'] ||= BigDecimal('0')
155
+ bs['9142'] += LucaBook::State
156
+ .range(start_date.year, start_date.month, end_date.year, end_date.month)
157
+ .net_income
158
+ bs.sort
159
+ end
160
+
161
+ def self.checksum(start_date, end_date)
162
+ digest = update_digest(String.new, File.read(latest_balance_path(start_date)))
163
+ term_by_month(start_date, end_date)
164
+ .map { |date| dir_digest(date.year, date.month) }
165
+ .each { |month_digest| digest = update_digest(digest, month_digest) }
166
+ digest
167
+ end
168
+
169
+ def self.gitref
170
+ digest = `git rev-parse HEAD`
171
+ $?.exitstatus == 0 ? digest.strip : nil
172
+ end
173
+
174
+ def self.term_by_month(start_date, end_date)
175
+ Enumerator.new do |yielder|
176
+ each_month = start_date
177
+ while each_month <= end_date
178
+ yielder << each_month
179
+ each_month = each_month.next_month
180
+ end
181
+ end
182
+ end
96
183
  end
97
184
  end