lucarecord 0.2.15 → 0.2.20

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: 3d7ac838f340b86b71208f40357ebbe7c4217b1ed6b69dcf012b773d8fa0a532
4
- data.tar.gz: c0e706fe9e1eef4abbb5d9600a5100931cd9384d45459481142fb3c594dbfd32
3
+ metadata.gz: e2644c50f67e64dfe2502400e04eddb6278a314d00f82eb5a51a6489797f0b48
4
+ data.tar.gz: c6d286442884c6675f18340ad2eb3322cd76515992530cbf4fc8536b66a6bbad
5
5
  SHA512:
6
- metadata.gz: 56f2f5575afb0a5e204a94ed66fa7b6ded3831316e9d71be17a02270f464b57551701c3938a447f479a4512255800d49bdc545e8f970f8d81f00c1be8caf5316
7
- data.tar.gz: be5bf831649fb6d6ad74ca44ae18c9cbb4eabf26193926979776fb99f843c687a8dead90917dafeaa3e9289c2ec8eb83e78a2082fad50305df1f4b8bcaab96a9
6
+ metadata.gz: 0122e43160066ce33dad8478fc99cd4bc639abf232788852ebce74328f664f413756ea91c73a6f6372f4dce9648a9c6776dd7f7ee1f0971930b71f0816a2841b
7
+ data.tar.gz: '09c51366863747bfea0cdf829d0829be6d084b79cbc729c3cb51ecea7f07196ca2c7ff0eea8b52d834d0af476de9cb953ef50d9e1b7e620de0c6f5d51c64f158'
@@ -0,0 +1,17 @@
1
+ ## LucaRecord 0.2.20
2
+
3
+ * UUID completion framework on prefix match
4
+
5
+ ## LucaRecord 0.2.19
6
+
7
+ * `LucaSupport::Code.decode_id()`
8
+ * `LucaSupport::Code.encode_term()` for multiple months search. Old `scan_term()` removed.
9
+
10
+ ## LucaRecord 0.2.18
11
+
12
+ * `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.
13
+
14
+ ## LucaRecord 0.2.17
15
+
16
+ * Change internal number format to BigDecimal.
17
+ * 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'
@@ -29,15 +30,15 @@ module LucaRecord # :nodoc:
29
30
 
30
31
  # find ID based record. Support uuid and encoded date.
31
32
  def find(id, basedir = @dirname)
32
- return enum_for(:find, id, basedir) unless block_given?
33
+ return enum_for(:find, id, basedir).first unless block_given?
33
34
 
34
35
  if id.length >= 40
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,25 +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)
94
- id = LucaSupport::Code.issue_random_id
95
- obj['id'] = id
96
- open_hashed(basedir, id, 'w') do |f|
97
- f.write(YAML.dump(obj.sort.to_h))
107
+ # create record both of uuid/date identified.
108
+ #
109
+ def create(obj, date: nil, codes: nil, basedir: @dirname)
110
+ validate_keys(obj)
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']
98
119
  end
99
- id
100
120
  end
101
121
 
102
- # define new transaction ID & write data at once
103
- def create_record!(obj, date_obj, codes = nil, basedir = @dirname)
104
- gen_record_file!(basedir, date_obj, codes) do |f|
105
- 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!('/', '') }
106
144
  end
107
145
  end
108
146
 
109
147
  def prepare_dir!(basedir, date_obj)
110
- dir_name = (Pathname(basedir) + encode_dirname(date_obj)).to_s
148
+ dir_name = (Pathname(basedir) + LucaSupport::Code.encode_dirname(date_obj)).to_s
111
149
  FileUtils.mkdir_p(dir_name) unless Dir.exist?(dir_name)
112
150
  dir_name
113
151
  end
@@ -121,6 +159,49 @@ module LucaRecord # :nodoc:
121
159
  File.write(path, YAML.dump(origin.sort.to_h))
122
160
  end
123
161
 
162
+ # update file with obj['id']
163
+ def save(obj, basedir = @dirname)
164
+ if obj['id'].nil?
165
+ create(obj, basedir)
166
+ else
167
+ validate_keys(obj)
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
179
+ end
180
+ end
181
+ obj['id']
182
+ end
183
+
184
+ # delete file by id
185
+ def delete(id, basedir = @dirname)
186
+ FileUtils.rm(Pathname(abs_path(basedir)) / id2path(id))
187
+ id
188
+ end
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
+
124
205
  # ----------------------------------------------------------------
125
206
  # :section: Path Utilities
126
207
  # ----------------------------------------------------------------
@@ -161,10 +242,6 @@ module LucaRecord # :nodoc:
161
242
  end
162
243
  end
163
244
 
164
- def encode_dirname(date_obj)
165
- date_obj.year.to_s + LucaSupport::Code.encode_month(date_obj)
166
- end
167
-
168
245
  # test if having required dirs/files under exec path
169
246
  def valid_project?(path = LucaSupport::Config::Pjdir)
170
247
  project_dir = Pathname(path)
@@ -177,6 +254,29 @@ module LucaRecord # :nodoc:
177
254
 
178
255
  private
179
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
+
180
280
  # open records with 'basedir/month/date-code' path structure.
181
281
  # Glob pattern can be specified like folloing examples.
182
282
  #
@@ -192,6 +292,7 @@ module LucaRecord # :nodoc:
192
292
 
193
293
  file_pattern = filename.nil? ? '*' : "#{filename}*"
194
294
  Dir.chdir(abs_path(basedir)) do
295
+ FileUtils.mkdir_p(subdir) if mode == 'w' && !Dir.exist?(subdir)
195
296
  Dir.glob("#{subdir}*/#{file_pattern}").sort.each do |subpath|
196
297
  next if skip_on_unmatch_code(subpath, code)
197
298
 
@@ -223,6 +324,17 @@ module LucaRecord # :nodoc:
223
324
  end
224
325
  end
225
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
+
226
338
  # Decode basic format.
227
339
  # If specific decode is needed, override this method in each class.
228
340
  #
@@ -234,18 +346,18 @@ module LucaRecord # :nodoc:
234
346
  when 'json'
235
347
  # TODO: implement JSON parse
236
348
  else
237
- YAML.load(io.read)
349
+ LucaSupport::Code.decimalize(YAML.load(io.read)).tap { |obj| validate_keys(obj) }
238
350
  end
239
351
  end
240
352
 
241
- def gen_record_file!(basedir, date_obj, codes = nil)
242
- d = prepare_dir!(abs_path(basedir), date_obj)
243
- filename = LucaSupport::Code.encode_date(date_obj) + new_record_id(abs_path(basedir), date_obj)
244
- if codes
245
- filename += codes.inject('') { |fragment, code| "#{fragment}-#{code}" }
353
+ def validate_keys(obj)
354
+ return nil unless @required
355
+
356
+ keys = obj.keys
357
+ [].tap do |errors|
358
+ @required.each { |r| errors << r unless keys.include?(r) }
359
+ raise "Missing keys: #{errors.join(' ')}" unless errors.empty?
246
360
  end
247
- path = Pathname(d) + filename
248
- File.open(path.to_s, 'w') { |f| yield(f) }
249
361
  end
250
362
 
251
363
  # TODO: replace with data_dir method
@@ -265,8 +377,10 @@ module LucaRecord # :nodoc:
265
377
 
266
378
  # AUTO INCREMENT
267
379
  def new_record_no(basedir, date_obj)
268
- dir_name = (Pathname(basedir) + encode_dirname(date_obj)).to_s
269
- 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)
270
384
 
271
385
  Dir.chdir(dir_name) do
272
386
  last_file = Dir.glob("#{LucaSupport::Code.encode_date(date_obj)}*").max
@@ -298,17 +412,6 @@ module LucaRecord # :nodoc:
298
412
  end
299
413
  end
300
414
 
301
- # parse data dir and respond existing months
302
- #
303
- def scan_terms(base_dir, query = nil)
304
- pattern = query.nil? ? "*" : "#{query}*"
305
- Dir.chdir(base_dir) do
306
- Dir.glob(pattern).select { |dir|
307
- FileTest.directory?(dir) && /^[0-9]/.match(dir)
308
- }.sort.map { |str| decode_term(str) }
309
- end
310
- end
311
-
312
415
  def load_config(path = nil)
313
416
  path = path.to_s
314
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.15'
4
+ VERSION = '0.2.20'
5
5
  end
@@ -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
@@ -44,7 +52,12 @@ module LucaSupport
44
52
  num.to_s.reverse.gsub!(/(\d{3})(?=\d)/, '\1,').reverse!
45
53
  end
46
54
 
55
+ # encode directory name from year and month.
47
56
  #
57
+ def encode_dirname(date_obj)
58
+ date_obj.year.to_s + encode_month(date_obj)
59
+ end
60
+
48
61
  # Month to code conversion.
49
62
  # Date, DateTime, String, Integer is valid input. If nil, returns empty String for consistency.
50
63
  #
@@ -63,6 +76,16 @@ module LucaSupport
63
76
  '0ABCDEFGHIJKL'.index(char)
64
77
  end
65
78
 
79
+ # Generate globbing phrase like ["2020[C-H]"] for range search.
80
+ #
81
+ def encode_term(start_year, start_month, end_year, end_month)
82
+ (start_year..end_year).to_a.map do |y|
83
+ g1 = y == start_year ? encode_month(start_month) : encode_month(1)
84
+ g2 = y == end_year ? encode_month(end_month) : encode_month(12)
85
+ "#{y}[#{g1}-#{g2}]"
86
+ end
87
+ end
88
+
66
89
  def decode_term(char)
67
90
  m = /^([0-9]{4})([A-La-l])/.match(char)
68
91
  [m[1].to_i, decode_month(m[2])]
@@ -72,6 +95,37 @@ module LucaSupport
72
95
  Digest::SHA1.hexdigest(SecureRandom.uuid)
73
96
  end
74
97
 
98
+ def decimalize(obj)
99
+ case obj.class.name
100
+ when 'Array'
101
+ obj.map { |i| decimalize(i) }
102
+ when 'Hash'
103
+ obj.inject({}) { |h, (k, v)| h[k] = decimalize(v); h }
104
+ when 'Integer'
105
+ BigDecimal(obj.to_s)
106
+ when 'String'
107
+ /^[0-9\.]+$/.match(obj) ? BigDecimal(obj) : obj
108
+ when 'Float'
109
+ raise 'already float'
110
+ else
111
+ obj
112
+ end
113
+ end
114
+
115
+ def readable(obj, len = LucaSupport::Config::DECIMAL_NUM)
116
+ case obj.class.name
117
+ when 'Array'
118
+ obj.map { |i| readable(i) }
119
+ when 'Hash'
120
+ 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]}"
124
+ else
125
+ obj
126
+ end
127
+ end
128
+
75
129
  #
76
130
  # convert effective/defunct data into current hash on @date.
77
131
  # not parse nested children.
@@ -97,7 +151,7 @@ module LucaSupport
97
151
  #
98
152
  def take_current(dat, item)
99
153
  target = dat.dig(item)
100
- if target.class.name == 'Array' && target.map(&:keys).flatten.include?('effective')
154
+ if target.is_a?(Array) && target.map(&:keys).flatten.include?('effective')
101
155
  latest = target
102
156
  .filter { |a| Date.parse(a.dig('effective').to_s) < @date }
103
157
  .max { |a, b| Date.parse(a.dig('effective').to_s) <=> Date.parse(b.dig('effective').to_s) }
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+ require 'yaml'
5
+
3
6
  #
4
7
  # startup config
5
8
  #
@@ -7,5 +10,11 @@ module LucaSupport
7
10
  module Config
8
11
  # Project top directory.
9
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
10
19
  end
11
20
  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.15
4
+ version: 0.2.20
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-05 00:00:00.000000000 Z
11
+ date: 2020-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail