lucarecord 0.2.18 → 0.2.23

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: 63ac71426bdf4eb871884b8b28c5aa363db9d51ef3ca0d84ab92a3fcf127ecde
4
- data.tar.gz: 2bc0d6ecde974a2f9ca9c901af5ac714c909f40dd84986746948065a4efb4ed7
3
+ metadata.gz: a408c56d2c0f47238bc8c17e4135867e1be31b9897f940370a9bc8e87f8d014e
4
+ data.tar.gz: 66fead2c3441f8b019f8d805fe564dbf9a9121abec424ea5d5482277d7bcc3ad
5
5
  SHA512:
6
- metadata.gz: 1f9cf84bfb09080f6f3e8cf15c8fb3cea0d6eca9d69acc10c23cd9b6a681daae9ab641181a928f438c870a438a0c4525c61bfad6a1832a2d148b7a3d37b3d6d3
7
- data.tar.gz: be35bd6b0a993b5fde415ba5e1a7b59228106c443594b164fa5df26f1eb284ae0556277b99b25f7697283ac194f6c25ffa7d7d8d3e1bfe2e22b128130f3f9cec
6
+ metadata.gz: a6f3d2e4773bc6c29c657a7fbe5864893efa39ec739ae1df4eece68fa82907bf99feb72f5a43888b59e39bc9f9a93f023fa4c0243e2429d3c04ba2c2f09d2e20
7
+ data.tar.gz: 7fd98c339f4b81430dc961b009233e6de65228f6df83d361b81f4040857045740f0d2d18363e29bd2579be6b3d19a5a09cca37f9e39cfc1a8703212c32ba4e78
@@ -1,3 +1,24 @@
1
+ ## LucaRecord 0.2.23
2
+
3
+ * Enhance Dictionary, supporting extensible options.
4
+
5
+ ## LucaRecord 0.2.22
6
+
7
+ * add `LucaSupport::View.nushell()`, render nushell table directly.
8
+
9
+ ## LucaRecord 0.2.21
10
+
11
+ * Enhance `LucaSupport::Code.delimit_num()`. Handle with BigDecimal, decimal length & delmiter customization.
12
+
13
+ ## LucaRecord 0.2.20
14
+
15
+ * UUID completion framework on prefix match
16
+
17
+ ## LucaRecord 0.2.19
18
+
19
+ * `LucaSupport::Code.decode_id()`
20
+ * `LucaSupport::Code.encode_term()` for multiple months search. Old `scan_term()` removed.
21
+
1
22
  ## LucaRecord 0.2.18
2
23
 
3
24
  * `find()`, `create()`, `save()` now supports both of uuid / historical records. If specified `date:` keyword option to `create()`, then generate historical record. `find()`, `save()` identifies with 'id' attribute.
@@ -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,64 +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)
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'].to_i 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
48
  end
49
+ [obj[main_key], options.compact]
75
50
  end
76
51
 
77
52
  #
@@ -120,6 +95,17 @@ module LucaRecord
120
95
  end
121
96
  end
122
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
+
123
109
  private
124
110
 
125
111
  def set_driver
@@ -138,7 +124,7 @@ module LucaRecord
138
124
 
139
125
  def max_score_code(str)
140
126
  res = @definitions.map do |k, v|
141
- [v, LucaSupport.match_score(str, k, 3)]
127
+ [v, match_score(str, k, 3)]
142
128
  end
143
129
  res.max { |x, y| x[1] <=> y[1] }
144
130
  end
@@ -54,8 +54,22 @@ module LucaRecord # :nodoc:
54
54
  def asof(year, month = nil, day = nil, basedir = @dirname)
55
55
  return enum_for(:search, year, month, day, nil, basedir) unless block_given?
56
56
 
57
- search(year, month, day, nil, basedir) do |data, path|
58
- yield data, path
57
+ search(year, month, day, nil, basedir) { |data, path| yield data, path }
58
+ end
59
+
60
+ # scan ranging data on multiple months
61
+ #
62
+ def term(start_year, start_month, end_year, end_month, code = nil, basedir = @dirname)
63
+ return enum_for(:term, start_year, start_month, end_year, end_month, code, basedir) unless block_given?
64
+
65
+ LucaSupport::Code.encode_term(start_year, start_month, end_year, end_month).each do |subdir|
66
+ open_records(basedir, subdir, nil, code) do |f, path|
67
+ if @record_type == 'raw'
68
+ yield f, path
69
+ else
70
+ yield load_data(f, path)
71
+ end
72
+ end
59
73
  end
60
74
  end
61
75
 
@@ -105,6 +119,31 @@ module LucaRecord # :nodoc:
105
119
  end
