tins 1.61.0 → 1.62.0

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/tins/duration.rb CHANGED
@@ -1,74 +1,289 @@
1
1
  # frozen_string_literal: false
2
2
 
3
+ require 'tins/enum_joined'
4
+
3
5
  module Tins
4
6
  # A class to represent durations with support for formatting and parsing time
5
7
  # intervals.
6
8
  class Duration
7
9
  include Comparable
8
10
 
9
- # Returns the number of seconds represented by the given duration string
10
- # according to the provided template format.
11
+ # Raised when a duration string cannot be parsed by any of the
12
+ # provided templates.
11
13
  #
12
- # @param [String] string The duration string to parse.
13
- # @param [String] template for the duration format, see {#format}.
14
- # Default: '%S%d+%h:%m:%s.%f'
14
+ # @see .parse
15
+ class ParserError < ArgumentError
16
+ end
17
+
18
+ # Resolves a {HUMAN_CONNECTORS} preset to its connector hash.
15
19
  #
16
- # @return [Integer, Float] The number of (fractional) seconds of the duration.
20
+ # Shared by {#to_human} and {.parse_human} so that both
21
+ # accept the same +connector:+ keyword argument without
22
+ # duplicating the nil / Symbol / Hash dispatch.
17
23
  #
18
- # @raise [ArgumentError] If the string doesn't match the expected template format
24
+ # @see HUMAN_CONNECTORS
25
+ # @see STYLE_DEFAULT_CONNECTOR
26
+ module ConnectorResolver
27
+ # Resolves a connector specification to a +Hash+ with keys
28
+ # +:words+, +:two_words+, and +:last_word+.
29
+ #
30
+ # @param connector [Symbol, Hash, nil] A preset name from
31
+ # {HUMAN_CONNECTORS}, a custom hash, or +nil+ (derive from
32
+ # +style+ via {STYLE_DEFAULT_CONNECTOR}).
33
+ # @param style [Symbol, Hash] The label style passed alongside,
34
+ # used only when +connector+ is +nil+.
35
+ #
36
+ # @return [Hash] The resolved connector hash.
37
+ #
38
+ # @raise [ArgumentError] If +connector+ is neither +nil+, a
39
+ # +Symbol+, nor a +Hash+.
40
+ #
41
+ # @see HUMAN_CONNECTORS
42
+ # @see STYLE_DEFAULT_CONNECTOR
43
+ def resolve_connector(connector, style)
44
+ case connector
45
+ when nil
46
+ key = style.is_a?(Symbol) ? style : :abbrev
47
+ HUMAN_CONNECTORS.fetch(STYLE_DEFAULT_CONNECTOR.fetch(key))
48
+ when Symbol then HUMAN_CONNECTORS.fetch(connector)
49
+ when Hash then connector
50
+ else raise ArgumentError,
51
+ "connector must be a Symbol or Hash, got #{connector.class}"
52
+ end
53
+ end
54
+ end
55
+ private_constant :ConnectorResolver
56
+ include ConnectorResolver
57
+
58
+ # Built-in label styles for {#to_human}. Keys are the unit symbols
59
+ # (+:days+, +:hours+, +:minutes+, +:seconds+); values are the literal
60
+ # strings appended directly after each unit's numeric value.
19
61
  #
20
- # @example Basic parsing
21
- # Tins::Duration.parse('6+05:04:03', template: '%S%d+%h:%m:%s') # => 536643
22
- # Tins::Duration.parse('6+05:04:03.21', template: '%S%d+%h:%m:%s.%f') # => 536643.21
62
+ # +:words+ uses spelled-out units (the default); +:abbrev+ uses
63
+ # single-letter suffixes. A caller may instead pass their own +Hash+ with the
64
+ # same keys to {#to_human} to override either built-in, e.g. for another
65
+ # language.
23
66
  #
24
- # @example Parsing negative durations
25
- # Tins::Duration.parse('-6+05:04:03', template: '%S%d+%h:%m:%s') # => -536643
67
+ # @see #to_human
68
+ HUMAN_STYLES = {
69
+ abbrev: { days: 'd', hours: 'h', minutes: 'm', seconds: 's' }.freeze,
70
+ words: {
71
+ days: ' days', hours: ' hours',
72
+ minutes: ' minutes', seconds: ' seconds'
73
+ }.freeze
74
+ }.freeze
75
+
76
+ # Named connector presets for {#to_human} and {.parse_human}.
26
77
  #
