rbnotes 0.4.10 → 0.4.15

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.
@@ -32,16 +32,7 @@ module Rbnotes::Commands
32
32
 
33
33
  def execute(args, conf)
34
34
  @opts = {}
35
- while args.size > 0
36
- arg = args.shift
37
- case arg
38
- when "-k", "--keep"
39
- @opts[:keep_timestamp] = true
40
- else
41
- args.unshift(arg)
42
- break
43
- end
44
- end
35
+ parse_opts(args)
45
36
 
46
37
  target_stamp = Rbnotes.utils.read_timestamp(args)
47
38
  editor = Rbnotes.utils.find_editor(conf[:editor])
@@ -100,5 +91,25 @@ editor program will be searched as same as add command. If none of
100
91
  editors is available, the execution fails.
101
92
  HELP
102
93
  end
94
+
95
+ # :stopdoc:
96
+
97
+ private
98
+
99
+ def parse_opts(args)
100
+ while args.size > 0
101
+ arg = args.shift
102
+ case arg
103
+ when "-k", "--keep"
104
+ @opts[:keep_timestamp] = true
105
+ else
106
+ args.unshift(arg)
107
+ break
108
+ end
109
+ end
110
+ end
111
+
112
+ # :startdoc:
113
+
103
114
  end
104
115
  end
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
@@ -7,12 +7,15 @@ module Rbnotes
7
7
  # :stopdoc:
8
8
 
9
9
  module ErrMsg
10
- MISSING_ARGUMENT = "Missing argument: %s"
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"
15
- INVALID_TIMESTAMP_PATTERN = "Invalid timestamp pattern: %s"
10
+ MISSING_ARGUMENT = "missing argument: %s"
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"
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"
16
19
  end
17
20
 
18
21
  # :startdoc:
@@ -75,4 +78,42 @@ module Rbnotes
75
78
  end
76
79
  end
77
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
+
78
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.
@@ -89,6 +103,19 @@ module Rbnotes
89
103
  tmpfile
90
104
  end
91
105
 
106
+ # Acceptable delimiters to separate a timestamp string for human
107
+ # being to read and input easily.
108
+ #
109
+ # Here is some examples:
110
+ #
111
+ # - "2021-04-15 15:34:56" -> "20210415153456" (a timestamp string)
112
+ # - "2020-04-15_15:34:56" -> (same as above)
113
+ # - "2020-04-15-15-34-56" -> (same as above)
114
+ # - "2020 04 15 15 34 56" -> (same as above)
115
+ # - "2020-04-15" -> "20200415" (a timestamp pattern)
116
+
117
+ TIMESTAMP_DELIMITERS = /[-:_\s]/
118
+
92
119
  ##
93
120
  # Generates a Textrepo::Timestamp object from a String which comes
94
121
  # from the command line arguments. When no argument is given,
@@ -99,27 +126,102 @@ module Rbnotes
99
126
 
100
127
  def read_timestamp(args)
101
128
  str = args.shift || read_arg($stdin)
129
+ raise NoArgumentError if str.nil?
130
+
131
+ str = remove_delimiters_from_timestamp_string(str)
102
132
  Textrepo::Timestamp.parse_s(str)
103
133
  end
104
134
 
105
135
  ##
106
- # Reads an argument from the IO object. Typically, it is intended
107
- # to be used with STDIN.
136
+ # Generates multiple Textrepo::Timestamp objects from the command
137
+ # line arguments. When no argument is given, try to read from
138
+ # STDIN.
139
+ #
140
+ # When multiple strings those point the identical time are
141
+ # included the arguments (passed or read form STDIN), the
142
+ # redundant strings will be removed.
143
+ #
144
+ # The order of the arguments will be preserved into the return
145
+ # value, even if the redundant strings were removed.
108
146
  #
109
147
  # :call-seq:
