lucarecord 0.2.21 → 0.2.26

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: 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