27
- # @example Custom template parsing
28
- # Tins::Duration.parse('05:04:03.21', template: '%h:%m:%s.%f') # => 18243.21
78
+ # Each entry maps to a +Hash+ with three keys:
79
+ # +:words+ (3+ items), +:two_words+ (exactly 2), and
80
+ # +:last_word+ (before the final item, 3+ only).
29
81
  #
30
- # The parser supports the following directives in templates:
31
- # - `%S` - Sign indicator (optional negative sign)
32
- # - `%d` - Days component (integer)
33
- # - `%h` - Hours component (integer)
34
- # - `%m` - Minutes component (integer)
35
- # - `%s` - Seconds component (integer)
36
- # - `%f` - Fractional seconds component (decimal)
37
- # - `%%` - Literal percent character
82
+ # +:none+ produces compact output (`"1m30s"`); +:spaces+
83
+ # separates with single spaces (`"1 minutes 30 seconds"`);
84
+ # +:prose+ uses grammatical connectors
85
+ # (`"1 minutes, and 30 seconds"`).
38
86
  #
39
- # The parser is greedy and consumes as much of the input string as possible
40
- # for each directive.
41
- # If a directive expects a specific format but doesn't find it, an
42
- # ArgumentError is raised.
43
- def self.parse(string, template: '%S%d+%h:%m:%s.%f')
44
- s, t = string.to_s.dup, template.dup
45
- d, sd = 0, 1
46
- loop do
47
- t.sub!(/\A(%[Sdhmsf%]|.)/) { |directive|
48
- case directive
49
- when '%S' then s.sub!(/\A-?/) { sd *= -1 if _1 == ?-; '' }
50
- when '%d' then s.sub!(/\A\d+/) { d += 86_400 * _1.to_i; '' }
51
- when '%h' then s.sub!(/\A\d+/) { d += 3_600 * _1.to_i; '' }
52
- when '%m' then s.sub!(/\A\d+/) { d += 60 * _1.to_i; '' }
53
- when '%s' then s.sub!(/\A\d+/) { d += _1.to_i; '' }
54
- when '%f' then s.sub!(/\A\d+/) { d += Float(?. + _1); '' }
55
- when '%%' then
56
- if s[0] == ?%
57
- s[0] = ''
58
- else
59
- raise "expected %s, got #{s.inspect}"
60
- end
61
- else
62
- if directive == s[0]
63
- s[0] = ''
87
+ # {#to_human} and {.parse_human} accept a +connector:+
88
+ # keyword argument that is either a +Symbol+ naming one of
89
+ # these presets or a +Hash+ with the three keys for
90
+ # full control. When +connector:+ is +nil+ (the default),
91
+ # the preset is derived from +style:+ via
92
+ # {STYLE_DEFAULT_CONNECTOR}.
93
+ #
94
+ # @see HUMAN_STYLES
95
+ # @see STYLE_DEFAULT_CONNECTOR
96
+ # @see #to_human
97
+ # @see .parse_human
98
+ HUMAN_CONNECTORS = {
99
+ none: { words: '', two_words: '', last_word: '' }.freeze,
100
+ spaces: { words: ' ', two_words: ' ', last_word: ' ' }.freeze,
101
+ prose: {
102
+ words: ', ', two_words: ' and ', last_word: ', and '
103
+ }.freeze
104
+ }.freeze
105
+
106
+ # Maps each built-in {HUMAN_STYLES} key to its default
107
+ # {HUMAN_CONNECTORS} preset. Used when +connector:+ is
108
+ # not explicitly provided.
109
+ #
110
+ # @see HUMAN_CONNECTORS
111
+ # @see HUMAN_STYLES
112
+ STYLE_DEFAULT_CONNECTOR = { abbrev: :none, words: :prose }.freeze
113
+
114
+ class << self
115
+ include ConnectorResolver
116
+
117
+ # Parses a duration string against one or more templates,
118
+ # returning the total number of seconds.
119
+ #
120
+ # Templates are tried in order; the first one that matches wins.
121
+ # If no template matches, a {ParserError} is raised.
122
+ #
123
+ # @param [String] string The duration string to parse.
124
+ # @param [String, Array<String>, nil] template A single template
125
+ # string, an array of template strings tried in order, or +nil+
126
+ # (the built-in default `'%S%d+%h:%m:%s.%f'`).
127
+ #
128
+ # @return [Integer, Float] The number of (fractional) seconds.
129
+ #
130
+ # @raise [ParserError] If no template matches the string.
131
+ #
132
+ # @see #from_string
133
+ # @see #format
134
+ #
135
+ # @example Default template
136
+ # Tins::Duration.parse('6+05:04:03') # => 536643
137
+ # Tins::Duration.parse('6+05:04:03.21') # => 536643.21
138
+ #
139
+ # @example Parsing negative durations
140
+ # Tins::Duration.parse('-6+05:04:03') # => -536643
141
+ #
142
+ # @example Custom template
143
+ # Tins::Duration.parse('05:04:03.21', template: '%h:%m:%s.%f')
144
+ # # => 18243.21
145
+ #
146
+ # @example Multiple templates (first match wins)
147
+ # Tins::Duration.parse('1m 30s',
148
+ # template: [ '%m minutes %s seconds', '%S%d+%h:%m:%s.%f' ]
149
+ # )
150
+ def parse(string, template: nil)
151
+ templates = Array(template).dup
152
+ templates.empty? and templates << '%S%d+%h:%m:%s.%f'
153
+ templates.each do |template|
154
+ result = parse_template(string, template:) and return result
155
+ rescue ParserError
156
+ end
157
+ raise ParserError, "#{string.inspect} did not match any of the " \
158
+ "templates in #{template.inspect}"
159
+ end
160
+
161
+ # Parses a duration string against a single template.
162
+ #
163
+ # Supported directives:
164
+ # - `%S` - Sign indicator (optional negative sign)
165
+ # - `%d` - Days component (integer)
166
+ # - `%h` - Hours component (integer)
167
+ # - `%m` - Minutes component (integer)
168
+ # - `%s` - Seconds component (integer)
169
+ # - `%f` - Fractional seconds component (decimal)
170
+ # - `%%` - Literal percent character
171
+ #
172
+ # The parser is greedy and consumes as much of the input string as
173
+ # possible for each directive. If a directive expects a specific
174
+ # format but doesn't find it, a {ParserError} is raised.
175
+ def parse_template(string, template:)
176
+ s, t = string.to_s.dup, template.dup
177
+ d, sd = 0, 1
178
+ loop do
179
+ t.sub!(/\A(%[Sdhmsf%]|.)/) { |directive|
180
+ case directive
181
+ when '%S' then s.sub!(/\A-?/) { sd *= -1 if _1 == ?-; '' }
182
+ when '%d' then s.sub!(/\A\d+/) { d += 86_400 * _1.to_i; '' }
183
+ when '%h' then s.sub!(/\A\d+/) { d += 3_600 * _1.to_i; '' }
184
+ when '%m' then s.sub!(/\A\d+/) { d += 60 * _1.to_i; '' }
185
+ when '%s' then s.sub!(/\A\d+/) { d += _1.to_i; '' }
186
+ when '%f' then s.sub!(/\A\d+/) { d += Float(?. + _1); '' }
187
+ when '%%' then
188
+ if s[0] == ?%
189
+ s[0] = ''
190
+ else
191
+ raise ParserError, "expected %s, got #{s.inspect}"
192
+ end
64
193
  else
65
- raise ArgumentError, "expected #{t.inspect}, got #{s.inspect}"
194
+ if directive == s[0]
195
+ s[0] = ''
196
+ else
197
+ raise ParserError, "expected #{t.inspect}, got #{s.inspect}"
198
+ end
66
199
  end
200
+ ''
201
+ } or break
202
+ end
203
+ sd * d
204
+ end
205
+ private :parse_template
206
+
207
+ # Creates a new {Duration} instance by parsing the given string
208
+ # with {#parse}.
209
+ #
210
+ # @param [String] string The duration string to parse.
211
+ # @param [String, nil] template for the duration format, see
212
+ # {#format}. Default: +nil+ (falls back to the built-in format).
213
+ #
214
+ # @return [Duration] A new {Duration} instance.
215
+ #
216
+ # @raise [Duration::ParserError] If the string doesn't match the
217
+ # expected template format.
218
+ #
219
+ # @see .parse
220
+ #
221
+ # @example Basic usage
222
+ # Tins::Duration.from_string('6+05:04:03.21')
223
+ # # => #<Tins::Duration: 6+05:04:03.210>
224
+ #
225
+ # @example Custom template
226
+ # Tins::Duration.from_string('05:04:03', template: '%h:%m:%s')
227
+ # # => #<Tins::Duration: 0+05:04:03.000>
228
+ def from_string(string, template: nil)
229
+ new(parse(string, template:))
230
+ end
231
+
232
+ # Parses a human-readable duration string (the inverse of {#to_human})
233
+ # and returns a new {Duration} instance.
234
+ #
235
+ # All 15 possible +to_human+ output shapes (every subset of 1–4 units,
236
+ # in canonical order) are generated as templates for the given +style+,
237
+ # joined with the supplied connectors, and tried in order against
238
+ # {#parse}. The first template that matches wins.
239
+ #
240
+ # @param [String] string The human-readable duration string.
241
+ # @param style [Symbol, Hash] Built-in label style (+:abbrev+ or
242
+ # +:words+, see {HUMAN_STYLES}) or a custom +Hash+. Default: +:words+.
243
+ # @param connector [Symbol, Hash, nil] Connector preset
244
+ # ({HUMAN_CONNECTORS}) or a custom +Hash+ with keys
245
+ # +:words+, +:two_words+, +:last_word+.
246
+ # Default: +nil+ (derived from +style:+).
247
+ #
248
+ # @return [Duration] A new {Duration} instance.
249
+ #
250
+ # @raise [ArgumentError] If +style+ is neither a +Symbol+ nor a +Hash+.
251
+ # @raise [ParserError] If no generated template matches the string.
252
+ #
253
+ # @see #to_human
254
+ # @see .parse
255
+ #
256
+ # @example Spelled-out units (default)
257
+ # Tins::Duration.parse_human('1 minutes and 30 seconds')
258
+ # # => #<Tins::Duration: 00:01:30>
259
+ #
260
+ # @example Abbreviated
261
+ # Tins::Duration.parse_human('1m30s', style: :abbrev)
262
+ # # => #<Tins::Duration: 00:01:30>
263
+ #
264
+ # @example Round-trip with #to_human
265
+ # d = Tins::Duration.new(3725)
266
+ # Tins::Duration.parse_human(d.to_human).to_f # => 3725.0
267
+ def parse_human(string, style: :words, connector: nil)
268
+ labels =
269
+ case style
270
+ when Hash then HUMAN_STYLES[:abbrev].merge(style)
271
+ when Symbol then HUMAN_STYLES.fetch(style)
272
+ else raise ArgumentError,
273
+ "style must be a Symbol or Hash, got #{style.class}"
67
274
  end
68
- ''
69
- } or break
275
+ conn = resolve_connector(connector, style)
276
+ units = [ [ :days, '%d' ], [ :hours, '%h' ],
277
+ [ :minutes, '%m' ], [ :seconds, '%s' ] ]
278
+ templates = (1..4).reverse_each.flat_map do |n|
279
+ units.combination(n).map do |combo|
280
+ parts = combo.map { |k, d| "#{d}#{labels[k]}" }
281
+ .extend(EnumJoined)
282
+ parts.joined(**conn)
283
+ end
284
+ end
285
+ new(parse(string, template: templates))
70
286
  end
