lucabook 0.2.22 → 0.2.27

Sign up to get free protection for your applications and to get access to all the features.
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