lucarecord 0.2.16 → 0.2.21

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: 222229f2cc5299302f8ff1df81a92ce9a507101e6035765cdaa1408ad8e93365
4
- data.tar.gz: 30102479db3e25fca972cf723d8f679891a179ed0b9ceb79cb03d2b22f25743b
3
+ metadata.gz: 94f10c954f77059dc814f1aac38335ca0e1597d0db8e7e754a93f6a67714cce1
4
+ data.tar.gz: fd0e684124f7683e49192b231fd8f245e09c1fdd281f88fac7dcc309f92713bf
5
5
  SHA512:
6
- metadata.gz: 4be9918878c1d9204ccbaac61cfefac3872b028d192e8013d177fda369092dcd1ff2884e7e0e53a1384e3e9b21f57cbca903b6f448bddb39ec2199dd4aefdf46
7
- data.tar.gz: b5730ce678fc0259a4550f14fddce5655509d07653c0fd8d405d9d1f0f3e48572cad1818790e29ead2d23144be95a3b9229aaf424bca4b71b61c12603b16c0fc
6
+ metadata.gz: bfd7fdcc8f63dc889fcd51ae8e6e9d8a842a2b87ac5e7c7235e6c1ca9aa5f3886d86f8af71de0bf5e379a428b4c9fa15a2f300118427849da46124fcf4ee7532
7
+ data.tar.gz: 7e39c7ada92fbc48aad6492362d9f22f8cfa2c3fa109183410d5de9f64641b8d566c206d165026a92f48dd2aa6b2aa310f69a1ff1d7fefc43f48226b5d2065e5
@@ -0,0 +1,21 @@
1
+ ## LucaRecord 0.2.21
2
+
3
+ * Enhance `LucaSupport::Code.delimit_num()`. Handle with BigDecimal, decimal length & delmiter customization.
4
+
5
+ ## LucaRecord 0.2.20
6
+
7
+ * UUID completion framework on prefix match
8
+
9
+ ## LucaRecord 0.2.19
10
+
11
+ * `LucaSupport::Code.decode_id()`
12
+ * `LucaSupport::Code.encode_term()` for multiple months search. Old `scan_term()` removed.
13
+
14
+ ## LucaRecord 0.2.18
15
+
16
+ * `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.
17
+
18
+ ## LucaRecord 0.2.17
19
+
20
+ * Change internal number format to BigDecimal.
21
+ * Number of Decimal is configurable through `decimal_number` in config.yml(default = 2). `country` setting can also affect.
@@ -20,7 +20,7 @@ module LucaRecord
20
20
  end
21
21
 
22
22
  def search(word, default_word = nil)
23
- res = max_score_code(word)
23
+ res = max_score_code(word.gsub(/[[:space:]]/, ''))
24
24
  if res[1] > 0.4
25
25
  res[0]
26
26
  else
@@ -65,12 +65,14 @@ module LucaRecord
65
65
  config[:type] ||= 'invalid'
66
66
  config[:debit_value] = @config['debit_value'].to_i if @config.dig('debit_value')
67
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')
68
+ config[:note] = @config['note'] if @config.dig('note')
69
69
  config[:encoding] = @config['encoding'] if @config.dig('encoding')
70
70
 
71
71
  config[:year] = @config['year'] if @config.dig('year')
72
72
  config[:month] = @config['month'] if @config.dig('month')
73
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')
74
76
  end
75
77
  end
76
78
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bigdecimal'
3
4
  require 'csv'
4
5
  require 'date'
5
6
  require 'fileutils'
@@ -35,9 +36,9 @@ module LucaRecord # :nodoc:
35
36
  open_hashed(basedir, id) do |f|
36
37
  yield load_data(f)
37
38
  end
38
- elsif id.length >= 9
39
- # TODO: need regexp match for more flexible coding(after AD9999)
40
- open_records(basedir, id[0, 5], id[5, 6]) do |f, path|
39
+ elsif id.length >= 7
40
+ parts = id.split('/')
41
+ open_records(basedir, parts[0], parts[1]) do |f, path|
41
42
  yield load_data(f, path)
42
43
  end
43
44
  else
@@ -53,8 +54,22 @@ module LucaRecord # :nodoc:
53
54
  def asof(year, month = nil, day = nil, basedir = @dirname)
54
55
  return enum_for(:search, year, month, day, nil, basedir) unless block_given?
55
56
 
56
- search(year, month, day, nil, basedir) do |data, path|
57
- 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
58
73
  end
59
74
  end
60
75
 
@@ -89,26 +104,48 @@ module LucaRecord # :nodoc:
89
104
  # of each concrete class.
90
105
  # ----------------------------------------------------------------
91
106
 
92
- # create hash based record
93
- def create(obj, basedir = @dirname)
107
+ # create record both of uuid/date identified.
108
+ #
109
+ def create(obj, date: nil, codes: nil, basedir: @dirname)
94
110
  validate_keys(obj)
95
- id = LucaSupport::Code.issue_random_id
96
- obj['id'] = id
97
- open_hashed(basedir, id, 'w') do |f|
98
- f.write(YAML.dump(obj.sort.to_h))
111
+ if date
112
+ create_record(obj, date, codes, basedir)
113
+ else
114
+ obj['id'] = LucaSupport::Code.issue_random_id
115
+ open_hashed(basedir, obj['id'], 'w') do |f|
116
+ f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
117
+ end
118
+ obj['id']
99
119
  end
100
- id
101
120
  end
102
121
 
103
- # define new transaction ID & write data at once
104
- def create_record!(obj, date_obj, codes = nil, basedir = @dirname)
105
- gen_record_file!(basedir, date_obj, codes) do |f|
106
- f.write(YAML.dump(obj.sort.to_h))
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!('/', '') }
107
144
  end
108
145
  end
109
146
 
110
147
  def prepare_dir!(basedir, date_obj)
111
- dir_name = (Pathname(basedir) + encode_dirname(date_obj)).to_s
148
+ dir_name = (Pathname(basedir) + LucaSupport::Code.encode_dirname(date_obj)).to_s
112
149
  FileUtils.mkdir_p(dir_name) unless Dir.exist?(dir_name)
113
150
  dir_name
114
151
  end
@@ -128,8 +165,17 @@ module LucaRecord # :nodoc:
128
165
  create(obj, basedir)
129
166
  else
130
167
  validate_keys(obj)
131
- open_hashed(basedir, obj['id'], 'w') do |f|
132
- f.write(YAML.dump(obj.sort.to_h))
168
+ if obj['id'].length < 40
169
+ parts = obj['id'].split('/')
170
+ raise 'invalid ID' if parts.length != 2
171
+
172
+ open_records(basedir, parts[0], parts[1], nil, 'w') do |f, path|
173
+ f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
174
+ end
175
+ else
176
+ open_hashed(basedir, obj['id'], 'w') do |f|
177
+ f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
178
+ end
133
179
  end
134
180
  end
135
181
  obj['id']
@@ -141,6 +187,21 @@ module LucaRecord # :nodoc:
141
187
  id
142
188
  end
143
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
+
144
205
  # ----------------------------------------------------------------
145
206
  # :section: Path Utilities
146
207
  # ----------------------------------------------------------------
@@ -181,10 +242,6 @@ module LucaRecord # :nodoc:
181
242
  end
182
243
  end
183
244
 
184
- def encode_dirname(date_obj)
185
- date_obj.year.to_s + LucaSupport::Code.encode_month(date_obj)
186
- end
187
-
188
245
  # test if having required dirs/files under exec path
189
246
  def valid_project?(path = LucaSupport::Config::Pjdir)
190
247
  project_dir = Pathname(path)
@@ -197,6 +254,29 @@ module LucaRecord # :nodoc:
197
254
 
198
255
  private
199
256
 
257
+ # define new transaction ID & write data at once
258
+ # ID format is like '2020H/A001', which means record no.1 of 2020/10/10.
259
+ # Any data format can be written with block.
260
+ #
261
+ def create_record(obj, date_obj, codes = nil, basedir = @dirname)
262
+ FileUtils.mkdir_p(abs_path(basedir)) unless Dir.exist?(abs_path(basedir))
263
+ subdir = "#{date_obj.year}#{LucaSupport::Code.encode_month(date_obj)}"
264
+ filename = LucaSupport::Code.encode_date(date_obj) + new_record_id(basedir, date_obj)
265
+ obj['id'] = "#{subdir}/#{filename}" if obj.is_a? Hash
266
+ filename += '-' + codes.join('-') if codes
267
+ Dir.chdir(abs_path(basedir)) do
268
+ FileUtils.mkdir_p(subdir) unless Dir.exist?(subdir)
269
+ File.open(Pathname(subdir) / filename, 'w') do |f|
270
+ if block_given?
271
+ yield(f)
272
+ else
273
+ f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
274
+ end
275
+ end
276
+ end
277
+ "#{subdir}/#{filename}"
278
+ end
279
+
200
280
  # open records with 'basedir/month/date-code' path structure.
201
281
  # Glob pattern can be specified like folloing examples.
202
282
  #
@@ -212,6 +292,7 @@ module LucaRecord # :nodoc:
212
292
 
213
293
  file_pattern = filename.nil? ? '*' : "#{filename}*"
214
294
  Dir.chdir(abs_path(basedir)) do
295
+ FileUtils.mkdir_p(subdir) if mode == 'w' && !Dir.exist?(subdir)
215
296
  Dir.glob("#{subdir}*/#{file_pattern}").sort.each do |subpath|
216
297
  next if skip_on_unmatch_code(subpath, code)
217
298
 
@@ -243,6 +324,17 @@ module LucaRecord # :nodoc:
243
324
  end
244
325
  end
245
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
+
246
338
  # Decode basic format.
247
339
  # If specific decode is needed, override this method in each class.
248
340
  #
@@ -254,7 +346,7 @@ module LucaRecord # :nodoc:
254
346
  when 'json'
255
347
  # TODO: implement JSON parse
256
348
  else
257
- YAML.load(io.read).tap { |obj| validate_keys(obj) }
349
+ LucaSupport::Code.decimalize(YAML.load(io.read)).tap { |obj| validate_keys(obj) }
258
350
  end
259
351
  end
260
352
 
@@ -268,16 +360,6 @@ module LucaRecord # :nodoc:
268
360
  end
269
361
  end
270
362
 
271
- def gen_record_file!(basedir, date_obj, codes = nil)
272
- d = prepare_dir!(abs_path(basedir), date_obj)
273
- filename = LucaSupport::Code.encode_date(date_obj) + new_record_id(abs_path(basedir), date_obj)
274
- if codes
275
- filename += codes.inject('') { |fragment, code| "#{fragment}-#{code}" }
276
- end
277
- path = Pathname(d) + filename
278
- File.open(path.to_s, 'w') { |f| yield(f) }
279
- end
280
-
281
363
  # TODO: replace with data_dir method
282
364
  def abs_path(base_dir)
283
365
  Pathname(LucaSupport::Config::Pjdir) / 'data' / base_dir
@@ -295,8 +377,10 @@ module LucaRecord # :nodoc:
295
377
 
296
378
  # AUTO INCREMENT
297
379
  def new_record_no(basedir, date_obj)
298
- dir_name = (Pathname(basedir) + encode_dirname(date_obj)).to_s
299
- raise 'No target dir exists.' unless Dir.exist?(dir_name)
380
+ raise 'No target dir exists.' unless Dir.exist?(abs_path(basedir))
381
+
382
+ dir_name = (Pathname(abs_path(basedir)) / LucaSupport::Code.encode_dirname(date_obj)).to_s
383
+ return 1 unless Dir.exist?(dir_name)
300
384
 
301
385
  Dir.chdir(dir_name) do
302
386
  last_file = Dir.glob("#{LucaSupport::Code.encode_date(date_obj)}*").max
@@ -328,17 +412,6 @@ module LucaRecord # :nodoc:
328
412
  end
329
413
  end
330
414
 
331
- # parse data dir and respond existing months
332
- #
333
- def scan_terms(base_dir, query = nil)
334
- pattern = query.nil? ? "*" : "#{query}*"
335
- Dir.chdir(base_dir) do
336
- Dir.glob(pattern).select { |dir|
337
- FileTest.directory?(dir) && /^[0-9]/.match(dir)
338
- }.sort.map { |str| decode_term(str) }
339
- end
340
- end
341
-
342
415
  def load_config(path = nil)
343
416
  path = path.to_s
344
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.16'
4
+ VERSION = '0.2.21'
5
5
  end
@@ -2,6 +2,7 @@
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'
@@ -3,12 +3,20 @@
3
3
  require 'date'
4
4
  require 'securerandom'
5
5
  require 'digest/sha1'
6
+ require 'luca_support/config'
6
7
 
7
8
  # implement Luca IDs convention
8
9
  module LucaSupport
9
10
  module Code
10
11
  module_function
11
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
+
12
20
  def encode_txid(num)
13
21
  txmap = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
14
22
  l = txmap.length
@@ -40,11 +48,35 @@ module LucaSupport
40
48
  '0123456789ABCDEFGHIJKLMNOPQRSTUV'.index(char)
41
49
  end
42
50
 
43
- def delimit_num(num)
44
- 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)
45
72
  end
46
73
 
74
+ # encode directory name from year and month.
47
75
  #
76
+ def encode_dirname(date_obj)
77
+ date_obj.year.to_s + encode_month(date_obj)
78
+ end
79
+
48
80
  # Month to code conversion.
49
81
  # Date, DateTime, String, Integer is valid input. If nil, returns empty String for consistency.
50
82
  #
@@ -63,6 +95,16 @@ module LucaSupport
63
95
  '0ABCDEFGHIJKL'.index(char)
64
96
  end
65
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
+ "#{y}[#{g1}-#{g2}]"
105
+ end
106
+ end
107
+
66
108
  def decode_term(char)
67
109
  m = /^([0-9]{4})([A-La-l])/.match(char)
68
110
  [m[1].to_i, decode_month(m[2])]
@@ -72,6 +114,37 @@ module LucaSupport
72
114
  Digest::SHA1.hexdigest(SecureRandom.uuid)
73
115
  end
74
116
 
117
+ def decimalize(obj)
118
+ case obj.class.name
119
+ when 'Array'
120
+ obj.map { |i| decimalize(i) }
121
+ when 'Hash'
122
+ obj.inject({}) { |h, (k, v)| h[k] = decimalize(v); h }
123
+ when 'Integer'
124
+ BigDecimal(obj.to_s)
125
+ when 'String'
126
+ /^[0-9\.]+$/.match(obj) ? BigDecimal(obj) : obj
127
+ when 'Float'
128
+ raise 'already float'
129
+ else
130
+ obj
131
+ end
132
+ end
133
+
134
+ def readable(obj, len = LucaSupport::Config::DECIMAL_NUM)
135
+ case obj.class.name
136
+ when 'Array'
137
+ obj.map { |i| readable(i) }
138
+ when 'Hash'
139
+ 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]}"
143
+ else
144
+ obj
145
+ end
146
+ end
147
+
75
148
  #
76
149
  # convert effective/defunct data into current hash on @date.
77
150
  # not parse nested children.
@@ -1,11 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
3
+ require 'pathname'
4
+ require 'yaml'
5
+
4
6
  # startup config
5
7
  #
6
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
+
7
16
  module Config
8
17
  # Project top directory.
9
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
10
25
  end
11
26
  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.16
4
+ version: 0.2.21
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-07 00:00:00.000000000 Z
11
+ date: 2020-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail