textrepo 0.4.3 → 0.5.2

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: 9af29cfb5945798beea4d787abc9ec472607841d6daea7dc8e62bf62a3ecc4b2
4
- data.tar.gz: 31f97fb8586b206b95f85feaaf653aa0e75a08379677e2019e067c5df03f0b3d
3
+ metadata.gz: c33d87733a93d357237cf217161c5d56c37e9b48d4f9a52b9a59ec8a945f6377
4
+ data.tar.gz: 2c8969d2ee93dfb1f6b84e49be43eb81630e63341ad77da2fbed3c59be465b78
5
5
  SHA512:
6
- metadata.gz: 934d63e39badb3ffa660b26e363a1caba78bd3dd906c8c7b24c79374659780def778e7f00b6902a1f80950055d885a954561a74b2301253a086195f48d05a79e
7
- data.tar.gz: d25676f649ba6308403f3a8e4e4d2f1d851b9d6026ebe8ee5668d7a8d37a4edc8135f13ead288a963afc81571cc1b0378499630c314334f5b3c223bf8e7dc3f4
6
+ metadata.gz: 711e6b19e280887f6558ecd62e39abf6146efbafe817ad9db329dd33c9fdd0e4b5d5feef4c8a246872c748142daee96518030ab378c2756dd1eb66ca40dbbade
7
+ data.tar.gz: 3b129941b8df7f41540a5088357bd562a11c3567635e8e26002a2a4222850b89a58d1147d0b3305b17835f0e720b86bf3a8b4ea0dccbfb01d098d00962bbe42d
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
  ## [Unreleased]
8
8
  Nothing to record here.
9
9
 
10
+ ## [0.5.2] - 2020-11-03
11
+ ### Changed
12
+ - Fix issue #34:
13
+ - fix FileSystemRepository#entries to accept "yyyymo" pattern as a
14
+ Timestamp pattern.
15
+ - Fix issue #33: fix typo in the doc for FileSystemRepository.new.
16
+ - Fix issue #31: unfriendly error message of Timestamp.parse_s.
17
+
18
+ ## [0.5.1] - 2020-11-02
19
+ ### Changed
20
+ - Fix issue #28.
21
+ - Modify `Repository#update` to do nothing when the given text is
22
+ identical to the one in the repository.
23
+
24
+ ## [0.5.0] - 2020-11-01
25
+ ### Added
26
+ - Add a new API `Repository#search`.
27
+ - Add a new API `Repository#exist?`. (0.4.3)
28
+
10
29
  ## [0.4.0] - 2020-10-14
11
30
  ### Added
12
31
  - 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,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require "open3"
2
3
 
3
4
  module Textrepo
4
5
 
@@ -19,6 +20,16 @@ module Textrepo
19
20
 
20
21
  attr_reader :extname
21
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
+
22
33
  ##
23
34
  # Default name for the repository which uses when no name is
24
35
  # specified in the configuration settings.
@@ -31,6 +42,11 @@ module Textrepo
31
42
 
32
43
  FAVORITE_EXTNAME = 'md'
33
44
 
45
+ ##
46
+ # Default searcher program to search text in the repository.
47
+
48
+ FAVORITE_SEARCHER = 'grep'
49
+
34
50
  ##
35
51
  # Creates a new repository object. The argument, `conf` must be a
36
52
  # Hash object. It should hold the follwoing values:
@@ -41,12 +57,33 @@ module Textrepo
41
57
  # - OPTIONAL: (if not specified, default values are used)
42
58
  # - :repository_name => basename of the root path for the repository
43
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
44
62
  #
45
63
  # The root path of the repository looks like the following:
46
64
  # - conf[:repository_base]/conf[:repository_name]
47
65
  #
48
- # Default values are set when `repository_name` and `default_extname`
66
+ # Default values are set when `:repository_name` and `:default_extname`
49
67
  # were not defined in `conf`.
