Skip to content

Commit

Permalink
Merge pull request #2418 from kylekatarnls/feature/smart-factory-time…
Browse files Browse the repository at this point in the history
…zone

Improve factory timezone handling
  • Loading branch information
kylekatarnls committed Aug 14, 2021
2 parents 6d59b9f + 9eebfac commit 369c0e2
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 15 deletions.
28 changes: 27 additions & 1 deletion src/Carbon/CarbonInterval.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
use Carbon\Traits\Options;
use Closure;
use DateInterval;
use DateTimeInterface;
use DateTimeZone;
use Exception;
use ReflectionException;
use ReturnTypeWillChange;
Expand Down Expand Up @@ -253,6 +255,22 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
protected $tzName;

/**
* Set the instance's timezone from a string or object.
*
* @param \DateTimeZone|string $tzName
*
* @return static
*/
public function setTimezone($tzName)
{
$this->tzName = $tzName;

return $this;
}

/**
* @internal
*
* Set the instance's timezone from a string or object and add/subtract the offset difference.
*
* @param \DateTimeZone|string $tzName
Expand Down Expand Up @@ -1763,12 +1781,20 @@ public function toDateInterval()
/**
* Convert the interval to a CarbonPeriod.
*
* @param array ...$params Start date, [end date or recurrences] and optional settings.
* @param DateTimeInterface|string|int ...$params Start date, [end date or recurrences] and optional settings.
*
* @return CarbonPeriod
*/
public function toPeriod(...$params)
{
if ($this->tzName) {
$tz = \is_string($this->tzName) ? new DateTimeZone($this->tzName) : $this->tzName;

if ($tz instanceof DateTimeZone) {
array_unshift($params, $tz);
}
}

return CarbonPeriod::create($this, ...$params);
}

Expand Down
72 changes: 64 additions & 8 deletions src/Carbon/CarbonPeriod.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use DatePeriod;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use Iterator;
use JsonSerializable;
Expand Down Expand Up @@ -639,7 +640,11 @@ public function __construct(...$arguments)
}

