ruby-ulid 0.0.9 → 0.0.10

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: cc7674472c14c213d6fe01c6994520b33bc82ab164b538948a4f1b532d0d88f2
4
- data.tar.gz: 85987c59ca6f119d40caaf69cc830a8e1b5cc2f9ea3250c315be2f9d8b7faa09
3
+ metadata.gz: ce2bb6c517e9fee4f758577e57d2c5eddfd16db9ee3c0d17ad41db8d9f1de383
4
+ data.tar.gz: 1cfb027a652e2035445483bdff2511c2837441e2e664286d60682fec6769cd17
5
5
  SHA512:
6
- metadata.gz: f56db0a74a7a6559189039a9f7f13a1f514b4fc956f7993a52ca16a7a978b4b93aa352b9f74b72da73c7b0485ebe1f80052d2e3c7b04a1baba231d06ba594f42
7
- data.tar.gz: 597260a0eb43c0fdc187aeb961521fbce22c68fc75b35d445e73d60564b72208aaf1e83a8428daa99423e001572f187872f4077445d6bced8b35d197d7e700ff
6
+ metadata.gz: ea00703256bc218a1e022dd732da22b37b9caeec30bdbdb4df945f9f185d408f0b746c36b36377dba2d4acaae5d2cf0b4db8b574ecc9da0e6e4b9b0af5c2404f
7
+ data.tar.gz: 6cd088ae1d539d8cb1d2690eb4248be413e527a87db7eb67b3c1adfc167c285cf2b90bb5701e82208586996649d56d1ef6c7c30396d1fbf0fc68220487711a54
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Kenichi Kamiya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # ruby-ulid
2
+
3
+ A handy `ULID` library
4
+
5
+ The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
6
+ Formal name is `Universally Unique Lexicographically Sortable Identifier`.
7
+ It has useful specs for actual applications.
8
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation methods for the ID.
9
+ Also having rbs signature files.
10
+
11
+ ---
12
+
13
+ ![ULIDlogo](https://raw.githubusercontent.com/kachick/ruby-ulid/main/logo.png)
14
+
15
+ ![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test.yml/badge.svg?branch=main)
16
+ [![Gem Version](https://badge.fury.io/rb/ruby-ulid.png)](http://badge.fury.io/rb/ruby-ulid)
17
+
18
+ ## Universally Unique Lexicographically Sortable Identifier
19
+
20
+ UUID can be suboptimal for many uses-cases because:
21
+
22
+ - It isn't the most character efficient way of encoding 128 bits of randomness
23
+ - UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address
24
+ - UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures
25
+ - UUID v4 provides no other information than randomness which can cause fragmentation in many data structures
26
+
27
+ Instead, herein is proposed ULID:
28
+
29
+ - 128-bit compatibility with UUID
30
+ - 1.21e+24 unique ULIDs per millisecond
31
+ - Lexicographically sortable!
32
+ - Canonically encoded as a 26 character string, as opposed to the 36 character UUID
33
+ - Uses Crockford's base32 for better efficiency and readability (5 bits per character)
34
+ - Case insensitive
35
+ - No special characters (URL safe)
36
+ - Monotonic sort order (correctly detects and handles the same millisecond)
37
+
38
+ ## Install
39
+
40
+ ```console
41
+ $ gem install ruby-ulid
42
+ #=> Installed
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ The generated `ULID` is an object not just a string.
48
+ It means easily get the timestamps and binary formats.
49
+
50
+ ```ruby
51
+ require 'ulid'
52
+
53
+ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
54
+ ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
55
+ ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
56
+ ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
57
+ ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
58
+ ```
59
+
60
+ You can parse from exists IDs
61
+
62
+ ```ruby
63
+ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
64
+ ```
65
+
66
+ ULID is sortable when they are generated different timestamp in milliseconds precision
67
+
68
+ ```ruby
69
+ ulids = 1000.times.map do
70
+ sleep(0.001)
71
+ ULID.generate
72
+ end
73
+ ulids.sort == ulids #=> true
74
+ ulids.uniq(&:to_time).size #=> 1000
75
+ ```
76
+
77
+ Providing monotonic generator for same milliseconds use-cases. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
78
+
79
+ ```ruby
80
+ ulids = 10000.times.map do
81
+ ULID.generate
82
+ end
83
+ ulids.uniq(&:to_time).size #=> 35 (the number will be changed by every creation)
84
+ ulids.sort == ulids #=> false
85
+
86
+
87
+ monotonic_generator = ULID::MonotonicGenerator.new
88
+ monotonic_ulids = 10000.times.map do
89
+ monotonic_generator.generate
90
+ end
91
+ monotonic_ulids.uniq(&:to_time).size #=> 34 (the number will be changed by every creation)
92
+ monotonic_ulids.sort == monotonic_ulids #=> true
93
+ ```
94
+
95
+ Providing converter for UUIDv4. (Of course the timestamp will be useless one.)
96
+
97
+ ```ruby
98
+ ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
99
+ #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
100
+ ```
101
+
102
+ For rough operations, `ULID.scan` might be useful.
103
+
104
+ ```ruby
105
+ json =<<'EOD'
106
+ {
107
+ "id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3",
108
+ "author": {
109
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
110
+ "name": "kachick"
111
+ },
112
+ "title": "My awesome blog post",
113
+ "comments": [
114
+ {
115
+ "id": "01F4GNCNC3CH0BCRZBPPDEKBKS",
116
+ "commenter": {
117
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
118
+ "name": "kachick"
119
+ }
120
+ },
121
+ {
122
+ "id": "01F4GNCXAMXQ1SGBH5XCR6ZH0M",
123
+ "commenter": {
124
+ "id": "01F4GND4RYYSKNAADHQ9BNXAWJ",
125
+ "name": "pankona"
126
+ }
127
+ }
128
+ ]
129
+ }
130
+ EOD
131
+
132
+ ULID.scan(json).to_a
133
+ #=>
134
+ [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
135
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
136
+ ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
137
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
138
+ ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
139
+ ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
140
+ ```
141
+
142
+ `ULID.min` and `ULID.max` return termination values for ULID spec.
143
+
144
+ ```ruby
145
+ ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
146
+ ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
147
+
148
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
149
+ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
150
+ ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
151
+ ```
152
+
153
+ ## Development
154
+
155
+ At first, you should install development dependencies
156
+
157
+ ```console
158
+ $ git clone git@github.com:kachick/ruby-ulid.git
159
+ $ cd ./ruby-ulid
160
+ $ bundle install
161
+ # Executing first time might take longtime, because development mode dependent active_support via steep
162
+ ```
163
+
164
+ Play with the behaviors in REPL.
165
+
166
+ ```console
167
+ $ ./bin/console
168
+ # Starting up IRB with loading developing ULID library
169
+ irb(main):001:0>
170
+ ```
171
+
172
+ ```ruby
173
+ # On IRB, you can check behaviors even if it is undocumented
174
+ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
175
+ ls ULID
176
+ ```
177
+
178
+ If you try to add/change/fix features, please update tests and ensure they are not broken.
179
+
180
+ ```console
181
+ $ bundle exec rake test
182
+ $ echo $?
183
+ 0
184
+ ```
185
+
186
+ If you try to improve any performance issue, please add benchmarking and check the result of before and after.
187
+
188
+ ```console
189
+ $ bundle exec ruby benchmark/the_added_file.rb
190
+ # Showing the results
191
+ ```
192
+
193
+ ## Documents
194
+
195
+ - [API documents generated by YARD](https://kachick.github.io/ruby-ulid/)
196
+
197
+ ## Author
198
+
199
+ Kenichi Kamiya - [@kachick](https://github.com/kachick)
data/Steepfile ADDED
@@ -0,0 +1,7 @@
1
+ target :lib do
2
+ signature 'sig'
3
+
4
+ check 'lib'
5
+
6
+ library 'securerandom'
7
+ end
data/lib/ulid.rb CHANGED
@@ -4,7 +4,6 @@
4
4
 
5
5
  require 'securerandom'
6
6
  require 'integer/base'
7
- require_relative 'ulid/version'
8
7
 
9
8
  # @see https://github.com/ulid/spec
10
9
  # @!attribute [r] milliseconds
@@ -23,16 +22,16 @@ class ULID
23
22
  # @see https://www.crockford.com/base32.html
24
23
  ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
25
24
 
26
- TIME_PART_LENGTH = 10
25
+ TIMESTAMP_PART_LENGTH = 10
27
26
  RANDOMNESS_PART_LENGTH = 16
28
- ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
27
+ ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
29
28
  TIMESTAMP_OCTETS_LENGTH = 6
30
29
  RANDOMNESS_OCTETS_LENGTH = 10
31
30
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
32
31
  MAX_MILLISECONDS = 281474976710655
33
32
  MAX_ENTROPY = 1208925819614629174706175
34
33
  MAX_INTEGER = 340282366920938463463374607431768211455
35
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIME_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
34
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
36
35
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
37
36
 
38
37
  # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
@@ -42,49 +41,6 @@ class ULID
42
41
  # @see https://bugs.ruby-lang.org/issues/15958
43
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
44
43
 
45
- class MonotonicGenerator
46
- attr_accessor :latest_milliseconds, :latest_entropy
47
-
48
- def initialize
49
- reset
50
- end
51
-
52
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
53
- # @return [ULID]
54
- def generate
55
- milliseconds = ULID.current_milliseconds
56
- reasonable_entropy = ULID.reasonable_entropy
57
-
58
- @latest_milliseconds ||= milliseconds
59
- @latest_entropy ||= reasonable_entropy
60
- if @latest_milliseconds != milliseconds
61
- @latest_milliseconds = milliseconds
62
- @latest_entropy = reasonable_entropy
63
- else
64
- @latest_entropy += 1
65
- end
66
-
67
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
68
- end
69
-
70
- # @return [self]
71
- def reset
72
- @latest_milliseconds = nil
73
- @latest_entropy = nil
74
- self
75
- end
76
-
77
- # @raise [TypeError] always raises exception and does not freeze self
78
- # @return [void]
79
- def freeze
80
- raise TypeError, "cannot freeze #{self.class}"
81
- end
82
- end
83
-
84
- MONOTONIC_GENERATOR = MonotonicGenerator.new
85
-
86
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
87
-
88
44
  # @param [Integer, Time] moment
89
45
  # @param [Integer] entropy
90
46
  # @return [ULID]
@@ -93,6 +49,18 @@ class ULID
93
49
  new milliseconds: milliseconds, entropy: entropy
94
50
  end
95
51
 
52
+ # @param [Integer, Time] moment
53
+ # @return [ULID]
54
+ def self.min(moment: 0)
55
+ generate(moment: moment, entropy: 0)
56
+ end
57
+
58
+ # @param [Integer, Time] moment
59
+ # @return [ULID]
60
+ def self.max(moment: MAX_MILLISECONDS)
61
+ generate(moment: moment, entropy: MAX_ENTROPY)
62
+ end
63
+
96
64
  # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
97
65
  # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
98
66
  # @return [ULID]
@@ -179,8 +147,8 @@ class ULID
179
147
  unless string.size == ENCODED_ID_LENGTH
180
148
  raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
181
149
  end
182
- timestamp = string.slice(0, TIME_PART_LENGTH)
183
- randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
150
+ timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
151
+ randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
184
152
  milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
185
153
  entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
186
154
  rescue => err
@@ -349,3 +317,12 @@ class ULID
349
317
  @matchdata ||= STRICT_PATTERN.match(to_str).freeze
350
318
  end
351
319
  end
320
+
321
+ require_relative 'ulid/version'
322
+ require_relative 'ulid/monotonic_generator'
323
+
324
+ class ULID
325
+ MONOTONIC_GENERATOR = MonotonicGenerator.new
326
+
327
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
328
+ end
@@ -0,0 +1,44 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ class ULID
6
+ class MonotonicGenerator
7
+ attr_accessor :latest_milliseconds, :latest_entropy
8
+
9
+ def initialize
10
+ reset
11
+ end
12
+
13
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
14
+ # @return [ULID]
15
+ def generate
16
+ milliseconds = ULID.current_milliseconds
17
+ reasonable_entropy = ULID.reasonable_entropy
18
+
19
+ @latest_milliseconds ||= milliseconds
20
+ @latest_entropy ||= reasonable_entropy
21
+ if @latest_milliseconds != milliseconds
22
+ @latest_milliseconds = milliseconds
23
+ @latest_entropy = reasonable_entropy
24
+ else
25
+ @latest_entropy += 1
26
+ end
27
+
28
+ ULID.new milliseconds: milliseconds, entropy: @latest_entropy
29
+ end
30
+
31
+ # @return [self]
32
+ def reset
33
+ @latest_milliseconds = nil
34
+ @latest_entropy = nil
35
+ self
36
+ end
37
+
38
+ # @raise [TypeError] always raises exception and does not freeze self
39
+ # @return [void]
40
+ def freeze
41
+ raise TypeError, "cannot freeze #{self.class}"
42
+ end
43
+ end
44
+ end
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.9'
5
+ VERSION = '0.0.10'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -2,7 +2,7 @@
2
2
  class ULID
3
3
  VERSION: String
4
4
  ENCODING_CHARS: Array[String]
5
- TIME_PART_LENGTH: 10
5
+ TIMESTAMP_PART_LENGTH: 10
6
6
  RANDOMNESS_PART_LENGTH: 16
7
7
  ENCODED_ID_LENGTH: 26
8
8
  TIMESTAMP_OCTETS_LENGTH: 6
@@ -36,6 +36,7 @@ class ULID
36
36
  def freeze: -> void
37
37
  end
38
38
 
39
+ type moment = Time | Integer
39
40
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
40
41
  type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
41
42
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
@@ -56,7 +57,7 @@ class ULID
56
57
  @strict_pattern: Regexp?
57
58
  @matchdata: MatchData?
58
59
 
59
- def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
60
+ def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
60
61
  def self.monotonic_generate: -> ULID
61
62
  def self.current_milliseconds: -> Integer
62
63
  def self.time_to_milliseconds: (Time time) -> Integer
@@ -64,6 +65,8 @@ class ULID
64
65
  def self.parse: (String string) -> ULID
65
66
  def self.from_uuidv4: (String uuid) -> ULID
66
67
  def self.from_integer: (Integer integer) -> ULID
68
+ def self.min: (?moment: moment) -> ULID
69
+ def self.max: (?moment: moment) -> ULID
67
70
  def self.valid?: (untyped string) -> bool
68
71
  def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
69
72
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
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.9
4
+ version: 0.0.10
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-04-30 00:00:00.000000000 Z
11
+ date: 2021-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -81,7 +81,11 @@ executables: []
81
81
  extensions: []
82
82
  extra_rdoc_files: []
83
83
  files:
84
+ - LICENSE
85
+ - README.md
86
+ - Steepfile
84
87
  - lib/ulid.rb
88
+ - lib/ulid/monotonic_generator.rb
85
89
  - lib/ulid/version.rb
86
90
  - sig/ulid.rbs
87
91
  homepage: https://github.com/kachick/ruby-ulid