Skip to content

Commit

Permalink
add DateFormat function
Browse files Browse the repository at this point in the history
  • Loading branch information
MohannadNaj committed Jan 5, 2024
1 parent 0d72cee commit 53842bf
Show file tree
Hide file tree
Showing 6 changed files with 593 additions and 1 deletion.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,39 @@ Schema::table('users', function (Blueprint $table): void {
});
```

#### Date Format

Use [PHP's date format](https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters) syntax to format a date column.

> **Warning**
> The following format characters are not supported: `N S L X B I O P T Z z x u v e p c r`
> **Note**
When using Sqlite, characters that produces a textual result (for example: `D` -> `Sun`,`F` -> `January`, `l` -> `Sunday`, `M` -> `Jan`), [Carbon](https://carbon.nesbot.com/docs/#api-localization) default localization will be used to build the SQL query.
```php
use Tpetry\QueryExpressions\Function\Date\DateFormat;
use Tpetry\QueryExpressions\Language\Alias;

// MySQL:
// SELECT url, DATE_FORMAT(created_at, '%Y-%m-%d') AS date, [....]
// PostgreSQL:
// SELECT url, TO_CHAR(created_at, 'YYYY-MM-DD') AS date, [....]
BlogVisit::select([
'url',
new Alias(new DateFormat('created_at', 'Y-m-d'), 'date'),
new Count('*'),
])->groupBy(
'url',
new DateFormat('created_at', 'Y-m-d')
)->get();
// | url | date | count |
// |-----------|------------|-------|
// | /example1 | 2023-05-16 | 2 |
// | /example1 | 2023-05-17 | 1 |
// | /example1 | 2023-05-18 | 1 |
```

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"php": "^8.1",
"illuminate/contracts": "^10.13.1",
"illuminate/database": "^10.13.1",
"illuminate/support": "^10.0"
"illuminate/support": "^10.0",
"nesbot/carbon": "^2.72"
},
"require-dev": {
"larastan/larastan": "^2.7.0",
Expand Down
118 changes: 118 additions & 0 deletions src/Function/Date/DateFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Tpetry\QueryExpressions\Function\Date;

use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Grammar;
use Tpetry\QueryExpressions\Concerns\IdentifiesDriver;
use Tpetry\QueryExpressions\Concerns\StringizeExpression;
use Tpetry\QueryExpressions\Function\String\Concat;
use Tpetry\QueryExpressions\Value\Value;

class DateFormat implements Expression
{
use DirectDateFormat;
use EmulatedDateFormat;
use IdentifiesDriver;
use StringizeExpression;

/**
* @var array<string>
*/
protected array $unsupportedCharacters = [
'B',
'c',
'e',
'I',
'L',
'N',
'O',
'P',
'p',
'r',
'S',
'T',
'u',
'v',
'X',
'x',
'z',
'Z',
];

public function __construct(
private readonly string|Expression $expression,
private readonly string $format
) {
}

public function getValue(Grammar $grammar): string
{
/** @var non-empty-array<int, Expression> $expressions */
$expressions = [];

$characters = $this->getFormatCharacters();

foreach ($characters as $character) {
$emulatedCharacter = $this->getEmulatableCharacter($grammar, $character);
$formatCharacter = $this->formatCharacters[$this->identify($grammar)][$character] ?? null;

if ($emulatedCharacter) {
$expressions[] = $this->getEmulatedExpression($grammar, $emulatedCharacter);
} elseif ($formatCharacter) {
$expressions[] = $this->getDateFormatExpression($grammar, $character);
} else {
$expressions[] = $this->getCharacterExpression($character);
}
}

return count($expressions) == 1 ?
(string) $expressions[0]->getValue($grammar) : (new Concat($expressions))->getValue($grammar);
}

protected function getCharacterExpression(string $character): Expression
{
$isEscaped = str_starts_with($character, '\\') && strlen($character) > 1;

return new Value(
$isEscaped ? substr($character, 1) : $character,
);
}

/**
* @return array<int, string>
*/
protected function getFormatCharacters(): array
{
$characters = str_split($this->format);

$characters = array_reduce(
array_keys($characters),
function (array $characters, int $index) {
if ($characters[$index] == '\\') {
$characters[$index + 1] = $characters[$index].($characters[$index + 1] ?? null);
unset($characters[$index]);
}

return $characters;
},
$characters
);

array_walk(
$characters,
function (string $character) {
if (in_array($character, $this->unsupportedCharacters)) {
throw new \InvalidArgumentException(sprintf(
'Unsupported format character: %s',
$character,
));
}
}
);

return $characters;
}
}
94 changes: 94 additions & 0 deletions src/Function/Date/DirectDateFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace Tpetry\QueryExpressions\Function\Date;

use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Grammar;
use Illuminate\Database\Query\Expression as QueryExpression;

/**
* @property-read string|\Illuminate\Database\Query\Expression $expression
*
* @uses \Tpetry\QueryExpressions\Concerns\IdentifiesDriver
* @uses \Tpetry\QueryExpressions\Concerns\StringizeExpression
*/
trait DirectDateFormat
{
/**
* @var array<'mysql'|'sqlite'|'pgsql'|'sqlsrv', array<string, string>>
*/
protected array $formatCharacters = [
'mysql' => [
'A' => '%p',
'd' => '%d',
'D' => '%a',
'F' => '%M',
'H' => '%H',
'h' => '%h',
'i' => '%i',
'j' => '%e',
'l' => '%W',
'm' => '%m',
'M' => '%b',
'n' => '%c',
'o' => '%x',
's' => '%s',
'W' => '%v',
'Y' => '%Y',
'y' => '%y',
],
'sqlite' => [
'A' => '%p',
'd' => '%d',
'H' => '%H',
'i' => '%M',
'm' => '%m',
's' => '%S',
'U' => '%s',
'Y' => '%Y',
],
'pgsql' => [
'A' => 'AM',
'd' => 'DD',
'D' => 'Dy',
'h' => 'HH12',
'H' => 'HH24',
'i' => 'MI',
'j' => 'FMDD',
'm' => 'MM',
'M' => 'Mon',
'n' => 'FMMM',
's' => 'SS',
'W' => 'IW',
'y' => 'YY',
'Y' => 'YYYY',
],
'sqlsrv' => [
'A' => 'tt',
'd' => 'dd',
'D' => 'ddd',
'h' => 'hh',
'H' => 'HH',
'i' => 'mm',
'm' => 'MM',
's' => 'ss',
'Y' => 'yyyy',
],
];

protected function getDateFormatExpression(Grammar $grammar, string $character): Expression
{
$formatCharacter = $this->formatCharacters[$this->identify($grammar)][$character];

return new QueryExpression(
match ($this->identify($grammar)) {
'mysql' => "DATE_FORMAT({$this->stringize($grammar, $this->expression)}, '{$formatCharacter}')",
'sqlite' => "STRFTIME('{$formatCharacter}', {$this->stringize($grammar, $this->expression)})",
'pgsql' => "TO_CHAR({$this->stringize($grammar, $this->expression)}, '{$formatCharacter}')",
'sqlsrv' => "FORMAT({$this->stringize($grammar, $this->expression)}, '{$formatCharacter}')",
}
);
}
}
Loading

0 comments on commit 53842bf

Please sign in to comment.