rbnotes 0.4.8 → 0.4.13

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.
data/lib/rbnotes/conf.rb CHANGED
@@ -31,17 +31,26 @@ module Rbnotes
31
31
 
32
32
  DIRNAME_COMMON_CONF = ".config"
33
33
 
34
- def initialize(conf_path = nil) # :nodoc:
35
- @conf_path = conf_path || File.join(base_path, FILENAME_CONF)
36
-
34
+ def initialize(path = nil) # :nodoc:
37
35
  @conf = {}
38
- if FileTest.exist?(@conf_path)
39
- yaml_str = File.open(@conf_path, "r") { |f| f.read }
40
- @conf = YAML.load(yaml_str)
36
+
37
+ unless path.nil?
38
+ abspath = File.expand_path(path)
39
+ raise NoConfFileError, path unless FileTest.exist?(abspath)
40
+ @conf[:path] = abspath
41
41
  else
42
- @conf.merge(DEFAULT_VALUES)
42
+ @conf[:path] = default_conf_path
43
43
  end
44
- self
44
+
45
+ values =
46
+ if FileTest.exist?(@conf[:path])
47
+ yaml_str = File.open(@conf[:path], "r") { |f| f.read }
48
+ YAML.load(yaml_str)
49
+ else
50
+ DEFAULT_VALUES
51
+ end
52
+ @conf.merge!(values)
53
+ @conf[:config_home] = config_home
45
54
  end
46
55
 
47
56
  def_delegators(:@conf,
@@ -89,7 +98,7 @@ module Rbnotes
89
98
  :test => "_test",
90
99
  }
91
100
 
92
- def base_path
101
+ def config_home
93
102
  path = nil
94
103
  xdg, user = ["XDG_CONFIG_HOME", "HOME"].map{|n| ENV[n]}
95
104
  if xdg
@@ -97,12 +106,17 @@ module Rbnotes
97
106
  else
98
107
  path = File.join(user, DIRNAME_COMMON_CONF, DIRNAME_RBNOTES)
99
108
  end
100
- return path
109
+ path
110
+ end
111
+
112
+ def default_conf_path
113
+ File.join(config_home, FILENAME_CONF)
101
114
  end
102
- end
103
115
 
104
116
  # :startdoc:
105
117
 
118
+ end
119
+
106
120
  class << self
107
121
  ##
108
122
  # Gets the instance of Rbnotes::Conf. An optional argument is to
data/lib/rbnotes/error.rb CHANGED
@@ -9,9 +9,13 @@ module Rbnotes
9
9
  module ErrMsg
10
10
  MISSING_ARGUMENT = "missing argument: %s"
11
11
  MISSING_TIMESTAMP = "missing timestamp: %s"
12
- NO_EDITOR = "No editor is available: %s"
13
- PROGRAM_ABORT = "External program was aborted: %s"
14
- UNKNOWN_KEYWORD = "Unknown keyword: %s"
12
+ NO_EDITOR = "no editor is available: %s"
13
+ PROGRAM_ABORT = "external program was aborted: %s"
14
+ UNKNOWN_KEYWORD = "unknown keyword: %s"
15
+ INVALID_TIMESTAMP_PATTERN = "invalid timestamp pattern: %s"
16
+ NO_CONF_FILE = "no configuration file: %s"
17
+ NO_TEMPLATE_FILE = "no template file: %s"
18
+ INVALID_TIMESTAMP_PATTERN_AS_DATE = "invalid timestamp pattern as date: %s"
15
19
  end
16
20
 
17
21
  # :startdoc:
@@ -64,4 +68,52 @@ module Rbnotes
64
68
  super(ErrMsg::UNKNOWN_KEYWORD % keyword)
65
69
  end
66
70
  end
