Skip to content

Commit 1efb85b

Browse files
michalsnneznaika0paulbalandan
authored
feat: add Time between(), min(), and max() methods (#10149)
Co-authored-by: neznaika0 <ozornick.ks@gmail.com> Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
1 parent 2cc937a commit 1efb85b

8 files changed

Lines changed: 354 additions & 18 deletions

File tree

system/I18n/TimeTrait.php

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -966,14 +966,7 @@ public function equals($testTime, ?string $timezone = null): bool
966966
*/
967967
public function sameAs($testTime, ?string $timezone = null): bool
968968
{
969-
if ($testTime instanceof DateTimeInterface) {
970-
$testTime = $testTime->format('Y-m-d H:i:s.u O');
971-
} elseif (is_string($testTime)) {
972-
$timezone = in_array($timezone, [null, '', '0'], true) ? $this->timezone : $timezone;
973-
$timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
974-
$testTime = new DateTime($testTime, $timezone);
975-
$testTime = $testTime->format('Y-m-d H:i:s.u O');
976-
}
969+
$testTime = $this->normalizeTime($testTime, $timezone)->format('Y-m-d H:i:s.u O');
977970

978971
$ourTime = $this->format('Y-m-d H:i:s.u O');
979972

@@ -1024,6 +1017,63 @@ public function isAfter($testTime, ?string $timezone = null): bool
10241017
return $ourTimestamp > $testTimestamp;
10251018
}
10261019

1020+
/**
1021+
* Determines if the current instance's time is between two others.
1022+
*
1023+
* If $start is after $end, the arguments are swapped.
1024+
*
1025+
* @param string|null $timezone Used only when $start or $end is a string.
1026+
*
1027+
* @throws Exception
1028+
*/
1029+
public function between(DateTimeInterface|string $start, DateTimeInterface|string $end, bool $inclusive = true, ?string $timezone = null): bool
1030+
{
1031+
$start = $this->normalizeTime($start, $timezone);
1032+
$end = $this->normalizeTime($end, $timezone);
1033+
1034+
if ($start->isAfter($end)) {
1035+
[$start, $end] = [$end, $start];
1036+
}
1037+
1038+
if ($inclusive) {
1039+
return ! $this->isBefore($start) && ! $this->isAfter($end);
1040+
}
1041+
1042+
return $this->isAfter($start) && $this->isBefore($end);
1043+
}
1044+
1045+
/**
1046+
* Returns the earlier of the current instance and the provided time.
1047+
*
1048+
* If null is provided, compares against now in the current timezone.
1049+
*
1050+
* @param string|null $timezone Used only when $time is a string or null.
1051+
*
1052+
* @throws Exception
1053+
*/
1054+
public function min(DateTimeInterface|string|null $time = null, ?string $timezone = null): static
1055+
{
1056+
$time = $this->normalizeTime($time, $timezone);
1057+
1058+
return $this->isAfter($time) ? $time : $this;
1059+
}
1060+
1061+
/**
1062+
* Returns the later of the current instance and the provided time.
1063+
*
1064+
* If null is provided, compares against now in the current timezone.
1065+
*
1066+
* @param string|null $timezone Used only when $time is a string or null.
1067+
*
1068+
* @throws Exception
1069+
*/
1070+
public function max(DateTimeInterface|string|null $time = null, ?string $timezone = null): static
1071+
{
1072+
$time = $this->normalizeTime($time, $timezone);
1073+
1074+
return $this->isBefore($time) ? $time : $this;
1075+
}
1076+
10271077
/**
10281078
* Determines if the current instance's time is in the past.
10291079
*
@@ -1114,17 +1164,10 @@ public function humanize()
11141164
*/
11151165
public function difference($testTime, ?string $timezone = null)
11161166
{
1117-
if (is_string($testTime)) {
1118-
$timezone = ($timezone !== null) ? new DateTimeZone($timezone) : $this->timezone;
1119-
$testTime = new DateTime($testTime, $timezone);
1120-
} elseif ($testTime instanceof static) {
1121-
$testTime = $testTime->toDateTime();
1122-
}
1123-
1124-
assert($testTime instanceof DateTime);
1167+
$testTime = $this->normalizeTime($testTime, $timezone)->toDateTime();
11251168

11261169
if ($this->timezone->getOffset($this) !== $testTime->getTimezone()->getOffset($this)) {
1127-
$testTime = $this->getUTCObject($testTime, $timezone);
1170+
$testTime = $this->getUTCObject($testTime);
11281171
$ourTime = $this->getUTCObject($this);
11291172
} else {
11301173
$ourTime = $this->toDateTime();
@@ -1157,12 +1200,36 @@ public function getUTCObject($time, ?string $timezone = null)
11571200
}
11581201

11591202
if ($time instanceof DateTime || $time instanceof DateTimeImmutable) {
1160-
$time = $time->setTimezone(new DateTimeZone('UTC'));
1203+
return $time->setTimezone(new DateTimeZone('UTC'));
11611204
}
11621205

11631206
return $time;
11641207
}
11651208

1209+
/**
1210+
* Returns a Time instance normalized to the current locale.
1211+
*
1212+
* If $time is a string, it will be parsed using the provided timezone,
1213+
* or the current instance's timezone when omitted. If null is provided,
1214+
* the current time is used in the same timezone.
1215+
*
1216+
* @throws Exception
1217+
*/
1218+
private function normalizeTime(DateTimeInterface|string|null $time, ?string $timezone = null): static
1219+
{
1220+
if ($time instanceof DateTimeInterface) {
1221+
return static::createFromInstance($time, $this->locale);
1222+
}
1223+
1224+
$timezone = in_array($timezone, [null, '', '0'], true) ? $this->timezone : $timezone;
1225+
1226+
if ($time === null) {
1227+
return static::now($timezone, $this->locale);
1228+
}
1229+
1230+
return new static($time, $timezone, $this->locale);
1231+
}
1232+
11661233
/**
11671234
* Returns the IntlCalendar object used for this object,
11681235
* taking into account the locale, date, etc.

tests/system/I18n/TimeLegacyTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,74 @@ public function testAfter(): void
923923
$this->assertTrue($time2->isAfter($time1));
924924
}
925925

926+
public function testBetweenInclusive(): void
927+
{
928+
$time = new TimeLegacy('2024-01-01 12:00:00.123456');
929+
$start = new TimeLegacy('2024-01-01 12:00:00.123456');
930+
$end = new TimeLegacy('2024-01-01 12:00:01.000000');
931+
932+
$this->assertTrue($time->between($start, $end));
933+
}
934+
935+
public function testBetweenExclusive(): void
936+
{
937+
$time = new TimeLegacy('2024-01-01 12:00:00.123456');
938+
$start = new TimeLegacy('2024-01-01 12:00:00.123456');
939+
$end = new TimeLegacy('2024-01-01 12:00:01.000000');
940+
941+
$this->assertFalse($time->between($start, $end, false));
942+
}
943+
944+
public function testBetweenSwapsReversedBounds(): void
945+
{
946+
$time = TimeLegacy::parse('2024-01-01 12:30:00', 'UTC');
947+
948+
$this->assertTrue($time->between('2024-01-01 13:00:00', '2024-01-01 12:00:00'));
949+
}
950+
951+
public function testBetweenSupportsTimezoneForStringInputs(): void
952+
{
953+
$time = TimeLegacy::parse('2024-01-01 12:30:00', 'UTC');
954+
955+
$this->assertTrue($time->between('2024-01-01 13:00:00', '2024-01-01 14:00:00', true, 'Europe/Warsaw'));
956+
}
957+
958+
public function testMinWithNullUsesNow(): void
959+
{
960+
TimeLegacy::setTestNow('2024-01-01 12:00:00', 'UTC');
961+
962+
$past = TimeLegacy::parse('2024-01-01 11:59:59', 'UTC');
963+
$future = TimeLegacy::parse('2024-01-01 12:00:01', 'UTC');
964+
965+
$this->assertSame($past, $past->min());
966+
$this->assertTrue($future->min()->sameAs(TimeLegacy::now('UTC')));
967+
}
968+
969+
public function testMinSupportsTimezoneForStringInputs(): void
970+
{
971+
$time = TimeLegacy::parse('2024-01-01 12:30:00', 'UTC');
972+
973+
$this->assertTrue($time->min('2024-01-01 13:00:00', 'Europe/Warsaw')->sameAs('2024-01-01 13:00:00', 'Europe/Warsaw'));
974+
}
975+
976+
public function testMaxWithNullUsesNow(): void
977+
{
978+
TimeLegacy::setTestNow('2024-01-01 12:00:00', 'UTC');
979+
980+
$past = TimeLegacy::parse('2024-01-01 11:59:59', 'UTC');
981+
$future = TimeLegacy::parse('2024-01-01 12:00:01', 'UTC');
982+
983+
$this->assertTrue($past->max()->sameAs(TimeLegacy::now('UTC')));
984+
$this->assertSame($future, $future->max());
985+
}
986+
987+
public function testMaxSupportsTimezoneForStringInputs(): void
988+
{
989+
$time = TimeLegacy::parse('2024-01-01 12:30:00', 'UTC');
990+
991+
$this->assertTrue($time->max('2024-01-01 13:00:00', 'Europe/Warsaw')->sameAs($time));
992+
}
993+
926994
public function testHumanizeYearsSingle(): void
927995
{
928996
TimeLegacy::setTestNow('March 10, 2017', 'America/Chicago');

tests/system/I18n/TimeTest.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,128 @@ public function testAfterWithMicroseconds(): void
10401040
$this->assertFalse($time2->isAfter($time1));
10411041
}
10421042

1043+
public function testBetweenInclusive(): void
1044+
{
1045+
$time = new Time('2024-01-01 12:00:00.123456');
1046+
$start = new Time('2024-01-01 12:00:00.123456');
1047+
$end = new Time('2024-01-01 12:00:01.000000');
1048+
1049+
$this->assertTrue($time->between($start, $end));
1050+
}
1051+
1052+
public function testBetweenExclusive(): void
1053+
{
1054+
$time = new Time('2024-01-01 12:00:00.123456');
1055+
$start = new Time('2024-01-01 12:00:00.123456');
1056+
$end = new Time('2024-01-01 12:00:01.000000');
1057+
1058+
$this->assertFalse($time->between($start, $end, false));
1059+
}
1060+
1061+
public function testBetweenSwapsReversedBounds(): void
1062+
{
1063+
$time = Time::parse('2024-01-01 12:30:00', 'UTC');
1064+
1065+
$this->assertTrue($time->between('2024-01-01 13:00:00', '2024-01-01 12:00:00'));
1066+
}
1067+
1068+
public function testBetweenSupportsTimezoneForStringInputs(): void
1069+
{
1070+
$time = Time::parse('2024-01-01 12:30:00', 'UTC');
1071+
1072+
$this->assertTrue($time->between('2024-01-01 13:00:00', '2024-01-01 14:00:00', true, 'Europe/Warsaw'));
1073+
}
1074+
1075+
public function testBetweenSupportsDateTimeImmutable(): void
1076+
{
1077+
$time = Time::parse('2024-01-01 12:30:00', 'UTC');
1078+
$start = new DateTimeImmutable('2024-01-01 12:00:00', new DateTimeZone('UTC'));
1079+
$end = new DateTimeImmutable('2024-01-01 13:00:00', new DateTimeZone('UTC'));
1080+
1081+
$this->assertTrue($time->between($start, $end));
1082+
}
1083+
1084+
public function testGetUTCObjectPreservesDateTimeImmutable(): void
1085+
{
1086+
$time = Time::parse('2024-01-01 12:30:00', 'Europe/Warsaw');
1087+
$immutable = new DateTimeImmutable('2024-01-01 13:30:00', new DateTimeZone('Europe/Warsaw'));
1088+
$utcTime = $time->getUTCObject($immutable);
1089+
1090+
$this->assertInstanceOf(DateTimeImmutable::class, $utcTime);
1091+
$this->assertSame('UTC', $utcTime->getTimezone()->getName());
1092+
$this->assertSame('2024-01-01 12:30:00.000000', $utcTime->format('Y-m-d H:i:s.u'));
1093+
}
1094+
1095+
public function testMinReturnsEarlierTime(): void
1096+
{
1097+
$time = new Time('2024-01-01 12:00:00');
1098+
$later = new Time('2024-01-01 12:00:01');
1099+
1100+
$this->assertSame($time, $time->min($later));
1101+
$this->assertTrue($later->min($time)->sameAs($time));
1102+
}
1103+
1104+
public function testMinReturnsCurrentInstanceOnEqualTime(): void
1105+
{
1106+
$time = new Time('2024-01-01 12:00:00');
1107+
$equal = new Time('2024-01-01 12:00:00');
1108+
1109+
$this->assertSame($time, $time->min($equal));
1110+
}
1111+
1112+
public function testMinSupportsTimezoneForStringInputs(): void
1113+
{
1114+
$time = Time::parse('2024-01-01 12:30:00', 'UTC');
1115+
1116+
$this->assertTrue($time->min('2024-01-01 13:00:00', 'Europe/Warsaw')->sameAs('2024-01-01 13:00:00', 'Europe/Warsaw'));
1117+
}
1118+
1119+
public function testMinWithNullUsesNow(): void
1120+
{
1121+
Time::setTestNow('2024-01-01 12:00:00', 'UTC');
1122+
1123+
$past = Time::parse('2024-01-01 11:59:59', 'UTC');
1124+
$future = Time::parse('2024-01-01 12:00:01', 'UTC');
1125+
1126+
$this->assertSame($past, $past->min());
1127+
$this->assertTrue($future->min()->sameAs(Time::now('UTC')));
1128+
}
1129+
1130+
public function testMaxReturnsLaterTime(): void
1131+
{
1132+
$time = new Time('2024-01-01 12:00:00');
1133+
$earlier = new Time('2024-01-01 11:59:59');
1134+
1135+
$this->assertSame($time, $time->max($earlier));
1136+
$this->assertTrue($earlier->max($time)->sameAs($time));
1137+
}
1138+
1139+
public function testMaxReturnsCurrentInstanceOnEqualTime(): void
1140+
{
1141+
$time = new Time('2024-01-01 12:00:00');
1142+
$equal = new Time('2024-01-01 12:00:00');
1143+
1144+
$this->assertSame($time, $time->max($equal));
1145+
}
1146+
1147+
public function testMaxSupportsTimezoneForStringInputs(): void
1148+
{
1149+
$time = Time::parse('2024-01-01 12:30:00', 'UTC');
1150+
1151+
$this->assertTrue($time->max('2024-01-01 13:00:00', 'Europe/Warsaw')->sameAs($time));
1152+
}
1153+
1154+
public function testMaxWithNullUsesNow(): void
1155+
{
1156+
Time::setTestNow('2024-01-01 12:00:00', 'UTC');
1157+
1158+
$past = Time::parse('2024-01-01 11:59:59', 'UTC');
1159+
$future = Time::parse('2024-01-01 12:00:01', 'UTC');
1160+
1161+
$this->assertTrue($past->max()->sameAs(Time::now('UTC')));
1162+
$this->assertSame($future, $future->max());
1163+
}
1164+
10431165
public function testIsPast(): void
10441166
{
10451167
Time::setTestNow('2025-12-30 12:00:00', 'Asia/Tehran');

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ Others
269269
- Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided.
270270
- **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``environment`` service as a mockable wrapper around the ``ENVIRONMENT`` constant.
271271
Framework internals that previously compared ``ENVIRONMENT`` directly now go through this service, making environment-specific branches reachable in tests via ``Services::injectMock()``. See :ref:`environment-detector-service`.
272+
- **Time:** Added ``Time::between()``, ``Time::min()``, and ``Time::max()`` comparison helpers. See :ref:`between <time-comparing-two-times-between>`, :ref:`min <time-comparing-two-times-min>`, and :ref:`max <time-comparing-two-times-max>`.
272273

273274
***************
274275
Message Changes

0 commit comments

Comments
 (0)