textrepo 0.4.2 → 0.5.1

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: c2806e2488be378495f29ed25dd9d04937d50706a5d7772b5d93fd4d242a675d
4
- data.tar.gz: a031757c1eb5fa5ec644b7c6674d030f93aebf07c93ad061ea96711532ab8b79
3
+ metadata.gz: b504229bd3ea2416e0a85cb8903fe0903b6f045a8a2549857a2b1b296f35e32a
4
+ data.tar.gz: baaf7776505e0c430d9c540d88e78847eebc406612b5f9c2cb11ba20c5f3371d
5
5
  SHA512:
6
- metadata.gz: bcf5e7162744e0d0ba1e0e1745c37b7718749d3aae2ac83c6c8af8f9c032d8b85af38abbbf61d3f19ec49e640060362bcc55085bb7e930868294068314f5da21
7
- data.tar.gz: d2074f1b5a09a8741e8172d51d90c31f7c17552473d834e866cb3f7f1d43d2f2633a7daa2aa50a12d2a17202aa58f43466c151793a9368eb7b3fb39977a0379b
6
+ metadata.gz: cb90a5d324536e0c2ba8f87c93923ba19bae2c835f31445f9d0f3701110f37598f44a638b496ac992dab30ea03f82643591ea807623a93bd0afdfa52505595ac
7
+ data.tar.gz: 1affcd1ff8cce4b3374610791ba733d5d5c8fd1d8b90adb5030725752c84c0f671d4ee9b6ca7f4539ebc2171baf1d2c92b9eb530dc84eed14d477df98729154e
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ Gemfile.lock
1
2
  *.gem
2
3
  *.rbc
3
4
  /.config
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
  ## [Unreleased]
8
8
  Nothing to record here.
9
9
 
10
+ ## [0.5.1] - 2020-11-02
11
+ ### Changed
12
+ - Fix issue #28.
13
+ - Modify `Repository#update` to do nothing when the given text is
14
+ identical to the one in the repository.
15
+
16
+ ## [0.5.0] - 2020-11-01
17
+ ### Added
18
+ - Add a new API `Repository#search`.
19
+ - Add a new API `Repository#exist?`. (0.4.3)
20
+
10
21
  ## [0.4.0] - 2020-10-14
11
22
  ### Added
12
23
  - Released to rubygems.org.
data/Rakefile CHANGED
@@ -42,5 +42,4 @@ RDoc::Task.new do |rdoc|
42
42
  rdoc.generator = "ri"
43
43
  rdoc.rdoc_dir = "doc"
44
44
  rdoc.rdoc_files.include("lib/**/*.rb")
45
- rdoc.markup = "markdown"
46
45
  end
@@ -1,12 +1,39 @@
1
1
  module Textrepo
2
+
3
+ ##
4
+ # Following errors might occur in repository operations:
5
+ # +---------------------------------+-----------------------+
6
+ # | operation (args) | error type |
7
+ # +---------------------------------+-----------------------+
8
+ # | create (timestamp, text) | Duplicate timestamp |
9
+ # | | Empty text |
10
+ # +---------------------------------+-----------------------+
11
+ # | read (timestamp) | Missing timestamp |
12
+ # +---------------------------------+-----------------------+
13
+ # | update (timestamp, text) | Mssing timestamp |
14
+ # | | Empty text |
15
+ # +---------------------------------+-----------------------+
16
+ # | delete (timestamp) | Missing timestamp |
17
+ # +---------------------------------+-----------------------+
18
+ # | search (pattern, stamp_pattern) | Invalid search result |
19
+ # +---------------------------------+-----------------------+
20
+
2
21
  class Error < StandardError; end
3
22
 
23
+ # :stopdoc:
4
24
  module ErrMsg
5
25
  UNKNOWN_REPO_TYPE = 'unknown type for repository: %s'
6
26
  DUPLICATE_TIMESTAMP = 'duplicate timestamp: %s'
7
27
  EMPTY_TEXT = 'empty text'
8
28
  MISSING_TIMESTAMP = 'missing timestamp: %s'
29
+ INVALID_TIMESTAMP_STRING = "invalid string as timestamp: %s"
30
+ INVALID_SEARCH_RESULT = "invalid result by searcher: %s"
9
31
  end
32
+ # :startdoc:
33
+
34
+ ##
35
+ # An error raised if unknown type was specified as the repository
36
+ # type.
10
37
 
11
38
  class UnknownRepoTypeError < Error
12
39
  def initialize(type)
@@ -14,20 +41,9 @@ module Textrepo
14
41
  end
15
42
  end
16
43
 
17
- # Following errors might occur in repository operations:
18
- # +--------------------------+---------------------+
19
- # | operation (args) | error type |
20
- # +--------------------------+---------------------+
21
- # | create (timestamp, text) | Duplicate timestamp |
22
- # | | Empty text |
23
- # +--------------------------+---------------------+
24
- # | read (timestamp) | Missing timestamp |
25
- # +--------------------------+---------------------+
26
- # | update (timestamp, text) | Mssing timestamp |
27
- # | | Empty text |
28
- # +--------------------------+---------------------+
29
- # | delete (timestamp) | Missing timestamp |
30
- # +--------------------------+---------------------+
44
+ ##
45
+ # An error raised if the specified timestamp has already exist in
46
+ # the repository.
31
47
 
32
48
  class DuplicateTimestampError < Error
33
49
  def initialize(timestamp)
@@ -35,16 +51,43 @@ module Textrepo
35
51
  end
36
52
  end
37
53
 
54
+ ##
55
+ # An error raised if the given text is empty.
56
+
38
57
  class EmptyTextError < Error
39
58
  def initialize
40
59
  super(ErrMsg::EMPTY_TEXT)
41
60
  end
42
61
  end
43
62
 
63
+ ##
64
+ # An error raised if the given timestamp has not exist in the
65
+ # repository.
66
+
44
67
  class MissingTimestampError < Error
45
68
  def initialize(timestamp)
46
69
  super(ErrMsg::MISSING_TIMESTAMP % timestamp)
47
70
  end
48
71
  end
49
72
 
73
+ ##
74
+ # An error raised if an argument is invalid to convert a
75
+ # Textrepo::Timestamp object.
76
+
77
+ class InvalidTimestampStringError < Error
78
+ def initialize(str)
79
+ super(ErrMsg::INVALID_TIMESTAMP_STRING % str)
80
+ end
81
+ end
82
+
83
+ ##
84
+ # An error raise if the search result is not suitable to use.
85
+ #
86
+
87
+ class InvalidSearchResultError < Error
88
+ def initialize(str)
89
+ super(ErrMsg::INVALID_SEARCH_RESULT % str)
90
+ end
91
+ end
92
+
50
93
  end
@@ -1,29 +1,52 @@
1
1
  require 'fileutils'
2
+ require "open3"
2
3
 
3
4
  module Textrepo
5
+
4
6
  ##
5
7
  # A concrete class which implements Repository interfaces. This
6
8
  # repository uses the default file system of the operating system as
7
9
  # a text storage.
10
+
8
11
  class FileSystemRepository < Repository
12
+
9
13
  ##
10
14
  # Repository root.
15
+
11
16
  attr_reader :path
12
17
 
13
18
  ##
14
19
  # Extension of notes sotres in the repository.
20
+
15
21
  attr_reader :extname
16
22
 
23
+ ##
24
+ # Searcher program name.
25
+
26
+ attr_reader :searcher
27
+
28
+ ##
29
+ # An array of options to pass to the searcher program.
30
+
31
+ attr_reader :searcher_options
32
+
17
33
  ##
18
34
  # Default name for the repository which uses when no name is
19
35
  # specified in the configuration settings.
36
+
20
37
  FAVORITE_REPOSITORY_NAME = 'notes'
21
38
 
22
39
  ##
23
40
  # Default extension of notes which uses when no extname is
24
41
  # specified in the configuration settings.
42
+
25
43
  FAVORITE_EXTNAME = 'md'
26
44
 
45
+ ##
46
+ # Default searcher program to search text in the repository.
47
+
48
+ FAVORITE_SEARCHER = 'grep'
49
+
27
50
  ##
28
51
  # Creates a new repository object. The argument, `conf` must be a
29
52
  # Hash object. It should hold the follwoing values:
@@ -34,13 +57,33 @@ module Textrepo
34
57
  # - OPTIONAL: (if not specified, default values are used)
