Skip to content

Commit

Permalink
Implement ULID#{+,-} to follow Range#step changes in ruby-3.4.0 (#591)
Browse files Browse the repository at this point in the history
* Copy ULID#next tests to ULID#succ

* Clarify succ, next, pred does not take arguments

* Implement ULID#+ to follow ruby-lang#18368

https://bugs.ruby-lang.org/issues/18368

* Implement ULID#- to make natural API with the ULID#+ era

* Update README

* Update RBS definitions

* Add version guard
  • Loading branch information
kachick committed Aug 26, 2024
1 parent d270926 commit cb72e2a
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 20 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
```

#### As element in Enumerable
#### As an element in Enumerable and Range

`ULID#next` and `ULID#succ` returns next(successor) ULID.\
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
Expand All @@ -301,6 +301,17 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
ULID.parse('00000000000000000000000000').pred #=> nil
```

`ULID#+` is also provided to realize `Range#step` since [ruby-3.4.0 spec changes](https://bugs.ruby-lang.org/issues/18368).

```ruby
# This code works only in ruby-3.4.0dev or later
(ULID.min...).step(42).take(3)
# =>
[ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000),
ULID(1970-01-01 00:00:00.000 UTC: 0000000000000000000000001A),
ULID(1970-01-01 00:00:00.000 UTC: 0000000000000000000000002M)]
```

#### Test helpers

`ULID.sample` returns random ULIDs.
Expand Down
48 changes: 29 additions & 19 deletions lib/ulid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -449,33 +449,43 @@ def randomness
@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH) || raise(UnexpectedError)
end

# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
def succ
succ_int = @integer.succ
if succ_int >= MAX_INTEGER
if succ_int == MAX_INTEGER
MAX
else
# @param [Integer] other
# @return [ULID, nil] when returning URID might be greater than `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
def +(other)
raise(ArgumentError, 'ULID#+ takes only integers') unless Integer === other

new_int = @integer + other
case new_int
when MAX_INTEGER
MAX
when 0
MIN
else
if new_int > MAX_INTEGER || new_int < 0
nil
else
ULID.from_integer(new_int)
end
else
ULID.from_integer(succ_int)
end
end

# @param [Integer] other
# @return [ULID, nil] when returning URID might be less than `00000000000000000000000000`, returns `nil` instead of ULID
def -(other)
raise(ArgumentError, 'ULID#- takes only integers') unless Integer === other

self + -other
end

# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
def succ
self + 1
end
alias_method(:next, :succ)

# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
def pred
pred_int = @integer.pred
if pred_int <= 0
if pred_int == 0
MIN
else
nil
end
else
ULID.from_integer(pred_int)
end
self - 1
end

# @return [Integer]
Expand Down
12 changes: 12 additions & 0 deletions sig/ulid.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,18 @@ class ULID < Object
# ```
def octets: -> octets

# Returns incremented ULID.\
# Especially providing for Range#step since ruby-3.4.0 spec changes
#
# See also [ruby-lang#18368](https://bugs.ruby-lang.org/issues/18368)
# ```
def +: (Integer other) -> ULID?

# Returns decremented ULID.\
# Providing for realizing natural API convention with the `ULID#+`
# ```
def -: (Integer other) -> ULID?

# Returns next(successor) ULID.\
# Especially `ULID#succ` makes it possible `Range[ULID]#each`.
#
Expand Down
12 changes: 12 additions & 0 deletions test/core/test_boundary_ulid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ def test_consistency_with_constants
assert_equal(0, @min.to_i)
end

