lucarecord 0.2.21 → 0.2.26

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: 94f10c954f77059dc814f1aac38335ca0e1597d0db8e7e754a93f6a67714cce1
4
- data.tar.gz: fd0e684124f7683e49192b231fd8f245e09c1fdd281f88fac7dcc309f92713bf
3
+ metadata.gz: 526cf95ca548f5f2b4617f0af76337fa1d9a05896b212a4468f2d68db4b3ab03
4
+ data.tar.gz: 203299e63b9835ca1da312059df927cc8d65d81bea25d299fd08cf1d5afca0fa
5
5
  SHA512:
6
- metadata.gz: bfd7fdcc8f63dc889fcd51ae8e6e9d8a842a2b87ac5e7c7235e6c1ca9aa5f3886d86f8af71de0bf5e379a428b4c9fa15a2f300118427849da46124fcf4ee7532
7
- data.tar.gz: 7e39c7ada92fbc48aad6492362d9f22f8cfa2c3fa109183410d5de9f64641b8d566c206d165026a92f48dd2aa6b2aa310f69a1ff1d7fefc43f48226b5d2065e5
6
+ metadata.gz: d53ff1db376c14b4b39f2975dc70ec0cb74a04dc25f7b17da193c532b819f76614399ad019e4101d73acde439bfdef7a0cc6773a8bacda88b0bc22dedeb8c5e8
7
+ data.tar.gz: 4feed97a4a7b5f7505f6aee51a71c7301606387f3ce2c1bf7086edf1c3bb5e3ceab42b43e7a5daa2576e94ba4ae8e054a3d5ec88d735b5ff47c903ee0a31290a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## LucaRecord 0.2.26
2
+
3
+ * Support #dig / #search for TSV dictionary
4
+ * Fix: shorten n-gram split factor on search word length < specified factor
5
+
6
+ ## LucaRecord 0.2.25
7
+
8
+ * Implement `dir_digest()` for data validation.
9
+ * support defunct without effective history record
10
+
11
+ ## LucaRecord 0.2.24
12
+
13
+ * Digit delimiter for `delimit_num` can be customized through `thousands_separator` and `decimal_separator` in config.yml.
14
+ * Const `CONFIG` and `PJDIR` is defined at `LucaRecord::Base`.
15
+ * add `LucaSupport::Code.keys_stringify()`
16
+
17
+ ## LucaRecord 0.2.23
18
+
19
+ * Enhance Dictionary, supporting extensible options.
20
+
21
+ ## LucaRecord 0.2.22
22
+
23
+ * add `LucaSupport::View.nushell()`, render nushell table directly.
24
+
1
25
  ## LucaRecord 0.2.21
2
26
 
3
27
  * Enhance `LucaSupport::Code.delimit_num()`. Handle with BigDecimal, decimal length & delmiter customization.
@@ -6,6 +6,8 @@ require 'luca_support'
6
6
 
7
7
  module LucaRecord
8
8
  class Base
9
+ CONFIG = LucaSupport::CONFIG
10
+ PJDIR = LucaSupport::PJDIR
9
11
  include LucaRecord::IO
10
12
  include LucaSupport::View
11
13
  end
@@ -6,7 +6,6 @@ require 'yaml'
6
6
  require 'pathname'
7
7
  require 'luca_support'
8
8
 
9
- #
10
9
  # Low level API
11
10
  #
12
11
  module LucaRecord
@@ -14,69 +13,49 @@ module LucaRecord
14
13
  include LucaSupport::Code
15
14
 
16
15
  def initialize(file = @filename)
17
- #@path = file
18
16
  @path = self.class.dict_path(file)
19
17
  set_driver
20
18
  end
21
19
 
22
- def search(word, default_word = nil)
23
- res = max_score_code(word.gsub(/[[:space:]]/, ''))
24
- if res[1] > 0.4
25
- res[0]
20
+ # Search code with n-gram word.
21
+ # If dictionary has Hash or Array, it returns [label, options].
22
+ #
23
+ def search(word, default_word = nil, main_key: 'label', options: nil)
24
+ definitions_lazyload
25
+ res, score = max_score_code(word.gsub(/[[:space:]]/, ''))
26
+ return default_word if score < 0.4
27
+
28
+ case res
29
+ when Hash
30
+ hash2multiassign(res, main_key, options: options)
31
+ when Array
32
+ res.map { |item| hash2multiassign(item, main_key, options: options) }
26
33
  else
27
- default_word
34
+ res
28
35
  end
29
36
  end
30
37
 
38
+ # Search with unique code.
31
39
  #
32
- # Column number settings for CSV/TSV convert
33
- #
34
- # :label
35
- # for double entry data
36
- # :counter_label
37
- # must be specified with label
38
- # :debit_label
39
- # for double entry data
40
- # * debit_value
41
- # :credit_label
42
- # for double entry data
43
- # * credit_value
44
- # :note
45
- # can be the same column as another label
46
- #
47
- # :encoding
48
- # file encoding
40
+ def dig(*args)
41
+ @data.dig(*args)
42
+ end
43
+
44
+ # Separate main item from other options.
45
+ # If options specified as Array of string, it works as safe list filter.
49
46
  #
50
- def csv_config
51
- {}.tap do |config|
52
- if @config.dig('label')
53
- config[:label] = @config['label'].to_i
54
- if @config.dig('counter_label')
55
- config[:counter_label] = @config['counter_label']
56
- config[:type] = 'single'
57
- end
58
- elsif @config.dig('debit_label')
59
- config[:debit_label] = @config['debit_label'].to_i
60
- if @config.dig('credit_label')
61
- config[:credit_label] = @config['credit_label'].to_i
62
- config[:type] = 'double'
63
- end
47
+ def hash2multiassign(obj, main_key = 'label', options: nil)
48
+ options = {}.tap do |opt|
49
+ obj.map do |k, v|
50
+ next if k == main_key
51
+ next if !options.nil? && !options.include?(k)
52
+
53
+ opt[k.to_sym] = v
64
54
  end
65
- config[:type] ||= 'invalid'
66
- config[:debit_value] = @config['debit_value'].to_i if @config.dig('debit_value')
67
- config[:credit_value] = @config['credit_value'].to_i if @config.dig('credit_value')
68
- config[:note] = @config['note'] if @config.dig('note')
69
- config[:encoding] = @config['encoding'] if @config.dig('encoding')
70
-
71
- config[:year] = @config['year'] if @config.dig('year')
72
- config[:month] = @config['month'] if @config.dig('month')
73
- config[:day] = @config['day'] if @config.dig('day')
74
- config[:default_debit] = @config['default_debit'] if @config.dig('default_debit')
75
- config[:default_credit] = @config['default_credit'] if @config.dig('default_credit')
76
55
  end
56
+ [obj[main_key], options.compact]
77
57
  end
78
58
 
79
- #
80
59
  # Load CSV with config options
81
60
  #
82
61
  def load_csv(path)
@@ -85,7 +64,6 @@ module LucaRecord
85
64
  end
86
65
  end
87
66
 
88
- #
89
67
  # load dictionary data
90
68
  #
91
69
  def self.load(file = @filename)
@@ -99,7 +77,6 @@ module LucaRecord
99
77
  end
100
78
  end
101
79
 
102
- #
103
80
  # generate dictionary from TSV file. Minimum assumption is as bellows:
104
81
  # 1st row is converted symbol.
105
82
  #
@@ -122,16 +99,33 @@ module LucaRecord
122
99
  end
123
100
  end
124
101
 
102
+ def self.validate(filename, target_key = :label)
103
+ errors = load(filename).map { |k, v| v[target_key].nil? ? k : nil }.compact
104
+ if errors.empty?
105
+ puts 'No error detected.'
106
+ nil
107
+ else
108
+ puts "Key #{errors.join(', ')} has nil #{target_key}."
109
+ errors.count
110
+ end
111
+ end
112
+
125
113
  private
126
114
 
127
115
  def set_driver
128
- input = self.class.load(@path)
129
- @config = input['config']
130
- @definitions = input['definitions']
116
+ @data = self.class.load(@path)
117
+ @config = @data['config']
118
+ @definitions = @data['definitions']
119
+ end
120
+
121
+ # Build Reverse dictionary for TSV data
122
+ #
123
+ def definitions_lazyload
124
+ @definitions ||= @data.each_with_object({}) { |(k, entry), h| h[entry[:label]] = k if entry[:label] }
131
125
  end
132
126
 
133
127
  def self.dict_path(filename)
134
- Pathname(LucaSupport::Config::Pjdir) / 'dict' / filename
128
+ Pathname(LucaSupport::PJDIR) / 'dict' / filename
135
129
  end
136
130
 
137
131
  def self.reverse(dict)
@@ -140,7 +134,7 @@ module LucaRecord
140
134
 
141
135
  def max_score_code(str)
142
136
  res = @definitions.map do |k, v|
143
- [v, LucaSupport.match_score(str, k, 3)]
137
+ [v, match_score(str, k, 2)]
144
138
  end
145
139
  res.max { |x, y| x[1] <=> y[1] }
146
140
  end
@@ -62,7 +62,7 @@ module LucaRecord # :nodoc:
62
62
  def term(start_year, start_month, end_year, end_month, code = nil, basedir = @dirname)
63
63
  return enum_for(:term, start_year, start_month, end_year, end_month, code, basedir) unless block_given?
64
64
 
65
- LucaSupport::Code.encode_term(start_year, start_month, end_year, end_month).each do |subdir|
65
+ LucaSupport::Code.encode_term(start_year, start_month, end_year, end_month).each do |subdir|
66
66
  open_records(basedir, subdir, nil, code) do |f, path|
67
67
  if @record_type == 'raw'
68
68
  yield f, path
@@ -243,7 +243,7 @@ module LucaRecord # :nodoc:
243
243
  end
244
244
 
245
245
  # test if having required dirs/files under exec path
246
- def valid_project?(path = LucaSupport::Config::Pjdir)
246
+ def valid_project?(path = LucaSupport::PJDIR)
247
247
  project_dir = Pathname(path)
248
248
  FileTest.file?((project_dir + 'config.yml').to_s) and FileTest.directory?( (project_dir + 'data').to_s)
249
249
  end
@@ -252,6 +252,17 @@ module LucaRecord # :nodoc:
252
252
  LucaSupport::Code.encode_txid(new_record_no(basedir, date_obj))
253
253
  end
254
254
 
255
+ # Calculate md5sum under specific month directory.
256
+ #
257
+ def dir_digest(year, month, basedir = @dirname)
258
+ subdir = year.to_s + LucaSupport::Code.encode_month(month)
259
+ digest = String.new
260
+ open_records(basedir, subdir).each do |f, path|
261
+ digest = update_digest(digest, f.read, path[1])
262
+ end
263
+ digest
264
+ end
265
+
255
266
  private
256
267
 
257
268
  # define new transaction ID & write data at once
@@ -300,6 +311,14 @@ module LucaRecord # :nodoc:
300
311
  File.open(subpath, mode) { |f| yield(f, id_set) }
301
312
  end
302
313
  end
314
+
315
+ # Calculate md5sum with original digest, file content and filename(optional).
316
+ #
317
+ def update_digest(digest, str, filename = nil)
318
+ str = filename.nil? ? str : filename + str
319
+ content = Digest::MD5.new.update(str).hexdigest
320
+ Digest::MD5.new.update(digest + content).hexdigest
321
+ end
303
322
  end
304
323
 
305
324
  # git object like structure
@@ -339,12 +358,14 @@ module LucaRecord # :nodoc:
339
358
  # If specific decode is needed, override this method in each class.
340
359
  #
341
360
  def load_data(io, path = nil)
342
- case @record_type
343
- when 'raw'
344
- # TODO: raw may be unneeded in favor of override
345
- io
346
- when 'json'
347
- # TODO: implement JSON parse
361
+ if @record_type
362
+ case @record_type
363
+ when 'raw'
364
+ # TODO: raw may be unneeded in favor of override
365
+ io
366
+ when 'json'
367
+ # TODO: implement JSON parse
368
+ end
348
369
  else
349
370
  LucaSupport::Code.decimalize(YAML.load(io.read)).tap { |obj| validate_keys(obj) }
350
371
  end
@@ -362,7 +383,7 @@ module LucaRecord # :nodoc:
362
383
 
363
384
  # TODO: replace with data_dir method
364
385
  def abs_path(base_dir)
365
- Pathname(LucaSupport::Config::Pjdir) / 'data' / base_dir
386
+ Pathname(LucaSupport::PJDIR) / 'data' / base_dir
366
387
  end
367
388
 
368
389
  # true when file doesn't have record on code
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaRecord
4
- VERSION = '0.2.21'
4
+ VERSION = '0.2.26'
5
5
  end
data/lib/luca_support.rb CHANGED
@@ -6,15 +6,4 @@ module LucaSupport
6
6
  autoload :Config, 'luca_support/config'
7
7
  autoload :Mail, 'luca_support/mail'
8
8
  autoload :View, 'luca_support/view'
9
-
10
- def self.match_score(a, b, n = 2)
11
- v_a = to_ngram(a, n)
12
- v_b = to_ngram(b, n)
13
-
14
- v_a.map { |item| v_b.include?(item) ? 1 : 0 }.sum / v_a.length.to_f
15
- end
16
-
17
- def self.to_ngram(str, n = 2)
18
- str.each_char.each_cons(n).map(&:join)
19
- end
20
9
  end
@@ -54,21 +54,24 @@ module LucaSupport
54
54
  def delimit_num(num, decimal: nil, delimiter: nil)