foreach ($arguments as $argument) {
if ($this->dateInterval === null &&
$parsedDate = null;

if ($argument instanceof DateTimeZone) {
$this->setTimezone($argument);
} elseif ($this->dateInterval === null &&
(
\is_string($argument) && preg_match(
'/^(-?\d(\d(?![\/-])|[^\d\/-]([\/-])?)*|P[T0-9].*|(?:\h*\d+(?:\.\d+)?\h*[a-z]+)+)$/i',
Expand All @@ -648,13 +653,13 @@ public function __construct(...$arguments)
$argument instanceof DateInterval ||
$argument instanceof Closure
) &&
$parsed = @CarbonInterval::make($argument)
$parsedInterval = @CarbonInterval::make($argument)
) {
$this->setDateInterval($parsed);
} elseif ($this->startDate === null && $parsed = Carbon::make($argument)) {
$this->setStartDate($parsed);
} elseif ($this->endDate === null && $parsed = Carbon::make($argument)) {
$this->setEndDate($parsed);
$this->setDateInterval($parsedInterval);
} elseif ($this->startDate === null && $parsedDate = $this->makeDateTime($argument)) {
$this->setStartDate($parsedDate);
} elseif ($this->endDate === null && ($parsedDate = $parsedDate ?? $this->makeDateTime($argument))) {
$this->setEndDate($parsedDate);
} elseif ($this->recurrences === null && $this->endDate === null && is_numeric($argument)) {
$this->setRecurrences($argument);
} elseif ($this->options === null && (\is_int($argument) || $argument === null)) {
Expand Down Expand Up @@ -1689,7 +1694,30 @@ public function __call($method, $parameters)
}

/**
* Set the instance's timezone from a string or object and add/subtract the offset difference.
* Set the instance's timezone from a string or object and apply it to start/end.
*
* @param \DateTimeZone|string $timezone
*
* @return static
*/
public function setTimezone($timezone)
{
$this->tzName = $timezone;
$this->timezone = $timezone;

if ($this->startDate) {
$this->setStartDate($this->startDate->setTimezone($timezone));
}

if ($this->endDate) {
$this->setEndDate($this->endDate->setTimezone($timezone));
}

return $this;
}

/**
* Set the instance's timezone from a string or object and add/subtract the offset difference to start/end.
*
* @param \DateTimeZone|string $timezone
*
Expand All @@ -1700,6 +1728,14 @@ public function shiftTimezone($timezone)
$this->tzName = $timezone;
$this->timezone = $timezone;

if ($this->startDate) {
$this->setStartDate($this->startDate->shiftTimezone($timezone));
}

if ($this->endDate) {
$this->setEndDate($this->endDate->shiftTimezone($timezone));
}

return $this;
}

Expand Down Expand Up @@ -2495,4 +2531,24 @@ private function orderCouple($first, $second): array
{
return $first > $second ? [$second, $first] : [$first, $second];
}

private function makeDateTime($value): ?DateTimeInterface
{
if ($value instanceof DateTimeInterface) {
return $value;
}

if (\is_string($value)) {
$value = trim($value);

if (!preg_match('/^P[0-9T]/', $value) &&
!preg_match('/^R[0-9]/', $value) &&
preg_match('/[a-z0-9]/i', $value)
) {
return Carbon::parse($value, $this->tzName);
}
}

return null;
}
}
9 changes: 8 additions & 1 deletion src/Carbon/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
namespace Carbon;

use Closure;
use DateTimeInterface;
use ReflectionMethod;

/**
Expand Down Expand Up @@ -289,7 +290,13 @@ public function __call($name, $arguments)
return \in_array($parameter->getName(), ['tz', 'timezone'], true);
});

if (\count($tzParameters)) {
if (isset($arguments[0]) && \in_array($name, ['instance', 'make', 'create', 'parse'], true)) {
if ($arguments[0] instanceof DateTimeInterface) {
$settings['innerTimezone'] = $settings['timezone'];
} elseif (\is_string($arguments[0]) && date_parse($arguments[0])['is_localtime']) {
unset($settings['timezone'], $settings['innerTimezone']);
}
} elseif (\count($tzParameters)) {
array_splice($arguments, key($tzParameters), 0, [$settings['timezone']]);
unset($settings['timezone']);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Carbon/Traits/Creator.php
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ private static function createNowInstance($tz)
*/
public static function create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $tz = null)
{
if (\is_string($year) && !is_numeric($year)) {
if (\is_string($year) && !is_numeric($year) || $year instanceof DateTimeInterface) {
return static::parse($year, $tz ?: (\is_string($month) || $month instanceof DateTimeZone ? $month : null));
}

Expand Down
7 changes: 4 additions & 3 deletions src/Carbon/Traits/Date.php
Original file line number Diff line number Diff line change
Expand Up @@ -1584,10 +1584,11 @@ public function setTimezone($value)
*/
public function shiftTimezone($value)
{
$offset = $this->offset;
$date = $this->setTimezone($value);
$dateTimeString = $this->format('Y-m-d H:i:s.u');

return $date->addRealMicroseconds(($offset - $date->offset) * static::MICROSECONDS_PER_SECOND);
return $this
->setTimezone($value)
->modify($dateTimeString);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Carbon/Traits/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ public function settings(array $settings)
$this->locale(...$locales);
}

if (isset($settings['innerTimezone'])) {
return $this->setTimezone($settings['innerTimezone']);
}

if (isset($settings['timezone'])) {
return $this->shiftTimezone($settings['timezone']);
}
Expand Down
3 changes: 3 additions & 0 deletions tests/Carbon/SettersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,9 @@ public function testShiftTimezone()
$this->assertSame(21600, $d2->getTimestamp() - $d->getTimestamp());
$this->assertSame('America/Toronto', $d2->tzName);
$this->assertSame('10:53:12.321654', $d2->format('H:i:s.u'));

$d = Carbon::parse('2018-03-25 00:53:12.321654 America/Toronto')->shiftTimezone('Europe/Oslo');
$this->assertSame('2018-03-25 00:53:12.321654 Europe/Oslo', $d->format('Y-m-d H:i:s.u e'));
}

public function testTimezoneUsingString()
Expand Down
3 changes: 3 additions & 0 deletions tests/CarbonImmutable/SettersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ public function testShiftTimezone()
$this->assertSame(21600, $d2->getTimestamp() - $d->getTimestamp());
$this->assertSame('America/Toronto', $d2->tzName);
$this->assertSame('10:53:12', $d2->format('H:i:s'));

$d = Carbon::parse('2018-03-25 00:53:12.321654 America/Toronto')->shiftTimezone('Europe/Oslo');
$this->assertSame('2018-03-25 00:53:12.321654 Europe/Oslo', $d->format('Y-m-d H:i:s.u e'));
}

public function testTimezoneUsingString()
Expand Down
10 changes: 10 additions & 0 deletions tests/CarbonInterval/ToPeriodTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,14 @@ public function provideToPeriodParameters(): Generator
'2018-05-14T17:30:00+00:00/PT30M/2018-05-14T18:00:00+02:00',
];
}

public function testToDatePeriodWithTimezone(): void
{
$period = CarbonInterval::minutes(30)
->setTimezone('Asia/Tokyo')
->toPeriod('2021-08-14 00:00', '2021-08-14 02:00');

$this->assertSame('2021-08-14 00:00 Asia/Tokyo', $period->start->format('Y-m-d H:i e'));
$this->assertSame('2021-08-14 02:00 Asia/Tokyo', $period->end->format('Y-m-d H:i e'));
}
}
6 changes: 5 additions & 1 deletion tests/CarbonPeriod/CreateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,11 @@ public function testCreateFromBaseClasses()
);