110
- # read_arg(IO) -> String
148
+ # read_multiple_timestamps(args) -> [String]
149
+
150
+ def read_multiple_timestamps(args)
151
+ strings = args.size < 1 ? read_multiple_args($stdin) : args
152
+ raise NoArgumentError if (strings.nil? || strings.empty?)
153
+ strings.uniq.map { |str|
154
+ str = remove_delimiters_from_timestamp_string(str)
155
+ Textrepo::Timestamp.parse_s(str)
156
+ }
157
+ end
158
+
159
+ ##
160
+ # Reads timestamp patterns in an array of arguments. It supports
161
+ # keywords expansion and enumeration of week. The function is
162
+ # intended to be used from Commands::List#execute and
163
+ # Commands::Pick#execute.
164
+ #
165
+ def read_timestamp_patterns(args, enum_week: false)
166
+ patterns = nil
167
+ if enum_week
168
+ patterns = []
169
+ while args.size > 0
170
+ arg = args.shift
171
+ begin
172
+ patterns.concat(timestamp_patterns_in_week(arg.dup))
173
+ rescue InvalidTimestampPatternAsDateError => _e
174
+ raise InvalidTimestampPatternAsDateError, args.unshift(arg)
175
+ end
176
+ end
177
+ else
178
+ patterns = expand_keyword_in_args(args)
179
+ end
180
+ patterns
181
+ end
182
+
183
+ ##
184
+ # Enumerates all timestamp patterns in a week which contains a
185
+ # given timestamp as a day of the week.
186
+ #
187
+ # The argument must be one of the followings:
188
+ # - "yyyymodd" (eg. "20201220")
189
+ # - "yymoddhhmiss" (eg. "20201220120048")
190
+ # - "yymoddhhmiss_sfx" (eg. "20201220120048_012")
191
+ # - "modd" (eg. "1220") (assums in the current year)
192
+ # - nil (assumes today)
193
+ #
194
+ # :call-seq:
195
+ # timestamp_patterns_in_week(String) -> [Array of Strings]
196
+ #
197
+ def timestamp_patterns_in_week(arg)
198
+ date_str = nil
199
+
200
+ if arg
201
+ date_str = remove_delimiters_from_timestamp_string(arg)
202
+ else
203
+ date_str = Textrepo::Timestamp.now[0, 8]
204
+ end
205
+
206
+ case date_str.size
207
+ when "yyyymodd".size
208
+ # nothing to do
209
+ when "yyyymoddhhmiss".size, "yyyymoddhhmiss_sfx".size
210
+ date_str = date_str[0, 8]
211
+ when "modd".size
212
+ this_year = Time.now.year.to_s
213
+ date_str = "#{this_year}#{date_str}"
214
+ else
215
+ raise InvalidTimestampPatternAsDateError, arg
216
+ end
111
217
 
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
218
  begin
119
- io.gets.split(":")[0].rstrip
120
- rescue NoMethodError => _
121
- nil
219
+ date = Date.parse(date_str)
220
+ rescue Date::Error => _e
221
+ raise InvalidTimestampPatternAsDateError, arg
122
222
  end
223
+
224
+ dates_in_week(date).map { |date| timestamp_pattern(date) }
123
225
  end
124
226
 
125
227
  ##
@@ -143,72 +245,53 @@ module Rbnotes
143
245
  # - "yeasterday" (or "ye")
144
246
  # - "this_week" (or "tw")
145
247
  # - "last_week" (or "lw")
248
+ # - "this_month" (or "tm")
249
+ # - "last_month" (or "lm")
146
250
  #
147
251
  # :call-seq:
148
252
  # expand_keyword_in_args(Array of Strings) -> Array of Strings
149
-
253
+ #
150
254
  def expand_keyword_in_args(args)
151
255
  return [nil] if args.empty?
152
256
 
153
257
  patterns = []
154
258
  while args.size > 0
155
259
  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))
260
+ if KEYWORDS.include?(arg)
261
+ patterns.concat(expand_keyword(arg))
159
262
  else
160
263
  patterns << arg
161
264
  end
162
265
  end
163
- patterns.sort.uniq
164
- end
165
-
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
266
+ patterns.uniq.sort
187
267
  end
188
268
 
189
269
  ##
190
270
  # Makes a headline with the timestamp and subject of the notes, it