55
55
  return nil if num.nil?
56
56
 
57
- decimal ||= LucaSupport::Config::DECIMAL_NUM
58
- str = case num
59
- when BigDecimal
60
- if decimal == 0
61
- num.floor.to_s.reverse.gsub!(/(\d{3})(?=\d)/, '\1 ').reverse!
62
- else
63
- fragments = num.floor(decimal).to_s('F').split('.')
64
- fragments[0].reverse!.gsub!(/(\d{3})(?=\d)/, '\1 ').reverse!
65
- fragments[1].gsub!(/(\d{3})(?=\d)/, '\1 ')
66
- fragments.join('.')
67
- end
68
- else
69
- num.to_s.reverse.gsub!(/(\d{3})(?=\d)/, '\1 ').reverse!
70
- end
71
- delimiter.nil? ? str : str.gsub!(/\s/, delimiter)
57
+ decimal ||= LucaSupport::CONFIG['decimal_num']
58
+ delimiter ||= LucaSupport::CONFIG['thousands_separator']
59
+ case num
60
+ when BigDecimal
61
+ if decimal == 0
62
+ num.floor.to_s.reverse!.gsub(/(\d{3})(?=\d)/, '\1 ').reverse!
63
+ .gsub(/\s/, delimiter)
64
+ else
65
+ fragments = num.floor(decimal).to_s('F').split('.')
66
+ fragments[0].reverse!.gsub!(/(\d{3})(?=\d)/, '\1 ')
67
+ fragments[0].reverse!.gsub!(/\s/, delimiter)
68
+ fragments[1].gsub!(/(\d{3})(?=\d)/, '\1 ')
69
+ fragments.join(LucaSupport::CONFIG['decimal_separator'])
70
+ end
71
+ else
72
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1 ').reverse!
73
+ .gsub(/\s/, delimiter)
74
+ end
72
75
  end
73
76
 
74
77
  # encode directory name from year and month.
@@ -101,7 +104,7 @@ module LucaSupport
101
104
  (start_year..end_year).to_a.map do |y|
102
105
  g1 = y == start_year ? encode_month(start_month) : encode_month(1)
103
106
  g2 = y == end_year ? encode_month(end_month) : encode_month(12)
104
- "#{y}[#{g1}-#{g2}]"
107
+ g1 == g2 ? "#{y}#{g1}" : "#{y}[#{g1}-#{g2}]"
105
108
  end
106
109
  end
107
110
 
@@ -114,6 +117,32 @@ module LucaSupport
114
117
  Digest::SHA1.hexdigest(SecureRandom.uuid)
115
118
  end
116
119
 
120
+ # Convert Hash keys to string recursively.
121
+ # Required for YAML compatibility.
122
+ #
123
+ def keys_stringify(dat)
124
+ case dat
125
+ when Array
126
+ dat.map { |d| keys_stringify(d) }
127
+ when Hash
128
+ dat.map { |k, v| [k.to_s, keys_stringify(v)] }.to_h
129
+ else
130
+ dat
131
+ end
132
+ end
133
+
134
+ def match_score(a, b, n = 2)
135
+ split_factor = [a.length, b.length, n].min
136
+ v_a = to_ngram(a, split_factor)
137
+ v_b = to_ngram(b, split_factor)
138
+
139
+ v_a.map { |item| v_b.include?(item) ? 1 : 0 }.sum / v_a.length.to_f
140
+ end
141
+
142
+ def to_ngram(str, n = 2)
143
+ str.each_char.each_cons(n).map(&:join)
144
+ end
145
+
117
146
  def decimalize(obj)
118
147
  case obj.class.name
119
148
  when 'Array'
@@ -131,15 +160,19 @@ module LucaSupport
131
160
  end
132
161
  end
133
162
 
134
- def readable(obj, len = LucaSupport::Config::DECIMAL_NUM)
135
- case obj.class.name
136
- when 'Array'
163
+ def readable(obj, len = LucaSupport::CONFIG['decimal_num'])
164
+ case obj
165
+ when Array
137
166
  obj.map { |i| readable(i) }
138
- when 'Hash'
167
+ when Hash
139
168
  obj.inject({}) { |h, (k, v)| h[k] = readable(v); h }
140
- when 'BigDecimal'
141
- parts = obj.round(len).to_s('F').split('.')
142
- len < 1 ? parts.first : "#{parts[0]}.#{parts[1][0, len]}"
169
+ when BigDecimal
170
+ if len == 0
171
+ obj.round # Integer is precise
172
+ else
173
+ parts = obj.round(len).to_s('F').split('.')
174
+ "#{parts[0]}.#{parts[1][0, len]}"
175
+ end
143
176
  else