71
+
72
+ ##
73
+ # An error raised when an invalid timestamp pattern was specified.
74
+
75
+ class InvalidTimestampPatternError < Error
76
+ def initialize(pattern)
77
+ super(ErrMsg::INVALID_TIMESTAMP_PATTERN % pattern)
78
+ end
79
+ end
80
+
81
+ ##
82
+ # An error raised when the specified configuration file does not
83
+ # exist.
84
+
85
+ class NoConfFileError < Error
86
+ def initialize(filename)
87
+ super(ErrMsg::NO_CONF_FILE % filename)
88
+ end
89
+ end
90
+
91
+ ##
92
+ # An error raised when no arguments is spcified.
93
+
94
+ class NoArgumentError < Error
95
+ def initialize
96
+ super
97
+ end
98
+ end
99
+
100
+ ##
101
+ # An error raised when the specified template files does not exist.
102
+ #
103
+ class NoTemplateFileError < Error
104
+ def initialize(filepath)
105
+ super(ErrMsg::NO_TEMPLATE_FILE % filepath)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # An error raised when the specified pattern cannot be converted
111
+ # into a date.
112
+ #
113
+ class InvalidTimestampPatternAsDateError < Error
114
+ def initialize(pattern)
115
+ super(ErrMsg::INVALID_TIMESTAMP_PATTERN_AS_DATE % pattern)
116
+ end
117
+ end
118
+
67
119
  end
@@ -0,0 +1,101 @@
1
+ module Rbnotes
2
+ ##
3
+ # Calculates statistics of the repository.
4
+ class Statistics
5
+ include Enumerable
6
+
7
+ def initialize(conf)
8
+ @repo = Textrepo.init(conf)
9
+ @values = construct_values(@repo)
10
+ end
11
+
12
+ def total_report
13
+ puts @repo.entries.size
14
+ end
15
+
16
+ def yearly_report
17
+ self.each_year { |year, monthly_values|
18
+ num_of_notes = monthly_values.map { |_mon, values| values.size }.sum
19
+ puts "#{year}: #{num_of_notes}"
20
+ }
21
+ end
22
+
23
+ def monthly_report
24
+ self.each { |year, mon, values|
25
+ num_of_notes = values.size
26
+ puts "#{year}/#{mon}: #{num_of_notes}"
27
+ }
28
+ end
29
+
30
+ def each(&block)
31
+ if block.nil?
32
+ @values.map { |year, monthly_values|
33
+ monthly_values.each { |mon, values|
34
+ [year, mon, values]
35
+ }
36
+ }.to_enum(:each)
37
+ else
38
+ @values.each { |year, monthly_values|
39
+ monthly_values.each { |mon, values|
40
+ yield [year, mon, values]
41
+ }
42
+ }
43
+ end
44
+ end
45
+
46
+ def years
47
+ @values.keys
48
+ end
49
+
50
+ def months(year)
51
+ @values[year] || []
52
+ end
53
+
54
+ def each_year(&block)
55
+ if block.nil?
56
+ @values.map { |year, monthly_values|
57
+ [year, monthly_values]
58
+ }.to_enum(:each)
59
+ else
60
+ @values.each { |year, monthly_values|
61
+ yield [year, monthly_values]
62
+ }
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def construct_values(repo)
69
+ values = {}
70
+ repo.each { |timestamp, text|
71
+ value = StatisticValue.new(timestamp, text)
72
+ y = value.year
73
+ m = value.mon
74
+ values[y] ||= {}
75
+ values[y][m] ||= []
76
+
77
+ values[y][m] << value
78
+ }
79
+ values
80
+ end
81
+
82
+ class StatisticValue
83
+
84
+ attr_reader :lines
85
+
86
+ def initialize(timestamp, text)
87
+ @timestamp = timestamp
88
+ @lines = text.size
89
+ end
90
+
91
+ def year
92
+ @timestamp[:year]
93
+ end
94
+
95
+ def mon
96
+ @timestamp[:mon]
97
+ end
98
+ end
99
+
100
+ end
101
+ end
data/lib/rbnotes/utils.rb CHANGED
@@ -7,6 +7,20 @@ require "io/console/size"
7
7
  require "unicode/display_width"
8
8
 
9
9
  module Rbnotes
10
+
11
+ class << self
12
+
13
+ ##
14
+ # Retrieves the singleton instance of Rbnotes::Utils class.
15
+ # Typical usage is as follows:
16
+ #
17
+ # Rbnotes.utils.find_editor("emacsclient")
18
+ #
19
+ def utils
20
+ Utils.instance
21
+ end
22
+ end
23
+
10
24
  ##
11
25
  # Defines several utility methods those are intended to be used in
12
26
  # Rbnotes classes.
@@ -99,27 +113,88 @@ module Rbnotes
99
113
 
100
114
  def read_timestamp(args)
101
115
  str = args.shift || read_arg($stdin)
116
+ raise NoArgumentError if str.nil?
102
117
  Textrepo::Timestamp.parse_s(str)
103
118
  end
104
119
 
105
120
  ##
