ruby-ulid 0.0.11 → 0.0.16

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: 91f6d4c9dad8e12663686099c487dbbf3ec5b7995e0e4b2b210ac8a16fbcd91c
4
- data.tar.gz: 1ca6e29d83771636b2a20aa71d90f15699d07d6a7a75f22c306094507bb51d89
3
+ metadata.gz: 15a83604732cdc37f8015ee9382884e950852f8c9c82aba107ea4b3c065c5a4d
4
+ data.tar.gz: 06e7b69a5838786f4d23da19f5e1f00fb3173014e7438459165b66703637851a
5
5
  SHA512:
6
- metadata.gz: 22dcc2541f7e4fa1a1d4014f996aef3d3bfcb69b002c1cc6ca4864e17bce50a327e1ec98453b4dab0773400b678055dc024a7287342a08916ce30772aadb418d
7
- data.tar.gz: 27c69c7319858a4c732f04f6944ff180f3dfc9a5df12bd67cd1e4cac91421a2bc00d1a79c8ff91190b425731f831fdaef1a2f5bc0c3ef4fdaff73a6ce6749e35
6
+ metadata.gz: 6ca46525e80b1d832a703b8cb867466327de333853ee236654f722dc6abc30f46fd149ca9633c42ececcea08db2a01a6d82ab60f8eeb61ebb947f0441436dd96
7
+ data.tar.gz: ccd08e78dfb047f82af02eeaf14ccd12fcb05fe882edeb2bf48c2e7ec5b9fd698feccf0b9f9e187aaff97c2e793f671a3f9d32c11e3741656c3df94f944bc808
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # ruby-ulid
2
2
 
3
- A handy `ULID` library
3
+ ## Overview
4
4
 
5
- The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
5
+ The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
6
6
  This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
7
- Also providing rbs signature files.
7
+ Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
8
8
 
9
9
  ---
10
10
 
@@ -28,19 +28,31 @@ Instead, herein is proposed ULID:
28
28
  - 1.21e+24 unique ULIDs per millisecond
29
29
  - Lexicographically sortable!
30
30
  - Canonically encoded as a 26 character string, as opposed to the 36 character UUID
31
- - Uses Crockford's base32 for better efficiency and readability (5 bits per character)
31
+ - Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) # See also exists issues in [Note](#note)
32
32
  - Case insensitive
33
33
  - No special characters (URL safe)
34
34
  - Monotonic sort order (correctly detects and handles the same millisecond)
35
35
 
36
- ## Install
36
+ ## Usage
37
+
38
+ ### Install
39
+
40
+ Require Ruby 2.6 or later
41
+
42
+ This command will install the latest version into your environment
37
43
 
38
44
  ```console
39
45
  $ gem install ruby-ulid
40
- #=> Installed
46
+ Should be installed!
41
47
  ```
42
48
 
43
- ## Usage
49
+ Add this line to your application/library's `Gemfile` is needed in basic use-case
50
+
51
+ ```ruby
52
+ gem 'ruby-ulid', '0.0.16'
53
+ ```
54
+
55
+ ### Generator and Parser
44
56
 
45
57
  The generated `ULID` is an object not just a string.
46
58
  It means easily get the timestamps and binary formats.
@@ -52,36 +64,17 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
52
64
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
53
65
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
54
66
  ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
55
- ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
56
- ```
57
-
58
- Generator can take `Time` instance
59
-
60
- ```ruby
61
- time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
62
- ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
63
- ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
64
-
65
- ulids = 1000.times.map do
66
- ULID.generate(moment: time)
67
- end
68
- ulids.sort == ulids #=> false
69
-
70
- ulids = 1000.times.map do |n|
71
- ULID.generate(moment: time + n)
72
- end
73
- ulids.sort == ulids #=> true
74
67
  ```
75
68
 
76
- You can parse from exists IDs
77
-
78
- FYI: Current parser/validator/matcher implementation aims `strict`, It might be changed in [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57).
69
+ You can get the objects from exists encoded ULIDs
79
70
 
80
71
  ```ruby
81
72
  ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
82
73
  ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
83
74
  ```
84
75
 
76
+ ### Sortable with the timestamp
77
+
85
78
  ULIDs are sortable when they are generated in different timestamp with milliseconds precision
86
79
 
87
80
  ```ruby