def test_plus
assert_nil(@max + 1)
assert_equal(ULID.parse('01BX5ZZKBM0000000000000000'), @max_entropy + 1)
assert_same(@max, ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZY') + 1)
end

def test_minus
assert_nil(@min - 1)
assert_equal(ULID.parse('01BX5ZZKBJZZZZZZZZZZZZZZZZ'), @min_entropy - 1)
assert_same(@min, ULID.parse('00000000000000000000000001') - 1)
end

def test_next
assert_nil(@max.next)
assert_equal(ULID.parse('01BX5ZZKBM0000000000000000'), @max_entropy.next)
Expand Down
65 changes: 65 additions & 0 deletions test/core/test_ulid_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class TestULIDInstance < Test::Unit::TestCase
<=>
==
===
+
-
encode
entropy
eql?
Expand Down Expand Up @@ -321,11 +323,71 @@ def test_octets
assert_false(ulid.octets.frozen?)
end

def test_plus
ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
assert_equal((ulid + 42).to_i, ulid.to_i + 42)
assert_equal((ulid + -42).to_i, ulid.to_i - 42)
assert_instance_of(ULID, ulid + 42)
assert_not_same(ulid + 42, ulid + 42)
assert_raises(ArgumentError) do
ulid.__send__(:+)
end
assert_raises(ArgumentError) do
ulid.__send__(:+, 42, 6174)
end

[nil, '42', 42.1, BasicObject.new, Object.new, ULID.sample].each do |evil|
err = assert_raises(ArgumentError) do
ulid + evil
end
assert_equal('ULID#+ takes only integers', err.message)
end
end

def test_minus
ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
assert_equal((ulid - 42).to_i, ulid.to_i - 42)
assert_equal((ulid - -42).to_i, ulid.to_i + 42)
assert_instance_of(ULID, ulid - 42)
assert_not_same(ulid - 42, ulid - 42)
assert_raises(ArgumentError) do
ulid.__send__(:-)
end
assert_raises(ArgumentError) do
ulid.__send__(:-, 42, 6174)
end

[nil, '42', 42.1, BasicObject.new, Object.new, ULID.sample].each do |evil|
err = assert_raises(ArgumentError) do
ulid - evil
end
assert_equal('ULID#- takes only integers', err.message)
end
end

def test_succ
ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
assert_equal(ulid.succ.to_i, ulid.to_i + 1)
assert_instance_of(ULID, ulid.succ)
assert_not_same(ulid.succ, ulid.succ)
assert_raises(ArgumentError) do
ulid.succ(1)
end

first = ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVRY')
assert_equal(ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVRZ'), first.succ)
assert_equal(ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVS0'), first.succ.succ)
assert_equal(ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVS1'), first.succ.succ.succ)
end

def test_next
ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
assert_equal(ulid.next.to_i, ulid.to_i + 1)
assert_instance_of(ULID, ulid.next)
assert_not_same(ulid.next, ulid.next)
assert_raises(ArgumentError) do
ulid.next(1)
end

first = ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVRY')
assert_equal(ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVRZ'), first.next)
Expand All @@ -338,6 +400,9 @@ def test_pred
assert_equal(ulid.pred.to_i, ulid.to_i - 1)
assert_instance_of(ULID, ulid.pred)
assert_not_same(ulid.pred, ulid.pred)
assert_raises(ArgumentError) do
ulid.pred(1)
end

first = ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVR2')
assert_equal(ULID.parse('01BX5ZZKBKACTAV9WEVGEMMVR1'), first.pred)
Expand Down
10 changes: 10 additions & 0 deletions test/core/test_ulid_usecase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ def test_range_elements
assert_equal([begin_ulid, ulid3, ulid5], exclude_end.step(2).to_a)
assert_equal([begin_ulid, ulid4], exclude_end.step(3).to_a)
assert_equal([begin_ulid], exclude_end.step(5).to_a)

omit_if(RUBY_VERSION < '3.4.0')
assert_equal(
[
ULID.parse('00000000000000000000000000'),
ULID.parse('0000000000000000000000001A'),
ULID.parse('0000000000000000000000002M')
],
(ULID.min...).step(42).take(3)
)
end

# https://github.com/kachick/ruby-ulid/issues/47
Expand Down

0 comments on commit cb72e2a

Please sign in to comment.