lucarecord 0.2.20 → 0.2.25

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: e2644c50f67e64dfe2502400e04eddb6278a314d00f82eb5a51a6489797f0b48
4
- data.tar.gz: c6d286442884c6675f18340ad2eb3322cd76515992530cbf4fc8536b66a6bbad
3
+ metadata.gz: 1982b60b00eddc3d201368a2f436835fa51c6ba58538de3fcd0ce962c4529246
4
+ data.tar.gz: 894ef1778f5a8be1f2091575ec86e84ceb61d0d158ba0cb4b8bc297c7070593a
5
5
  SHA512:
6
- metadata.gz: 0122e43160066ce33dad8478fc99cd4bc639abf232788852ebce74328f664f413756ea91c73a6f6372f4dce9648a9c6776dd7f7ee1f0971930b71f0816a2841b
7
- data.tar.gz: '09c51366863747bfea0cdf829d0829be6d084b79cbc729c3cb51ecea7f07196ca2c7ff0eea8b52d834d0af476de9cb953ef50d9e1b7e620de0c6f5d51c64f158'
6
+ metadata.gz: 396bfbd54619361753b576fdda9f715b115ab274df10dce07abb1a5f180ed161946c72bae02c4926ff05d828e20af6920c689a412c71234c5baef072f870b528
7
+ data.tar.gz: 633d8920f547c893b2b1ac6645bdd108439d3568cfa3ec738c70ec61f1f658374e598bce0144a5a85142a15e4353d102e35b48ea84988f4dae06b0658a3952f7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## LucaRecord 0.2.25
2
+
3
+ * Implement `dir_digest()` for data validation.
4
+ * support defunct without effective history record
5
+
6
+ ## LucaRecord 0.2.24
7
+
8
+ * Digit delimiter for `delimit_num` can be customized through `thousands_separator` and `decimal_separator` in config.yml.
9
+ * Const `CONFIG` and `PJDIR` is defined at `LucaRecord::Base`.
10
+ * add `LucaSupport::Code.keys_stringify()`
11
+
12
+ ## LucaRecord 0.2.23
13
+
14
+ * Enhance Dictionary, supporting extensible options.
15
+
16
+ ## LucaRecord 0.2.22
17
+
18
+ * add `LucaSupport::View.nushell()`, render nushell table directly.
19
+
20
+ ## LucaRecord 0.2.21
21
+
22
+ * Enhance `LucaSupport::Code.delimit_num()`. Handle with BigDecimal, decimal length & delmiter customization.
23
+
1
24
  ## LucaRecord 0.2.20
2
25
 
3
26
  * UUID completion framework on prefix match
@@ -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,66 +13,40 @@ 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 word with n-gram.
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
+ res, score = max_score_code(word.gsub(/[[:space:]]/, ''))
25
+ return default_word if score < 0.4
26
+
27
+ case res
28
+ when Hash
29
+ hash2multiassign(res, main_key, options: options)
30
+ when Array
31
+ res.map { |item| hash2multiassign(item, main_key, options: options) }
26
32
  else
27
- default_word
33
+ res
28
34
  end
29
35
  end
30
36
 
37
+ # Separate main item from other options.
38
+ # If options specified as Array of string, it works as safe list filter.
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
49
- #
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
40
+ def hash2multiassign(obj, main_key = 'label', options: nil)
41
+ options = {}.tap do |opt|
42
+ obj.map do |k, v|
43
+ next if k == main_key
44
+ next if !options.nil? && !options.include?(k)
45
+
46
+ opt[k.to_sym] = v
64
47
  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
48
  end
49
+ [obj[main_key], options.compact]
77
50
  end
78
51
 
79
52
  #
@@ -122,6 +95,17 @@ module LucaRecord
122
95
  end
123
96
  end
124
97
 
98
+ def self.validate(filename, target_key = :label)
99
+ errors = load(filename).map { |k, v| v[target_key].nil? ? k : nil }.compact
100
+ if errors.empty?
101
+ puts 'No error detected.'
102
+ nil
103
+ else
104
+ "Key #{errors.join(', ')} has nil #{target_key}."
105
+ errors.count
106
+ end
107
+ end
108
+
125
109
  private
126
110
 
127
111
  def set_driver
@@ -131,7 +115,7 @@ module LucaRecord
131
115
  end
132
116
 
133
117
  def self.dict_path(filename)
134
- Pathname(LucaSupport::Config::Pjdir) / 'dict' / filename
118
+ Pathname(LucaSupport::PJDIR) / 'dict' / filename
135
119
  end
136
120
 