@@ -89,8 +82,21 @@ ulids = 1000.times.map do
89
82
  sleep(0.001)
90
83
  ULID.generate
91
84
  end
92
- ulids.sort == ulids #=> true
93
85
  ulids.uniq(&:to_time).size #=> 1000
86
+ ulids.sort == ulids #=> true
87
+ ```
88
+
89
+ `ULID.generate` can take fixed `Time` instance
90
+
91
+ ```ruby
92
+ time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
93
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
94
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
95
+
96
+ ulids = 1000.times.map do |n|
97
+ ULID.generate(moment: time + n)
98
+ end
99
+ ulids.sort == ulids #=> true
94
100
  ```
95
101
 
96
102
  The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
@@ -103,32 +109,72 @@ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in en
103
109
  ulids.sort == ulids #=> false
104
110
  ```
105
111
 
106
- If you want to prefer `sortable` rather than the `strict randomness`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
112
+ ### How to keep `Sortable` even if in same timestamp
113
+
114
+ If you want to prefer `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
107
115
  (Though it starts with new random value when changed the timestamp)
108
116
 
109
117
  ```ruby
110
118
  monotonic_generator = ULID::MonotonicGenerator.new
111
- monotonic_ulids = 10000.times.map do
119
+ ulids = 10000.times.map do
112
120
  monotonic_generator.generate
113
121
  end
114
- sample_ulids_by_the_time = monotonic_ulids.uniq(&:to_time)
115
- sample_ulids_by_the_time.size #=> 34 (the size is not fixed, might be changed in environment)
116
- sample_ulids_by_the_time.take(10).map(&:randomness)
117
- #=>
118
- ["JZW56CTA8704D5AQ",
119
- "JGEBH2A2B2EA97MW",
120
- "0XPE4NS3MZH0NAJ4",
121
- "E0S3ZAVADFBPW57Y",
122
- "E5CX1T6281443THQ",
123
- "3SK8WHSH03CVF7J2",
124
- "DDS35BT0R20P3V49",
125
- "60KG2W9FVEN1ZX8C",
126
- "X59YJVXXVH7AXJJE",
127
- "1ZBQ7SNGFKXGH1Y4"]
122
+ sample_ulids_by_the_time = ulids.uniq(&:to_time)
123
+ sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
124
+
125
+ # In same milliseconds creation, it just increments the end of randomness part
126
+ ulids.take(5) #=>
127
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
128
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
129
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
130
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
131
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
132
+
133
+ # When the milliseconds is updated, it starts with new randomness
134
+ sample_ulids_by_the_time.take(5) #=>
135
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
136
+ # ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
137
+ # ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K),
138
+ # ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
139
+ # ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
140
+
141
+ ulids.sort == ulids #=> true
142
+ ```
143
+
144
+ ### Filtering IDs with `Time`
145
+
146
+ `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
147
+
148
+ ```ruby
149
+ include_end = ulid1..ulid2
150
+ exclude_end = ulid1...ulid2
151
+
152
+ ulids.grep(one_of_the_above)
153
+ ulids.grep_v(one_of_the_above)
154
+ ```
155
+
156
+ When want to filter ULIDs with `Time`, we should consider to handle the precision.
157
+ So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
128
158
 
129
- monotonic_ulids.sort == monotonic_ulids #=> true
159
+ ```ruby
160
+ # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
161
+ include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
162
+ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
163
+
164
+ # Below patterns are acceptable
165
+ pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
166
+ until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
167
+ until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
168
+ until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
169
+
170
+ # So you can use the generated range objects as below
171
+ ulids.grep(one_of_the_above)
172
+ ulids.grep_v(one_of_the_above)
173
+ #=> I hope the results should be actually you want!
130
174
  ```
131
175
 
176
+ ### Scanner for string (e.g. `JSON`)
177
+
132
178
  For rough operations, `ULID.scan` might be useful.
133
179
 
134
180
  ```ruby
@@ -169,6 +215,8 @@ ULID.scan(json).to_a
169
215
  ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
170
216
  ```
171
217
 
218
+ ### Some methods to help manipulations
219
+
172
220
  `ULID.min` and `ULID.max` return termination values for ULID spec.
173
221
 