71
- sd * d
72
287
  end
73
288
 
74
289
  # Initializes a new Duration object with the specified number of seconds.
@@ -91,9 +306,15 @@ module Tins
91
306
  }
92
307
  end
93
308
 
94
- # Converts the original seconds value to a floating-point number.
309
+ # Returns the duration's magnitude in seconds as a floating-point
310
+ # number.
311
+ #
312
+ # A duration represents the time span between two points; the span
313
+ # itself is always non-negative. The sign is a direction qualifier,
314
+ # tracked separately — see {#negative?}.
95
315
  #
96
- # @return [Float] the original seconds value as a floating-point number
316
+ # @return [Float] The magnitude of the duration in seconds.
317
+ # Always non-negative.
97
318
  def to_f
98
319
  @original_seconds.to_f
99
320
  end
@@ -156,6 +377,74 @@ module Tins
156
377
  @fractional_seconds > 0
157
378
  end
158
379
 
380
+ # Returns a human-readable string representation of the duration.
381
+ # By default, uses spelled-out units (e.g. `1 minutes and 30 seconds`);
382
+ # pass +style: :abbrev+ for compact single-letter suffixes
383
+ # (e.g. `1m30s`), or pass your own +Hash+ keyed by the unit symbols
384
+ # (+:days+, +:hours+, +:minutes+, +:seconds+) for arbitrary wording.
385
+ #
386
+ # Fractional seconds are truncated (not rounded).
387
+ #
388
+ # @param max [Integer] Maximum number of time units to include,
389
+ # starting from the largest. Default: +nil+ (all units).
390
+ # @param connector [Symbol, Hash, nil] Connector preset
391
+ # ({HUMAN_CONNECTORS}) or a custom +Hash+ with keys
392
+ # +:words+, +:two_words+, +:last_word+.
393
+ # Default: +nil+ (derived from +style:+).
394
+ # @param style [Symbol, Hash] Built-in label style (+:abbrev+ or
395
+ # +:words+, see {HUMAN_STYLES}) or a custom +Hash+ mapping the unit
396
+ # symbols to label strings. A +Hash+ is merged onto the
397
+ # +:abbrev+ defaults, so omitting a key keeps the single-letter label.
398
+ # Default: +:words+.
399
+ #
400
+ # @raise [ArgumentError] If +style+ is neither a +Symbol+ nor a +Hash+.
401
+ #
402
+ # @return [String] Human-readable duration string.
403
+ #
404
+ # @see Tins::EnumJoined#joined
405
+ #
406
+ # @example Basic usage (default +:words+ style)
407
+ # Tins::Duration.new(90).to_human
408
+ # # => "1 minutes and 30 seconds"
409
+ # Tins::Duration.new(3725).to_human
410
+ # # => "1 hours, 2 minutes, and 5 seconds"
411
+ # Tins::Duration.new(86_400).to_human # => "1 days"
412
+ # Tins::Duration.new(0).to_human # => "0 seconds"
413
+ #
414
+ # @example Abbreviated units
415
+ # Tins::Duration.new(90).to_human(style: :abbrev)
416
+ # # => "1m30s"
417
+ #
418
+ # @example Custom labels (e.g. another language)
419
+ # Tins::Duration.new(90).to_human(
420
+ # style: { minutes: ' Min.', seconds: ' Sek.' }
421
+ # )
422
+ # # => "1 Min.30 Sek."
423
+ #
424
+ # @example Negative durations
425
+ # Tins::Duration.new(-90).to_human
426
+ # # => "-1 minutes and 30 seconds"
427
+ def to_human(max: nil, style: :words, connector: nil)
428
+ labels =
429
+ case style
430
+ when Hash then HUMAN_STYLES[:abbrev].merge(style)
431
+ when Symbol then HUMAN_STYLES.fetch(style)
432
+ else raise ArgumentError,
433
+ "style must be a Symbol or Hash, got #{style.class}"
434
+ end
435
+ conn = resolve_connector(connector, style)
436
+ parts = [
437
+ [ @days, :days ], [ @hours, :hours ],
438
+ [ @minutes, :minutes ], [ @seconds, :seconds ]
439
+ ].select { |v, _| v > 0 }
440
+ parts << [ 0, :seconds ] if parts.empty?
441
+ parts = parts.first(max) if max
442
+ items = parts.map { |v, k| "#{v}#{labels[k]}" }
443
+ .extend(EnumJoined)
444
+ result = items.joined(**conn)
445
+ @negative ? "-#{result}" : result
446
+ end
447
+
159
448
  # Formats the duration according to the given template and precision.
160
449
  #
161
450
  # The template string supports the following directives:
@@ -0,0 +1,68 @@
1
+ module Tins
2
+ # Adds +joined+ to any +Enumerable+, providing human-friendly
3
+ # sentence-style joining with configurable connectors, similar
4
+ # to Rails' +Array#to_sentence+.
5
+ #
6
+ # @example Polite usage (include into your own class):
7
+ # require 'tins/enum_joined'
8
+ #
9
+ # class MyList
10
+ # include Enumerable
11
+ # include Tins::EnumJoined
12
+ #
13
+ # def each
14
+ # %w[foo bar baz].each { |x| yield x }
15
+ # end
16
+ # end
17
+ #
18
+ # MyList.new.joined # => "foo, bar, and baz"
19
+ #
20
+ # @example Rude usage (mixin into ::Enumerable globally):
21
+ # require 'tins/xt/enum_joined'
22
+ #
23
+ # [1, 2, 3].joined # => "1, 2, and 3"
24
+ # Set['a', 'b', 'c'].joined # => "a, b, and c"
25
+ module EnumJoined
26
+ # Join all elements into a human-readable sentence.
27
+ #
28
+ # Uses three connectors, mirroring Rails' +Array#to_sentence+:
29
+ #
30
+ # | elements | connectors used |
31
+ # |----------|-------------------------------|
32
+ # | 0 | (none) — returns `""` |
33
+ # | 1 | (none) — returns the element |
34
+ # | 2 | +two_words+ |
35
+ # | 3+ | +words+ + +last_word+ |
36
+ #
37
+ # @param [String] words
38
+ # Separator between all items except the last two.
39
+ # Default: `', '`.
40
+ # @param [String] two_words
41
+ # Separator between the only two items.
42
+ # Default: `' and '`.
43
+ # @param [String] last_word
44
+ # Separator before the last item (3+ items only).
45
+ # Default: `', and '` (Oxford comma).
46
+ #
47
+ # @return [String] the joined sentence
48
+ #
49
+ # @example Default connectors
50
+ # [1, 2].joined # => "1 and 2"
51
+ # [1, 2, 3].joined # => "1, 2, and 3"
52
+ #
53
+ # @example Custom connectors
54
+ # [1, 2, 3].joined(words: ' / ', last_word: ' / ')
55
+ # # => "1 / 2 / 3"
56
+ def joined(words: ', ', two_words: ' and ', last_word: ', and ')
57
+ items = map(&:to_s)
58
+ case items.size
59
+ when 0 then ''
60
+ when 1 then items.first
61
+ when 2 then items.first + two_words + items.last
62
+ else
63
+ head, tail = items[0..-2], items.last
64
+ head.join(words) + last_word + tail
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,4 +1,37 @@
1
1
  module Tins
2
+ # Adds +squeeze+ and +squeeze!+ to any +Enumerable+, providing
3
+ # the same "collapse consecutive duplicates" semantics as
4
+ # +String#squeeze+ but for generic collections.
5
+ #
6
+ # Two elements are considered duplicates when +item == previous+.
7
+ # You can optionally restrict squeezing to a subset of elements by
8
+ # passing case-equality matchers (+===+) or a predicate block.
9
+ #
10
+ # @example Polite usage (include into your own class):
11
+ # require 'tins/enum_squeeze'
12
+ #
13
+ # class Repeater
14
+ # include Enumerable
15
+ # include Tins::EnumSqueeze
16
+ #
17
+ # def each
18
+ # [1, 1, 2, 2, 3].each { |i| yield i }
19
+ # end
20
+ # end
21
+ #
22
+ # Repeater.new.squeeze # => [1, 2, 3]
23
+ #
24
+ # @example Rude usage (mixin into ::Enumerable globally):
25
+ # require 'tins/xt/enum_squeeze'
26
+ #
27
+ # # Now any custom Enumerable gains squeeze/squeeze!:
28
+ # enum = (1..5).to_a # Array already has its own #squeeze,
29
+ # # so this is more useful for custom Enumerables:
30
+ # class Repeater
31
+ # include Enumerable
32
+ # def each = [1, 1, 2, 2, 3].each { |i| yield i }
33
+ # end
34
+ # Repeater.new.squeeze # => [1, 2, 3]
2
35
  module EnumSqueeze
3
36
  A_THING_THAT_IS_NOT_A_THING = Object.new.freeze
4
37
 
data/lib/tins/null.rb CHANGED
@@ -22,7 +22,7 @@ module Tins
22
22
  #
23
23
  # # You can use the null object:
24
24
  # user = find_user(id) || Tins::NULL
25
- # user.name # => "" (instead of nil)
25
+ # user.name.to_s # => "" (instead of nil)
26
26
  #
27
27
  # @example With NullPlus for debugging
28
28
  # user = find_user(id) || Tins::NullPlus.new(value: "Unknown", caller: caller)
data/lib/tins/version.rb CHANGED
@@ -1,6 +1,11 @@
1
+ # Version information for the Tins gem.
2
+ #
3
+ # This module is auto-generated by GemHadar and exposes the
4
+ # current release version as a set of constants. Do not edit it
5
+ # by hand; it is rewritten on every `rake version` invocation.
1
6
  module Tins
2
- # Tins version
3
- VERSION = '1.61.0'
7
+ # The current version of the Tins gem as a String.
8
+ VERSION = '1.62.0'
4
9
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
10
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
11
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -0,0 +1,7 @@
1
+ require 'tins/enum_joined'
2
+
3
+ module Tins
4
+ module ::Enumerable
5
+ include EnumJoined
6
+ end
7
+ end
data/lib/tins/xt.rb CHANGED
@@ -45,4 +45,5 @@ module Tins
45
45
  require 'tins/xt/deep_transform'
46
46
  require 'tins/xt/minimize'
47
47
  require 'tins/xt/enum_squeeze'
48
+ require 'tins/xt/enum_joined'
48
49
  end