Skip to content

Commit

Permalink
Merge pull request #60 from PhpGt/extractor
Browse files Browse the repository at this point in the history
feature: construct from other ulid
  • Loading branch information
g105b authored Sep 13, 2023
2 parents 55ce1f5 + 5e62d9a commit 2b66808
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 48 deletions.
51 changes: 51 additions & 0 deletions src/Base32.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
namespace Gt\Ulid;

class Base32 {
public function __construct(
private string $skipCharacters = "ilou"
) {}

public function fromDec(int $base10):string {
$skipCharacters = str_split($this->skipCharacters);

$base10 = abs($base10);
$converted = base_convert((string)$base10, 10, 32);

for($i = 0, $len = strlen($converted); $i < $len; $i++) {
$ord = ord($converted[$i]);
foreach($skipCharacters as $skip) {
$skipOrd = ord($skip);
if($ord >= $skipOrd) {
$ord++;
}
$converted[$i] = chr($ord);
}
}

return strtoupper($converted);
}

public function toDec(string $base32): int {
$skipCharacters = str_split($this->skipCharacters);
$base32 = strtoupper($base32);

for ($i = 0, $len = strlen($base32); $i < $len; $i++) {
$ord = ord($base32[$i]);
$adjustment = 0;

foreach ($skipCharacters as $skip) {
$skipOrd = ord(strtoupper($skip));
if ($ord > $skipOrd) {
$adjustment++;
}
}

$ord -= $adjustment;
$base32[$i] = chr($ord);
}

return intval(base_convert($base32, 32, 10));
}

}
58 changes: 58 additions & 0 deletions src/Extractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
namespace Gt\Ulid;

class Extractor {
private Base32 $base32;

public function __construct(
private int $length = Ulid::DEFAULT_TOTAL_LENGTH,
private int $timestampLength = Ulid::DEFAULT_TIMESTAMP_LENGTH,
?Base32 $base32 = null,
) {
$this->base32 = $base32 ?? new Base32();
}

public function extractPrefix(string $initString):?string {
$underscorePos = strpos($initString, "_");
if($underscorePos === false) {
return null;
}

return substr($initString, 0, $underscorePos);
}

public function extractTimestamp(string $initString):int {
$underscorePos = $this->positionAfterUnderscore($initString);

$timestampString = substr(
$initString,
$underscorePos,
$this->timestampLength,
);
return $this->base32->toDec($timestampString);
}

public function extractRandomString(string $initString):string {
$underscorePos = $this->positionAfterUnderscore($initString);

$initString = substr(
$initString,
$underscorePos,
);

return substr(
$initString,
$this->timestampLength,
$this->length,
);
}

protected function positionAfterUnderscore(string $initString):int {
$underscorePos = strpos($initString, "_");
if($underscorePos) {
$underscorePos += 1;
}

return $underscorePos ?: 0;
}
}
63 changes: 25 additions & 38 deletions src/Ulid.php
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
<?php
namespace Gt\Ulid;

use DateTime;
use Stringable;

class Ulid implements Stringable {
const DEFAULT_TOTAL_LENGTH = 20;
const DEFAULT_TIMESTAMP_LENGTH = 10;

private float $timestamp;
private int $timestamp;
private string $randomString;
private Base32 $base32;

public function __construct(
private ?string $prefix = null,
float|int $timestamp = null,
int $timestamp = null,
private int $length = self::DEFAULT_TOTAL_LENGTH,
private int $timestampLength = self::DEFAULT_TIMESTAMP_LENGTH,
string $init = null,
) {
$this->base32 = new Base32();

if($init) {
$extractor = new Extractor();
$this->prefix = $extractor->extractPrefix($init);
$this->timestamp = $extractor->extractTimestamp($init);
$this->randomString = $extractor->extractRandomString($init);
return;
}

if(is_null($timestamp)) {
$timestamp = microtime(true);
$timestamp = (int)round(microtime(true) * 1000);
}

$this->timestamp = $timestamp;

$this->randomString = "";
for($i = 0; $i < $this->length - $this->timestampLength; $i++) {
$rnd = random_int(0, 31);
$this->randomString .= $this->base32($rnd);
$this->randomString .= $this->base32->fromDec($rnd);
}
}

Expand All @@ -39,9 +50,9 @@ public function __toString():string {
$randomString,
]);

if($this->prefix) {
if($prefix = $this->getPrefix()) {
$string = implode("_", [
$this->prefix,
$prefix,
$string,
]);
}
Expand All @@ -50,16 +61,19 @@ public function __toString():string {
}

public function getPrefix():?string {
return $this->prefix;
if(!$this->prefix) {
return null;
}

return strtoupper($this->prefix);
}

public function getTimestamp():float {
public function getTimestamp():int {
return $this->timestamp;
}

public function getTimestampString():string {
$timestamp = round($this->timestamp * 1000);
$base32Timestamp = $this->base32((int)$timestamp);
$base32Timestamp = $this->base32->fromDec($this->timestamp);
return substr(
str_pad(
$base32Timestamp,
Expand All @@ -75,31 +89,4 @@ public function getTimestampString():string {
public function getRandomString():string {
return $this->randomString;
}

private function base32(int $number):string {
$skipCharacters = ["i", "l", "o", "u"];
if($number < 0) {
$number = -$number;
}
$converted = base_convert((string)$number, 10, 32);

for($i = 0, $len = strlen($converted); $i < $len; $i++) {
$ord = ord($converted[$i]);
foreach($skipCharacters as $skip) {
$skipOrd = ord($skip);
if($ord >= $skipOrd) {
$ord++;
}
$converted[$i] = chr($ord);
}
}

return strtoupper($converted);
}

public function getDateTime():DateTime {
$dateTime = new DateTime();
$dateTime->setTimestamp($this->timestamp);
return $dateTime;
}
}
34 changes: 34 additions & 0 deletions test/phpunit/Base32Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
namespace Gt\Ulid\Test;

use Gt\Ulid\Base32;
use PHPUnit\Framework\TestCase;

class Base32Test extends TestCase {
public function testFromDec():void {
$sut = new Base32();
$b32 = $sut->fromDec(105);
self::assertSame(105, $sut->toDec($b32));
}

public function testFromDec_timestamp():void {
$timestamp = strtotime("5th April 1988 17:24:00");
$sut = new Base32();
$b32 = $sut->fromDec($timestamp);
self::assertSame($timestamp, $sut->toDec($b32));
}

public function testFromDec_timestampWayInTheFuture():void {
$timestamp = strtotime("31st December 9999 23:59:59");
$sut = new Base32();
$b32 = $sut->fromDec($timestamp);
self::assertSame($timestamp, $sut->toDec($b32));
}

public function testFromDec_timestampWayInThePast():void {
$timestamp = strtotime("1st January -5000 13:05:00");
$sut = new Base32();
$b32 = $sut->fromDec($timestamp);
self::assertSame($timestamp, $sut->toDec($b32));
}
}
49 changes: 49 additions & 0 deletions test/phpunit/ExtractorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
namespace Gt\Ulid\Test;

use Gt\Ulid\Extractor;
use Gt\Ulid\Ulid;
use PHPUnit\Framework\TestCase;

class ExtractorTest extends TestCase {
public function testExtractTimestamp():void {
$currentTimestamp = 1694626973857;
$ulid = "01HA7T72510000000000";

$sut = new Extractor();
$timestamp = $sut->extractTimestamp($ulid);
self::assertSame($currentTimestamp, $timestamp);
}

public function testExtractTimestamp_specificKnownDate():void {
$knownTimestamp = strtotime("5th April 1988 17:24:00");

$ulid = self::createMock(Ulid::class);
$ulid->method("__toString")
->willReturn("0000H5J61G" . "DYSS02F0S3");

$sut = new Extractor();
$timestamp = $sut->extractTimestamp($ulid);
self::assertSame($knownTimestamp, $timestamp);
}

public function testExtractPrefix():void {
$ulid = "example_1234567890";
$sut = new Extractor();
self::assertSame("example", $sut->extractPrefix($ulid));
}

public function testExtractPrefix_noPrefix():void {
$ulid = "1234567890";
$sut = new Extractor();
self::assertNull($sut->extractPrefix($ulid));
}

public function testExtractRandomString():void {
$random = "AABBCCDDEE";
$timestamp = "0000000000";
$ulid = "example_" . $timestamp . $random;
$sut = new Extractor();
self::assertSame($random, $sut->extractRandomString($ulid));
}
}
24 changes: 14 additions & 10 deletions test/phpunit/UlidTest.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<?php
namespace Gt\Ulid\Test;

use DateTime;
use Gt\Ulid\Ulid;
use PHPUnit\Framework\TestCase;

class UlidTest extends TestCase {
public function testGetPrefix():void {
$sut = new Ulid("customer");
self::assertStringStartsWith("CUSTOMER_", $sut);
}

public function testGetTimestamp():void {
$sut = new Ulid();
$timestamp = microtime(true);
self::assertSame(round($timestamp), round($sut->getTimestamp()));
$now = round(microtime(true) * 1000);
self::assertSame(round($now / 1000), round($sut->getTimestamp() / 1000));
}

public function testGetTimestamp_setInConstructor():void {
$timestamp = (float)strtotime("5th April 1988");
$timestamp = strtotime("5th April 1988");
$sut = new Ulid(timestamp: $timestamp);
self::assertSame($timestamp, $sut->getTimestamp());
}
Expand Down Expand Up @@ -83,14 +87,14 @@ public function testConstruct_setTimestampLength():void {

public function testConstruct_prefix():void {
$sut = new Ulid("customer");
self::assertStringStartsWith("customer_", $sut);
self::assertStringStartsWith("CUSTOMER_", $sut);
self::assertGreaterThan(strlen("customer_") + 10, strlen($sut));
}

public function testGetDateTime():void {
$sut = new Ulid();
$dateTime = $sut->getDateTime();
$now = new DateTime();
self::assertSame($now->format("Y-m-d H:i:s"), $dateTime->format("Y-m-d H:i:s"));
public function testConstruct_existingUlid():void {
$existingUlid = new Ulid();
$existingString = (string)$existingUlid;
$sut = new Ulid(init: $existingString);
self::assertSame($existingString, (string)$sut);
}
}

0 comments on commit 2b66808

Please sign in to comment.