35
58
  # - :repository_name => basename of the root path for the repository
36
59
  # - :default_extname => extname for a file stored into in the repository
60
+ # - :searcher => a program to search like `grep`
61
+ # - :searcher_options => an Array of option to pass to the searcher
37
62
  #
38
63
  # The root path of the repository looks like the following:
39
64
  # - conf[:repository_base]/conf[:repository_name]
40
65
  #
41
- # Default values are set when `repository_name` and `default_extname`
66
+ # Default values are set when `:repository_name` and `:default_extname`
42
67
  # were not defined in `conf`.
43
68
  #
69
+ # Be careful to set `:searcher_options`, it must be to specify the
70
+ # searcher behavior equivalent to `grep` with "-inR". The default
71
+ # value for the searcher options is defined for BSD grep (default
72
+ # grep on macOS), GNU grep, and ripgrep (aka rg). They are:
73
+ #
74
+ # "grep" => ["-i", "-n", "-R", "-E"]
75
+ # "egrep" => ["-i", "-n", "-R"]
76
+ # "ggrep" => ["-i", "-n", "-R", "-E"]
77
+ # "gegrep" => ["-i", "-n", "-R"]
78
+ # "rg" => ["-S", "-n", "--no-heading", "--color", "never"]
79
+ #
80
+ # If use those 3 searchers, it is not recommended to set
81
+ # `:searcher_options`. The default value works well in
82
+ # `textrepo`.
83
+ #
84
+ # :call-seq:
85
+ # new(Hash or Hash like object) -> FileSystemRepository
86
+
44
87
  def initialize(conf)
45
88
  super
46
89
  base = conf[:repository_base]
@@ -48,6 +91,8 @@ module Textrepo
48
91
  @path = File.expand_path("#{name}", base)
49
92
  FileUtils.mkdir_p(@path)
50
93
  @extname = conf[:default_extname] || FAVORITE_EXTNAME
94
+ @searcher = find_searcher(conf[:searcher])
95
+ @searcher_options = conf[:searcher_options]
51
96
  end
52
97
 
53
98
  ##
@@ -55,8 +100,8 @@ module Textrepo
55
100
  # text and is associated to the timestamp.
56
101
  #
57
102
  # :call-seq:
58
- # create(Timestamp, Array) => Timestamp
59
- #
103
+ # create(Timestamp, Array) -> Timestamp
104
+
60
105
  def create(timestamp, text)
61
106
  abs = abspath(timestamp)
62
107
  raise DuplicateTimestampError, timestamp if FileTest.exist?(abs)
@@ -71,8 +116,8 @@ module Textrepo
71
116
  # content.
72
117
  #
73
118
  # :call-seq:
74
- # read(Timestamp) => Array
75
- #
119
+ # read(Timestamp) -> Array
120
+
76
121
  def read(timestamp)
77
122
  abs = abspath(timestamp)
78
123
  raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
@@ -87,21 +132,25 @@ module Textrepo
87
132
  # Updates the file content in the repository. A new timestamp
88
133
  # will be attached to the text.
89
134
  #
90
- # :call-seq:
91
- # update(Timestamp, Array) => Timestamp
135
+ # See the documentation of Repository#update to know about errors
136
+ # and constraints of this method.
92
137
  #
138
+ # :call-seq:
139
+ # update(Timestamp, Array) -> Timestamp
140
+
93
141
  def update(timestamp, text)
94
142
  raise EmptyTextError if text.empty?
95
- org_abs = abspath(timestamp)
96
- raise MissingTimestampError, timestamp unless FileTest.exist?(org_abs)
143
+ raise MissingTimestampError, timestamp unless exist?(timestamp)
144
+
145
+ # does nothing if given text is the same in the repository one
146
+ return timestamp if read(timestamp) == text
97
147
 
98
148
  # the text must be stored with the new timestamp
99
149
  new_stamp = Timestamp.new(Time.now)
100
- new_abs = abspath(new_stamp)
101
- write_text(new_abs, text)
150
+ write_text(abspath(new_stamp), text)
102
151
 
103
- # delete the original file in the repository
104
- FileUtils.remove_file(org_abs)
152
+ # delete the original text file in the repository
153
+ FileUtils.remove_file(abspath(timestamp))
105
154
 