106
- # Reads an argument from the IO object. Typically, it is intended
107
- # to be used with STDIN.
121
+ # Generates multiple Textrepo::Timestamp objects from the command
122
+ # line arguments. When no argument is given, try to read from
123
+ # STDIN.
124
+ #
125
+ # When multiple strings those point the identical time are
126
+ # included the arguments (passed or read form STDIN), the
127
+ # redundant strings will be removed.
128
+ #
129
+ # The order of the arguments will be preserved into the return
130
+ # value, even if the redundant strings were removed.
108
131
  #
109
132
  # :call-seq:
110
- # read_arg(IO) -> String
133
+ # read_multiple_timestamps(args) -> [String]
134
+
135
+ def read_multiple_timestamps(args)
136
+ strings = args.size < 1 ? read_multiple_args($stdin) : args
137
+ raise NoArgumentError if (strings.nil? || strings.empty?)
138
+ strings.uniq.map { |str| Textrepo::Timestamp.parse_s(str) }
139
+ end
140
+
141
+ ##
142
+ # Reads timestamp patterns in an array of arguments. It supports
143
+ # keywords expansion and enumeration of week. The function is
144
+ # intended to be used from Commands::List#execute and
145
+ # Commands::Pick#execute.
146
+ #
147
+ def read_timestamp_patterns(args, enum_week: false)
148
+ patterns = nil
149
+ if enum_week
150
+ arg = args.shift
151
+ begin
152
+ patterns = timestamp_patterns_in_week(arg.dup)
153
+ rescue InvalidTimestampPatternAsDateError => _e
154
+ raise InvalidTimestampPatternAsDateError, args.unshift(arg)
155
+ end
156
+ else
157
+ patterns = expand_keyword_in_args(args)
158
+ end
159
+ patterns
160
+ end
161
+
162
+ ##
163
+ # Enumerates all timestamp patterns in a week which contains a
164
+ # given timestamp as a day of the week.
165
+ #
166
+ # The argument must be one of the followings:
167
+ # - "yyyymodd" (eg. "20201220")
168
+ # - "yymoddhhmiss" (eg. "20201220120048")
169
+ # - "yymoddhhmiss_sfx" (eg. "20201220120048_012")
170
+ # - "modd" (eg. "1220") (assums in the current year)
171
+ # - nil (assumes today)
172
+ #
173
+ # :call-seq:
174
+ # timestamp_patterns_in_week(String) -> [Array of Strings]
175
+ #
176
+ def timestamp_patterns_in_week(arg)
177
+ date_str = arg || Textrepo::Timestamp.now[0, 8]
178
+
179
+ case date_str.size
180
+ when "yyyymodd".size
181
+ # nothing to do
182
+ when "yyyymoddhhmiss".size, "yyyymoddhhmiss_sfx".size
183
+ date_str = date_str[0, 8]
184
+ when "modd".size
185
+ this_year = Time.now.year.to_s
186
+ date_str = "#{this_year}#{date_str}"
187
+ else
188
+ raise InvalidTimestampPatternAsDateError, arg
189
+ end
111
190
 
112
- def read_arg(io)
113
- # assumes the reading line looks like:
114
- #
115
- # foo bar baz ...
116
- #
117
- # then, only the first string is interested
118
191
  begin
119
- io.gets.split(":")[0].rstrip
120
- rescue NoMethodError => _
121
- nil
192
+ date = Date.parse(date_str)
193
+ rescue Date::Error => _e
194
+ raise InvalidTimestampPatternAsDateError, arg
122
195
  end
196
+
197
+ dates_in_week(date).map { |date| timestamp_pattern(date) }
123
198
  end
124
199
 
125
200
  ##
@@ -143,19 +218,20 @@ module Rbnotes
143
218
  # - "yeasterday" (or "ye")
144
219
  # - "this_week" (or "tw")
145
220
  # - "last_week" (or "lw")
221
+ # - "this_month" (or "tm")
222
+ # - "last_month" (or "lm")
146
223
  #
147
224
  # :call-seq:
148
225
  # expand_keyword_in_args(Array of Strings) -> Array of Strings
149
-
226
+ #
150
227
  def expand_keyword_in_args(args)
151
228
  return [nil] if args.empty?
152
229
 
153
230
  patterns = []
154
231
  while args.size > 0
155
232
  arg = args.shift