144
177
  obj
145
178
  end
@@ -155,7 +188,6 @@ module LucaSupport
155
188
  end
156
189
  end
157
190
 
158
- #
159
191
  # return current value with effective/defunct on target @date
160
192
  # For multiple attribues, return hash on other than 'val'. Examples are as bellows:
161
193
  #
@@ -168,18 +200,23 @@ module LucaSupport
168
200
  # point: 1000
169
201
  # => { 'effective' => 2020-1-1, 'rank' => 5, 'point' => 1000 }
170
202
  #
203
+ # - defunct: 2020-1-1
204
+ # val: 3000
205
+ # => nil
206
+ #
171
207
  def take_current(dat, item)
172
- target = dat.dig(item)
173
- if target.is_a?(Array) && target.map(&:keys).flatten.include?('effective')
174
- latest = target
175
- .filter { |a| Date.parse(a.dig('effective').to_s) < @date }
176
- .max { |a, b| Date.parse(a.dig('effective').to_s) <=> Date.parse(b.dig('effective').to_s) }
177
- return nil if !latest.dig('defunct').nil? && Date.parse(latest.dig('defunct').to_s) < @date
178
-
179
- latest.dig('val') || latest
180
- else
181
- target
182
- end
208
+ target = dat&.dig(item)
209
+ return target unless target.is_a?(Array)
210
+
211
+ keys = target.map(&:keys).flatten
212
+ return target if !keys.include?('effective') && !keys.include?('defunct')
213
+
214
+ latest = target
215
+ .reject { |a| a['defunct'] && Date.parse(a['defunct'].to_s) < @date }
216
+ .filter { |a| a['effective'] && Date.parse(a['effective'].to_s) < @date }
217
+ .max { |a, b| Date.parse(a['effective'].to_s) <=> Date.parse(b['effective'].to_s) }
218
+
219
+ latest&.dig('val') || latest
183
220
  end
184
221
 
185
222
  def has_status?(dat, status)
@@ -8,19 +8,15 @@ require 'yaml'
8
8
  module LucaSupport
9
9
  PJDIR = ENV['LUCA_TEST_DIR'] || Dir.pwd.freeze
10
10
  CONFIG = begin
11
- YAML.load_file(Pathname(PJDIR) / 'config.yml', **{})
11
+ {
12
+ 'decimal_separator' => '.',
13
+ 'thousands_separator' => ','
14
+ }.merge(YAML.load_file(Pathname(PJDIR) / 'config.yml'))
12
15
  rescue Errno::ENOENT
13
- {}
16
+ {
17
+ 'decimal_separator' => '.',
18
+ 'thousands_separator' => ','
19
+ }
14
20
  end
15
-
16
- module Config
17
- # Project top directory.
18
- Pjdir = ENV['LUCA_TEST_DIR'] || Dir.pwd.freeze
19
- if File.exist?(Pathname(Pjdir) / 'config.yml')
20
- # DECIMAL_NUM = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['decimal_number']
21
- COUNTRY = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['country']
22
- DECIMAL_NUM ||= 0 if COUNTRY == 'jp'
23
- end
24
- DECIMAL_NUM ||= 2
25
- end
21
+ CONFIG['decimal_num'] ||= CONFIG['country'] == 'jp' ? 0 : 2
26
22
  end
@@ -34,13 +34,22 @@ module LucaSupport
34
34
  out
35
35
  end
36
36
 
37
+ # Search existing file and return path under:
38
+ # 1. 'templates/' in Project directory that data resides
39
+ # 2. 'templates/' in Library directory that calls LucaSupport::View#search_template
40
+ #
37
41
  def search_template(file, dir = 'templates')
38
42
  # TODO: load config
39
- [@pjdir, lib_path].each do |base|
43
+ [LucaSupport::PJDIR, lib_path].each do |base|
40
44
  path = (Pathname(base) / dir / file)
41
45
  return path.to_path if path.file?
42
46
  end
43
47
  nil
44
48
  end
49
+
50
+ def nushell(yml)
51
+ require 'open3'
52
+ Open3.pipeline_w(%(nu -c 'cat - | from yaml')) { |stdin| stdin.puts yml }
53
+ end
45
54
  end
46
55
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lucarecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.21
4
+ version: 0.2.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuma Takahiro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-21 00:00:00.000000000 Z
11
+ date: 2021-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  requirements: []
112
- rubygems_version: 3.1.2
112
+ rubygems_version: 3.2.3
113
113
  signing_key:
114
114
  specification_version: 4
115
115
  summary: ERP File operation framework