106
155
  new_stamp
107
156
  end
@@ -110,8 +159,8 @@ module Textrepo
110
159
  # Deletes the file in the repository.
111
160
  #
112
161
  # :call-seq:
113
- # delete(Timestamp) => Array
114
- #
162
+ # delete(Timestamp) -> Array
163
+
115
164
  def delete(timestamp)
116
165
  abs = abspath(timestamp)
117
166
  raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
@@ -126,8 +175,8 @@ module Textrepo
126
175
  # Finds entries of text those timestamp matches the specified pattern.
127
176
  #
128
177
  # :call-seq:
129
- # entries(String = nil) => Array
130
- #
178
+ # entries(String = nil) -> Array of Timestamp instances
179
+
131
180
  def entries(stamp_pattern = nil)
132
181
  results = []
133
182
 
@@ -135,7 +184,7 @@ module Textrepo
135
184
  when "yyyymoddhhmiss_lll".size
136
185
  stamp = Timestamp.parse_s(stamp_pattern)
137
186
  if exist?(stamp)
138
- results << stamp.to_s
187
+ results << stamp
139
188
  end
140
189
  when 0, "yyyymoddhhmiss".size, "yyyymodd".size
141
190
  results += find_entries(stamp_pattern)
@@ -158,19 +207,49 @@ module Textrepo
158
207
  results
159
208
  end
160
209
 
210
+ ##
211
+ # Check the existence of text which is associated with the given
212
+ # timestamp.
213
+ #
214
+ # :call-seq:
215
+ # exist?(Timestamp) -> true or false
216
+
217
+ def exist?(timestamp)
218
+ FileTest.exist?(abspath(timestamp))
219
+ end
220
+
221
+ ##
222
+ # Searches a pattern in all text. The given pattern is a word to
223
+ # search or a regular expression. The pattern would be passed to
224
+ # a searcher program as it passed.
225
+ #
226
+ # See the document for Textrepo::Repository#search to know about
227
+ # the search result.
228
+ #
229
+ # :call-seq:
230
+ # search(String for pattern, String for Timestamp pattern) -> Array
231
+
232
+ def search(pattern, stamp_pattern = nil)
233
+ result = nil
234
+ if stamp_pattern.nil?
235
+ result = invoke_searcher_at_repo_root(@searcher, pattern)
236
+ else
237
+ result = invoke_searcher_for_entries(@searcher, pattern, entries(stamp_pattern))
238
+ end
239
+ construct_search_result(result)
240
+ end
241
+
161
242
  # :stopdoc:
243
+
162
244
  private
163
245
  def abspath(timestamp)
164
246
  filename = timestamp_to_pathname(timestamp) + ".#{@extname}"
165
247
  File.expand_path(filename, @path)
166
248
  end
167
249
 
168
- ##
169
- # ```
170
250
  # %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
171
251
  # "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
172
252
  # "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
173
- # ```
174
253
  def timestamp_to_pathname(timestamp)
175
254
  yyyy, mo = Timestamp.split_stamp(timestamp.to_s)[0..1]
176
255
  File.join(yyyy, mo, timestamp.to_s)
@@ -187,15 +266,156 @@ module Textrepo
187
266
  File.basename(text_path).delete_suffix(".#{@extname}")
188
267
  end
189
268
 
190
- def exist?(timestamp)
191
- FileTest.exist?(abspath(timestamp))
192
- end
193
-
194
269
  def find_entries(stamp_pattern)
195
270
  Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
