Skip to content
Merged
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
46 changes: 41 additions & 5 deletions src/Snowflake.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Godruoyi\Snowflake;

use Closure;
use DateTimeInterface;

class Snowflake
{
Expand Down Expand Up @@ -75,11 +76,43 @@ public function id(): string
$currentTime = $this->getCurrentMillisecond();
}

return $this->buildId($currentTime, $this->getStartTimeStamp(), $sequence);
}

/**
* Generate snowflake id for a specific timestamp.
*
* @param int|DateTimeInterface $timestamp The timestamp to generate ID for (in milliseconds or DateTime object)
* @return string
* @throws SnowflakeException
*/
public function idForTimestamp(int|DateTimeInterface $timestamp): string
{
$currentTime = $timestamp instanceof DateTimeInterface
? (int) $timestamp->format('Uv')
: $timestamp;

// Validate timestamp is not earlier than start time
$startTime = $this->getStartTimeStamp();
if ($currentTime < $startTime) {
throw new SnowflakeException('The provided timestamp cannot be earlier than the start time');
}

// Get sequence number (auto-increment if overflow)
while (($sequence = $this->callResolver($currentTime)) > (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH))) {
$currentTime++;
}

return $this->buildId($currentTime, $startTime, $sequence);
}

protected function buildId(int $currentTime, float|int $startTime, int $sequence): string
{
$workerLeftMoveLength = self::MAX_SEQUENCE_LENGTH;
$datacenterLeftMoveLength = self::MAX_WORKID_LENGTH + $workerLeftMoveLength;
$timestampLeftMoveLength = self::MAX_DATACENTER_LENGTH + $datacenterLeftMoveLength;

return (string) ((($currentTime - $this->getStartTimeStamp()) << $timestampLeftMoveLength)
return (string) ((($currentTime - $startTime) << $timestampLeftMoveLength)
| ($this->datacenter << $datacenterLeftMoveLength)
| ($this->workerId << $workerLeftMoveLength)
| ($sequence));
Expand Down Expand Up @@ -130,7 +163,10 @@ public function setStartTimeStamp(int $millisecond): self
$maxTimeDiff = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH);

if ($missTime > $maxTimeDiff) {
throw new SnowflakeException(sprintf('The current microtime - starttime is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', self::MAX_TIMESTAMP_LENGTH));
throw new SnowflakeException(sprintf(
'The current microtime - starttime is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this',
Comment thread
godruoyi marked this conversation as resolved.
self::MAX_TIMESTAMP_LENGTH
));
}

$this->startTime = $millisecond;
Expand All @@ -143,7 +179,7 @@ public function setStartTimeStamp(int $millisecond): self
*/
public function getStartTimeStamp(): float|int
{
if (! is_null($this->startTime)) {
if (!is_null($this->startTime)) {
return $this->startTime;
Comment thread
godruoyi marked this conversation as resolved.
}

Expand Down Expand Up @@ -186,11 +222,11 @@ protected function callResolver(int $currentTime): int
{
$resolver = $this->getSequenceResolver();

if (! is_null($resolver) && is_callable($resolver)) {
if (!is_null($resolver) && is_callable($resolver)) {
return $resolver($currentTime);
}

return ! ($resolver instanceof SequenceResolver)
return !($resolver instanceof SequenceResolver)
? $this->getDefaultSequenceResolver()->sequence($currentTime)
Comment thread
godruoyi marked this conversation as resolved.
: $resolver->sequence($currentTime);
}
Expand Down
44 changes: 44 additions & 0 deletions tests/SnowflakeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
namespace Tests;

use Closure;
use DateTime;
use Godruoyi\Snowflake\RandomSequenceResolver;
use Godruoyi\Snowflake\SequenceResolver;
use Godruoyi\Snowflake\Snowflake;
use Godruoyi\Snowflake\SnowflakeException;

class SnowflakeTest extends TestCase
{
Expand Down Expand Up @@ -265,4 +267,46 @@ public function test_generate_id(): void

$this->assertNotEmpty($snowflake->id());
}

/** @dataProvider idForTimestampDataProvider */
public function test_id_for_timestamp(DateTime|int $dt): void
{
$snowflake = new Snowflake(10, 1);
$snowflake->setStartTimeStamp((new DateTime('2026-04-22T00:00:00.000Z'))->getTimestamp() * 1000);
$id = $snowflake->idForTimestamp($dt);
$parsed = $snowflake->parseId($id, true);
$this->assertEquals(10, $parsed['datacenter']);
$this->assertEquals(1, $parsed['workerid']);
$timestamp = $parsed['timestamp'];

$this->assertEquals(60 * 60 * 1000, $timestamp);
}

public static function idForTimestampDataProvider(): array
{
return [
'datetime' => [new DateTime('2026-04-22T01:00:00.000Z')],
'timestamp' => [strtotime('2026-04-22T01:00:00.000Z') * 1000],
];
}

/**
* @dataProvider invalidIdForTimestampDataProvider
*/
public function test_cannot_create_id_for_timestamp_when_timestamp_is_before_start_time(DateTime|int $invalidDt): void
{
$this->expectException(SnowflakeException::class);
$this->expectExceptionMessage('The provided timestamp cannot be earlier than the start time');
$snowflake = new Snowflake(10, 1);
$snowflake->setStartTimeStamp((new DateTime('2026-04-22T00:00:00.000Z'))->getTimestamp() * 1000);
$snowflake->idForTimestamp($invalidDt);
}

public static function invalidIdForTimestampDataProvider(): array
{
return [
'as datetime' => [new DateTime('2025-01-01T00:00:00.000Z')],
'as timestamp' => [strtotime('1900-01-01') * 1000],
];
}
}
Loading