156
- if ["today", "to", "yesterday", "ye",
157
- "this_week", "tw", "last_week", "lw"].include?(arg)
158
- patterns.concat(Rbnotes.utils.expand_keyword(arg))
233
+ if KEYWORDS.include?(arg)
234
+ patterns.concat(expand_keyword(arg))
159
235
  else
160
236
  patterns << arg
161
237
  end
@@ -163,52 +239,32 @@ module Rbnotes
163
239
  patterns.sort.uniq
164
240
  end
165
241
 
166
- ##
167
- # Expands a keyword to timestamp strings.
168
- #
169
- # :call-seq:
170
- # expand_keyword(keyword as String) -> Array of timestamp Strings
171
-
172
- def expand_keyword(keyword)
173
- patterns = []
174
- case keyword
175
- when "today", "to"
176
- patterns << timestamp_pattern(date_of_today)
177
- when "yesterday", "ye"
178
- patterns << timestamp_pattern(date_of_yesterday)
179
- when "this_week", "tw"
180
- patterns.concat(dates_in_this_week.map { |d| timestamp_pattern(d) })
181
- when "last_week", "lw"
182
- patterns.concat(dates_in_last_week.map { |d| timestamp_pattern(d) })
183
- else
184
- raise UnknownKeywordError, keyword
185
- end
186
- patterns
187
- end
188
-
189
242
  ##
190
243
  # Makes a headline with the timestamp and subject of the notes, it
191
244
  # looks like as follows:
192
245
  #
193
- # |<------------------ console column size ------------------->|
194
- # +-- timestamp ---+ +- subject (the 1st line of each note) -+
195
- # | | | |
196
- # 20101010001000_123: I love Macintosh. [EOL]
197
- # 20100909090909_999: This is very very long long loooong subje[EOL]
198
- # ++
199
- # ^--- delimiter (2 characters)
246
+ # |<--------------- console column size -------------------->|
247
+ # | |+-- timestamp ---+ +-subject (the 1st line of note) -+
248
+ # | | | |
249
+ # | |20101010001000_123: I love Macintosh. [EOL]
250
+ # | |20100909090909_999: This is very very long looong subj[EOL]
251
+ # |<->| | |
252
+ # ^--- pad ++
253
+ # ^--- delimiter (2 characters)
200
254
  #
201
255
  # The subject part will truncate when it is long.
202
256
 
203
- def make_headline(timestamp, text)
257
+ def make_headline(timestamp, text, pad = nil)
204
258
  _, column = IO.console_size
205
259
  delimiter = ": "
206
260
  timestamp_width = timestamp.to_s.size
207
261
  subject_width = column - timestamp_width - delimiter.size - 1
262
+ subject_width -= pad.size unless pad.nil?
208
263
 
209
264
  subject = remove_heading_markup(text[0])
210
265
 
211
266
  ts_part = "#{timestamp.to_s} "[0..(timestamp_width - 1)]
267
+ ts_part.prepend(pad) unless pad.nil?
212
268
  sj_part = truncate_str(subject, subject_width)
213
269
 
214
270
  ts_part + delimiter + sj_part
@@ -217,6 +273,7 @@ module Rbnotes
217
273
  ##
218
274
  # Finds all notes those timestamps match to given patterns in the
219
275
  # given repository. Returns an Array contains Timestamp objects.
276
+ # The returned Array is sorted by Timestamp.
220
277
  #
221
278
  # :call-seq:
222
279
  # find_notes(Array of timestamp patterns, Textrepo::Repository)
@@ -230,6 +287,74 @@ module Rbnotes
230
287
  # :stopdoc:
231
288
 
232
289
  private