137
121
  def self.reverse(dict)
@@ -140,7 +124,7 @@ module LucaRecord
140
124
 
141
125
  def max_score_code(str)
142
126
  res = @definitions.map do |k, v|
143
- [v, LucaSupport.match_score(str, k, 3)]
127
+ [v, match_score(str, k, 3)]
144
128
  end
145
129
  res.max { |x, y| x[1] <=> y[1] }
146
130
  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
@@ -339,12 +350,14 @@ module LucaRecord # :nodoc:
339
350
  # If specific decode is needed, override this method in each class.
340
351
  #
341
352
  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
353
+ if @record_type
354
+ case @record_type
355
+ when 'raw'
356
+ # TODO: raw may be unneeded in favor of override
357
+ io
358
+ when 'json'
359
+ # TODO: implement JSON parse
360
+ end
348
361
  else
349
362
  LucaSupport::Code.decimalize(YAML.load(io.read)).tap { |obj| validate_keys(obj) }
350
363
  end
@@ -362,7 +375,7 @@ module LucaRecord # :nodoc:
362
375
 
363
376
  # TODO: replace with data_dir method
364
377
  def abs_path(base_dir)
365
- Pathname(LucaSupport::Config::Pjdir) / 'data' / base_dir
378
+ Pathname(LucaSupport::PJDIR) / 'data' / base_dir
366
379
  end
367
380
 
368
381
  # true when file doesn't have record on code
@@ -420,5 +433,13 @@ module LucaRecord # :nodoc:
420
433
  {}
421
434
  end
422
435
  end
436
+
437
+ # Calculate md5sum with original digest, file content and filename(optional).
438
+ #
439
+ def update_digest(digest, str, filename = nil)
440
+ str = filename.nil? ? str : filename + str
441
+ content = Digest::MD5.new.update(str).hexdigest
442
+ Digest::MD5.new.update(digest + content).hexdigest
443
+ end
423
444
  end
424
445
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaRecord
4
- VERSION = '0.2.20'
4
+ VERSION = '0.2.25'
5
5
  end
data/lib/luca_support.rb CHANGED
@@ -2,18 +2,8 @@
2
2
 
3
3
  module LucaSupport
4
4
  autoload :Code, 'luca_support/code'
5
+ autoload :CONFIG, 'luca_support/config'
5
6
  autoload :Config, 'luca_support/config'
6
7
  autoload :Mail, 'luca_support/mail'
7
8
  autoload :View, 'luca_support/view'
8
-
9
- def self.match_score(a, b, n = 2)
10
- v_a = to_ngram(a, n)
11
- v_b = to_ngram(b, n)
12
-
13
- v_a.map { |item| v_b.include?(item) ? 1 : 0 }.sum / v_a.length.to_f
14
- end
15
-
16
- def self.to_ngram(str, n = 2)
17
- str.each_char.each_cons(n).map(&:join)
18
- end
19
9
  end
@@ -48,8 +48,30 @@ module LucaSupport
48
48
  '0123456789ABCDEFGHIJKLMNOPQRSTUV'.index(char)
49
49
  end
50
50
 
51
- def delimit_num(num)
52
- num.to_s.reverse.gsub!(/(\d{3})(?=\d)/, '\1,').reverse!
51
+ # Format number in 3-digit-group.
52
+ # Long decimal is just ommitted with floor().
53
+ #
54
+ def delimit_num(num, decimal: nil, delimiter: nil)
55
+ return nil if num.nil?
56
+
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
53
75
  end
54
76
 
55
77
  # encode directory name from year and month.
@@ -82,7 +104,7 @@ module LucaSupport
82
104
  (start_year..end_year).to_a.map do |y|
83
105
  g1 = y == start_year ? encode_month(start_month) : encode_month(1)
84
106
  g2 = y == end_year ? encode_month(end_month) : encode_month(12)
85
- "#{y}[#{g1}-#{g2}]"
107
+ g1 == g2 ? "#{y}#{g1}" : "#{y}[#{g1}-#{g2}]"
86
108
  end
87
109
  end
88
110
 
@@ -95,6 +117,31 @@ module LucaSupport
95
117
  Digest::SHA1.hexdigest(SecureRandom.uuid)
96
118
  end
97
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
+ v_a = to_ngram(a, n)
136
+ v_b = to_ngram(b, n)
137
+
138
+ v_a.map { |item| v_b.include?(item) ? 1 : 0 }.sum / v_a.length.to_f
139
+ end
140
+
141
+ def to_ngram(str, n = 2)
142
+ str.each_char.each_cons(n).map(&:join)
143
+ end
144
+
98
145
  def decimalize(obj)
99
146
  case obj.class.name
100
147
  when 'Array'
@@ -112,15 +159,19 @@ module LucaSupport
112
159
  end
113
160
  end
114
161
 
115
- def readable(obj, len = LucaSupport::Config::DECIMAL_NUM)
116
- case obj.class.name
117
- when 'Array'
162
+ def readable(obj, len = LucaSupport::CONFIG['decimal_num'])
163
+ case obj
164
+ when Array
118
165
  obj.map { |i| readable(i) }
119
- when 'Hash'
166
+ when Hash
120
167
  obj.inject({}) { |h, (k, v)| h[k] = readable(v); h }
121
- when 'BigDecimal'
122
- parts = obj.round(len).to_s('F').split('.')
123
- len < 1 ? parts.first : "#{parts[0]}.#{parts[1][0, len]}"
168
+ when BigDecimal
169
+ if len == 0
170
+ obj.round # Integer is precise
171
+ else
172
+ parts = obj.round(len).to_s('F').split('.')
173
+ "#{parts[0]}.#{parts[1][0, len]}"
174
+ end
124
175
  else
125
176
  obj
126
177
  end
@@ -136,7 +187,6 @@ module LucaSupport
136
187
  end
137
188
  end
138
189
 
139
- #
140
190
  # return current value with effective/defunct on target @date
141
191
  # For multiple attribues, return hash on other than 'val'. Examples are as bellows:
142
192
  #
@@ -149,18 +199,23 @@ module LucaSupport
149
199
  # point: 1000
150
200
  # => { 'effective' => 2020-1-1, 'rank' => 5, 'point' => 1000 }
151
201
  #
202
+ # - defunct: 2020-1-1
203
+ # val: 3000
204
+ # => nil
205
+ #
152
206
  def take_current(dat, item)
153
- target = dat.dig(item)
154
- if target.is_a?(Array) && target.map(&:keys).flatten.include?('effective')
155
- latest = target
156
- .filter { |a| Date.parse(a.dig('effective').to_s) < @date }
157
- .max { |a, b| Date.parse(a.dig('effective').to_s) <=> Date.parse(b.dig('effective').to_s) }
158
- return nil if !latest.dig('defunct').nil? && Date.parse(latest.dig('defunct').to_s) < @date
159
-
160
- latest.dig('val') || latest
161
- else
162
- target
163
- end
207
+ target = dat&.dig(item)
208
+ return target unless target.is_a?(Array)
209
+
210
+ keys = target.map(&:keys).flatten
211
+ return target if !keys.include?('effective') && !keys.include?('defunct')
212
+
213
+ latest = target
214
+ .reject { |a| a['defunct'] && Date.parse(a['defunct'].to_s) < @date }
215
+ .filter { |a| a['effective'] && Date.parse(a['effective'].to_s) < @date }
216
+ .max { |a, b| Date.parse(a['effective'].to_s) <=> Date.parse(b['effective'].to_s) }
217
+
218
+ latest&.dig('val') || latest
164
219
  end
165
220
 
166
221
  def has_status?(dat, status)
@@ -3,18 +3,20 @@
3
3
  require 'pathname'
4
4
  require 'yaml'
5
5
 
6
- #
7
6
  # startup config
8
7
  #
9
8
  module LucaSupport
10
- module Config
11
- # Project top directory.
12
- Pjdir = ENV['LUCA_TEST_DIR'] || Dir.pwd.freeze
13
- if File.exist?(Pathname(Pjdir) / 'config.yml')
14
- # DECIMAL_NUM = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['decimal_number']
15
- COUNTRY = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['country']
16
- DECIMAL_NUM ||= 0 if COUNTRY == 'jp'
17
- end
18
- DECIMAL_NUM ||= 2
19
- end
9
+ PJDIR = ENV['LUCA_TEST_DIR'] || Dir.pwd.freeze
10
+ CONFIG = begin
11
+ {
12
+ 'decimal_separator' => '.',
13
+ 'thousands_separator' => ','
14
+ }.merge(YAML.load_file(Pathname(PJDIR) / 'config.yml'))
15
+ rescue Errno::ENOENT
16
+ {
17
+ 'decimal_separator' => '.',
18
+ 'thousands_separator' => ','
19
+ }
20
+ end
21
+ CONFIG['decimal_num'] ||= CONFIG['country'] == 'jp' ? 0 : 2
20
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.20
4
+ version: 0.2.25
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-19 00:00:00.000000000 Z
11
+ date: 2021-03-13 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