Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for decoding timestamp from existing ULID #38

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ ulid2 = ULID.generate(time, suffix: an_event_identifier)
ulid1 == ulid2 # true
```

**I want to decode the timestamp portion of an existing ULID value**

You can also decode the timestamp component of a ULID into a `Time` instance (to millisecond precision).

```ruby
time_t1 = Time.new(2022, 1, 4, 6, 3)
ulid = ULID.generate(time_t1)
time_t2 = ULID.decode_time(ulid)
time_t2 == time_t1 # true
```

## Specification

Below is the current specification of ULID as implemented in this repository. *Note: the binary format has not been implemented.*
Expand Down
2 changes: 2 additions & 0 deletions lib/ulid.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'ulid/version'
require 'ulid/generator'
require 'ulid/decoder'

module ULID
extend Generator
extend Decoder
end
26 changes: 26 additions & 0 deletions lib/ulid/decoder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen-string-literal: true

require 'ulid/generator'

module ULID
module Decoder
def decode_time(string)
if string.size != Generator::ENCODED_LENGTH
raise ArgumentError, 'string is not of ULID length'
end

epoch_time_ms = string.slice(0, 10).split('').reverse.each_with_index.reduce(0) do |carry, (char, index)|
encoding_index = Generator::ENCODING.index(char.bytes.first)

if encoding_index.nil?
raise ArgumentError, "invalid character found: #{char}"
end

carry += encoding_index * (Generator::ENCODING.size**index).to_i
carry
end

Time.at(0, epoch_time_ms, :millisecond)
end
end
end
27 changes: 27 additions & 0 deletions spec/lib/ulid_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,31 @@
end
end
end

describe 'decoding a timestamp' do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ULIDs are case-insensitive. This currently fails:

it 'decodes in a case-insensitve manner' do
  ulid = ULID.generate.downcase
  assert ULID.decode_time(ulid).is_a?(Time)
end

For completeness, I'd like to see a few known input/output pairs (but please don't feel obliged to include them):

it 'correctly parses known test vectors' do
  {
    "0000000000Y2GBSG3FEFT635J1" => Time.at(0),
    "013XRZP292318JWRM98F0YAPV9" => Time.at(1234567891234/1000r),
    "ZZZZZZZZZZ-this-is-ignored" => Time.at(1125899906842623/1000r),
  }.each do |vector, expected_time|
    assert_equal expected_time, ULID.decode_time(vector)
  end
end

it 'decodes the timestamp as Time' do
ulid = ULID.generate
assert ULID.decode_time(ulid).is_a?(Time)
end

it 'decodes the timestamp into milliseconds' do
input_time = Time.now.utc
ulid = ULID.generate(input_time)
output_time = ULID.decode_time(ulid).utc
assert_equal (input_time.to_r * 1000).to_i, (output_time.to_r * 1000).to_i
end

it 'rejects strings with invalid characters' do
assert_raises(ArgumentError) do
bad_ulid = ULID.generate.tr('0', '-')
ULID.decode_time(bad_ulid)
end
end

it 'rejects strings of incorrect length' do
assert_raises(ArgumentError) do
ULID.decode_time(ULID.generate + 'f')
end
end
end
end