196
- timestamp_str(e)
271
+ begin
272
+ Timestamp.parse_s(timestamp_str(e))
273
+ rescue InvalidTimestampStringError => _
274
+ # Just ignore the erroneous entry, since it is not a text in
275
+ # the repository. It may be a garbage, or some kind of
276
+ # hidden stuff of the repository, ... etc.
277
+ nil
278
+ end
279
+ }.compact
280
+ end
281
+
282
+ ##
283
+ # The upper limit of files to search at one time. The value has
284
+ # no reason to select. It seems to me that not too much, not too
285
+ # little to handle in one process to search.
286
+
287
+ LIMIT_OF_FILES = 20
288
+
289
+ ##
290
+ # When no timestamp pattern was given, invoke the searcher with
291
+ # the repository root path as its argument and the recursive
292
+ # searching option. The search could be done in only one process.
293
+
294
+ def invoke_searcher_at_repo_root(searcher, pattern)
295
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
296
+ pattern, @path)
297
+ output = []
298
+ output += o.lines.map(&:chomp) if s.success? && (! o.empty?)
299
+ output
300
+ end
301
+
302
+ ##
303
+ # When a timestamp pattern was given, at first, list target files,
304
+ # then invoke the searcher for those files. Since the number of
305
+ # target files may be so much, it seems to be dangerous to pass
306
+ # all of them to a single search process at one time.
307
+ #
308
+ # One more thing to mention, the searcher, like `grep`, does not
309
+ # add the filename at the beginning of the search result line, if
310
+ # the target is one file. This behavior is not suitable in this
311
+ # purpose. The code below adds the filename when the target is
312
+ # one file.
313
+
314
+ def invoke_searcher_for_entries(searcher, pattern, entries)
315
+ output = []
316
+
317
+ num_of_entries = entries.size
318
+ if num_of_entries == 1
319
+ # If the search taget is one file, the output needs special
320
+ # treatment.
321
+ file = abspath(entries[0])
322
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
323
+ pattern, file)
324
+ if s.success? && (! o.empty)
325
+ output += o.lines.map { |line|
326
+ # add filename at the beginning of the search result line
327
+ [file, line.chomp].join(":")
328
+ }
329
+ end
330
+ elsif num_of_entries > LIMIT_OF_FILES
331
+ output += invoke_searcher_for_entries(searcher, pattern, entries[0..(LIMIT_OF_FILES - 1)])
332
+ output += invoke_searcher_for_entries(searcher, pattern, entries[LIMIT_OF_FILES..-1])
333
+ else
334
+ # When the number of target is less than the upper limit,
335
+ # invoke the searcher with all of target files as its
336
+ # arguments.
337
+ files = find_files(entries)
338
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
339
+ pattern, *files)
340
+ if s.success? && (! o.empty)
341
+ output += o.lines.map(&:chomp)
342
+ end
343
+ end
344
+
345
+ output
346
+ end
347
+
348
+ SEARCHER_OPTS = {
349
+ # case insensitive, print line number, recursive search, work as egrep
350
+ "grep" => ["-i", "-n", "-R", "-E"],
351
+ # case insensitive, print line number, recursive search
352
+ "egrep" => ["-i", "-n", "-R"],
353
+ # case insensitive, print line number, recursive search, work as gegrep
354
+ "ggrep" => ["-i", "-n", "-R", "-E"],
355
+ # case insensitive, print line number, recursive search
356
+ "gegrep" => ["-i", "-n", "-R"],
357
+ # smart case, print line number, no color
358
+ "rg" => ["-S", "-n", "--no-heading", "--color", "never"],
359
+ }
360
+
361
+ def find_searcher_options(searcher)
362
+ @searcher_options || SEARCHER_OPTS[File.basename(searcher)] || ""
363
+ end
364
+
365
+ def find_files(timestamps)
366
+ timestamps.map{|stamp| abspath(stamp)}
367
+ end
368
+
369
+ ##
370
+ # The argument must be an Array contains the searcher output.
371
+ # Each item is constructed from 3 parts:
372
+ # "<pathname>:<integer>:<text>"
373
+ #
374
+ # For example, it may looks like:
375
+ #
376
+ # "/somewhere/2020/11/20201101044300.md:18:foo is foo"
377
+ #
378
+ # Or it may contains more ":" in the text part as:
379
+ #
380
+ # "/somewhere/2020/11/20201101044500.md:119:apple:orange:grape"
381
+ #
382
+ # In the latter case, `split(":")` will split it too much. That is,
383
+ # the result will be:
384
+ #
385
+ # ["/somewhere/2020/11/20201101044500.md", "119", "apple", "orange", "grape"]
386
+ #
387
+ # Text part must be joined with ":".
388
+
389
+ def construct_search_result(output)
390
+ output.map { |line|
391
+ begin
392
+ pathname, num, *match_text = line.split(":")
393
+ [Timestamp.parse_s(timestamp_str(pathname)),
394
+ num.to_i,
395
+ match_text.join(":")]
396
+ rescue InvalidTimestampStringError, TypeError => _
397
+ raise InvalidSearchResultError, [@searcher, @searcher_options.join(" ")].join(" ")
398
+ end
399
+ }.compact
400
+ end
401
+
402
+ def find_searcher(program = nil)
403
+ candidates = [FAVORITE_SEARCHER]
404
+ candidates.unshift(program) unless program.nil? || candidates.include?(program)
405
+ search_paths = ENV["PATH"].split(":")
406
+ candidates.map { |prog|
407
+ find_in_paths(prog, search_paths)
408
+ }[0]
409
+ end
410
+
411
+ def find_in_paths(prog, paths)
412
+ paths.each { |p|
413
+ abspath = File.expand_path(prog, p)
414
+ return abspath if FileTest.exist?(abspath) && FileTest.executable?(abspath)
197
415
  }