$this->assertSame(
$this->standardizeDates(['2018-04-16', '2018-05-16', '2018-06-16']),
[
'2018-04-16 00:00:00 -04:00',
'2018-05-16 00:00:00 -04:00',
'2018-06-16 00:00:00 -04:00',
],
$this->standardizeDates($period)
);
}
Expand Down
44 changes: 44 additions & 0 deletions tests/CarbonPeriod/SettersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,48 @@ public function testFluentSetters()
$this->assertSame(20, $period->getDateInterval()->dayz);
$this->assertSame($opt, $period->getOptions());
}

public function testSetTimezone(): void
{
$period = CarbonPeriod::create(
'2018-03-25 00:00 America/Toronto',
'PT1H',
'2018-03-25 12:00 Europe/London'
)->setTimezone('Europe/Oslo');

$this->assertSame('2018-03-25 06:00 Europe/Oslo', $period->getStartDate()->format('Y-m-d H:i e'));
$this->assertSame('2018-03-25 13:00 Europe/Oslo', $period->getEndDate()->format('Y-m-d H:i e'));

$period = CarbonPeriod::create(
'2018-03-25 00:00 America/Toronto',
'PT1H',
5
)->setTimezone('Europe/Oslo');

$this->assertSame('2018-03-25 06:00 Europe/Oslo', $period->getStartDate()->format('Y-m-d H:i e'));
$this->assertNull($period->getEndDate());
$this->assertSame('2018-03-25 10:00 Europe/Oslo', $period->calculateEnd()->format('Y-m-d H:i e'));
}

public function testShiftTimezone(): void
{
$period = CarbonPeriod::create(
'2018-03-25 00:00 America/Toronto',
'PT1H',
'2018-03-25 12:00 Europe/London'
)->shiftTimezone('Europe/Oslo');

$this->assertSame('2018-03-25 00:00 Europe/Oslo', $period->getStartDate()->format('Y-m-d H:i e'));
$this->assertSame('2018-03-25 12:00 Europe/Oslo', $period->getEndDate()->format('Y-m-d H:i e'));

$period = CarbonPeriod::create(
'2018-03-26 00:00 America/Toronto',
'PT1H',
5
)->shiftTimezone('Europe/Oslo');

$this->assertSame('2018-03-26 00:00 Europe/Oslo', $period->getStartDate()->format('Y-m-d H:i e'));
$this->assertNull($period->getEndDate());
$this->assertSame('2018-03-26 04:00 Europe/Oslo', $period->calculateEnd()->format('Y-m-d H:i e'));
}
}
36 changes: 36 additions & 0 deletions tests/Factory/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,41 @@ public function testFactoryTimezone()
$this->assertSame('2020-09-03 00:00:00.000000 America/Toronto', $factory->today()->format('Y-m-d H:i:s.u e'));
$this->assertSame('2020-09-04 00:00:00.000000 America/Toronto', $factory->tomorrow()->format('Y-m-d H:i:s.u e'));
$this->assertSame('2020-09-04 09:39:04.123456 America/Toronto', $factory->parse('2020-09-04 09:39:04.123456')->format('Y-m-d H:i:s.u e'));

$factory = new Factory([
'timezone' => 'Asia/Shanghai',
]);

$baseDate = Carbon::parse('2021-08-01 08:00:00', 'UTC');

$date = $factory->createFromTimestamp($baseDate->getTimestamp());
$this->assertSame('2021-08-01T16:00:00+08:00', $date->format('c'));

$date = $factory->make('2021-08-01 08:00:00');
$this->assertSame('2021-08-01T08:00:00+08:00', $date->format('c'));

$date = $factory->make($baseDate);
$this->assertSame('2021-08-01T16:00:00+08:00', $date->format('c'));

$date = $factory->create($baseDate);
$this->assertSame('2021-08-01T16:00:00+08:00', $date->format('c'));

$date = $factory->parse($baseDate);
$this->assertSame('2021-08-01T16:00:00+08:00', $date->format('c'));

$date = $factory->instance($baseDate);
$this->assertSame('2021-08-01T16:00:00+08:00', $date->format('c'));

$date = $factory->make('2021-08-01 08:00:00+00:20');
$this->assertSame('2021-08-01T08:00:00+00:20', $date->format('c'));

$date = $factory->parse('2021-08-01T08:00:00Z');
$this->assertSame('2021-08-01T08:00:00+00:00', $date->format('c'));

$date = $factory->create('2021-08-01 08:00:00 UTC');
$this->assertSame('2021-08-01T08:00:00+00:00', $date->format('c'));

$date = $factory->make('2021-08-01 08:00:00 Europe/Paris');
$this->assertSame('2021-08-01T08:00:00+02:00', $date->format('c'));
}
}

0 comments on commit 369c0e2

Please sign in to comment.