174
222
  ```ruby
@@ -180,7 +228,10 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
180
228
  ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
181
229
  ```
182
230
 
183
- `ULID#next` and `ULID#succ` returns next(successor) ULID
231
+ `ULID#next` and `ULID#succ` returns next(successor) ULID.
232
+ Especially `ULID#succ` makes it possible `Range[ULID]#each`.
233
+
234
+ NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
184
235
 
185
236
  ```ruby
186
237
  ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
@@ -196,15 +247,87 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
196
247
  ULID.parse('00000000000000000000000000').pred #=> nil
197
248
  ```
198
249
 
199
- UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
250
+ ### UUIDv4 converter for migration use-cases
251
+
252
+ `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
253
+ The imported timestamp is meaningless. So ULID's benefit will lost
200
254
 
201
255
  ```ruby
202
- ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
203
- #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
256
+ # Basically reversible
257
+ ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
258
+ ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
259
+
260
+ uuid_v4s = 10000.times.map { SecureRandom.uuid }
261
+ uuid_v4s.uniq.size == 10000 #=> Probably `true`
262
+
263
+ ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
264
+ ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
265
+
266
+ # NOTE: Some boundary values are not reversible. See below.
267
+
268
+ ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
269
+ ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
270
+
271
+ # These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
272
+ reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
273
+ reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
274
+
275
+ # But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
276
+ ULID.min == reversed_min #=> false
277
+ ULID.max == reversed_max #=> false
278
+ ```
279
+
280
+ ## How to migrate from other gems
281
+
282
+ As far as I know, major prior arts are below
283
+
284
+ ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
285
+
286
+ It is just providing basic `String` generator only.
287
+ So you can replace the code as below
288
+
289
+ ```diff
290
+ -ULID.generate
291
+ +ULID.generate.to_s
204
292
  ```
205
293
 
294
+ NOTE: It had crucial issue for handling precision, in version before `1.3.0`, when you extract timestamps from old generated ULIDs, it might be not accurate value.
295
+
296
+ 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
297
+ 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
298
+ 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
299
+
300
+ ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
301
+
302
+ It is providing basic generator(except monotonic generator) and parser.
303
+ Major methods can be replaced as below.
304
+
305
+ ```diff
306
+ -ULID.generate
307
+ +ULID.generate.to_s
308
+ -ULID.at(time)
309
+ +ULID.generate(moment: time).to_s
310
+ -ULID.time(string)
311
+ +ULID.parse(string).to_time
312
+ -ULID.min_ulid_at(time)
313
+ +ULID.min(moment: time).to_s
314
+ -ULID.max_ulid_at(time)
315
+ +ULID.max(moment: time).to_s
316
+ ```
317
+
318
+ NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
319
+
320
+ 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
321
+ 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
322
+ 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
323
+
206
324
  ## References
207
325
 
326
+ - [Repository](https://github.com/kachick/ruby-ulid)
208
327
  - [API documents](https://kachick.github.io/ruby-ulid/)
209
328
  - [ulid/spec](https://github.com/ulid/spec)
210
- - [Another choices are UUIDv6, UUIDv7, UUIDv8. But they are still in draft state](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html)
329
+
330
+ ## Note
331
+
332
+ - Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
333
+ - Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
data/lib/ulid.rb CHANGED
@@ -16,6 +16,7 @@ class ULID
16
16
  class Error < StandardError; end
17
17
  class OverflowError < Error; end
18
18
  class ParserError < Error; end
19
+ class SetupError < ScriptError; end
19
20
 
20
21
  encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
22
  # Crockford's Base32. Excluded I, L, O, U.
@@ -35,7 +36,7 @@ class ULID
35
36
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
37
 
37
38
  # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
- UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i
39
+ UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i.freeze
39
40
 
40
41
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
42
  # @see https://bugs.ruby-lang.org/issues/15958
@@ -45,20 +46,19 @@ class ULID
45
46
  # @param [Integer] entropy
46
47
  # @return [ULID]
47
48
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
48
- milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
49
- new milliseconds: milliseconds, entropy: entropy
49
+ new milliseconds: milliseconds_from_moment(moment), entropy: entropy
50
50
  end
51
51
 
52
52
  # @param [Integer, Time] moment
53
53
  # @return [ULID]
54
54
  def self.min(moment: 0)
55
- generate(moment: moment, entropy: 0)
55
+ 0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
56
56
  end
57
57
 
58
58
  # @param [Integer, Time] moment
59
59
  # @return [ULID]
60
60
  def self.max(moment: MAX_MILLISECONDS)
61
- generate(moment: moment, entropy: MAX_ENTROPY)
61
+ MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
62
62
  end
63
63
 
64
64
  # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
@@ -122,27 +122,82 @@ class ULID
122
122
  new milliseconds: milliseconds, entropy: entropy
123
123
  end
124
124
 
125
+ # @param [Range<Time>, Range<nil>] time_range
126
+ # @return [Range<ULID>]
127
+ # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
128
+ def self.range(time_range)
129
+ raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
130
+ begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
131
+
132
+ case begin_time
133
+ when Time
134
+ begin_ulid = min(moment: begin_time)
135
+ when nil
136
+ begin_ulid = min
137
+ else
138
+ raise argument_error_for_range_building(time_range)
139
+ end
140
+
141
+ case end_time
142
+ when Time
143
+ if exclude_end
144
+ end_ulid = min(moment: end_time)
145
+ else
146
+ end_ulid = max(moment: end_time)
147
+ end
148
+ when nil
149
+ # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
150
+ end_ulid = max
151
+ exclude_end = false
152
+ else
153
+ raise argument_error_for_range_building(time_range)
154
+ end
155
+
156
+ begin_ulid.freeze
157
+ end_ulid.freeze
158
+
159
+ Range.new(begin_ulid, end_ulid, exclude_end)
160
+ end
161
+
162
+ # @param [Time] time
163
+ # @return [Time]
164
+ def self.floor(time)
165
+ if RUBY_VERSION >= '2.7'
166
+ time.floor(3)
167
+ else
168
+ Time.at(0, milliseconds_from_time(time), :millisecond)
169
+ end
170
+ end
171
+
125
172
  # @return [Integer]
126
173
  def self.current_milliseconds
127
- time_to_milliseconds(Time.now)
174
+ milliseconds_from_time(Time.now)
128
175
  end
129
176
 
130
177
  # @param [Time] time
131
178
  # @return [Integer]
132
- def self.time_to_milliseconds(time)
179
+ def self.milliseconds_from_time(time)
133
180
  (time.to_r * 1000).to_i
134
181
  end
135
182
 
183
+ # @param [Time, Integer] moment
184
+ # @return [Integer]
185
+ def self.milliseconds_from_moment(moment)
186
+ moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
187
+ end
188
+
136
189
  # @return [Integer]
137
190
  def self.reasonable_entropy
138
191
  SecureRandom.random_number(MAX_ENTROPY)
139
192
  end
140
193
 
194
+ # @api private
195
+ # @deprecated Just exists to compare performance with old implementation. ref: https://github.com/kachick/ruby-ulid/issues/7
141
196
  # @param [String, #to_str] string
142
197
  # @return [ULID]
143
198
  # @raise [ParserError] if the given format is not correct for ULID specs
144
199
  # @raise [OverflowError] if the given value is larger than the ULID limit
145
- def self.parse(string)
200
+ def self.parse_with_integer_base(string)
146
201
  begin
147
202
  string = string.to_str
148
203
  unless string.size == ENCODED_ID_LENGTH
@@ -159,6 +214,67 @@ class ULID
159
214
  new milliseconds: milliseconds, entropy: entropy
160
215
  end
161
216
 
217
+ n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
218
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
219
+
220
+ n32_char_by_number = {}
221
+ n32_chars.each_with_index do |char, index|
222
+ n32_char_by_number[index] = char
223
+ end
224
+ n32_char_by_number.freeze
225
+
226
+ # Currently supporting only for `subset for actual use-case`
227
+ # See below
228
+ # * https://github.com/ulid/spec/pull/57
229
+ # * https://github.com/kachick/ruby-ulid/issues/57
230
+ # * https://github.com/kachick/ruby-ulid/issues/78
231
+ crockford_base32_mappings = {
232
+ 'J' => 18,
233
+ 'K' => 19,
234
+ 'M' => 20,
235
+ 'N' => 21,
236
+ 'P' => 22,
237
+ 'Q' => 23,
238
+ 'R' => 24,
239
+ 'S' => 25,
240
+ 'T' => 26,
241
+ 'V' => 27,
242
+ 'W' => 28,
243
+ 'X' => 29,
244
+ 'Y' => 30,
245
+ 'Z' => 31
246
+ }.freeze
247
+
248
+ REPLACING_MAP = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
249
+ if n = crockford_base32_mappings[encoding_char]
250
+ char_32 = n32_char_by_number.fetch(n)
251
+ map[encoding_char] = char_32
252
+ end
253
+ end.freeze
254
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless REPLACING_MAP.keys == crockford_base32_mappings.keys
255
+ REPLACING_PATTERN = /[#{REPLACING_MAP.keys.join}]/.freeze
256
+
257
+ def self.parse(string)
258
+ begin
259
+ string = string.to_str
260
+ raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
261
+ n32encoded = convert_crockford_base32_to_n32(string.upcase)
262
+ timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
263
+ randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
264
+ milliseconds = timestamp.to_i(32)
265
+ entropy = randomness.to_i(32)
266
+ rescue => err
267
+ raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
268
+ end
269
+
270
+ new milliseconds: milliseconds, entropy: entropy
271
+ end
272
+
273
+ # @api private
274
+ private_class_method def self.convert_crockford_base32_to_n32(string)
275
+ string.gsub(REPLACING_PATTERN, REPLACING_MAP)
276
+ end
277
+
162
278
  # @return [Boolean]
163
279
  def self.valid?(string)
164
280
  parse(string)
@@ -168,6 +284,7 @@ class ULID
168
284
  true
169
285
  end
170
286
 
287
+ # @api private
171
288
  # @param [Integer] integer
172
289
  # @param [Integer] length
173
290
  # @return [Array<Integer>]
@@ -179,6 +296,7 @@ class ULID
179
296
  digits.reverse!
180
297
  end
181
298
 
299
+ # @api private
182
300
  # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
183
301
  # @param [Array<Integer>] reversed_digits
184
302
  # @return [Integer]
@@ -191,8 +309,14 @@ class ULID
191
309
  num
192
310
  end
193
311
 
312
+ # @return [ArgumentError]
313
+ private_class_method def self.argument_error_for_range_building(argument)
314
+ ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
315
+ end
316
+
194
317
  attr_reader :milliseconds, :entropy
195
318
 
319
+ # @api private
196
320
  # @param [Integer] milliseconds
197
321
  # @param [Integer] entropy
198
322
  # @return [void]
@@ -210,10 +334,9 @@ class ULID
210
334
  end
211
335
 
212
336
  # @return [String]
213
- def to_str
337
+ def to_s
214
338
  @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
215
339
  end
216
- alias_method :to_s, :to_str
217
340
 
218
341
  # @return [Integer]
219
342
  def to_i
@@ -228,7 +351,7 @@ class ULID
228
351
 
229
352
  # @return [String]
230
353
  def inspect
231
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
354
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
232
355
  end
233
356
 
234
357
  # @return [Boolean]
@@ -255,7 +378,13 @@ class ULID
255
378
 
256
379
  # @return [Time]
257
380
  def to_time
258
- @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
381
+ @time ||= begin
382
+ if RUBY_VERSION >= '2.7'
383
+ Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
384
+ else
385
+ Time.at(0, @milliseconds, :millisecond).utc.freeze
386
+ end
387
+ end
259
388
  end
260
389
 
261
390
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
@@ -283,11 +412,13 @@ class ULID
283
412
  @randomness ||= matchdata[:randomness].freeze
284
413
  end
285
414
 
415
+ # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
286
416
  # @return [Regexp]
287
417
  def pattern
288
418
  @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
289
419
  end
290
420
 
421
+ # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
291
422
  # @return [Regexp]
292
423
  def strict_pattern
293
424
  @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
@@ -308,15 +439,21 @@ class ULID
308
439
  @pred ||= self.class.from_integer(pre_int)
309
440
  end
310
441
 
442
+ # @return [String]
443
+ def to_uuidv4
444
+ @uuidv4 ||= begin
445
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
446
+ array = octets.pack('C*').unpack('NnnnnN')
447
+ array[2] = (array[2] & 0x0fff) | 0x4000
448
+ array[3] = (array[3] & 0x3fff) | 0x8000
449
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
450
+ end
451
+ end
452
+
311
453
  # @return [self]
312
454
  def freeze
313
- # Evaluate all caching
314
- inspect
315
- octets
316
- to_i
317
- succ
318
- pred
319
- strict_pattern
455
+ # Need to cache before freezing, because frozen objects can't assign instance variables
456
+ cache_all_instance_variables
320
457
  super
321
458
  end
322
459
 
@@ -324,7 +461,18 @@ class ULID
324
461
 
325
462
  # @return [MatchData]
326
463
  def matchdata
327
- @matchdata ||= STRICT_PATTERN.match(to_str).freeze
464
+ @matchdata ||= STRICT_PATTERN.match(to_s).freeze
465
+ end
466
+
467
+ # @return [void]
468
+ def cache_all_instance_variables
469
+ inspect
470
+ octets
471
+ to_i
472
+ succ
473
+ pred
474
+ strict_pattern
475
+ to_uuidv4
328
476
  end
329
477
  end
330
478
 
@@ -332,7 +480,9 @@ require_relative 'ulid/version'
332
480
  require_relative 'ulid/monotonic_generator'
333
481
 
334
482
  class ULID
483
+ MIN = parse('00000000000000000000000000').freeze
484
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
335
485
  MONOTONIC_GENERATOR = MonotonicGenerator.new
336
486
 
337
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
487
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :REPLACING_PATTERN, :REPLACING_MAP
338
488
  end
@@ -4,35 +4,37 @@
4
4
 
5
5
  class ULID
6
6
  class MonotonicGenerator
7
+ # @api private
7
8
  attr_accessor :latest_milliseconds, :latest_entropy
8
9
 
9
10
  def initialize
10
11
  reset
11
12
  end
12
13
 
13
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
14
+ # @param [Time, Integer] moment
14
15
  # @return [ULID]
15
- def generate
16
- milliseconds = ULID.current_milliseconds
17
- reasonable_entropy = ULID.reasonable_entropy
16
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
17
+ # @raise [ArgumentError] if the given moment(milliseconds) is negative number
18
+ def generate(moment: ULID.current_milliseconds)
19
+ milliseconds = ULID.milliseconds_from_moment(moment)
20
+ raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
18
21
 
19
- @latest_milliseconds ||= milliseconds
20
- @latest_entropy ||= reasonable_entropy
21
- if @latest_milliseconds != milliseconds
22
+ if @latest_milliseconds < milliseconds
22
23
  @latest_milliseconds = milliseconds
23
- @latest_entropy = reasonable_entropy
24
+ @latest_entropy = ULID.reasonable_entropy
24
25
  else
25
26
  @latest_entropy += 1
26
27
  end
27
28
 
28
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
29
+ ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
29
30
  end
30
31
 
31
- # @return [self]
32
+ # @api private
33
+ # @return [void]
32
34
  def reset
33
- @latest_milliseconds = nil
34
- @latest_entropy = nil
35
- self
35
+ @latest_milliseconds = 0
36
+ @latest_entropy = ULID.reasonable_entropy
37
+ nil
36
38
  end
37
39
 
38
40
  # @raise [TypeError] always raises exception and does not freeze self
data/lib/ulid/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  class ULID
5
- VERSION = '0.0.11'
5
+ VERSION = '0.0.16'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -15,9 +15,16 @@ class ULID
15
15
  PATTERN: Regexp
16
16
  STRICT_PATTERN: Regexp
17
17
  UUIDV4_PATTERN: Regexp
18
+ REPLACING_MAP: Hash[String, String]
19
+ REPLACING_PATTERN: Regexp
18
20
  MONOTONIC_GENERATOR: MonotonicGenerator
21
+ MIN: ULID
22
+ MAX: ULID
19
23
  include Comparable
20
24
 
25
+ # The `moment` is a `Time` or `Intger of the milliseconds`
26
+ type moment = Time | Integer
27
+
21
28
  class Error < StandardError
22
29
  end
23
30
 
@@ -27,16 +34,18 @@ class ULID
27
34
  class ParserError < Error
28
35
  end
29
36
 
37
+ class SetupError < ScriptError
38
+ end
39
+
30
40
  class MonotonicGenerator
31
- attr_accessor latest_milliseconds: Integer?
32
- attr_accessor latest_entropy: Integer?
41
+ attr_accessor latest_milliseconds: Integer
42
+ attr_accessor latest_entropy: Integer
33
43
  def initialize: -> void
34
- def generate: -> ULID
44
+ def generate: (?moment: moment) -> ULID
35
45
  def reset: -> void
36
46
  def freeze: -> void
37
47
  end
38
48
 
39
- type moment = Time | Integer
40
49
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
41
50
  type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
42
51
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
@@ -55,12 +64,16 @@ class ULID
55
64
  @next: ULID?
56
65
  @pattern: Regexp?
57
66
  @strict_pattern: Regexp?
67
+ @uuidv4: String?
58
68
  @matchdata: MatchData?
59
69
 
60
70
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
61
71
  def self.monotonic_generate: -> ULID
62
72
  def self.current_milliseconds: -> Integer
63
- def self.time_to_milliseconds: (Time time) -> Integer
73
+ def self.milliseconds_from_time: (Time time) -> Integer
74
+ def self.milliseconds_from_moment: (moment moment) -> Integer
75
+ def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
76
+ def self.floor: (Time time) -> Time
64
77
  def self.reasonable_entropy: -> Integer
65
78
  def self.parse: (String string) -> ULID
66
79
  def self.from_uuidv4: (String uuid) -> ULID
@@ -72,11 +85,11 @@ class ULID
72
85
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
73
86
  def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
74
87
  def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
88
+ def self.convert_crockford_base32_to_n32: (String) -> String
75
89
  attr_reader milliseconds: Integer
76
90
  attr_reader entropy: Integer
77
91
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
78
- def to_str: -> String
79
- alias to_s to_str
92
+ def to_s: -> String
80
93
  def to_i: -> Integer
81
94
  alias hash to_i
82
95
  def <=>: (ULID other) -> Integer
@@ -93,11 +106,14 @@ class ULID
93
106
  def octets: -> octets
94
107
  def timestamp_octets: -> timestamp_octets
95
108
  def randomness_octets: -> randomness_octets
109
+ def to_uuidv4: -> String
96
110
  def next: -> ULID?
97
111
  alias succ next
98
112
  def pred: -> ULID?
99
113
  def freeze: -> self
100
114
 
101
115
  private
116
+ def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
102
117
  def matchdata: -> MatchData
118
+ def cache_all_instance_variables: -> void
103
119
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Kamiya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-01 00:00:00.000000000 Z
11
+ date: 2021-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.2.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rbs
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.2.0
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.2.0
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: benchmark-ips
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -70,10 +84,10 @@ dependencies:
70
84
  - - "<"
71
85
  - !ruby/object:Gem::Version
72
86
  version: '2'
73
- description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
74
- useful specs for applications (e.g. `Database key`). \n This gem aims to provide
75
- the generator, monotonic generator, parser and handy manipulation features around
76
- the ULID.\n Also providing `rbs` signature files.\n"
87
+ description: |2
88
+ The ULID(Universally Unique Lexicographically Sortable Identifier) has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
89
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
90
+ Also providing `ruby/rbs` signature files.
77
91
  email:
78
92
  - kachick1+ruby@gmail.com
79
93
  executables: []
@@ -82,7 +96,6 @@ extra_rdoc_files: []
82
96
  files:
83
97
  - LICENSE
84
98
  - README.md
85
- - Steepfile
86
99
  - lib/ulid.rb
87
100
  - lib/ulid/monotonic_generator.rb
88
101
  - lib/ulid/version.rb
@@ -102,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
115
  requirements:
103
116
  - - ">="
104
117
  - !ruby/object:Gem::Version
105
- version: '2.5'
118
+ version: 2.6.0
106
119
  required_rubygems_version: !ruby/object:Gem::Requirement
107
120
  requirements:
108
121
  - - ">="
data/Steepfile DELETED
@@ -1,7 +0,0 @@
1
- target :lib do
2
- signature 'sig'
3
-
4
- check 'lib'
5
-
6
- library 'securerandom'
7
- end