@@ -29,45 +29,165 @@ class Ulid
2929 * Generate a new ULID.
3030 *
3131 * @return string The generated ULID.
32- * @throws \Exception
3332 */
3433 public static function generate (bool $ upperCase = true ): string
3534 {
36- $ time = ( int )( microtime (true ) * 1000000 );
35+ $ time = self :: microtimeToUlidTime ( \ microtime (true ));
3736 $ timeChars = self ::encodeTime ($ time );
3837 $ randChars = self ::encodeRandomness ();
3938 $ ulid = $ timeChars . $ randChars ;
4039
41- return $ upperCase ? strtoupper ($ ulid ) : strtolower ($ ulid );
40+ $ ulid = $ upperCase ? \strtoupper ($ ulid ) : \strtolower ($ ulid );
41+
42+ return $ ulid ;
4243 }
4344
44- private static function encodeTime (int $ time ): string
45+ /**
46+ * @param int $time
47+ *
48+ * @return string
49+ */
50+ public static function encodeTime (int $ time ): string
4551 {
52+ $ encodingCharsArray = str_split (self ::ENCODING_CHARS );
4653 $ timeChars = '' ;
4754 for ($ i = 0 ; $ i < 10 ; $ i ++) {
48- $ mod = $ time % self ::ENCODING_LENGTH ;
49- $ timeChars = self :: ENCODING_CHARS [$ mod ] . $ timeChars ;
50- $ time = ($ time - $ mod ) / self ::ENCODING_LENGTH ;
55+ $ mod = \floor ( $ time % self ::ENCODING_LENGTH ) ;
56+ $ timeChars = $ encodingCharsArray [$ mod ] . $ timeChars ;
57+ $ time = (int )(( $ time - $ mod ) / self ::ENCODING_LENGTH ) ;
5158 }
5259 return $ timeChars ;
5360 }
5461
55- /**
56- * @throws \Exception
57- */
58- private static function encodeRandomness (): string
62+ public static function encodeRandomness (): string
5963 {
60- $ randomBytes = random_bytes (10 ); // 80 bits
64+ $ encodingCharsArray = str_split (self ::ENCODING_CHARS );
65+ $ randomBytes = \random_bytes (10 ); // 80 bits
66+ // Check if the random bytes were generated successfully.
67+ if (false === $ randomBytes ) {
68+ throw new \RuntimeException ('Failed to generate random bytes ' );
69+ }
70+
6171 $ randChars = '' ;
6272 for ($ i = 0 ; $ i < 16 ; $ i ++) {
63- $ randValue = ord ($ randomBytes [$ i % 10 ]);
64- if ($ i % 2 === 0 ) {
73+ $ randValue = \ ord ($ randomBytes [$ i % 10 ]);
74+ if (0 === $ i % 2 ) {
6575 $ randValue >>= 3 ; // take the upper 5 bits
6676 } else {
6777 $ randValue &= 31 ; // take the lower 5 bits
6878 }
69- $ randChars .= self :: ENCODING_CHARS [$ randValue ];
79+ $ randChars .= $ encodingCharsArray [$ randValue ];
7080 }
7181 return $ randChars ;
7282 }
83+
84+ /**
85+ * @param string $ulid
86+ *
87+ * @return array
88+ */
89+ public static function decode (string $ ulid ): array
90+ {
91+ if (!self ::isValid ($ ulid )) {
92+ throw new \InvalidArgumentException ('Invalid ULID string ' );
93+ }
94+
95+ $ time = self ::decodeTime ($ ulid );
96+ $ rand = self ::decodeRandomness ($ ulid );
97+
98+ return [
99+ 'time ' => $ time ,
100+ 'rand ' => $ rand ,
101+ ];
102+ }
103+
104+ /**
105+ * @param string $ulid
106+ *
107+ * @return int
108+ */
109+ public static function decodeTime (string $ ulid ): int
110+ {
111+ // $encodingCharsArray = str_split(self::ENCODING_CHARS);
112+
113+ // Check if the ULID string is valid.
114+ if (!self ::isValid ($ ulid )) {
115+ throw new \InvalidArgumentException ('Invalid ULID string ' );
116+ }
117+
118+ $ time = 0 ;
119+ for ($ i = 0 ; $ i < 10 ; $ i ++) {
120+ $ char = $ ulid [$ i ];
121+ $ value = \strpos (self ::ENCODING_CHARS , $ char );
122+ $ exponent = 9 - $ i ;
123+ $ time += $ value * \bcpow ((string )self ::ENCODING_LENGTH , (string )$ exponent );
124+ }
125+
126+ return $ time ;
127+ }
128+
129+ /**
130+ * @param string $ulid
131+ *
132+ * @return int
133+ */
134+ public static function decodeRandomness (string $ ulid ): int
135+ {
136+ if (26 !== strlen ($ ulid )) {
137+ throw new \InvalidArgumentException ('Invalid ULID length ' ); // Changed line
138+ }
139+
140+ $ rand = 0 ;
141+ for ($ i = 10 ; $ i < 26 ; $ i ++) {
142+ $ char = $ ulid [$ i ];
143+ $ value = \strpos (self ::ENCODING_CHARS , $ char );
144+
145+ // Check if the random value is within the valid range.
146+ if ($ value < 0 || $ value >= self ::ENCODING_LENGTH ) {
147+ throw new \InvalidArgumentException ('Invalid ULID random value ' );
148+ }
149+ $ exponent = 15 - $ i ;
150+ $ rand += $ value * \bcpow ((string )self ::ENCODING_LENGTH , (string )$ exponent );
151+ }
152+
153+ return $ rand ;
154+ }
155+
156+ /**
157+ * @param string $ulid
158+ *
159+ * @return bool
160+ */
161+ public static function isValid (string $ ulid ): bool
162+ {
163+ // Check the length of the ULID string before throwing an exception.
164+ if (26 !== strlen ($ ulid )) {
165+ return false ;
166+ }
167+
168+ // Throw an exception if the ULID is invalid.
169+ try {
170+ self ::decodeRandomness ($ ulid );
171+ } catch (\InvalidArgumentException $ e ) {
172+ return false ;
73173}
174+
175+ return true ;
176+ }
177+
178+ /**
179+ * @param float $microtime
180+ *
181+ * @return int
182+ */
183+ public static function microtimeToUlidTime (float $ microtime ): int
184+ {
185+ $ timestamp = $ microtime * 1000000 ;
186+ $ unixEpoch = 946684800000000 ; // Microseconds since the Unix epoch.
187+
188+ return (int )($ timestamp - $ unixEpoch );
189+ }
190+ }
191+
192+
193+
0 commit comments