106
120
  end
107
121
 
122
+ # If multiple ID matched, return short ID and human readable label.
123
+ #
124
+ def id_completion(phrase, label: 'name', basedir: @dirname)
125
+ list = prefix_search(phrase, basedir: basedir)
126
+ case list.length
127
+ when 1
128
+ list
129
+ when 0
130
+ raise 'No match on specified phrase'
131
+ else
132
+ (3..list[0].length).each do |l|
133
+ if list.map { |id| id[0, l] }.uniq.length == list.length
134
+ return list.map { |id| { id: id[0, l], label: find(id).dig(label) } }
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def prefix_search(phrase, basedir: @dirname)
141
+ glob_str = phrase.length <= 3 ? "#{phrase}*/*" : "#{id2path(phrase)}*"
142
+ Dir.chdir(abs_path(basedir)) do
143
+ Dir.glob(glob_str).to_a.map! { |path| path.gsub!('/', '') }
144
+ end
145
+ end
146
+
108
147
  def prepare_dir!(basedir, date_obj)
109
148
  dir_name = (Pathname(basedir) + LucaSupport::Code.encode_dirname(date_obj)).to_s
110
149
  FileUtils.mkdir_p(dir_name) unless Dir.exist?(dir_name)
@@ -148,6 +187,21 @@ module LucaRecord # :nodoc:
148
187
  id
149
188
  end
150
189
 
190
+ # change filename with new code set
191
+ #
192
+ def change_codes(id, new_codes, basedir = @dirname)
193
+ raise 'invalid id' if id.split('/').length != 2
194
+
195
+ newfile = new_codes.empty? ? id : id + '-' + new_codes.join('-')
196
+ Dir.chdir(abs_path(basedir)) do
197
+ origin = Dir.glob("#{id}*")
198
+ raise 'duplicated files' if origin.length != 1
199
+
200
+ File.rename(origin.first, newfile)
201
+ end
202
+ newfile
203
+ end
204
+
151
205
  # ----------------------------------------------------------------
152
206
  # :section: Path Utilities
153
207
  # ----------------------------------------------------------------
@@ -270,6 +324,17 @@ module LucaRecord # :nodoc:
270
324
  end
271
325
  end
272
326
 
327
+ # parse data dir and respond existing months
328
+ #
329
+ def scan_terms(query = nil, base_dir = @dirname)
330
+ pattern = query.nil? ? "*" : "#{query}*"
331
+ Dir.chdir(abs_path(base_dir)) do
332
+ Dir.glob(pattern).select { |dir|
333
+ FileTest.directory?(dir) && /^[0-9]/.match(dir)
334
+ }.sort.map { |str| decode_term(str) }
335
+ end
336
+ end
337
+
273
338
  # Decode basic format.
274
339
  # If specific decode is needed, override this method in each class.
275
340
  #
@@ -281,8 +346,7 @@ module LucaRecord # :nodoc:
281
346
  when 'json'
282
347
  # TODO: implement JSON parse
283
348
  else
284
- YAML.load(io.read).tap { |obj| validate_keys(obj) }
285
- .inject({}) { |h, (k, v)| h[k] = LucaSupport::Code.decimalize(v); h }
349
+ LucaSupport::Code.decimalize(YAML.load(io.read)).tap { |obj| validate_keys(obj) }
286
350
  end
287
351
  end
288
352
 
@@ -348,17 +412,6 @@ module LucaRecord # :nodoc:
348
412
  end
349
413
  end
350
414
 
351
- # parse data dir and respond existing months
352
- #
353
- def scan_terms(base_dir, query = nil)
354
- pattern = query.nil? ? "*" : "#{query}*"
355
- Dir.chdir(base_dir) do
356
- Dir.glob(pattern).select { |dir|
357
- FileTest.directory?(dir) && /^[0-9]/.match(dir)
358
- }.sort.map { |str| decode_term(str) }
359
- end
360
- end
361
-
362
415
  def load_config(path = nil)
363
416
  path = path.to_s
364
417
  if File.exist?(path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaRecord
4
- VERSION = '0.2.18'
4
+ VERSION = '0.2.23'
5
5
  end
@@ -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
@@ -10,6 +10,13 @@ module LucaSupport
10
10
  module Code
11
11
  module_function
12
12
 
13
+ # Parse historical id into Array of date & transaction id.
14
+ #
15
+ def decode_id(id_str)
16
+ m = %r(^(?<year>[0-9]+)(?<month>[A-L])/?(?<day>[0-9A-V])(?<txid>[0-9A-Z]{,3})).match(id_str)
17
+ ["#{m[:year]}-#{decode_month(m[:month])}-#{decode_date(m[:day])}", decode_txid(m[:txid])]
18
+ end
19
+
13
20
  def encode_txid(num)
14
21
  txmap = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
15
22
  l = txmap.length
@@ -41,8 +48,27 @@ module LucaSupport
41
48
  '0123456789ABCDEFGHIJKLMNOPQRSTUV'.index(char)
42
49
  end
43
50
 
44
- def delimit_num(num)
45
- 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
+ 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)
46
72
  end
47
73
 
48
74
  # encode directory name from year and month.
@@ -69,6 +95,16 @@ module LucaSupport
69
95
  '0ABCDEFGHIJKL'.index(char)
70
96
  end
71
97
 
98
+ # Generate globbing phrase like ["2020[C-H]"] for range search.
99
+ #
100
+ def encode_term(start_year, start_month, end_year, end_month)
101
+ (start_year..end_year).to_a.map do |y|
102
+ g1 = y == start_year ? encode_month(start_month) : encode_month(1)
103
+ g2 = y == end_year ? encode_month(end_month) : encode_month(12)
104
+ g1 == g2 ? "#{y}#{g1}" : "#{y}[#{g1}-#{g2}]"
105
+ end
106
+ end
107
+
72
108
  def decode_term(char)
73
109
  m = /^([0-9]{4})([A-La-l])/.match(char)
74
110
  [m[1].to_i, decode_month(m[2])]
@@ -78,6 +114,17 @@ module LucaSupport
78
114
  Digest::SHA1.hexdigest(SecureRandom.uuid)
79
115
  end
80
116
 
117
+ def match_score(a, b, n = 2)
118
+ v_a = to_ngram(a, n)
119
+ v_b = to_ngram(b, n)
120
+
121
+ v_a.map { |item| v_b.include?(item) ? 1 : 0 }.sum / v_a.length.to_f
122
+ end
123
+
124
+ def to_ngram(str, n = 2)
125
+ str.each_char.each_cons(n).map(&:join)
126
+ end
127
+
81
128
  def decimalize(obj)
82
129
  case obj.class.name
83
130
  when 'Array'
@@ -96,14 +143,18 @@ module LucaSupport
96
143
  end
97
144
 
98
145
  def readable(obj, len = LucaSupport::Config::DECIMAL_NUM)
99
- case obj.class.name
100
- when 'Array'
146
+ case obj
147
+ when Array
101
148
  obj.map { |i| readable(i) }
102
- when 'Hash'
149
+ when Hash
103
150
  obj.inject({}) { |h, (k, v)| h[k] = readable(v); h }
104
- when 'BigDecimal'
105
- parts = obj.round(len).to_s('F').split('.')
106
- len < 1 ? parts.first : "#{parts[0]}.#{parts[1][0, len]}"
151
+ when BigDecimal
152
+ if len == 0
153
+ obj.round # Integer is precise
154
+ else
155
+ parts = obj.round(len).to_s('F').split('.')
156
+ "#{parts[0]}.#{parts[1][0, len]}"
157
+ end
107
158
  else
108
159
  obj
109
160
  end
@@ -3,15 +3,21 @@
3
3
  require 'pathname'
4
4
  require 'yaml'
5
5
 
6
- #
7
6
  # startup config
8
7
  #
9
8
  module LucaSupport
9
+ PJDIR = ENV['LUCA_TEST_DIR'] || Dir.pwd.freeze
10
+ CONFIG = begin
11
+ YAML.load_file(Pathname(PJDIR) / 'config.yml', **{})
12
+ rescue Errno::ENOENT
13
+ {}
14
+ end
15
+
10
16
  module Config
11
17
  # Project top directory.
12
18
  Pjdir = ENV['LUCA_TEST_DIR'] || Dir.pwd.freeze
13
19
  if File.exist?(Pathname(Pjdir) / 'config.yml')
14
- DECIMAL_NUM = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['decimal_number']
20
+ # DECIMAL_NUM = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['decimal_number']
15
21
  COUNTRY = YAML.load_file(Pathname(Pjdir) / 'config.yml', **{})['country']
16
22
  DECIMAL_NUM ||= 0 if COUNTRY == 'jp'
17
23
  end
@@ -42,5 +42,10 @@ module LucaSupport
42
42
  end
43
43
  nil
44
44
  end
45
+
46
+ def nushell(yml)
47
+ require 'open3'
48
+ Open3.pipeline_w(%(nu -c 'cat - | from yaml')) { |stdin| stdin.puts yml }
49
+ end
45
50
  end
46
51
  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.18
4
+ version: 0.2.23
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-08 00:00:00.000000000 Z
11
+ date: 2020-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail