tins 1.58.0 → 1.60.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 564a0581732d7b53d9882960e13662d1d279f7edf7d1bc4c6d717a5dc2f27188
4
- data.tar.gz: 36ff7350160cdcc8c606ba6c91aa74d9fa7c0617caebbe7af80473f1ce499829
3
+ metadata.gz: c17298f1ee629a9226613814a7590bf107ab45e978cedf938407dd3160d1174a
4
+ data.tar.gz: b0246606c5dee0de323b62f91623a5d8f3b37f7da486c800e0fbef87a5a538a5
5
5
  SHA512:
6
- metadata.gz: 7bf1f364ec0c9a8b9e8db2fbcde42b9b93574f09fc2ebcadc089c62a4510cb766534e85ea40f2073311d36a9e64644194877e6673f3888f1a1327504004b6f6f
7
- data.tar.gz: cdd850325aff0d57ccd781d8859e940161952bff48d77d72e260785ea4029fe57bfb18123471d83c1548a3e56105b2190fcb3e273aa0eb1c4e26710a0103c3df
6
+ metadata.gz: a24706b341766327830215b3ccb48cc498087e7559c1b230757e363cf2db9797737bf32d05e91e05a9eabda331918131e758ea899985865162ea5c65ba308e13
7
+ data.tar.gz: d55dea1d7ad3db342c8e60097bf716f8f74937f340c63ac76fc1bd6cbeb516ffd853e5cee7ad674b16143f318b36d424f4cd4345f02b71a6689aee8756733197
data/CHANGES.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-02 v1.60.0
4
+
5
+ * Add unpadded `%H`, `%M`, and `%u` format directives to `Duration#format` for
6
+ unpadded hours, minutes, and seconds, complementing the existing zero-padded
7
+ `%h`, `%m`, and `%s`
8
+ * Update the directive regex in `format` from `/%[DdhmSs%]/` to
9
+ `/%[DdHhMmuSs%]/` to match the new directives
10
+ * Extend YARD documentation with an `@example` block demonstrating unpadded
11
+ components in a natural-language template
12
+ * Add `test_format_unpadded_directives` covering all three new directives in
13
+ both template and standalone usage
14
+ * Fix `assert_equal` argument order in `test_parse_percentage`
15
+
16
+ ## 2026-09-25 v1.59.0
17
+
18
+ * Add `*sels` and `&block` parameters to `squeeze` and `squeeze!`, allowing
19
+ squeezing to be restricted to elements matching case-equality (`===`)
20
+ selectors or a predicate block
21
+ * Raise `ArgumentError` when both `*sels` and `&block` are provided
22
+ * Change hot-loop comparison to `item != prev || !block.(item)` so non-matching
23
+ elements are never collapsed
24
+ * Forward `(*sels, &block)` from `squeeze!` to `squeeze`
25
+ * Extend YARD documentation with `@param` tags and `@example` blocks covering
26
+ range, type, predicate, and `String`/`Enumerable` invariance
27
+ * Add 12 new test cases in `tests/enum_squeeze_test.rb` covering single/multi
28
+ value selectors, type selectors, range selectors, predicate blocks, the
29
+ mutual-exclusion guard, the `String#squeeze`/`Enumerable#squeeze` invariance
30
+ property, and `squeeze!` forwarding with selectors and blocks
31
+
3
32
  ## 2026-09-24 v1.58.0
4
33
 
5
34
  * Freeze the `UNSET` sentinel constant in `Tins::Delegate` to prevent
data/lib/tins/duration.rb CHANGED
@@ -161,8 +161,11 @@ module Tins
161
161
  # The template string supports the following directives:
162
162
  # - `%S` - Sign indicator (negative sign if duration is negative)
163
163
  # - `%d` - Days component
164
+ # - `%H` - Hours component (unpadded)
164
165
  # - `%h` - Hours component (zero-padded to 2 digits)
166
+ # - `%M` - Minutes component (unpadded)
165
167
  # - `%m` - Minutes component (zero-padded to 2 digits)
168
+ # - `%u` - Seconds component (unpadded)
166
169
  # - `%s` - Seconds component (zero-padded to 2 digits)
167
170
  # - `%f` - Fractional seconds component (without the leading decimal point)
168
171
  # - `%D` - Smart format (automatically includes days, fractional seconds, and sign)
@@ -186,13 +189,21 @@ module Tins
186
189
  #
187
190
  # @example Custom precision
188
191
  # duration.format('%s.%f', precision: 2) # => "04.12"
192
+ #
193
+ # @example Unpadded components
194
+ # duration = Tins::Duration.new(93_784)
195
+ # duration.format('%d day(s), %H hours, %M minutes, and %u seconds')
196
+ # # => "1 day(s), 2 hours, 3 minutes, and 4 seconds"
189
197
  def format(template = '%S%d+%h:%m:%s.%f', precision: nil)
190
- result = template.gsub(/%[DdhmSs%]/) { |directive|
198
+ result = template.gsub(/%[DdHhMmuSs%]/) { |directive|
191
199
  case directive
192
200
  when '%S' then ?- if negative?
193
201
  when '%d' then @days
202
+ when '%H' then '%u' % @hours
194
203
  when '%h' then '%02u' % @hours
204
+ when '%M' then '%u' % @minutes
195
205
  when '%m' then '%02u' % @minutes
206
+ when '%u' then '%u' % @seconds
196
207
  when '%s' then '%02u' % @seconds
197
208
  when '%D' then format_smart
198
209
  when '%%' then '%'
@@ -10,6 +10,19 @@ module Tins
10
10
  # Two elements are considered duplicates when
11
11
  # +item == previous+ is true.
12
12
  #
13
+ # Optionally restrict squeezing to elements matching one or
14
+ # more selectors via case-equality (+===+), mirroring how
15
+ # +String#squeeze+ uses character-class expressions.
16
+ # Elements not matching any selector are never collapsed.
17
+ #
18
+ # @param [Array] sels
19
+ # Case-equality matchers (types, ranges, regexes, or any
20
+ # object responding to +===+) defining which elements are
21
+ # eligible for squeezing.
22
+ # @param [Proc, nil] block
23
+ # A predicate; when it returns false for an element, that
24
+ # element is never collapsed. Mutually exclusive with +sels+.
25
+ #
13
26
  # @return [Array] a new array with consecutive duplicates removed
14
27
  #
15
28
  # @example Basic usage:
@@ -20,12 +33,40 @@ module Tins
20
33
  #
21
34
  # @example Mirrors String#squeeze semantics:
22
35
  # "aabbcc".squeeze # => "abc"
23
- def squeeze
36
+ #
37
+ # @example Squeeze only elements in a range:
38
+ # [1, 3, 2, 2, 4, 6, 3, 3, 7].squeeze(2..3)
39
+ # # => [1, 3, 2, 4, 6, 3, 7]
40
+ #
41
+ # @example Squeeze only elements of a given type:
42
+ # [[1], '1', [1], [1], '1', '1', [1]].squeeze(String)
43
+ # # => [[1], "1", [1], [1], "1", [1]]
44
+ #
45
+ # @example Squeeze using a predicate block:
46
+ # [1, 3, 2, 2, 4, 6, 3, 3, 7].squeeze(&:even?)
47
+ # # => [1, 3, 2, 4, 6, 3, 3, 7]
48
+ #
49
+ # @example Enumerable/String invariance:
50
+ # str = 'fooaabaaz'
51
+ # str.squeeze(?a) == str.split('').squeeze(?a).join
52
+ # # => true
53
+ def squeeze(*sels, &block)
54
+ !sels.empty? && block and raise ArgumentError,
55
+ 'you cannot pass both *sels and &block'
56
+
57
+ unless block
58
+ if sels.empty?
59
+ block = -> x { true }
60
+ else
61
+ block = -> x { sels.any? { _1 === x } }
62
+ end
63
+ end
64
+
24
65
  result = []
25
66
  prev = A_THING_THAT_IS_NOT_A_THING
26
67
 
27
68
  each do |item|
28
- unless item == prev
69
+ if item != prev || !block.(item)
29
70
  result << item
30
71
  prev = item
31
72
  end
@@ -39,6 +80,11 @@ module Tins
39
80
  # Destructive counterpart to +squeeze+: the receiver is modified
40
81
  # via +replace+; no new object is allocated.
41
82
  #
83
+ # @param [Array] sels
84
+ # Case-equality matchers (see +squeeze+).
85
+ # @param [Proc, nil] block
86
+ # A predicate (see +squeeze+).
87
+ #
42
88
  # @return [self] if one or more consecutive duplicates were removed
43
89
  # @return [nil] if the receiver was already free of consecutive
44
90
  # duplicates
@@ -55,9 +101,14 @@ module Tins
55
101
  # b = [1, 2, 3]
56
102
  # b.squeeze! # => nil
57
103
  # b # => [1, 2, 3]
58
- def squeeze!
104
+ #
105
+ # @example Squeezing with a selector:
106
+ # a = [1, 3, 2, 2, 4, 6, 3, 3, 7]
107
+ # a.squeeze!(2..3)
108
+ # a # => [1, 3, 2, 4, 6, 3, 7]
109
+ def squeeze!(*sels, &block)
59
110
  respond_to?(:replace) or raise 'cannot be squeezed in place!'
60
- squeezed = squeeze
111
+ squeezed = squeeze(*sels, &block)
61
112
  return if squeezed.count == count
62
113
  replace squeezed
63
114
  end
data/lib/tins/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Tins
2
2
  # Tins version
3
- VERSION = '1.58.0'
3
+ VERSION = '1.60.0'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -96,7 +96,18 @@ module Tins
96
96
  def test_parse_percentage
97
97
  duration_string = '1%2'
98
98
  seconds = Tins::Duration.parse(duration_string, template: '%h%%%m')
99
- assert_equal seconds, 3720
99
+ assert_equal 3720, seconds
100
+ end
101
+
102
+ def test_format_unpadded_directives
103
+ d = Tins::Duration.new(93_784) # 1d 2h 3m 4s
104
+ assert_equal(
105
+ '1 day(s), 2 hours, 3 minutes, and 4 seconds',
106
+ d.format('%d day(s), %H hours, %M minutes, and %u seconds')
107
+ )
108
+ assert_equal '2', d.format('%H')
109
+ assert_equal '3', d.format('%M')
110
+ assert_equal '4', d.format('%u')
100
111
  end
101
112
  end
102
113
  end
@@ -50,6 +50,71 @@ module Tins
50
50
  assert_equal [1, 2, 3], result
51
51
  end
52
52
 
53
+ # --- squeeze with selectors ---
54
+
55
+ def test_squeeze_single_value_selector
56
+ assert_equal(
57
+ [1, 3, 2, 2, 4, 6, 3, 7],
58
+ [1, 3, 2, 2, 4, 6, 3, 3, 7].squeeze(3)
59
+ )
60
+ end
61
+
62
+ def test_squeeze_multiple_value_selectors
63
+ assert_equal(
64
+ [1, 3, 2, 4, 6, 3, 7],
65
+ [1, 3, 2, 2, 4, 6, 3, 3, 7].squeeze(3, 2)
66
+ )
67
+ end
68
+
69
+ def test_squeeze_type_selector
70
+ assert_equal(
71
+ [[1], "1", [1], [1], "1", [1]],
72
+ [[1], '1', [1], [1], '1', '1', [1]].squeeze(String)
73
+ )
74
+ end
75
+
76
+ def test_squeeze_multiple_type_selectors
77
+ assert_equal(
78
+ [[1], "1", [1], "1", [1]],
79
+ [[1], '1', [1], [1], '1', '1', [1]].squeeze(String, Array)
80
+ )
81
+ end
82
+
83
+ def test_squeeze_range_selector
84
+ # Only 2 and 3 are in range; 5,5 survives (out of scope)
85
+ assert_equal(
86
+ [1, 3, 2, 4, 6, 3, 7, 5, 5, 7],
87
+ [1, 3, 2, 2, 4, 6, 3, 3, 7, 5, 5, 7].squeeze(2..3)
88
+ )
89
+ end
90
+
91
+ def test_squeeze_even_block
92
+ assert_equal(
93
+ [1, 3, 2, 4, 6, 3, 3, 7],
94
+ [1, 3, 2, 2, 4, 6, 3, 3, 7].squeeze(&:even?)
95
+ )
96
+ end
97
+
98
+ def test_squeeze_odd_block
99
+ assert_equal(
100
+ [1, 3, 2, 2, 4, 6, 3, 7],
101
+ [1, 3, 2, 2, 4, 6, 3, 3, 7].squeeze(&:odd?)
102
+ )
103
+ end
104
+
105
+ def test_squeeze_raises_when_both_sels_and_block
106
+ assert_raise(ArgumentError) do
107
+ [1, 1, 2].squeeze(Integer) { |x| true }
108
+ end
109
+ end
110
+
111
+ def test_string_array_invariance
112
+ str = 'fooaabaaz'
113
+ sels = [?a]
114
+ assert_equal str.squeeze(*sels),
115
+ str.split('').squeeze(*sels).join
116
+ end
117
+
53
118
  # --- squeeze! (bang) ---
54
119
 
55
120
  def test_squeeze_bang_modifies_in_place
@@ -80,5 +145,27 @@ module Tins
80
145
  enum = [1, 1, 2].each # Enumerator has no #replace
81
146
  assert_raise(RuntimeError) { enum.squeeze! }
82
147
  end
148
+
149
+ # --- squeeze! with selectors ---
150
+
151
+ def test_squeeze_bang_with_selector
152
+ a = [1, 3, 2, 2, 4, 6, 3, 3, 7]
153
+ result = a.squeeze!(3)
154
+ assert_equal [1, 3, 2, 2, 4, 6, 3, 7], a
155
+ assert_same a, result
156
+ end
157
+
158
+ def test_squeeze_bang_with_selector_no_change
159
+ a = [1, 2, 4, 6, 7]
160
+ assert_nil a.squeeze!(3)
161
+ assert_equal [1, 2, 4, 6, 7], a
162
+ end
163
+
164
+ def test_squeeze_bang_with_block
165
+ a = [1, 3, 2, 2, 4, 6, 3, 3, 7]
166
+ result = a.squeeze!(&:even?)
167
+ assert_equal [1, 3, 2, 4, 6, 3, 3, 7], a
168
+ assert_same a, result
169
+ end
83
170
  end
84
171
  end
data/tins.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: tins 1.58.0 ruby lib
2
+ # stub: tins 1.60.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "tins".freeze
6
- s.version = "1.58.0".freeze
6
+ s.version = "1.60.0".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tins
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.58.0
4
+ version: 1.60.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank