lucabook 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require "optparse"
4
+ require "luca_book/console"
5
+
6
+ def list(args, params)
7
+ if params["c"] or params["code"]
8
+ code = params["c"] || params["code"]
9
+ LucaBookConsole.new.by_code(code, args.dig(0), args.dig(1))
10
+ elsif args.length > 0
11
+ LucaBookConsole.new.by_month(args[0], args.dig(1))
12
+ else
13
+ LucaBookConsole.new.all
14
+ end
15
+ end
16
+
17
+ def report(args, params)
18
+ if params['bs']
19
+ LucaBook::State.term(*args).bs.to_yaml
20
+ elsif params['pl']
21
+ LucaBook::State.term(*args).pl.to_yaml
22
+ else
23
+ LucaBook::State.term(*args).to_yaml
24
+ end
25
+ end
26
+
27
+ cmd = ARGV.shift
28
+
29
+ case cmd
30
+ when "list"
31
+ params = {}
32
+ OptionParser.new do |opt|
33
+ opt.banner = 'Usage: luca list [year month]'
34
+ opt.on('-c', '--code VAL', 'search with code'){|v| params["code"] = v }
35
+ opt.on_tail('List records. If you specify code and/or month, search on each criteria.')
36
+ args = opt.parse!(ARGV)
37
+ list(args, params)
38
+ end
39
+ when "report"
40
+ params = {}
41
+ OptionParser.new do |opt|
42
+ opt.banner = 'Usage: luca report'
43
+ opt.on('--bs', 'show Balance sheet'){|v| params["bs"] = v }
44
+ opt.on('--pl', 'show Income statement'){|v| params["pl"] = v }
45
+ args = opt.parse!(ARGV)
46
+ report(args, params)
47
+ end
48
+ when "--help"
49
+ puts 'Usage: luca subcommand'
50
+ puts ' list: list records'
51
+ puts ' report: show reports'
52
+ else
53
+ puts 'Invalid subcommand'
54
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_book/version'
4
+
5
+ module LucaBook
6
+ autoload :Journal, 'luca_book/journal'
7
+ autoload :State, 'luca_book/state'
8
+ end
@@ -0,0 +1,134 @@
1
+ require 'luca_book'
2
+
3
+ class LucaBookConsole
4
+
5
+ def initialize(dir_path=nil)
6
+ @report = LucaBookReport.new(dir_path)
7
+ end
8
+
9
+ def all
10
+ array = @report.scan_terms(@report.book.pjdir).map{|y,m| y}.uniq.map{|year|
11
+ @report.book.search(year)
12
+ }.flatten
13
+ show_records(array)
14
+ end
15
+
16
+ def by_code(code, year=nil, month=nil)
17
+ array = @report.by_code(code, year, month)
18
+ show_records(array)
19
+ end
20
+
21
+ def by_month(year, month)
22
+ array = @report.book.search(year, month)
23
+ show_records(array)
24
+ end
25
+
26
+ def show_records(records)
27
+ print "#{cnsl_fmt("ID")} #{cnsl_fmt("debit")} #{cnsl_fmt("credit")} #{cnsl_fmt("")*2}"
28
+ print "#{cnsl_fmt("balance")}" unless records.first.dig(:balance).nil?
29
+ puts
30
+ records.each do |h|
31
+ puts "#{cnsl_fmt(h.dig(:id))} #{"-"*85}"
32
+ lines = [h.dig(:debit)&.length, h.dig(:credit)&.length]&.max || 0
33
+ lines.times do |i|
34
+ puts "#{cnsl_fmt("")} #{cnsl_fmt(h.dig(:debit, i, :amount))} #{cnsl_code(h.dig(:debit, i))}" if h.dig(:debit, i, :amount)
35
+ puts "#{cnsl_fmt("")*2} #{cnsl_fmt(h.dig(:credit, i, :amount))} #{cnsl_code(h.dig(:credit, i))}" if h.dig(:credit, i, :amount)
36
+ end
37
+ puts "#{cnsl_fmt(""*15)*5} #{cnsl_fmt(h.dig(:balance))}" unless h.dig(:balance).nil?
38
+ puts "#{cnsl_fmt(""*15)} #{h.dig(:note)}"
39
+ end
40
+ end
41
+
42
+ def bs
43
+ target = []
44
+ report = []
45
+ output = @report.accumulate_all do |f|
46
+ target << f[:target]
47
+ report << f[:current]
48
+ #diff << f[:diff]
49
+ end
50
+ puts "---- BS ----"
51
+ target.each_slice(6) do |v|
52
+ puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
53
+ end
54
+ convert_collection(report).each do |h|
55
+ if /^[0-9]/.match(h[:code])
56
+ if /[^0]$/.match(h[:code])
57
+ print " "
58
+ print " " if h[:code].length > 3
59
+ end
60
+ puts cnsl_label(h[:label], h[:code])
61
+ h[:value].each_slice(6) do |v|
62
+ puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
63
+ end
64
+ end
65
+ end
66
+ puts "---- ----"
67
+ end
68
+
69
+ def pl
70
+ target = []
71
+ report = []
72
+ output = @report.accumulate_all do |f|
73
+ target << f[:target]
74
+ report << f[:diff]
75
+ #current << f[:current]
76
+ end
77
+ puts "---- PL ----"
78
+ target.each_slice(6) do |v|
79
+ puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
80
+ end
81
+ convert_collection(report).each do |h|
82
+ if /^[A-Z]/.match(h[:code])
83
+ total = [h[:value].inject(:+)] + Array.new(h[:value].length)
84
+ if /[^0]$/.match(h[:code])
85
+ print " "
86
+ print " " if h[:code].length > 3
87
+ end
88
+ puts cnsl_label(h[:label], h[:code])
89
+ h[:value].each_slice(6).with_index(0) do |v, i|
90
+ puts "#{cnsl_fmt(total[i], 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
91
+ end
92
+ end
93
+ end
94
+ puts "---- ----"
95
+ end
96
+
97
+ def convert_collection(obj)
98
+ {}.tap {|res|
99
+ obj.each do |month|
100
+ month.each do |k,v|
101
+ if res.has_key?(k)
102
+ res[k] << v
103
+ else
104
+ res[k] = [v]
105
+ end
106
+ end
107
+ end
108
+ }.sort.map do |k,v|
109
+ {code: k, label: @report.dict.dig(k, :label), value: v}
110
+ end
111
+ end
112
+
113
+ def cnsl_label(label, code)
114
+ if /[0]$/.match(code)
115
+ cnsl_bold(label) + " " + "-"*80
116
+ else
117
+ label
118
+ end
119
+ end
120
+
121
+ def cnsl_bold(str)
122
+ "\e[1m#{str}\e[0m"
123
+ end
124
+
125
+ def cnsl_code(obj)
126
+ code = @report.dict.dig(obj&.dig(:code))&.dig(:label) || ""
127
+ end
128
+
129
+ def cnsl_fmt(str, width=15, length=nil)
130
+ length ||= width
131
+ sprintf("%#{width}.#{length}s", str)
132
+ end
133
+
134
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_support/config'
4
+ require 'luca_record/dict'
5
+
6
+ module LucaBook
7
+ class Dict < LucaRecord::Dict
8
+
9
+ @filename = 'dict.tsv'
10
+
11
+ Data = load
12
+ end
13
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'json'
5
+ require 'luca_book'
6
+ require 'luca_support'
7
+ #require 'luca_book/dict'
8
+ require 'luca_record'
9
+
10
+ module LucaBook
11
+ class Import
12
+ DEBIT_DEFAULT = '仮払金'
13
+ CREDIT_DEFAULT = '仮受金'
14
+
15
+ def initialize(path)
16
+ raise 'no such file' unless FileTest.file?(path)
17
+
18
+ @target_file = path
19
+ # TODO: yaml need to be configurable
20
+ @dict = LucaRecord::Dict.new('import.yaml')
21
+ @code_map = LucaRecord::Dict.reverse(LucaRecord::Dict.load('base.tsv'))
22
+ @config = @dict.csv_config
23
+ end
24
+
25
+ # === JSON Format:
26
+ # {
27
+ # "date": "2020-05-04",
28
+ # "debit" : [
29
+ # {
30
+ # "label": "savings accounts",
31
+ # "value": 20000
32
+ # }
33
+ # ],
34
+ # "credit" : [
35
+ # {
36
+ # "label": "trade notes receivable",
37
+ # "value": 20000
38
+ # }
39
+ # ],
40
+ # "note": "settlement for the last month trade"
41
+ # }
42
+ #
43
+ def import_json(io)
44
+ d = JSON.parse(io)
45
+ validate(d)
46
+
47
+ # dict = LucaBook::Dict.reverse_dict(LucaBook::Dict::Data)
48
+ d['debit'].each { |h| h['code'] = @dict.search(h['label'], DEBIT_DEFAULT) }
49
+ d['credit'].each { |h| h['code'] = @dict.search(h['label'], CREDIT_DEFAULT) }
50
+
51
+ LucaBook.new.create!(d)
52
+ end
53
+
54
+ def import_csv
55
+ @dict.load_csv(@target_file) do |row|
56
+ if @config[:type] == 'single'
57
+ LucaBook::Journal.create!(parse_single(row))
58
+ elsif @config[:type] == 'double'
59
+ p parse_double(row)
60
+ else
61
+ p row
62
+ end
63
+ end
64
+ end
65
+
66
+ #
67
+ # convert single entry data
68
+ #
69
+ def parse_single(row)
70
+ value = row.dig(@config[:credit_value])&.empty? ? row[@config[:debit_value]] : row[@config[:credit_value]]
71
+ {}.tap do |d|
72
+ d['date'] = parse_date(row)
73
+ if row.dig(@config[:credit_value])&.empty?
74
+ d['debit'] = [
75
+ { 'code' => search_code(row[@config[:label]], DEBIT_DEFAULT) }
76
+ ]
77
+ d['credit'] = [
78
+ { 'code' => @code_map.dig(@config[:counter_label]) }
79
+ ]
80
+ else
81
+ d['debit'] = [
82
+ { 'code' => @code_map.dig(@config[:counter_label]) }
83
+ ]
84
+ d['credit'] = [
85
+ { 'code' => search_code(row[@config[:label]], CREDIT_DEFAULT) }
86
+ ]
87
+ end
88
+ d['debit'][0]['value'] = value
89
+ d['credit'][0]['value'] = value
90
+ d['note'] = row[@config[:note]]
91
+ end
92
+ end
93
+
94
+ #
95
+ # convert double entry data
96
+ #
97
+ def parse_double(row)
98
+ {}.tap do |d|
99
+ d['date'] = parse_date(row)
100
+ d['debit'] = {
101
+ 'code' => search_code(row[@config[:debit_label]], DEBIT_DEFAULT),
102
+ 'value' => row.dig(@config[:debit_value])
103
+ }
104
+ d['credit'] = {
105
+ 'code' => search_code(row[@config[:credit_label]], CREDIT_DEFAULT),
106
+ 'value' => row.dig(@config[:credit_value])
107
+ }
108
+ d['note'] = row[@config[:note]]
109
+ end
110
+ end
111
+
112
+ def search_code(label, default_label)
113
+ @code_map.dig(@dict.search(label, default_label))
114
+ end
115
+
116
+ def parse_date(row)
117
+ return nil if row.dig(@config[:year]).empty?
118
+
119
+ "#{row.dig(@config[:year])}-#{row.dig(@config[:month])}-#{row.dig(@config[:day])}"
120
+ end
121
+
122
+ def validate(obj)
123
+ raise 'NoDateKey' if ! obj.has_key?('date')
124
+ raise 'NoDebitKey' if ! obj.has_key?('debit')
125
+ raise 'NoDebitValue' if obj['debit'].length < 1
126
+ raise 'NoCreditKey' if ! obj.has_key?('credit')
127
+ raise 'NoCreditValue' if obj['credit'].length < 1
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,80 @@
1
+ #
2
+ # manipulate files based on transaction date
3
+ #
4
+
5
+ require 'csv'
6
+ require 'date'
7
+ require 'luca_record'
8
+
9
+ module LucaBook
10
+ class Journal < LucaRecord::Base
11
+ @dirname = 'journals'
12
+
13
+ #
14
+ # create journal from hash
15
+ #
16
+ def self.create!(d)
17
+ date = Date.parse(d['date'])
18
+
19
+ debit_amount = serialize_on_key(d['debit'], 'value')
20
+ credit_amount = serialize_on_key(d['credit'], 'value')
21
+ raise 'BalanceUnmatch' if debit_amount.inject(:+) != credit_amount.inject(:+)
22
+
23
+ debit_code = serialize_on_key(d['debit'], 'code')
24
+ credit_code = serialize_on_key(d['credit'], 'code')
25
+
26
+ # TODO: limit code length for filename
27
+ codes = (debit_code + credit_code).uniq
28
+ create_record!(date, codes) do |f|
29
+ f << debit_code
30
+ f << debit_amount
31
+ f << credit_code
32
+ f << credit_amount
33
+ f << []
34
+ f << [d.dig('note')]
35
+ end
36
+ end
37
+
38
+ #
39
+ # collect values on specified key
40
+ #
41
+ def self.serialize_on_key(array_of_hash, key)
42
+ array_of_hash.map { |h| h[key] }
43
+ end
44
+
45
+ #
46
+ # override de-serializing journal format
47
+ #
48
+ def self.load_data(io, path)
49
+ {}.tap do |record|
50
+ body = false
51
+ record[:id] = path[0] + path[1]
52
+ CSV.new(io, headers: false, col_sep: "\t", encoding: 'UTF-8')
53
+ .each.with_index(0) do |line, i|
54
+ case i
55
+ when 0
56
+ record[:debit] = line.map { |row| { code: row } }
57
+ when 1
58
+ line.each_with_index do |amount, i|
59
+ record[:debit][i][:amount] = amount.to_i # TODO: bigdecimal support
60
+ end
61
+ when 2
62
+ record[:credit] = line.map { |row| { code: row } }
63
+ when 3
64
+ line.each_with_index do |amount, i|
65
+ record[:credit][i][:amount] = amount.to_i # TODO: bigdecimal support
66
+ end
67
+ else
68
+ if line.empty?
69
+ record[:note] ||= []
70
+ body = true
71
+ next
72
+ end
73
+ record[:note] << line.join(' ') if body
74
+ end
75
+ record[:note]&.join('\n')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ # move to LucaBook::State
2
+ class LucaBookReport
3
+ end
@@ -0,0 +1,250 @@
1
+
2
+ require 'csv'
3
+ require 'pathname'
4
+ require 'date'
5
+ require 'luca_support'
6
+ require 'luca_record'
7
+ require 'luca_record/dict'
8
+ require 'luca_book'
9
+
10
+ #
11
+ # Statement on specified term
12
+ #
13
+ module LucaBook
14
+ class State < LucaRecord::Base
15
+ @dirname = 'journals'
16
+ @record_type = 'raw'
17
+
18
+ attr_reader :statement
19
+
20
+ def initialize(data)
21
+ @data = data
22
+ @dict = LucaRecord::Dict.load('base.tsv')
23
+ end
24
+
25
+ # TODO: not compatible with LucaRecord::Base.open_records
26
+ def search_tag(code)
27
+ count = 0
28
+ Dir.children(LucaSupport::Config::Pjdir).sort.each do |dir|
29
+ next if ! FileTest.directory?(LucaSupport::Config::Pjdir+dir)
30
+
31
+ open_records(datadir, dir, 3) do |row, i|
32
+ next if i == 2
33
+ count += 1 if row.include?(code)
34
+ end
35
+ end
36
+ puts "#{code}: #{count}"
37
+ end
38
+
39
+ def self.term(from_year, from_month, to_year = from_year, to_month = from_month)
40
+ date = Date.new(from_year.to_i, from_month.to_i, -1)
41
+ last_date = Date.new(to_year.to_i, to_month.to_i, -1)
42
+ raise 'invalid term specified' if date > last_date
43
+
44
+ reports = [].tap do |r|
45
+ while date <= last_date do
46
+ r << accumulate_month(date.year, date.month)
47
+ date = date.next_month
48
+ end
49
+ end
50
+ new reports
51
+ end
52
+
53
+ def by_code(code, year=nil, month=nil)
54
+ raise "not supported year range yet" if ! year.nil? && month.nil?
55
+
56
+ bl = @book.load_start.dig(code) || 0
57
+ full_term = scan_terms(LucaSupport::Config::Pjdir)
58
+ if ! month.nil?
59
+ pre_term = full_term.select{|y,m| y <= year.to_i && m < month.to_i }
60
+ bl += pre_term.map{|y,m| self.class.net(y, m)}.inject(0){|sum, h| sum + h[code]}
61
+ [{ code: code, balance: bl, note: "#{code} #{dict.dig(code, :label)}" }] + records_with_balance(year, month, code, bl)
62
+ else
63
+ start = { code: code, balance: bl, note: "#{code} #{dict.dig(code, :label)}" }
64
+ full_term.map {|y, m| y }.uniq.map {|y|
65
+ records_with_balance(y, nil, code, bl)
66
+ }.flatten.prepend(start)
67
+ end
68
+ end
69
+
70
+ def records_with_balance(year, month, code, balance)
71
+ @book.search(year, month, nil, code).each do |h|
72
+ balance += self.class.calc_diff(amount_by_code(h[:debit], code), code) - @book.calc_diff(amount_by_code(h[:credit], code), code)
73
+ h[:balance] = balance
74
+ end
75
+ end
76
+
77
+ #
78
+ # TODO: useless method. consider to remove
79
+ #
80
+ def accumulate_all
81
+ current = @book.load_start
82
+ target = []
83
+ Dir.chdir(@book.pjdir) do
84
+ net_records = scan_terms(@book.pjdir).map do |year, month|
85
+ target << [year, month]
86
+ accumulate_month(year, month)
87
+ end
88
+ all_keys = net_records.map{|h| h.keys}.flatten.uniq
89
+ net_records.each.with_index(0) do |diff, i|
90
+ all_keys.each {|key| diff[key] = 0 unless diff.has_key?(key)}
91
+ diff.each do |k,v|
92
+ if current[k]
93
+ current[k] += v
94
+ else
95
+ current[k] = v
96
+ end
97
+ end
98
+ f = { target: "#{target[i][0]}-#{target[i][1]}", diff: diff.sort, current: current.sort }
99
+ yield f
100
+ end
101
+ end
102
+ end
103
+
104
+ def to_yaml
105
+ YAML.dump(code2label).tap { |data| puts data }
106
+ end
107
+
108
+ def code2label
109
+ @statement ||= @data
110
+ @statement.map do |report|
111
+ {}.tap do |h|
112
+ report.each { |k, v| h[@dict.dig(k, :label)] = v }
113
+ end
114
+ end
115
+ end
116
+
117
+ def bs
118
+ @statement = @data.map do |data|
119
+ data.select { |k, v| /^[0-9].+/.match(k) }
120
+ end
121
+ self
122
+ end
123
+
124
+ def pl
125
+ @statement = @data.map do |data|
126
+ data.select { |k, v| /^[A-F].+/.match(k) }
127
+ end
128
+ self
129
+ end
130
+
131
+ def self.accumulate_month(year, month)
132
+ monthly_record = net(year, month)
133
+ total_subaccount(monthly_record)
134
+ end
135
+
136
+ def amount_by_code(items, code)
137
+ items
138
+ .select{|item| item.dig(:code) == code }
139
+ .inject(0){|sum, item| sum + item[:amount] }
140
+ end
141
+
142
+ def self.total_subaccount(report)
143
+ report.dup.tap do |res|
144
+ report.each do |k, v|
145
+ if k.length >= 4
146
+ if res[k[0, 3]]
147
+ res[k[0, 3]] += v
148
+ else
149
+ res[k[0, 3]] = v
150
+ end
151
+ end
152
+ end
153
+ res['10'] = sum_matched(report, /^[123].[^0]/)
154
+ res['40'] = sum_matched(report, /^[4].[^0]}/)
155
+ res['50'] = sum_matched(report, /^[56].[^0]/)
156
+ res['70'] = sum_matched(report, /^[78].[^0]/)
157
+ res['90'] = sum_matched(report, /^[9].[^0]/)
158
+ res['A0'] = sum_matched(report, /^[A].[^0]/)
159
+ res['B0'] = sum_matched(report, /^[B].[^0]/)
160
+ res['BA'] = res['A0'] - res['B0']
161
+ res['C0'] = sum_matched(report, /^[C].[^0]/)
162
+ res['CA'] = res['BA'] - res['C0']
163
+ res['D0'] = sum_matched(report, /^[D].[^0]/)
164
+ res['E0'] = sum_matched(report, /^[E].[^0]/)
165
+ res['EA'] = res['CA'] + res['D0'] - res['E0']
166
+ res['F0'] = sum_matched(report, /^[F].[^0]/)
167
+ res['G0'] = sum_matched(report, /^[G].[^0]/)
168
+ res['GA'] = res['EA'] + res['F0'] - res['G0']
169
+ res['HA'] = res['GA'] - sum_matched(report, /^[H].[^0]/)
170
+ end
171
+ end
172
+
173
+ def self.sum_matched(report, reg)
174
+ report.select { |k, v| reg.match(k)}.values.sum
175
+ end
176
+
177
+ # for assert purpose
178
+ def self.gross(year, month = nil, code = nil, date_range = nil, rows = 4)
179
+ if ! date_range.nil?
180
+ raise if date_range.class != Range
181
+ # TODO: date based range search
182
+ end
183
+
184
+ sum = { debit: {}, credit: {} }
185
+ idx_memo = []
186
+ asof(year, month) do |f, _path|
187
+ CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
188
+ .each_with_index do |row, i|
189
+ break if i >= rows
190
+ case i
191
+ when 0
192
+ idx_memo = row.map(&:to_s)
193
+ idx_memo.each { |r| sum[:debit][r] ||= 0 }
194
+ when 1
195
+ row.each_with_index { |r, i| sum[:debit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
196
+ when 2
197
+ idx_memo = row.map(&:to_s)
198
+ idx_memo.each { |r| sum[:credit][r] ||= 0 }
199
+ when 3
200
+ row.each_with_index { |r, i| sum[:credit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
201
+ else
202
+ puts row # for debug
203
+ end
204
+ end
205
+ end
206
+ sum
207
+ end
208
+
209
+ # netting vouchers in specified term
210
+ def self.net(year, month = nil, code = nil, date_range = nil)
211
+ g = gross(year, month, code, date_range)
212
+ idx = (g[:debit].keys + g[:credit].keys).uniq.sort
213
+ {}.tap do |sum|
214
+ idx.each do |code|
215
+ sum[code] = g.dig(:debit, code).nil? ? 0 : calc_diff(g[:debit][code], code)
216
+ sum[code] -= g.dig(:credit, code).nil? ? 0 : calc_diff(g[:credit][code], code)
217
+ end
218
+ end
219
+ end
220
+
221
+ def load_start
222
+ file = LucaSupport::Config::Pjdir + 'start.tsv'
223
+ {}.tap do |dic|
224
+ load_tsv(file) do |row|
225
+ dic[row[0]] = row[2].to_i if ! row[2].nil?
226
+ end
227
+ end
228
+ end
229
+
230
+ def self.calc_diff(num, code)
231
+ amount = /\./.match(num.to_s) ? BigDecimal(num) : num.to_i
232
+ amount * pn_debit(code.to_s)
233
+ end
234
+
235
+ def self.pn_debit(code)
236
+ case code
237
+ when /^[0-4BCEGH]/
238
+ 1
239
+ when /^[5-9ADF]/
240
+ -1
241
+ else
242
+ nil
243
+ end
244
+ end
245
+
246
+ def dict
247
+ LucaBook::Dict::Data
248
+ end
249
+ end
250
+ end