191
271
  # looks like as follows:
192
272
  #
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)
273
+ # |<--------------- console column size -------------------->|
274
+ # | |+-- timestamp ---+ +-subject (the 1st line of note) -+
275
+ # | | | |
276
+ # | |20101010001000_123: I love Macintosh. [EOL]
277
+ # | |20100909090909_999: This is very very long looong subj[EOL]
278
+ # |<->| | |
279
+ # ^--- pad ++
280
+ # ^--- delimiter (2 characters)
200
281
  #
201
282
  # The subject part will truncate when it is long.
202
283
 
203
- def make_headline(timestamp, text)
284
+ def make_headline(timestamp, text, pad = nil)
204
285
  _, column = IO.console_size
205
286
  delimiter = ": "
206
287
  timestamp_width = timestamp.to_s.size
207
288
  subject_width = column - timestamp_width - delimiter.size - 1
289
+ subject_width -= pad.size unless pad.nil?
208
290
 
209
291
  subject = remove_heading_markup(text[0])
210
292
 
211
293
  ts_part = "#{timestamp.to_s} "[0..(timestamp_width - 1)]
294
+ ts_part.prepend(pad) unless pad.nil?
212
295
  sj_part = truncate_str(subject, subject_width)
213
296
 
214
297
  ts_part + delimiter + sj_part
@@ -217,6 +300,7 @@ module Rbnotes
217
300
  ##
218
301
  # Finds all notes those timestamps match to given patterns in the
219
302
  # given repository. Returns an Array contains Timestamp objects.
303
+ # The returned Array is sorted by Timestamp.
220
304
  #
221
305
  # :call-seq:
222
306
  # find_notes(Array of timestamp patterns, Textrepo::Repository)
@@ -227,20 +311,86 @@ module Rbnotes
227
311
  }.flatten.sort{ |a, b| b <=> a }.uniq
228
312
  end
229
313
 
314
+ # :stopdoc:
315
+
316
+ private
317
+
230
318
  ##
231
- # Enumerates all timestamp patterns in a week which contains a
232
- # given timestamp as a day of the week.
319
+ # Reads an argument from the IO object. Typically, it is intended
320
+ # to be used with STDIN.
233
321
  #
234
322
  # :call-seq:
235
- # timestamp_patterns_in_week(timestamp) -> [Array of Strings]
323
+ # read_arg(IO) -> String
236
324
 
237
- def timestamp_patterns_in_week(timestamp)
238
- dates_in_week(start_date_in_the_week(timestamp.time)).map { |date| timestamp_pattern(date) }
325
+ def read_arg(io)
326
+ read_multiple_args(io)[0]
239
327
  end
240
328
 
241
- # :stopdoc:
329
+ ##
330
+ # Reads arguments from the IO object. Typically, it is intended
331
+ # to be used with STDIN.
332
+ #
333
+ # :call-seq:
334
+ # read_multiple_arg(IO) -> [String]
335
+
336
+ def read_multiple_args(io)
337
+ strings = io.readlines
338
+ strings.map { |str|
339
+ # assumes the reading line looks like:
340
+ #
341
+ # foo bar baz ...
342
+ #
343
+ # then, only the first string is interested
344
+ begin
345
+ str.split(":")[0].rstrip
346
+ rescue NoMethodError => _
347
+ nil
348
+ end
349
+ }.compact
350
+ end
351
+
352
+ def remove_delimiters_from_timestamp_string(stamp_str) # :nodoc:
353
+ str = stamp_str.gsub(TIMESTAMP_DELIMITERS, "")
354
+ base_size = "yyyymiddhhmoss".size
355
+ if str.size > base_size # when suffix is specified
356
+ str = str[0...base_size] + "_" + str[base_size..-1]
357
+ end
358
+ str
359
+ end
360
+
361
+ ##
362
+ # Expands a keyword to timestamp strings.
363
+ #
364
+ # :call-seq:
365
+ # expand_keyword(keyword as String) -> Array of timestamp Strings
366
+ #
367
+ def expand_keyword(keyword)
368
+ patterns = []
369
+ case keyword
370
+ when "today", "to"
371
+ patterns << timestamp_pattern(Date.today)
372
+ when "yesterday", "ye"
373
+ patterns << timestamp_pattern(Date.today.prev_day)
374
+ when "this_week", "tw"
375
+ patterns.concat(dates_in_this_week.map { |d| timestamp_pattern(d) })
376
+ when "last_week", "lw"
377
+ patterns.concat(dates_in_last_week.map { |d| timestamp_pattern(d) })
378
+ when "this_month", "tm"
379
+ patterns.concat(dates_in_this_month.map { |d| timestamp_pattern(d) })
380
+ when "last_month", "lm"
381
+ patterns.concat(dates_in_last_month.map { |d| timestamp_pattern(d) })
382
+ else
383
+ raise UnknownKeywordError, keyword
384
+ end
385
+ patterns
386
+ end
387
+
388
+ KEYWORDS = %w(
389
+ today to yesterday ye
390
+ this_week tw last_week lw
391
+ this_month tm last_month lm
392
+ )
242
393
 
243
- private
244
394
  def search_in_path(name)
245
395
  search_paths = ENV["PATH"].split(":")
246
396
  found = search_paths.map { |path|
@@ -258,49 +408,61 @@ module Rbnotes
258
408
  date.strftime("%Y%m%d")
259
409
  end
260
410
 
261
- def date_of_today
262
- date(Time.now)
263
- end
264
-
265
- def date_of_yesterday
266
- date(Time.now).prev_day
267
- end
268
-
269
411
  def date(time)
270
412
  Date.new(time.year, time.mon, time.day)
271
413
  end
272
414
 
273
415
  def dates_in_this_week
274
- dates_in_week(start_date_in_this_week)
416
+ dates_in_week(Date.today)
275
417
  end
276
418
 
277
419
  def dates_in_last_week
278
- dates_in_week(start_date_in_last_week)
420
+ dates_in_week(Date.today.prev_day(7))
279
421
  end
280
422
 
281
- def start_date_in_this_week
282
- start_date_in_the_week(Time.now)
423
+ def dates_in_week(date)
424
+ start_date = start_date_of_week(date)
425
+ dates = [start_date]
426
+ 1.upto(6) { |i| dates << start_date.next_day(i) }
427
+ dates
283
428
  end
284
429
 
285
- def start_date_in_last_week
286
- start_date_in_this_week.prev_day(7)
430
+ def start_date_of_week(date)
431
+ # week day in monday start calendar
432
+ date.prev_day((date.wday - 1) % 7)
287
433
  end
288
434
 
289
- def start_date_in_the_week(time)
290
- parts = [:year, :mon, :day].map { |sym| time.send(sym) }
291
- Date.new(*parts).prev_day(wday(time))
435
+ def first_date_of_this_month
436
+ today = Time.now
437
+ date(Time.new(today.year, today.mon, 1))
292
438
  end
293
439
 
294
- def wday(time)
295
- (time.wday - 1) % 7
440
+ def dates_in_this_month
441
+ dates_in_month(first_date_of_this_month)
296
442
  end
297
443
 
298
- def dates_in_week(start_date)
299
- dates = [start_date]
300
- 1.upto(6) { |i| dates << start_date.next_day(i) }
444
+ def dates_in_last_month
445
+ dates_in_month(first_date_of_this_month.prev_month)
446
+ end
447
+
448
+ def dates_in_month(first_date)
449
+ days = days_in_month(first_date.mon, leap: first_date.leap?)
450
+ dates = [first_date]
451
+ 1.upto(days - 1) { |i| dates << first_date.next_day(i) }
301
452
  dates
302
453
  end
303
454
 
455
+ DAYS = {
456
+ # 1 2 3 4 5 6 7 8 9 10 11 12
457
+ # Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
458
+ false => [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
459
+ true => [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
460
+ }
461
+
462
+ def days_in_month(mon, leap: false)
463
+ DAYS[leap][mon]
464
+ end
465
+
304
466
  def truncate_str(str, size)
305
467
  count = 0
306
468
  result = ""