416
+ nil
198
417
  end
418
+ # :startdoc:
199
419
 
200
420
  end
201
421
  end
@@ -1,31 +1,81 @@
1
1
  module Textrepo
2
2
  class Repository
3
- attr_reader :type, :name
3
+
4
+ ##
5
+ # Repository type. It specifies which concrete repository class
6
+ # will instantiated. For example, the type `:file_system` specifies
7
+ # `FileSystemRepository`.
8
+
9
+ attr_reader :type
10
+
11
+ ##
12
+ # Repository name. The usage of the value of `name` depends on a
13
+ # concrete repository class. For example, `FileSystemRepository`
14
+ # uses it as a part of the repository path.
15
+
16
+ attr_reader :name
17
+
18
+ ##
19
+ # Create a new repository. The argument must be an object which
20
+ # can be accessed like a Hash object.
4
21
 
5
22
  def initialize(conf)
6
23
  @type = conf[:repository_type]
7
24
  @name = conf[:repository_name]
8
25
  end
9
26
 
27
+ ##
10
28
  # Stores text data into the repository with the specified timestamp.
11
29
  # Returns the timestamp.
30
+ #
31
+ # :call-seq:
32
+ # create(Timestamp, Array) -> Timestamp
33
+
12
34
  def create(timestamp, text); timestamp; end
13
35
 
36
+ ##
14
37
  # Reads text data from the repository, which is associated to the
15
38
  # timestamp. Returns an array which contains the text.
39
+ #
40
+ # :call-seq:
41
+ # read(Timestamp) -> Array
42
+
16
43
  def read(timestamp); []; end
17
44
 
18
- # Updates the content with text in the repository, which is
19
- # associated to the timestamp. Returns the timestamp.
45
+ ##
46
+ # Updates the content with given text in the repository, which is
47
+ # associated to the given timestamp. Returns the timestamp newly
48
+ # generated during the execution.
49
+ #
50
+ # If the given Timestamp is not existed as a Timestamp attached to
51
+ # text in the repository, raises MissingTimestampError.
52
+ #
53
+ # If the given text is empty, raises EmptyTextError.
54
+ #
55
+ # If the given text is identical to the text in the repository,
56
+ # does nothing. Returns the given timestamp itself.
57
+ #
58
+ # :call-seq:
59
+ # update(Timestamp, Array) -> Timestamp
60
+
20
61
  def update(timestamp, text); timestamp; end
21
62
 
63
+ ##
22
64
  # Deletes the content in the repository, which is associated to
23
65
  # the timestamp. Returns an array which contains the deleted text.
66
+ #
67
+ # :call-seq:
68
+ # delete(Timestamp) -> Array
69
+
24
70
  def delete(timestamp); []; end
25
71
 
72
+ ##
26
73
  # Finds all entries of text those have timestamps which mathes the
27
74
  # specified pattern of timestamp. Returns an array which contains
28
- # timestamps. A pattern must be one of the following:
75
+ # instances of Timestamp. If none of text was found, an empty
76
+ # array would be returned.
77
+ #
78
+ # A pattern must be one of the following:
29
79
  #
30
80
  # - yyyymoddhhmiss_lll : whole stamp
31
81
  # - yyyymoddhhmiss : omit millisecond part
@@ -37,17 +87,50 @@ module Textrepo
37
87
  # If `stamp_pattern` is omitted, the recent entries will be listed.