290
+
291
+ ##
292
+ # Reads an argument from the IO object. Typically, it is intended
293
+ # to be used with STDIN.
294
+ #
295
+ # :call-seq:
296
+ # read_arg(IO) -> String
297
+
298
+ def read_arg(io)
299
+ read_multiple_args(io)[0]
300
+ end
301
+
302
+ ##
303
+ # Reads arguments from the IO object. Typically, it is intended
304
+ # to be used with STDIN.
305
+ #
306
+ # :call-seq:
307
+ # read_multiple_arg(IO) -> [String]
308
+
309
+ def read_multiple_args(io)
310
+ strings = io.readlines
311
+ strings.map { |str|
312
+ # assumes the reading line looks like:
313
+ #
314
+ # foo bar baz ...
315
+ #
316
+ # then, only the first string is interested
317
+ begin
318
+ str.split(":")[0].rstrip
319
+ rescue NoMethodError => _
320
+ nil
321
+ end
322
+ }.compact
323
+ end
324
+
325
+ ##
326
+ # Expands a keyword to timestamp strings.
327
+ #
328
+ # :call-seq:
329
+ # expand_keyword(keyword as String) -> Array of timestamp Strings
330
+ #
331
+ def expand_keyword(keyword)
332
+ patterns = []
333
+ case keyword
334
+ when "today", "to"
335
+ patterns << timestamp_pattern(Date.today)
336
+ when "yesterday", "ye"
337
+ patterns << timestamp_pattern(Date.today.prev_day)
338
+ when "this_week", "tw"
339
+ patterns.concat(dates_in_this_week.map { |d| timestamp_pattern(d) })
340
+ when "last_week", "lw"
341
+ patterns.concat(dates_in_last_week.map { |d| timestamp_pattern(d) })
342
+ when "this_month", "tm"
343
+ patterns.concat(dates_in_this_month.map { |d| timestamp_pattern(d) })
344
+ when "last_month", "lm"
345
+ patterns.concat(dates_in_last_month.map { |d| timestamp_pattern(d) })
346
+ else
347
+ raise UnknownKeywordError, keyword
348
+ end
349
+ patterns
350
+ end
351
+
352
+ KEYWORDS = %w(
353
+ today to yesterday ye
354
+ this_week tw last_week lw
355
+ this_month tm last_month lm
356
+ )
357
+
233
358
  def search_in_path(name)
234
359
  search_paths = ENV["PATH"].split(":")
235
360
  found = search_paths.map { |path|
@@ -247,45 +372,61 @@ module Rbnotes
247
372
  date.strftime("%Y%m%d")
248
373
  end
249
374
 
250
- def date_of_today
251
- date(Time.now)
252
- end
253
-
254
- def date_of_yesterday
255
- date(Time.now).prev_day
256
- end
257
-
258
375
  def date(time)
259
376
  Date.new(time.year, time.mon, time.day)
260
377
  end
261
378
 
262
379
  def dates_in_this_week
263
- dates_in_week(start_date_in_this_week)
380
+ dates_in_week(Date.today)
264
381
  end
265
382
 
266
383
  def dates_in_last_week
267
- dates_in_week(start_date_in_last_week)
384
+ dates_in_week(Date.today.prev_day(7))
385
+ end
386
+
387
+ def dates_in_week(date)
388
+ start_date = start_date_of_week(date)
389
+ dates = [start_date]
390
+ 1.upto(6) { |i| dates << start_date.next_day(i) }
391
+ dates
268
392
  end
269
393
 
270
- def start_date_in_this_week
394
+ def start_date_of_week(date)
395
+ # week day in monday start calendar
396
+ date.prev_day((date.wday - 1) % 7)
397
+ end
398
+
399
+ def first_date_of_this_month
271
400
  today = Time.now
272
- Date.new(today.year, today.mon, today.day).prev_day(wday(today))
401
+ date(Time.new(today.year, today.mon, 1))
273
402
  end
274
403
 
275
- def start_date_in_last_week
276
- start_date_in_this_week.prev_day(7)
404
+ def dates_in_this_month
405
+ dates_in_month(first_date_of_this_month)
277
406
  end
278
407
 
279
- def wday(time)
280
- (time.wday - 1) % 7
408
+ def dates_in_last_month
409
+ dates_in_month(first_date_of_this_month.prev_month)
281
410
  end
282
411
 
283
- def dates_in_week(start_date)
284
- dates = [start_date]
285
- 1.upto(6) { |i| dates << start_date.next_day(i) }
412
+ def dates_in_month(first_date)
413
+ days = days_in_month(first_date.mon, leap: first_date.leap?)
414
+ dates = [first_date]
415
+ 1.upto(days - 1) { |i| dates << first_date.next_day(i) }
286
416
  dates
287
417
  end
288
418
 
419
+ DAYS = {
420
+ # 1 2 3 4 5 6 7 8 9 10 11 12
421
+ # Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
422
+ false => [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
423
+ true => [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
424
+ }
425
+
426
+ def days_in_month(mon, leap: false)
427
+ DAYS[leap][mon]
428
+ end
429
+
289
430
  def truncate_str(str, size)
290
431
  count = 0
291
432
  result = ""