Skip to content

Commit

Permalink
Implement ULID.parse_variant_format (#206)
Browse files Browse the repository at this point in the history
Closes #143
Resolves #57
Resolves #78
Updates #205 (Renamed non released method)
  • Loading branch information
kachick authored Jul 3, 2022
1 parent 5163465 commit bef91dc
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 35 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,29 +340,29 @@ ULID.sample(5, period: ulid1.to_time..ulid2.to_time)

I'm afraid so, we should consider [Current ULID spec](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#universally-unique-lexicographically-sortable-identifier) has `orthographical variants of the format` possibilities.

>Case insensitive
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase.
However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).

>Uses Crockford's base32 for better efficiency and readability (5 bits per character)
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.
And accepts freestyle inserting `Hyphens (-)`.
To consider this patterns or not is different in each implementations.

Current parser/validator/matcher basically aims to cover `subset of Crockford's base32`.
I have suggested it would be clarified in [ulid/spec#57](https://github.com/ulid/spec/pull/57).

>Case insensitive
I can understand it might be considered in actual use-case.
But it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57).

Be that as it may, this gem provides API for handling the nasty possibilities.
This gem provides some methods to handle the nasty possibilities.

`ULID.normalize`, `ULID.normalized?`, `ULID.valid_as_variants?`
`ULID.normalize`, `ULID.normalized?`, `ULID.valid_as_variant_format?` and `ULID.parse_variant_format`

```ruby
ULID.normalize('01g70y0y7g-z1xwdarexergsddd') #=> "01G70Y0Y7GZ1XWDAREXERGSDDD"
ULID.normalized?('01g70y0y7g-z1xwdarexergsddd') #=> false
ULID.normalized?('01G70Y0Y7GZ1XWDAREXERGSDDD') #=> true
ULID.valid_as_variants?('01g70y0y7g-z1xwdarexergsddd') #=> true
ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true
ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
```

#### UUIDv4 converter (experimental)
Expand Down
20 changes: 15 additions & 5 deletions lib/ulid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,26 @@ def self.parse(string)
from_integer(CrockfordBase32.decode(string))
end

# @param [String, #to_str] string
# @return [ULID]
# @raise [ParserError] if the given format is not correct for ULID specs
def self.parse_variant_format(string)
string = String.try_convert(string)
raise(ArgumentError, 'ULID.parse_variant_format takes only strings') unless string

normalized_in_crockford = CrockfordBase32.normalize(string)
parse(normalized_in_crockford)
end

# @param [String, #to_str] string
# @return [String]
# @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
def self.normalize(string)
string = String.try_convert(string)
raise(ArgumentError, 'ULID.normalize takes only strings') unless string

normalized_in_crockford = CrockfordBase32.normalize(string)
# Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
parse(normalized_in_crockford).to_s
parse_variant_format(string).to_s
end

# @param [String, #to_str] string
Expand All @@ -292,23 +302,23 @@ def self.normalized?(string)

# @param [String, #to_str] string
# @return [Boolean]
def self.valid_as_variants?(string)
def self.valid_as_variant_format?(string)
normalize(string)
rescue Exception
false
else
true
end

# @deprecated Use [.valid_as_variants?] or [.normalized?] instead
# @deprecated Use [.valid_as_variant_format?] or [.normalized?] instead
#
# Returns `true` if it is normalized string.
# Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs.
#
# @return [Boolean]
def self.valid?(string)
warn_kwargs = (RUBY_VERSION >= '3.0') ? { category: :deprecated } : {}
Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variants? or ULID.normalized? instead.', **warn_kwargs)
Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.', **warn_kwargs)
string = String.try_convert(string)
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
end
Expand Down
25 changes: 20 additions & 5 deletions sig/ulid.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,21 @@ class ULID < Object
# ```
def self.parse: (_ToStr string) -> ULID

# Get ULID instance from unnormalized String that encoded in Crockford's base32.
#
# http://www.crockford.com/base32.html
#
# * Ignore Hyphens (-)
# * Mapping 0 O o => 0, 1 I i L l => 1
#
# ```ruby
# ulid = ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD')
# #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
# ```
#
# See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
def self.parse_variant_format: (_ToStr string) -> ULID

# ```ruby
# # Currently experimental feature, so needed to load the extension.
# require 'ulid/uuid'
Expand Down Expand Up @@ -337,16 +352,16 @@ class ULID < Object
# Returns `true` if it is valid in ULID format variants
#
# ```ruby
# ULID.valid_as_variants?(ULID.generate.to_s.downcase) #=> true
# ULID.valid_as_variants?('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> true
# ULID.valid_as_variants?('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> false
# ULID.valid_as_variant_format?(ULID.generate.to_s.downcase) #=> true
# ULID.valid_as_variant_format?('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> true
# ULID.valid_as_variant_format?('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> false
# ```
#
# See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
def self.valid_as_variants?: (_ToStr string) -> bool
def self.valid_as_variant_format?: (_ToStr string) -> bool
| (untyped) -> false

# DEPRECATED Use valid_as_variants? instead
# DEPRECATED Use valid_as_variant_format? instead
#
# Returns `true` if it is normalized string.
# Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs.
Expand Down
4 changes: 2 additions & 2 deletions steep_expectations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
diagnostics:
- range:
start:
line: 311
line: 321
character: 12
end:
line: 311
line: 321
character: 16
severity: ERROR
message: Type `singleton(::Warning)` does not have method `warn`
Expand Down
65 changes: 52 additions & 13 deletions test/core/test_ulid_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def test_exposed_methods
:at,
:normalized?,
:parse,
:valid_as_variants?
:valid_as_variant_format?,
:parse_variant_format
].sort,
exposed_methods.sort
)
Expand Down Expand Up @@ -106,6 +107,44 @@ def test_parse
end
end

def test_parse_variant_format
string = +'01G70Y0Y7G-ZLXWDIREXERGSDoD'
dup_string = string.dup
parsed = ULID.parse_variant_format(string)

# Ensure the string is not modified in parser
assert_false(string.frozen?)
assert_equal(dup_string, string)

assert_instance_of(ULID, parsed)
assert_equal('01G70Y0Y7GZ1XWD1REXERGSD0D', parsed.to_s)
assert_equal(ULID.parse_variant_format(string), ULID.parse_variant_format('01G70Y0Y7GZ1XWD1REXERGSD0D'))

[
'',
"01ARZ3NDEKTSV4RRFFQ69G5FAV\n",
'01ARZ3NDEKTSV4RRFFQ69G5FAU',
'01ARZ3NDEKTSV4RRFFQ69G5FA',
'01G70Y0Y7G_ZLXWDIREXERGSDoD'
].each do |invalid|
err = assert_raises(ULID::ParserError) do
ULID.parse_variant_format(invalid)
end
assert_match(/does not match to/, err.message)
end

assert_raises(ArgumentError) do
ULID.parse_variant_format
end

[nil, 42, string.to_sym, BasicObject.new, Object.new, parsed].each do |evil|
err = assert_raises(ArgumentError) do
ULID.parse_variant_format(evil)
end
assert_equal('ULID.parse_variant_format takes only strings', err.message)
end
end

def test_new
err = assert_raises(NoMethodError) do
ULID.new(milliseconds: 0, entropy: 42)
Expand Down Expand Up @@ -137,7 +176,7 @@ def test_from_milliseconds_and_entropy
end

def test_valid?
assert_warning('ULID.valid? is deprecated. Use ULID.valid_as_variants? or ULID.normalized? instead.') do
assert_warning('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.') do
assert_equal(false, ULID.valid?(nil))
assert_equal(false, ULID.valid?(''))
assert_equal(false, ULID.valid?(BasicObject.new))
Expand Down Expand Up @@ -254,16 +293,16 @@ def test_normalized?
end
end

def test_valid_as_variants?
assert_true(ULID.valid_as_variants?('01G70Y0Y7G-Z1XWDAREXERGSDDD'))
def test_valid_as_variant_format?
assert_true(ULID.valid_as_variant_format?('01G70Y0Y7G-Z1XWDAREXERGSDDD'))

nasty = '-olarz3-noekisv4rrff-q6ig5fav--'
assert_true(ULID.valid_as_variants?(nasty))
assert_true(ULID.valid_as_variants?(ULID.normalize(nasty)))
assert_true(ULID.valid_as_variant_format?(nasty))
assert_true(ULID.valid_as_variant_format?(ULID.normalize(nasty)))

normalized = '01ARZ3NDEKTSV4RRFFQ69G5FAV'
assert_true(ULID.valid_as_variants?(normalized))
assert_true(ULID.valid_as_variants?(normalized.downcase))
assert_true(ULID.valid_as_variant_format?(normalized))
assert_true(ULID.valid_as_variant_format?(normalized.downcase))

[
'',
Expand All @@ -272,20 +311,20 @@ def test_valid_as_variants?
'01ARZ3NDEKTSV4RRFFQ69G5FA',
'80000000000000000000000000'
].each do |invalid|
assert_false(ULID.valid_as_variants?(invalid))
assert_false(ULID.valid_as_variant_format?(invalid))
end

ULID.sample(1000).each do |sample|
assert_true(ULID.valid_as_variants?(sample.to_s))
assert_true(ULID.valid_as_variants?(sample.to_s.downcase))
assert_true(ULID.valid_as_variant_format?(sample.to_s))
assert_true(ULID.valid_as_variant_format?(sample.to_s.downcase))
end

assert_raises(ArgumentError) do
ULID.valid_as_variants?
ULID.valid_as_variant_format?
end

[nil, 42, normalized.to_sym, BasicObject.new, Object.new, ULID.parse(normalized)].each do |evil|
assert_false(ULID.valid_as_variants?(evil))
assert_false(ULID.valid_as_variant_format?(evil))
end
end

Expand Down

0 comments on commit bef91dc

Please sign in to comment.