38
88
  # Then, how many entries are listed depends on the implementaiton
39
89
  # of the concrete repository class.
90
+ #
91
+ # :call-seq:
92
+ # entries(String) -> Array of Timestamp instances
93
+
40
94
  def entries(stamp_pattern = nil); []; end
41
95
 
96
+ ##
97
+ # Check the existence of text which is associated with the given
98
+ # timestamp.
99
+ #
100
+ # :call-seq:
101
+ # exist?(Timestamp) -> true or false
102
+
103
+ def exist?(timestamp); false; end
104
+
105
+ ##
106
+ # Searches a pattern (word or regular expression) in text those
107
+ # matches to a given timestamp pattern. Returns an Array of
108
+ # search results. If no match, returns an empty Array.
109
+ #
110
+ # See the document for Repository#entries about a timestamp
111
+ # pattern. When nil is passed as a timestamp pattern, searching
112
+ # applies to all text in the repository.
113
+ #
114
+ # Each entry of the result Array is constructed from 3 items, (1)
115
+ # timestamp (Timestamp), (2) line number (Integer), (3) matched
116
+ # line (String).
117
+ #
118
+ # :call-seq:
119
+ # search(String for pattern, String for Timestamp pattern) -> Array
120
+
121
+ def search(pattern, stamp_pattern = nil); []; end
122
+
42
123
  end
43
124
 
44
125
  require_relative 'file_system_repository'
45
126
 
127
+ ##
46
128
  # Returns an instance which derived from Textrepo::Repository class.
47
- # `conf` must be a Hash object which has a value of
48
- # `:repository_type` and `:repository_name` at least. Some concrete
49
- # class derived from Textrepo::Repository may require more key-value
50
- # pairs in `conf`.
129
+ # `conf` must be an object which can be accessed like a Hash object.
130
+ # And it must also has a value of `:repository_type` and
131
+ # `:repository_name` at least. Some concrete class derived from
132
+ # Textrepo::Repository may require more key-value pairs in `conf`.
133
+
51
134
  def init(conf)
52
135
  type = conf[:repository_type]
53
136
  klass_name = type.to_s.split(/_/).map(&:capitalize).join + "Repository"
@@ -1,23 +1,47 @@
1
1
  module Textrepo
2
2
  ##
3
- # Timstamp is generated from a Time object. It converts a time to
3
+ # Timestamp is generated from a Time object. It converts a time to
4
4
  # string in the obvious format, such "20201023122400".
5
5
  #
6
+ # Since the obvious format contains only year, month, day, hour,
7
+ # minute, and second, the resolution of time is a second. That is,
8
+ # two Time object those are different only in second will generates
9
+ # equal Timestamp objects.
10
+ #
11
+ # If a client program of Textrepo::Timestamp wants to distinguish
12
+ # those Time objects, an attribute `suffix` could be used.
13
+ #
14
+ # For example, the `suffix` will be converted into a 3 character
15
+ # string, such "012", "345", "678", ... etc. So, millisecond part
16
+ # of a Time object will be suitable to pass as `suffix` when
17
+ # creating a Timestamp object.
18
+
6
19
  class Timestamp
7
20
  include Comparable
8
21
 
9
- attr_reader :time, :suffix
22
+ ##
23
+ # Time object which generates the Timestamp object.
24
+
25
+ attr_reader :time
10
26
 
11
27
  ##
12
- # :call-seq:
13
- # new(Time, Integer = nil)
28
+ # An integer specified in `new` method to create the Timestamp object.
29
+
30
+ attr_reader :suffix
31
+
32
+ ##
33
+ # Creates a Timestamp object from a Time object. In addition, an
34
+ # Integer can be passed as a suffix use.
14
35
  #
36
+ # :call-seq:
37
+ # new(Time, Integer = nil) -> Timestamp
38
+
15
39
  def initialize(time, suffix = nil)
16
40
  @time = time
17
41
  @suffix = suffix
18
42
  end
19
43
 
20
- def <=>(other)
44
+ def <=>(other) # :nodoc:
21
45
  result = (self.time <=> other.time)
22
46
 
23
47
  sfx = self.suffix || 0
@@ -29,12 +53,10 @@ module Textrepo
29
53
  ##
30
54
  # Generate an obvious time string.
31
55
  #
32
- # ```
33
- # %Y %m %d %H %M %S suffix
34
- # "2020-12-30 12:34:56 (0 | nil)" => "20201230123456"
35
- # "2020-12-30 12:34:56 (7)" => "20201230123456_007"
36
- # ```
37
- #
56
+ # %Y %m %d %H %M %S suffix
57
+ # "2020-12-30 12:34:56 (0 | nil)" -> "20201230123456"
58
+ # "2020-12-30 12:34:56 (7)" -> "20201230123456_007"
59
+
38
60
  def to_s
39
61
  s = @time.strftime("%Y%m%d%H%M%S")
40
62
  s += "_#{"%03u" % @suffix}" unless @suffix.nil? || @suffix == 0
@@ -43,21 +65,41 @@ module Textrepo
43
65
 
44
66
  class << self
45
67
  ##
46
- # ```
47
- # yyyymoddhhmiss sfx yyyy mo dd hh mi ss sfx
48
- # "20201230123456" => "2020", "12", "30", "12", "34", "56"
49
- # "20201230123456_789" => "2020", "12", "30", "12", "34", "56", "789"
50
- # ```
68
+ # Splits a string which represents a timestamp into components.
69
+ # Each component represents a part of constructs to instantiate
70
+ # a Time object.
51
71
  #
72
+ # yyyymoddhhmiss sfx yyyy mo dd hh mi ss sfx
73
+ # "20201230123456" -> "2020", "12", "30", "12", "34", "56"
74
+ # "20201230123456_789" -> "2020", "12", "30", "12", "34", "56", "789"
75
+ #
76
+ # Raises InvalidTimestampStringError if nil was passed as an arguemnt.
77
+
52
78
  def split_stamp(stamp_str)
79
+ raise InvalidTimestampStringError, stamp_str if stamp_str.nil?
53
80
  # yyyy mo dd hh mi ss sfx
54
81
  a = [0..3, 4..5, 6..7, 8..9, 10..11, 12..13, 15..17].map {|r| stamp_str[r]}
55
82
  a[-1].nil? ? a[0..-2] : a
56
83
  end
57
84
 
85
+ ##
86
+ # Generate a Timestamp object from a string which represents a
87
+ # timestamp, such "20201028163400".
88
+ #
89
+ # Raises InvalidTimestampStringError if cannot convert the
90
+ # argument into a Timestamp object.
91
+ #
92
+ # :call-seq:
93
+ # parse_s("20201028163400") -> Timestamp
94
+ # parse_s("20201028163529_034") -> Timestamp
95
+
58
96
  def parse_s(stamp_str)
59
- year, mon, day, hour, min, sec , sfx = split_stamp(stamp_str).map(&:to_i)
60
- Timestamp.new(Time.new(year, mon, day, hour, min, sec), sfx)
97
+ begin
98
+ ye, mo, da, ho, mi, se, sfx = split_stamp(stamp_str).map(&:to_i)
99
+ Timestamp.new(Time.new(ye, mo, da, ho, mi, se), sfx)
100
+ rescue InvalidTimestampStringError, ArgumentError => _
101
+ raise InvalidTimestampStringError, stamp_str
102
+ end
61
103
  end
62
104
 
63
105
  end
@@ -1,3 +1,3 @@
1
1
  module Textrepo
2
- VERSION = '0.4.2'
2
+ VERSION = '0.5.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textrepo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mnbi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-23 00:00:00.000000000 Z
11
+ date: 2020-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -36,7 +36,6 @@ files:
36
36
  - ".travis.yml"
37
37
  - CHANGELOG.md
38
38
  - Gemfile
39
- - Gemfile.lock
40
39
  - LICENSE.txt
41
40
  - README.md
42
41
  - Rakefile
@@ -1,22 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- textrepo (0.4.2)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- minitest (5.14.2)
10
- rake (13.0.1)
11
-
12
- PLATFORMS
13
- ruby
14
-
15
- DEPENDENCIES
16
- bundler (~> 2.1)
17
- minitest (~> 5.0)
18
- rake (~> 13.0)
19
- textrepo!
20
-
21
- BUNDLED WITH
22
- 2.1.4