68
+ #
69
+ # Be careful to set `:searcher_options`, it must be to specify the
70
+ # searcher behavior equivalent to `grep` with "-inRE". The
71
+ # default values for the searcher options is defined for BSD grep
72
+ # (default grep on macOS), GNU grep, and ripgrep (aka rg). They
73
+ # are:
74
+ #
75
+ # "grep" => ["-i", "-n", "-R", "-E"]
76
+ # "egrep" => ["-i", "-n", "-R"]
77
+ # "ggrep" => ["-i", "-n", "-R", "-E"]
78
+ # "gegrep" => ["-i", "-n", "-R"]
79
+ # "rg" => ["-S", "-n", "--no-heading", "--color", "never"]
80
+ #
81
+ # If use those searchers, it is not recommended to set
82
+ # `:searcher_options`. The default value works well in
83
+ # `textrepo`.
84
+ #
85
+ # :call-seq:
86
+ # new(Hash or Hash like object) -> FileSystemRepository
50
87
 
51
88
  def initialize(conf)
52
89
  super
@@ -55,14 +92,16 @@ module Textrepo
55
92
  @path = File.expand_path("#{name}", base)
56
93
  FileUtils.mkdir_p(@path)
57
94
  @extname = conf[:default_extname] || FAVORITE_EXTNAME
95
+ @searcher = find_searcher(conf[:searcher])
96
+ @searcher_options = conf[:searcher_options]
58
97
  end
59
98
 
60
99
  ##
61
100
  # Creates a file into the repository, which contains the specified
62
101
  # text and is associated to the timestamp.
63
-
102
+ #
64
103
  # :call-seq:
65
- # create(Timestamp, Array) => Timestamp
104
+ # create(Timestamp, Array) -> Timestamp
66
105
 
67
106
  def create(timestamp, text)
68
107
  abs = abspath(timestamp)
@@ -76,9 +115,9 @@ module Textrepo
76
115
  ##
77
116
  # Reads the file content in the repository. Then, returns its
78
117
  # content.
79
-
118
+ #
80
119
  # :call-seq:
81
- # read(Timestamp) => Array
120
+ # read(Timestamp) -> Array
82
121
 
83
122
  def read(timestamp)
84
123
  abs = abspath(timestamp)
@@ -93,31 +132,35 @@ module Textrepo
93
132
  ##
94
133
  # Updates the file content in the repository. A new timestamp
95
134
  # will be attached to the text.
96
-
135
+ #
136
+ # See the documentation of Repository#update to know about errors
137
+ # and constraints of this method.
138
+ #
97
139
  # :call-seq:
98
- # update(Timestamp, Array) => Timestamp
140
+ # update(Timestamp, Array) -> Timestamp
99
141
 
100
142
  def update(timestamp, text)
101
143
  raise EmptyTextError if text.empty?
102
- org_abs = abspath(timestamp)
103
- raise MissingTimestampError, timestamp unless FileTest.exist?(org_abs)
144
+ raise MissingTimestampError, timestamp unless exist?(timestamp)
145
+
146
+ # does nothing if given text is the same in the repository one
147
+ return timestamp if read(timestamp) == text
104
148
 
105
149
  # the text must be stored with the new timestamp
106
150
  new_stamp = Timestamp.new(Time.now)
107
- new_abs = abspath(new_stamp)
108
- write_text(new_abs, text)
151
+ write_text(abspath(new_stamp), text)
109
152
 
110
- # delete the original file in the repository
111
- FileUtils.remove_file(org_abs)
153
+ # delete the original text file in the repository
154
+ FileUtils.remove_file(abspath(timestamp))
112
155
 
113
156
  new_stamp
114
157
  end
115
158
 
116
159
  ##
117
160
  # Deletes the file in the repository.
118
-
161
+ #
119
162
  # :call-seq:
120
- # delete(Timestamp) => Array
163
+ # delete(Timestamp) -> Array
121
164
 
122
165
  def delete(timestamp)
123
166
  abs = abspath(timestamp)
@@ -131,9 +174,9 @@ module Textrepo
131
174
 
132
175
  ##
133
176
  # Finds entries of text those timestamp matches the specified pattern.
134
-
177
+ #
135
178
  # :call-seq:
136
- # entries(String = nil) => Array
179
+ # entries(String = nil) -> Array of Timestamp instances
137
180
 
138
181
  def entries(stamp_pattern = nil)
139
182
  results = []
@@ -142,9 +185,9 @@ module Textrepo
142
185
  when "yyyymoddhhmiss_lll".size
143
186
  stamp = Timestamp.parse_s(stamp_pattern)
144
187
  if exist?(stamp)
145
- results << stamp.to_s
188
+ results << stamp
146
189
  end
147
- when 0, "yyyymoddhhmiss".size, "yyyymodd".size
190
+ when 0, "yyyymoddhhmiss".size, "yyyymodd".size, "yyyymo".size
148
191
  results += find_entries(stamp_pattern)
149
192
  when 4 # "yyyy" or "modd"
150
193
  pat = nil
@@ -168,7 +211,7 @@ module Textrepo
168
211
  ##
169
212
  # Check the existence of text which is associated with the given
170
213
  # timestamp.
171
-
214
+ #
172
215
  # :call-seq:
173
216
  # exist?(Timestamp) -> true or false
174
217
 
@@ -176,6 +219,27 @@ module Textrepo
176
219
  FileTest.exist?(abspath(timestamp))
177
220
  end
178
221
 
222
+ ##
223
+ # Searches a pattern in all text. The given pattern is a word to
224
+ # search or a regular expression. The pattern would be passed to
225
+ # a searcher program as it passed.
226
+ #
227
+ # See the document for Textrepo::Repository#search to know about
228
+ # the search result.
229
+ #
230
+ # :call-seq:
231
+ # search(String for pattern, String for Timestamp pattern) -> Array
232
+
233
+ def search(pattern, stamp_pattern = nil)
234
+ result = nil
235
+ if stamp_pattern.nil?
236
+ result = invoke_searcher_at_repo_root(@searcher, pattern)
237
+ else
238
+ result = invoke_searcher_for_entries(@searcher, pattern, entries(stamp_pattern))
239
+ end
240
+ construct_search_result(result)
241
+ end
242
+
179
243
  # :stopdoc:
180
244
 
181
245
  private
@@ -205,9 +269,154 @@ module Textrepo
205
269
 
206
270
  def find_entries(stamp_pattern)
207
271
  Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
208
- timestamp_str(e)
272
+ begin
273
+ Timestamp.parse_s(timestamp_str(e))
274
+ rescue InvalidTimestampStringError => _
275
+ # Just ignore the erroneous entry, since it is not a text in
276
+ # the repository. It may be a garbage, or some kind of
277
+ # hidden stuff of the repository, ... etc.
278
+ nil
279
+ end
280
+ }.compact
281
+ end
282
+
283
+ ##
284
+ # The upper limit of files to search at one time. The value has
285
+ # no reason to select. It seems to me that not too much, not too
286
+ # little to handle in one process to search.
287
+
288
+ LIMIT_OF_FILES = 20
289
+
290
+ ##
291
+ # When no timestamp pattern was given, invoke the searcher with
292
+ # the repository root path as its argument and the recursive
293
+ # searching option. The search could be done in only one process.
294
+
295
+ def invoke_searcher_at_repo_root(searcher, pattern)
296
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
297
+ pattern, @path)
298
+ output = []
299
+ output += o.lines.map(&:chomp) if s.success? && (! o.empty?)
300
+ output
301
+ end
302
+
303
+ ##
304
+ # When a timestamp pattern was given, at first, list target files,
305
+ # then invoke the searcher for those files. Since the number of
306
+ # target files may be so much, it seems to be dangerous to pass
307
+ # all of them to a single search process at one time.
308
+ #
309
+ # One more thing to mention, the searcher, like `grep`, does not
310
+ # add the filename at the beginning of the search result line, if
311
+ # the target is one file. This behavior is not suitable in this
312
+ # purpose. The code below adds the filename when the target is
313
+ # one file.
314
+
315
+ def invoke_searcher_for_entries(searcher, pattern, entries)
316
+ output = []
317
+
318
+ num_of_entries = entries.size
319
+ if num_of_entries == 1
320
+ # If the search taget is one file, the output needs special
321
+ # treatment.
322
+ file = abspath(entries[0])
323
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
324
+ pattern, file)
325
+ if s.success? && (! o.empty)
326
+ output += o.lines.map { |line|
327
+ # add filename at the beginning of the search result line
328
+ [file, line.chomp].join(":")
329
+ }
330
+ end
331
+ elsif num_of_entries > LIMIT_OF_FILES
332
+ output += invoke_searcher_for_entries(searcher, pattern, entries[0..(LIMIT_OF_FILES - 1)])
333
+ output += invoke_searcher_for_entries(searcher, pattern, entries[LIMIT_OF_FILES..-1])
334
+ else
335
+ # When the number of target is less than the upper limit,
336
+ # invoke the searcher with all of target files as its
337
+ # arguments.
338
+ files = find_files(entries)
339
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
340
+ pattern, *files)
341
+ if s.success? && (! o.empty)
342
+ output += o.lines.map(&:chomp)
343
+ end
344
+ end
345
+
346
+ output
347
+ end
348
+
349
+ SEARCHER_OPTS = {
350
+ # case insensitive, print line number, recursive search, work as egrep
351
+ "grep" => ["-i", "-n", "-R", "-E"],
352
+ # case insensitive, print line number, recursive search
353
+ "egrep" => ["-i", "-n", "-R"],
354
+ # case insensitive, print line number, recursive search, work as gegrep
355
+ "ggrep" => ["-i", "-n", "-R", "-E"],
356
+ # case insensitive, print line number, recursive search
357
+ "gegrep" => ["-i", "-n", "-R"],
358
+ # smart case, print line number, no color
359
+ "rg" => ["-S", "-n", "--no-heading", "--color", "never"],
360
+ }
361
+
362
+ def find_searcher_options(searcher)
363
+ @searcher_options || SEARCHER_OPTS[File.basename(searcher)] || ""
364
+ end
365
+
366
+ def find_files(timestamps)
367
+ timestamps.map{|stamp| abspath(stamp)}
368
+ end
369
+
370
+ ##
371
+ # The argument must be an Array contains the searcher output.
372
+ # Each item is constructed from 3 parts:
373
+ # "<pathname>:<integer>:<text>"
374
+ #
375
+ # For example, it may looks like:
376
+ #
377
+ # "/somewhere/2020/11/20201101044300.md:18:foo is foo"
378
+ #
379
+ # Or it may contains more ":" in the text part as:
380
+ #
381
+ # "/somewhere/2020/11/20201101044500.md:119:apple:orange:grape"
382
+ #
383
+ # In the latter case, `split(":")` will split it too much. That is,
384
+ # the result will be:
385
+ #
386
+ # ["/somewhere/2020/11/20201101044500.md", "119", "apple", "orange", "grape"]
387
+ #
388
+ # Text part must be joined with ":".
389
+
390
+ def construct_search_result(output)
391
+ output.map { |line|
392
+ begin
393
+ pathname, num, *match_text = line.split(":")
394
+ [Timestamp.parse_s(timestamp_str(pathname)),
395
+ num.to_i,
396
+ match_text.join(":")]
397
+ rescue InvalidTimestampStringError, TypeError => _
398
+ raise InvalidSearchResultError, [@searcher, @searcher_options.join(" ")].join(" ")
399
+ end
400
+ }.compact
401
+ end
402
+
403
+ def find_searcher(program = nil)
404
+ candidates = [FAVORITE_SEARCHER]
405
+ candidates.unshift(program) unless program.nil? || candidates.include?(program)
406
+ search_paths = ENV["PATH"].split(":")
407
+ candidates.map { |prog|
408
+ find_in_paths(prog, search_paths)
409
+ }[0]
410
+ end
411
+
412
+ def find_in_paths(prog, paths)
413
+ paths.each { |p|
414
+ abspath = File.expand_path(prog, p)
415
+ return abspath if FileTest.exist?(abspath) && FileTest.executable?(abspath)
209
416
  }
417
+ nil
210
418
  end
419
+ # :startdoc:
211
420
 
212
421
  end
213
422
  end
@@ -27,7 +27,7 @@ module Textrepo
27
27
  ##
28
28
  # Stores text data into the repository with the specified timestamp.
29
29
  # Returns the timestamp.
30
-
30
+ #
31
31
  # :call-seq:
32
32
  # create(Timestamp, Array) -> Timestamp
33
33
 
@@ -36,16 +36,25 @@ module Textrepo
36
36
  ##
37
37
  # Reads text data from the repository, which is associated to the
38
38
  # timestamp. Returns an array which contains the text.
39
-
39
+ #
40
40
  # :call-seq:
41
41
  # read(Timestamp) -> Array
42
42
 
43
43
  def read(timestamp); []; end
44
44
 
45
45
  ##
46
- # Updates the content with text in the repository, which is
47
- # associated to the timestamp. Returns the timestamp.
48
-
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
+ #
49
58
  # :call-seq:
50
59
  # update(Timestamp, Array) -> Timestamp
51
60
 
@@ -54,7 +63,7 @@ module Textrepo
54
63
  ##
55
64
  # Deletes the content in the repository, which is associated to
56
65
  # the timestamp. Returns an array which contains the deleted text.
57
-
66
+ #
58
67
  # :call-seq:
59
68
  # delete(Timestamp) -> Array
60
69
 
@@ -63,8 +72,8 @@ module Textrepo
63
72
  ##
64
73
  # Finds all entries of text those have timestamps which mathes the
65
74
  # specified pattern of timestamp. Returns an array which contains
66
- # timestamps. If none of text was found, an empty array would be
67
- # returned.
75
+ # instances of Timestamp. If none of text was found, an empty
76
+ # array would be returned.
68
77
  #
69
78
  # A pattern must be one of the following:
70
79
  #
@@ -78,9 +87,9 @@ module Textrepo
78
87
  # If `stamp_pattern` is omitted, the recent entries will be listed.
79
88
  # Then, how many entries are listed depends on the implementaiton
80
89
  # of the concrete repository class.
81
-
90
+ #
82
91
  # :call-seq:
83
- # entries(String) -> Array
92
+ # entries(String) -> Array of Timestamp instances
84
93
 
85
94
  def entries(stamp_pattern = nil); []; end
86
95
 
@@ -92,6 +101,25 @@ module Textrepo
92
101
  # exist?(Timestamp) -> true or false
93
102
 
94
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
+
95
123
  end
96
124
 
97
125
  require_relative 'file_system_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,48 @@ 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 => e
101
+ emsg = if stamp_str.nil?
102
+ "(nil)"
103
+ elsif stamp_str.empty?
104
+ "(empty string)"
105
+ else
106
+ stamp_str
107
+ end
108
+ raise InvalidTimestampStringError, emsg
109
+ end
61
110
  end
62
111
 
63
112
  end
@@ -1,3 +1,3 @@
1
1
  module Textrepo
2
- VERSION = '0.4.3'
2
+ VERSION = '0.5.2'
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.3
4
+ version: 0.5.2
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-26 00:00:00.000000000 Z
11
+ date: 2020-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler