uxid 0.2.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 329b2b3ae00dc22cfc6a32ce123ad4f22a1211cc2d1851d1797211b838b59983
4
- data.tar.gz: 586e48ce10bd900cf51a1fafc363640443d2f22e6ac845ddcaac85ae810467bc
3
+ metadata.gz: 9ddc9c523df0274739dd977feb61799c5bccba6c0319ac9caa89a97b16d23178
4
+ data.tar.gz: 5f3e9b5660bf0406fe6be9882ddbf0adffa3851c4ef64304cf091df89d860893
5
5
  SHA512:
6
- metadata.gz: a5516fd55ea9f94f64c4a7535b4d1bb751ba729021aad89e28a47fd23b7adb42183d2267374a28badacb0aab704fd591eb22a7d7f4ae0d3a589eb0ee04b2c384
7
- data.tar.gz: a19e5b88e3e0d2b88931bfb146d2323595e848dbf4d6675726afc476c4a1a1a3810870bd1e34e7f92bbbf1b8014419bf97a7bebd5c3392f4a7f624043f56a555
6
+ metadata.gz: 985179a75fa0cdf3bfb8f11b7dc4307c2096fd32f19ef8b1b670c3bfc3acbf7fa429b7a47a2f42f3557733a9d6f908dea47895319ce76cdd267fc16bd6a1f248
7
+ data.tar.gz: 36bc846998cf71c55e698b3bad2c7df452d3257116c447eb4ad001308ec09964c23fae324aea7d9f3dfd3cf0e434d7549a10517d673432b3a88720cf7fbbbbb9
data/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
3
3
  * (Nothing yet)
4
4
 
5
5
 
6
+ ### 1.0.0 / 2026-10-19
7
+
8
+ A rewrite of the gem under its existing name, and a breaking release: the
9
+ 0.2.x API is gone. The gem now implements the wire format of the Elixir
10
+ reference implementation, [uxid-ex](https://github.com/riddler/uxid-ex), and
11
+ passes every case of that implementation's conformance corpus at the tag
12
+ `v2.10.0`, vendored under `conformance/` (`conformance/SOURCE.json` records the
13
+ tag, its commit and the corpus hash). An ID written by 0.2 with a symbol size
14
+ has the layout of the same size in 1.0 apart from its case: 0.2 wrote upper
15
+ case and 1.0 writes lowercase by default (`case: :upper` keeps upper case), and
16
+ `UXID.decode` reads either case. To stay on 0.2, pin `gem "uxid", "~> 0.2"`;
17
+ the README's "Upgrading from 0.2" lists what changes for a caller.
18
+
19
+ **Added**
20
+
21
+ * `UXID.generate(from: "...")` builds a deterministic id: the marker `z` (`Z` in an upper-case id) and then the first bits of the SHA-256 of the prefix, the `from` string and the size, as uxid-ex's wire format defines it, so the same arguments always give the same id and it sorts after every time-based id with its prefix and case. `from` (and the prefix) are hashed as their bytes: valid text in an encoding other than UTF-8 is transcoded to UTF-8 first, and a binary String, one not valid in its own encoding, or one Ruby cannot transcode is hashed as its own bytes, so a raw-byte key such as a 16-byte UUID works. `rand:` with `from:` raises `UXID::Error` with code `:rand_with_from`; the time and random-field options do not apply to a deterministic id and are not read.
22
+ * `UXID.decode(id, delimiter: "_", now: nil)` reads an id back as uxid-ex's wire format defines it and never raises on malformed input. A well-formed id gives a frozen `UXID::Decoded` with `prefix` (nil when there is no delimiter), `size` (`:xs` to `:xl`), `compact?`, `deterministic?` and `time` (milliseconds, nil for a deterministic id); the random field is not read back. Anything else gives a frozen `UXID::Invalid` whose `reason` is `:empty`, `:invalid_length` or `:invalid_character`, the first rule the body fails in that order, with lengths counted in bytes; a value that is not a String is `:invalid_character`. Both answer `valid?`. Body lengths 10 and 18 are read as standard xs and standard m, so a compact xs or compact m id decodes to its size but not its time. A compact time takes its high bits from `now:` (default the system clock), less 2**40 ms when that puts it more than a day ahead.
23
+ * `UXID.valid?(id, prefix: nil, delimiter: "_")` is whether the id decodes and, when `prefix:` is given, has exactly that prefix; `UXID.deterministic?(id, delimiter: "_")` is whether it decodes to a deterministic body. Neither raises on the id.
24
+ * `UXID::Monotonic.new(prefix: nil, size: :xl, draws: nil, **options)` is a monotonic generator, and its `#generate(time: nil, **options)` gives ids that increase strictly for each prefix and random width, within one millisecond and when the clock moves back, by the monotonic rule of uxid-ex's wire format: a later time takes a fresh random value, an equal or earlier one keeps the last time and adds a step drawn uniformly from 1 to 2**(4w), and a step that overflows the field moves to the next millisecond with a fresh value. The options are `UXID.generate`'s; those given to `new` are the defaults of every call. Size `:xs` uses compact time unless `compact_time:` is given, and a request with no random field is refused. The state belongs to the instance, guarded by a Mutex inside it, so one generator may be shared across threads; nothing is kept at class level or globally. Random values come from the byte source (default `SecureRandom`), or from `draws:`, an object answering `seed(width)` and `step(width)`, for replaying recorded draws. `from:` raises `UXID::Error` with code `:from_with_monotonic`, and `rand:` with `:rand_with_monotonic`.
25
+
26
+ **Changed**
27
+
28
+ * Breaking: `UXID.generate` now builds the id uxid-ex's wire format defines, from the keyword options prefix, size, rand_size, compact_time, case, delimiter and time; 0.2.x ids are not reproduced, so callers re-read the option list in the method's doc. Random bytes come from an injectable `byte_source` (default `SecureRandom.random_bytes`), and `rand:` fixes the random field for test vectors only. A refused request raises `UXID::Error`, an `ArgumentError` whose `#code` names the refusal.
29
+ * Breaking: Ruby 3.2 or later is required, where 0.2 asked for 2.3; stay on 0.2 on an older Ruby.
30
+
31
+ **Removed**
32
+
33
+ * Breaking: `UXID.new`, `UXID::Model` and the 0.2 encoder class and constants are removed, and `UXID.generate` no longer takes a Hash; call `UXID.generate` with keyword options, or pin `~> 0.2`.
34
+
35
+
6
36
  ### 0.2.3 / 2020-11-29
7
37
 
8
38
  * Updates project URL
data/README.md CHANGED
@@ -1,75 +1,173 @@
1
1
  # UXID
2
2
 
3
- ![MIT License][badge_license_url]
4
- ![Gem Version][badge_version_url]
3
+ [![MIT License][badge_license_url]](LICENSE)
4
+ [![CI][badge_ci_url]](https://github.com/riddler/uxid-rb/actions/workflows/ci.yml)
5
+ [![Gem Version][badge_version_url]](https://rubygems.org/gems/uxid)
6
+
7
+ User eXperience focused IDentifiers for Ruby: prefixed, K-sortable,
8
+ Stripe-style IDs a person can read, copy and route back to their resource. An
9
+ ID looks like `usr_01m5ahke2t2v9sczzafsbf0y2e`: the prefix names the resource,
10
+ and the body carries a timestamp and randomness in lowercase Crockford Base32.
11
+ The IDs are wire-compatible with the Elixir reference implementation,
12
+ [uxid-ex](https://github.com/riddler/uxid-ex), and the gem is checked against
13
+ that implementation's conformance corpus.
14
+
15
+ ## Why UXID
16
+
17
+ An auto-increment key leaks how many rows a table holds and invites
18
+ enumeration; a random UUID fixes both but is long, unordered and anonymous, so
19
+ in a log line, a URL or a support thread nobody can tell what it points at, and
20
+ a double-click selects only part of it. A UXID names its resource in its
21
+ prefix, selects whole on a double-click, reads aloud without ambiguity, sorts
22
+ by creation time so it indexes well, and is generated in the application with
23
+ no coordination between nodes. Its size is tunable for low-cardinality
24
+ resources, a monotonic generator keeps a burst within one millisecond unique
25
+ and ordered, and a deterministic mode maps the same input to the same ID.
26
+
27
+ ## Install
28
+
29
+ Add `uxid` to your Gemfile:
5
30
 
31
+ ```ruby
32
+ gem "uxid", "~> 1.0"
33
+ ```
6
34
 
7
- **U**ser e**X**perience focused **ID**entifiers (UXIDs) are identifiers which:
35
+ And then execute:
8
36
 
9
- * Describe the resource (aid in debugging and investigation)
10
- * Work well with copy and paste (double clicking selects the entire ID)
11
- * Can be shortened for low cardinality resources
12
- * Are secure against enumeration attacks
13
- * Can be generated by application code (not tied to the datastore)
14
- * Are K-sortable (lexicographically sortable by time - works well with datastore indexing)
15
- * Do not require any coordination (human or automated) at startup, or generation
16
- * Are very unlikely to collide (more likely with less randomness)
17
- * Are easily and accurately transmitted to another human using a telephone
37
+ $ bundle install
18
38
 
19
- Many of the concepts of Stripe IDs have been used in this library.
39
+ Or install it yourself as:
20
40
 
21
- ## Usage
41
+ $ gem install uxid
22
42
 
23
- #### Generating UXIDs
43
+ ## Basic usage
24
44
 
25
45
  ```ruby
26
- # No options generates a basic ULID
27
- UXID.generate # "01EMDGJF0DQXQJ8FM78XE97Y3H"
28
-
29
- # A prefix can be provided
30
- UXID.generate prefix: "cus" # "cus_01EMDGJF0DQXQJ8FM78XE97Y3H"
31
-
32
- # The amount of entropy can be decreased for smaller cardinality resources
33
- # T-Shirt sizes can be used (:xs, :s, :m, :l, :xl) or (:xsmall, :small, :medium, :large, :xlarge)
34
- UXID.generate prefix: "cus", size: :small # "cus_01EQRH884AQYY1"
46
+ require "uxid"
47
+
48
+ # No options generates a plain ID: a time field and a random field
49
+ UXID.generate # "01m5ahke2tebfrqcks88r0d5ta"
50
+
51
+ # A prefix names the resource
52
+ UXID.generate(prefix: "usr") # "usr_01m5ahke2t2v9sczzafsbf0y2e"
53
+
54
+ # Shrink the random part for low-cardinality resources
55
+ # Sizes: :xs :s :m :l :xl (or :xsmall :small :medium :large :xlarge)
56
+ UXID.generate(prefix: "cus", size: :small) # "cus_01m5ahke2t2e40"
57
+
58
+ # Deterministic: the same input gives the same ID, with the prefix as namespace
59
+ UXID.generate(prefix: "usr", from: "alice@example.com")
60
+ # => "usr_zcvt7epac0t1ebcsjfyf7cwz25"
61
+
62
+ # Read an ID back, or check it
63
+ UXID.decode("usr_01hf7yat00")
64
+ # => #<data UXID::Decoded prefix="usr", size=:xs, compact=false, deterministic=false, time=1700000000000>
65
+ UXID.valid?("usr_01hf7yat00", prefix: "usr") # => true
66
+ UXID.valid?("usr_01hf7yat00", prefix: "cus") # => false
67
+ UXID.deterministic?("usr_zcvt7epac0t1ebcsjfyf7cwz25") # => true
68
+
69
+ # Monotonic: IDs from one generator increase strictly, also within one
70
+ # millisecond. You hold the generator; the gem keeps no global state.
71
+ generator = UXID::Monotonic.new(prefix: "usr", size: :s)
72
+ generator.generate # "usr_01m5ahke2te6h0"
73
+ generator.generate # a larger one
35
74
  ```
36
75
 
76
+ `UXID.decode` never raises on malformed input: it answers a `UXID::Invalid`
77
+ whose `reason` is `:empty`, `:invalid_length` or `:invalid_character`. A request
78
+ the wire format refuses, such as `rand_size: 11`, raises `UXID::Error`, an
79
+ `ArgumentError` whose `code` names the refusal.
37
80
 
38
- ## Installation
81
+ ## Upgrading from 0.2
39
82
 
40
- Add this line to your application's Gemfile:
83
+ Version 1.0.0 is a different format and API under the same gem name. It is a
84
+ breaking release, not an upgrade path for 0.2 code. To stay on 0.2, pin it:
41
85
 
42
86
  ```ruby
43
- gem "uxid"
87
+ gem "uxid", "~> 0.2"
44
88
  ```
45
89
 
46
- And then execute:
47
-
48
- $ bundle install
49
-
50
- Or install it yourself as:
51
-
52
- $ gem install uxid
53
-
90
+ To move to 1.0:
91
+
92
+ - **The API is replaced.** `UXID.generate` takes keyword options (`prefix`,
93
+ `size`, `rand_size`, `compact_time`, `case`, `delimiter`, `time`, `rand`,
94
+ `byte_source`, `from`) and no longer takes a Hash. `UXID.new`, `UXID::Model`
95
+ and the 0.2 encoder class and constants are gone. `UXID.decode`,
96
+ `UXID.valid?`, `UXID.deterministic?` and `UXID::Monotonic` are new.
97
+ - **Sizes are symbols or their names.** An Integer `size` is refused with an
98
+ `ArgumentError`; use `rand_size` for a specific number of random bytes, from
99
+ 0 to 10. A String `size` such as `"small"` was read by 0.2 as the largest
100
+ size, with a warning; in 1.0 it means what the name says, so such a call
101
+ gives a shorter ID and no error.
102
+ - **IDs are written in lowercase by default.** 0.2 wrote upper case. Pass
103
+ `case: :upper` to keep writing it. `UXID.decode` reads either case.
104
+ - **Errors are `UXID::Error`.** It is an `ArgumentError`, where 0.2's was a
105
+ `StandardError`, and its `code` names the refusal.
106
+ - **Ruby 3.2 or later** is required, where 0.2 asked for 2.3.
107
+ - **Stored IDs.** An ID written by 0.2 with a symbol size has the layout of the
108
+ same size in 1.0, apart from its case, and `UXID.decode` reads it. Compare IDs
109
+ in one case, or normalize them, if you mix the two.
110
+
111
+ ## Compatibility
112
+
113
+ Ruby 3.2 or later (`required_ruby_version` in the gemspec). CI runs the test
114
+ suite on every Ruby minor from 3.2 up to the version `mise.toml` pins, and the
115
+ full quality gate on that pinned version. There are no runtime dependencies;
116
+ random bytes come from `SecureRandom` unless you give a `byte_source`. One
117
+ `UXID::Monotonic` instance may be shared across threads: its state is guarded
118
+ by a Mutex inside the instance, and nothing is kept at class level or globally.
119
+ IDs from different generators carry no order between them.
120
+
121
+ ## Documentation
122
+
123
+ - Look up
124
+ - Every public method and class carries a YARD doc comment in `lib/`:
125
+ `UXID.generate`, `UXID.decode`, `UXID.valid?`, `UXID.deterministic?`,
126
+ `UXID::Monotonic`, `UXID::Decoded`, `UXID::Invalid` and `UXID::Error`.
127
+ - [UXID wire format][guide_wire_format_url]: the normative layout of an ID -
128
+ alphabet, bit packing, sizes, prefix grammar, deterministic and monotonic
129
+ bodies, and decoding - which this gem implements.
130
+ - [The changelog](CHANGELOG.md): what changed in each version.
131
+ - [The conformance corpus](conformance/README.md): the cases every
132
+ implementation of the wire format passes, vendored here from uxid-ex.
133
+ - Understand
134
+ - [Why a UXID has a prefix, a time and randomness][guide_why_url]: what each
135
+ part of an ID is for, and what you trade when you tune it.
136
+ - [Why monotonic mode exists and what it costs][guide_monotonic_url]:
137
+ same-millisecond uniqueness and ordering, and the security tradeoff.
138
+ - [_Designing APIs for humans: object IDs_][stripe_ids_url]: the Stripe ID
139
+ design many of UXID's choices follow.
54
140
 
55
141
  ## Development
56
142
 
57
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
143
+ After checking out the repo, run `mise install` for the Ruby `mise.toml` pins
144
+ and `mise exec -- bundle install` for the development tools. Then
145
+ `mise exec -- bundle exec rake test` runs the tests, and
146
+ `mise exec -- bundle exec rake` runs the full gate: RuboCop, the corpus
147
+ integrity check and the tests under a coverage minimum.
58
148
 
59
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
149
+ A release is published by CI, never from a checkout: pushing a tag `vX.Y.Z`
150
+ that names `UXID::VERSION` runs `.github/workflows/release.yml`, which builds
151
+ and pushes the gem to [rubygems.org](https://rubygems.org). `rake release`
152
+ refuses to run anywhere else.
60
153
 
61
154
  ## Contributing
62
155
 
63
- Bug reports and pull requests are welcome on GitHub at https://github.com/riddler/uxid.
64
-
156
+ Bug reports and pull requests are welcome on GitHub at
157
+ https://github.com/riddler/uxid-rb.
65
158
 
66
159
  ## License
67
160
 
68
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
161
+ The gem is available as open source under the terms of the
162
+ [MIT License](https://opensource.org/licenses/MIT).
69
163
 
70
164
  <!-- LINKS -->
71
- [mit_license_url]: http://opensource.org/licenses/MIT
165
+ [guide_wire_format_url]: https://hexdocs.pm/uxid/wire-format.html
166
+ [guide_why_url]: https://hexdocs.pm/uxid/why-prefix-time-randomness.html
167
+ [guide_monotonic_url]: https://hexdocs.pm/uxid/monotonic.html
168
+ [stripe_ids_url]: https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a
72
169
 
73
170
  <!-- BADGES -->
74
171
  [badge_license_url]: https://img.shields.io/badge/license-MIT-brightgreen.svg?cacheSeconds=3600?style=flat-square
172
+ [badge_ci_url]: https://github.com/riddler/uxid-rb/actions/workflows/ci.yml/badge.svg
75
173
  [badge_version_url]: https://badge.fury.io/rb/uxid.svg
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UXID
4
+ # What UXID.decode reads from a well-formed id: its prefix, its size and
5
+ # time form, whether it is deterministic, and its time. The random field is
6
+ # not read back. A frozen value: two decodings of the same id are equal.
7
+ #
8
+ # @!attribute [r] prefix
9
+ # @return [String, nil] everything before the last delimiter, nil when the
10
+ # id has no delimiter; an id that starts with the delimiter (not one a
11
+ # producer emits) has the empty string, which reads as no prefix
12
+ # @!attribute [r] size
13
+ # @return [Symbol] the size the body length gives: :xs, :s, :m, :l or :xl
14
+ # @!attribute [r] compact
15
+ # @return [Boolean] whether the body length is a compact one; see
16
+ # {#compact?}
17
+ # @!attribute [r] deterministic
18
+ # @return [Boolean] whether the body starts with the marker z or Z; see
19
+ # {#deterministic?}
20
+ # @!attribute [r] time
21
+ # @return [Integer, nil] the time in milliseconds since the Unix epoch, nil
22
+ # for a deterministic body. A compact time's high bits come from the
23
+ # decoder's clock (UXID.decode's :now)
24
+ Decoded = Data.define(:prefix, :size, :compact, :deterministic, :time) do
25
+ # @return [true] a decoded id is always valid; {Invalid#valid?} is false,
26
+ # so a caller can branch on the result without a type check
27
+ def valid?
28
+ true
29
+ end
30
+
31
+ # @return [Boolean] whether the body length is a compact one
32
+ def compact?
33
+ compact
34
+ end
35
+
36
+ # @return [Boolean] whether the body is deterministic (no time)
37
+ def deterministic?
38
+ deterministic
39
+ end
40
+ end
41
+
42
+ # What UXID.decode answers for a string it refuses: the reason, named as
43
+ # the wire-format spec and its conformance corpus name it. A frozen value.
44
+ #
45
+ # @!attribute [r] reason
46
+ # @return [Symbol] :empty, :invalid_length or :invalid_character
47
+ Invalid = Data.define(:reason) do
48
+ # @return [false] a refused id is never valid; {Decoded#valid?} is true
49
+ def valid?
50
+ false
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UXID
4
+ # Reads an id back to a Decoded, or refuses it with an Invalid. Internal:
5
+ # the public entry point is UXID.decode, which documents the reading.
6
+ module Decoder
7
+ # Body length in bytes to the size and whether the time is compact. The
8
+ # lengths 10 and 18 are also those of compact xs and compact m; the wire
9
+ # format reads both as standard.
10
+ LENGTHS = {
11
+ 10 => [:xs, false], 13 => [:s, true], 14 => [:s, false], 18 => [:m, false],
12
+ 21 => [:l, true], 22 => [:l, false], 24 => [:xl, true], 26 => [:xl, false]
13
+ }.freeze
14
+
15
+ # Each byte of the alphabet, in either case, to its 5-bit value.
16
+ VALUES = (Encoder::LOWER.bytes.each_with_index.to_a + Encoder::UPPER.bytes.each_with_index.to_a).to_h.freeze
17
+
18
+ MARKERS = ["z".ord, "Z".ord].freeze
19
+ COMPACT_WINDOW = 2**40
20
+ ONE_DAY = 86_400_000
21
+
22
+ module_function
23
+
24
+ # @param id [Object] the id; anything other than a String is refused
25
+ # @param delimiter [String] the non-empty string between prefix and body
26
+ # @param now [Integer, Time, nil] the decoder's clock for a compact time
27
+ # @return [Decoded, Invalid]
28
+ def decode(id, delimiter, now)
29
+ check_options(delimiter, now)
30
+ return Invalid.new(reason: :invalid_character) unless id.is_a?(String)
31
+
32
+ prefix, body = split(id, delimiter)
33
+ reason = refusal(body)
34
+ reason ? Invalid.new(reason: reason) : decoded(prefix, body, now)
35
+ end
36
+
37
+ def check_options(delimiter, now)
38
+ unless delimiter.is_a?(String) && !delimiter.empty?
39
+ raise ArgumentError, "delimiter: must be a non-empty String, got: #{delimiter.inspect}"
40
+ end
41
+ return if now.nil? || now.is_a?(Integer) || now.is_a?(Time)
42
+
43
+ raise ArgumentError, "now: must be an Integer of milliseconds, a Time or nil, got: #{now.inspect}"
44
+ end
45
+
46
+ # The body is everything after the last delimiter and the prefix
47
+ # everything before it; with no delimiter the whole id is the body. The
48
+ # split is on bytes, so an id that is not valid in its encoding splits
49
+ # without raising; the prefix keeps the id's encoding.
50
+ def split(id, delimiter)
51
+ bytes = id.b
52
+ at = bytes.rindex(delimiter.b)
53
+ return [nil, bytes] if at.nil?
54
+
55
+ prefix = bytes.byteslice(0, at).force_encoding(id.encoding).freeze
56
+ [prefix, bytes.byteslice(at + delimiter.bytesize, bytes.bytesize)]
57
+ end
58
+
59
+ # The first rule the body fails, in the spec's order, or nil when it is
60
+ # well formed. Lengths are in bytes.
61
+ def refusal(body)
62
+ return :empty if body.empty?
63
+ return :invalid_length unless LENGTHS.key?(body.bytesize)
64
+
65
+ :invalid_character unless body.each_byte.all? { |byte| VALUES.key?(byte) }
66
+ end
67
+
68
+ def decoded(prefix, body, now)
69
+ size, compact = LENGTHS.fetch(body.bytesize)
70
+ deterministic = MARKERS.include?(body.getbyte(0))
71
+ time =
72
+ if deterministic then nil
73
+ elsif compact then compact_time(value(body, 8), now)
74
+ else standard_time(body)
75
+ end
76
+ Decoded.new(prefix: prefix, size: size, compact: compact, deterministic: deterministic, time: time)
77
+ end
78
+
79
+ # The first 10 characters, the first holding the time's top 3 bits; the
80
+ # first character's higher bits are not read.
81
+ def standard_time(body)
82
+ ((VALUES.fetch(body.getbyte(0)) & 7) << 45) | value(body.byteslice(1, 9), 9)
83
+ end
84
+
85
+ # The low 40 bits from the id and the rest from the clock; a result more
86
+ # than a day ahead of the clock is from the window before.
87
+ def compact_time(low, now)
88
+ clock = millis(now)
89
+ time = ((clock >> 40) << 40) | low
90
+ time - clock > ONE_DAY ? time - COMPACT_WINDOW : time
91
+ end
92
+
93
+ def value(body, count)
94
+ body.each_byte.first(count).reduce(0) { |acc, byte| (acc << 5) | VALUES.fetch(byte) }
95
+ end
96
+
97
+ def millis(now)
98
+ case now
99
+ when nil then Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
100
+ when Time then (now.to_r * 1000).floor
101
+ else now
102
+ end
103
+ end
104
+ end
105
+
106
+ private_constant :Decoder
107
+ end