lucarecord 0.2.20 → 0.2.25

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