File Coverage

blib/lib/Data/ULID.pm
Criterion Covered Total %
statement 98 98 100.0
branch 17 24 70.8
condition 6 8 75.0
subroutine 27 27 100.0
pod 0 5 0.0
total 148 162 91.3


line stmt bran cond sub pod time code
1              
2             use strict;
3 2     2   103575 use warnings;
  2         15  
  2         61  
4 2     2   10  
  2         4  
  2         84  
5             our $VERSION = '1.1.3_02';
6              
7             use base qw(Exporter);
8 2     2   12 our @EXPORT_OK = qw/ulid binary_ulid ulid_date ulid_to_uuid uuid_to_ulid/;
  2         3  
  2         320  
9             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
10              
11             use Time::HiRes qw/time/;
12 2     2   14 use Crypt::PRNG qw/random_bytes/;
  2         5  
  2         16  
13 2     2   1173  
  2         7056  
  2         150  
14             use constant HAS_DATETIME => eval { require DateTime; 1 };
15 2     2   16  
  2         4  
  2         3  
  2         1755  
  2         1108098  
16             BEGIN {
17             use Config;
18 2     2   21 use constant CAN_SKIP_BIGINTS => $Config{ivsize} >= 8;
  2         5  
  2         108  
19 2     2   14  
  2         6  
  2         304  
20             if (!CAN_SKIP_BIGINTS) {
21 2     2   2621 require Math::BigInt;
22             Math::BigInt->VERSION(1.999808);
23             Math::BigInt->import(try => 'GMP,LTM');
24             }
25             }
26              
27             ### EXPORTED ULID FUNCTIONS
28              
29             return _encode(_ulid(shift));
30             }
31 6     6 0 3260  
32             return _pack(_ulid(shift));
33             }
34              
35 5     5 0 1441 my $ulid = shift;
36              
37             die "ulid_date() requires DateTime module" unless HAS_DATETIME;
38             die "ulid_date() needs a normal or binary ULID as parameter" unless $ulid;
39 4     4 0 1843  
40             my ($ts, $rand) = _ulid($ulid);
41 4         5  
42 4 50       13 return DateTime->from_epoch(epoch => _unfix_ts($ts));
43             }
44 4         8  
45             my $ulid = shift or die "Need ULID to convert";
46 4         13 my $bin = _pack(_ulid($ulid));
47             return _uuid_bin2str($bin)
48             }
49              
50 1 50   1 0 12 my $uuid = shift or die "Need UUID to convert";
51 1         3 my $bin_uuid = _uuid_str2bin($uuid);
52 1         3 return _encode(_ulid($bin_uuid));
53             }
54              
55             ### HELPER FUNCTIONS
56 3 50   3 0 320  
57 3         8 my $uuid = shift;
58 3         7  
59             return $uuid if length($uuid) == 36;
60             die "Invalid uuid" unless length $uuid == 16;
61             my @offsets = (4, 2, 2, 2, 6);
62              
63             return join(
64 1     1   2 '-',
65             map { unpack 'H*', $_ }
66 1 50       4 map { substr $uuid, 0, $_, ''}
67 1 50       9 @offsets);
68 1         3 }
69              
70             my $uuid = shift;
71              
72 5         25 return $uuid if length $uuid == 16;
73 1         8 $uuid =~ s/-//g;
  5         10  
74              
75             return pack 'H*', $uuid;
76             }
77              
78 3     3   4 my $arg = shift;
79             my $ts;
80 3 50       8  
81 3         16 if ($arg) {
82             if (ref $arg && $arg->isa('DateTime')) {
83 3         17 $ts = $arg->hires_epoch;
84             }
85             elsif (length($arg) == 16) {
86             return _unpack($arg);
87 19     19   30 }
88 19         27 else {
89             $arg = _normalize($arg);
90 19 100       58 die "Invalid ULID supplied: wrong length" unless length($arg) == 26;
91 17 100 66     70 return _decode($arg);
    100          
92 1         4 }
93             }
94              
95 7         22 return (_fix_ts($ts || time()), random_bytes(10));
96             }
97              
98 9         22 my ($ts, $rand) = @_;
99 9 50       24 return _zero_pad($ts, 6, "\x00") . _zero_pad($rand, 10, "\x00");
100 9         22 }
101              
102             my ($ts, $rand) = unpack 'a6a10', shift;
103             return ($ts, $rand);
104 3   66     41 }
105              
106             my $ts = shift;
107              
108 6     6   12 if (CAN_SKIP_BIGINTS) {
109 6         11 $ts *= 1000;
110             return pack 'Nn', int($ts / (2 << 15)), $ts % (2 << 15);
111             } else {
112             $ts .= '000';
113 7     7   31 $ts =~ s/\.(\d{3}).*$/$1/;
114 7         24 return Math::BigInt->new($ts)->to_bytes;
115             }
116             }
117              
118 3     3   5 my $ts = shift;
119              
120 3         6 if (CAN_SKIP_BIGINTS) {
121 3         6 my ($high, $low) = unpack 'Nn', $ts;
122 3         41 return ($high * (2 << 15) + $low) / 1000;
123             } else {
124             $ts = Math::BigInt->from_bytes($ts);
125             $ts =~ s/(\d{3})$/.$1/;
126             return $ts;
127             }
128             }
129              
130             my ($ts, $rand) = @_;
131 4     4   6 return sprintf('%010s%016s', _encode_b32($ts), _encode_b32($rand));
132             }
133 4         6  
134 4         13 my ($ts, $rand) = map { _decode_b32($_) } unpack 'A10A16', shift;
135 4         21 return ($ts, $rand);
136             }
137              
138             my ($value, $mul, $char) = @_;
139             $char ||= '0';
140              
141             my $padded = length($value) % $mul;
142             return $value if $padded == 0;
143              
144 9     9   291 my $padding = substr $value, 0, $padded, '';
145 9         19  
146             return $value if $padding eq $char x $padded;
147             return $char x ($mul - $padded) . $padding . $value;
148             }
149 9     9   36  
  18         37  
150 9         38 ### BASE32 ENCODER / DECODER
151              
152             my $ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
153              
154 48     48   88 my %ALPHABET_MAP = do {
155 48   100     151 my $num = 0;
156             map { $_ => substr sprintf('0000%b', $num++), -5 } split //, $ALPHABET;
157 48         72 };
158 48 100       143  
159             my %ALPHABET_MAP_REVERSE = map { $ALPHABET_MAP{$_} => $_ } keys %ALPHABET_MAP;
160 18         49  
161             my $s = uc(shift);
162 18 100       111 my $re = "[^$ALPHABET]";
163 2         7  
164             $s =~ s/$re//g;
165             return $s;
166             }
167              
168             my $bits = _zero_pad(unpack('B*', shift), 5);
169             my $len = length $bits;
170              
171             my $result = '';
172             for (my $i = 0; $i < $len; $i += 5) {
173             $result .= $ALPHABET_MAP_REVERSE{substr $bits, $i, 5};
174             }
175             return $result;
176             }
177              
178 9     9   19 my $encoded = join '', map { $ALPHABET_MAP{$_} } split //, uc shift;
179 9         32 return pack 'B*', _zero_pad($encoded, 8);
180             }
181 9         67  
182 9         21 1;
183              
184              
185             =pod
186 18     18   56  
187 18         33 =head1 NAME
188              
189 18         21 Data::ULID - Universally Unique Lexicographically Sortable Identifier
190 18         38  
191 227         462  
192             =head1 SYNOPSIS
193 18         96  
194             use Data::ULID qw/ulid binary_ulid ulid_date/;
195              
196             my $ulid = ulid(); # e.g. 01ARZ3NDEKTSV4RRFFQ69G5FAV
197 18     18   62 my $bin_ulid = binary_ulid($ulid);
  234         397  
198 18         70 my $datetime_obj = ulid_date($ulid); # e.g. 2016-06-13T13:25:20
199             my $uuid = ulid_to_uuid($ulid);
200             my $ulid2 = uuid_to_ulid($uuid);
201              
202              
203             =head1 DESCRIPTION
204              
205             =head2 Background
206              
207             This is an implementation in Perl of the ULID identifier type introduced by
208             Alizain Feerasta. The original implementation (in Javascript) can be found at
209             L<https://github.com/alizain/ulid>.
210              
211             ULIDs have several advantages over UUIDs in many contexts. The advantages
212             include:
213              
214             =over
215              
216             =item *
217              
218             Lexicographically sortable
219              
220             =item *
221              
222             The canonical representation is shorter than UUID (26 vs 36 characters)
223              
224             =item *
225              
226             Case insensitve and safely chunkable.
227              
228             =item *
229              
230             URL-safe
231              
232             =item *
233              
234             Timestamp can always be easily extracted if so desired.
235              
236             =item *
237              
238             Limited compatibility with UUIDS, since both are 128-bit formats.
239             Some conversion back and forth is possible.
240              
241             =back
242              
243             =head2 Canonical representation
244              
245             The canonical representation of a ULID is a 26-byte, base32-encoded string
246             consisting of (1) a 10-byte timestamp with millisecond-resolution; and (2) a
247             16-byte random part.
248              
249             Without paramters, the C<ulid()> function returns a new ULID in the canonical
250             representation, with the current time (up to the nearest millisecond) in the
251             timestamp part.
252              
253             $ulid = ulid();
254              
255             Given a DateTime object as parameter, the function will set the timestamp part
256             based on that:
257              
258             $ulid = ulid($datetime_obj);
259              
260             Given a binary ULID as parameter, it returns the same ULID in canonical
261             format:
262              
263             $ulid = ulid($binary_ulid);
264              
265             =head2 Binary representation
266              
267             The binary representation of a ULID is 16 octets long, with each component in
268             network byte order (most significant byte first). The components are (1) a
269             48-bit (6-byte) timestamp in a 32-bit and a 16-bit chunk; (2) an 80-bit
270             (10-byte) random part in a 16-bit and two 32-bit chunks.
271              
272             The C<binary_ulid()> function returns a ULID in binary representation. Like
273             C<ulid()>, it can take no parameters or a DateTime, but it can also take a
274             ULID in the canonical representation and convert it to binary:
275              
276             $binary_ulid = binary_ulid($canonical_ulid);
277              
278             =head2 Datetime extraction
279              
280             The C<ulid_date()> function takes a ULID (canonical or binary) and returns
281             a DateTime object corresponding to the timestamp it encodes.
282              
283             $datetime = ulid_date($ulid);
284              
285             =head2 UUID conversion
286              
287             Very limited conversion between UUIDs and ULIDs is provided.
288              
289             In order to convert a UUID to ULID:
290              
291             $ulid = uuid_to_ulid($uuid);
292              
293             Both binary and hexadecimal UUIDs (with or without separators) are accepted.
294             The return value is a ULID string in the canonical Base32 form. Note that the
295             "timestamp" of such a ULID is not to be relied upon.
296              
297             A ULID can also be converted to a UUID:
298              
299             $uuid = ulid_to_uuid($binary_or_canonical_ulid);
300              
301             The UUID returned by this function is a string in the standard hyphenated
302             hexadecimal format. Note that the variant and version indicators of such a
303             UUID are meaningless.
304              
305             =head2 UUID conversion limitations
306              
307             Since both ULIDs and UUIDs are 128-bit, conversion back and forth is possible
308             in principle. However, the two formats have different semantics. Also, any
309             given UUID version has at most 122 bits of variance (4 bits being reserved as
310             variant and version indicators), while all 128 bits of the ULID format can
311             vary without violating the format description. This means that the conversion
312             can never be made perfect.
313              
314             It would be possible to maintain the approximate timestamp of a Version 1 UUID
315             when converting to ULID, as well as to keep the timestamp of a ULID when
316             converting to UUID. However, since many UUIDs are not of Version 1, and given
317             the different semantics of the two formats, the conversion provided by this
318             module is much simpler and does not preserve the timestamps. In fact, about
319             the only desirable property that the chosen conversion method has is that it
320             is uniformly bidirectional, i.e.
321              
322             $uuid eq ulid_to_uuid(ulid_to_uuid($uuid))
323              
324             and
325              
326             $ulid eq uuid_to_ulid(ulid_to_uuid($ulid))
327              
328             This approach has two immediate consequences:
329              
330             =over
331              
332             =item 1.
333              
334             The "timestamps" of ULIDs created by converting UUIDs are meaningless.
335              
336             =item 2.
337              
338             The variant and version indicators of UUIDs created by converting ULIDs are
339             similarly wrong. Such UUIDs should only be used in contexts where no checking
340             of these fields will be performed and no attempt will be made to extract or
341             validate non-random information (i.e. timestamp, MAC address or namespace).
342              
343             =back
344              
345              
346             =head1 AUTHOR
347              
348             Baldur Kristinsson, December 2016
349              
350              
351             =head1 LICENSE
352              
353             This is free software. It may be copied, distributed and modified under the
354             same terms as Perl itself.
355              
356             =cut
357