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 +4 -4
- data/CHANGES.md +29 -0
- data/lib/tins/duration.rb +12 -1
- data/lib/tins/enum_squeeze.rb +55 -4
- data/lib/tins/version.rb +1 -1
- data/tests/duration_test.rb +12 -1
- data/tests/enum_squeeze_test.rb +87 -0
- data/tins.gemspec +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c17298f1ee629a9226613814a7590bf107ab45e978cedf938407dd3160d1174a
|
|
4
|
+
data.tar.gz: b0246606c5dee0de323b62f91623a5d8f3b37f7da486c800e0fbef87a5a538a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(/%[
|
|
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 '%'
|
data/lib/tins/enum_squeeze.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
data/tests/duration_test.rb
CHANGED
|
@@ -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
|
|
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
|
data/tests/enum_squeeze_test.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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]
|