Skip to content

Commit 1f53af9

Browse files
committedMay 8, 2025·
feat: Add DateTime value objects and related error handling
Fizes #11
·
v0.6.4v0.5.0
1 parent 3e5c190 commit 1f53af9

File tree

4 files changed

+834
-2
lines changed

4 files changed

+834
-2
lines changed
 

‎src/DateTime/LocalTime.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ final public static function of(int $hour, int $minute, int $second = 0, int $mi
9494
*/
9595
final public static function ofSecondOfDay(int $secondOfDay, int $microOfSecond = 0): static
9696
{
97+
// NOTE: 事前条件
98+
// @phpstan-ignore-next-line
99+
assert($secondOfDay >= 0 && $secondOfDay < self::SECONDS_PER_DAY);
100+
97101
/** @var int<0, 23> */
98102
$hours = intdiv($secondOfDay, self::SECONDS_PER_HOUR);
99103

@@ -340,8 +344,8 @@ final public function getMicro(): int
340344
*/
341345
final public function toSecondOfDay(): int
342346
{
343-
return $this->hour * 3600
344-
+ $this->minute * 60
347+
return $this->hour * self::SECONDS_PER_HOUR
348+
+ $this->minute * self::SECONDS_PER_MINUTE
345349
+ $this->second;
346350
}
347351

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WizDevelop\PhpValueObject\Tests\Unit\DateTime;
6+
7+
use AssertionError;
8+
use DateTimeImmutable;
9+
use PHPUnit\Framework\Attributes\CoversClass;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\Attributes\Group;
12+
use PHPUnit\Framework\Attributes\Test;
13+
use PHPUnit\Framework\Attributes\TestDox;
14+
use WizDevelop\PhpValueObject\DateTime\LocalDate;
15+
use WizDevelop\PhpValueObject\Error\ValueObjectError;
16+
use WizDevelop\PhpValueObject\Tests\TestCase;
17+
18+
/**
19+
* LocalDateクラスの異常系テスト
20+
*/
21+
#[TestDox('LocalDateクラスの異常系テスト')]
22+
#[Group('DateTime')]
23+
#[CoversClass(LocalDate::class)]
24+
final class LocalDateErrorTest extends TestCase
25+
{
26+
// ------------------------------------------
27+
// 範囲外の値によるエラーのテスト
28+
// ------------------------------------------
29+
30+
/**
31+
* @return array<string, array{int}>
32+
*/
33+
public static function 無効な年のデータを提供(): array
34+
{
35+
return [
36+
'最小値未満の年' => [LocalDate::MIN_YEAR - 1],
37+
// '最大値を超える年' => [LocalDate::MAX_YEAR + 1], // HACK: DateTimeImmutableインスタンスにすると2000年に変換されるため、テストできない
38+
'極端に小さい年' => [-10000],
39+
// '極端に大きい年' => [10000], // HACK: DateTimeImmutableインスタンスにすると2000年に変換されるため、テストできない
40+
];
41+
}
42+
43+
#[Test]
44+
#[DataProvider('無効な年のデータを提供')]
45+
public function 無効な年でインスタンスを作成するとエラーとなる(int $invalidYear): void
46+
{
47+
$result = LocalDate::tryFrom(new DateTimeImmutable("{$invalidYear}-01-01"));
48+
49+
$this->assertFalse($result->isOk());
50+
$this->assertInstanceOf(ValueObjectError::class, $result->unwrapErr());
51+
52+
$error = $result->unwrapErr();
53+
$this->assertStringContainsString('', $error->getMessage());
54+
$this->assertStringContainsString((string)$invalidYear, $error->getMessage());
55+
}
56+
57+
/**
58+
* @return array<string, array{int}>
59+
*/
60+
public static function 無効な月のデータを提供(): array
61+
{
62+
return [
63+
'0月' => [0],
64+
'13月' => [13],
65+
'極端に小さい月' => [-5],
66+
'極端に大きい月' => [100],
67+
];
68+
}
69+
70+
#[Test]
71+
#[DataProvider('無効な月のデータを提供')]
72+
public function 無効な月でインスタンスを作成するとエラーとなる(int $invalidMonth): void
73+
{
74+
// DateTimeImmutableでは無効な月を指定するとエラーになるため、
75+
// tryFromメソッドを直接テストすることはできないので、
76+
// このテストは省略します。実際のコードでは月の範囲チェックされています。
77+
78+
// 代わりに、アサーションで範囲外の月を検証するコードをテストします
79+
$this->expectException(AssertionError::class);
80+
81+
// PHPStanのエラーを回避するため
82+
// @phpstan-ignore-next-line
83+
LocalDate::of(2023, $invalidMonth, 1);
84+
}
85+
86+
/**
87+
* @return array<string, array{int}>
88+
*/
89+
public static function 無効な日のデータを提供(): array
90+
{
91+
return [
92+
'0日' => [0],
93+
'32日' => [32],
94+
'極端に小さい日' => [-5],
95+
'極端に大きい日' => [100],
96+
];
97+
}
98+
99+
#[Test]
100+
#[DataProvider('無効な日のデータを提供')]
101+
public function 無効な日でインスタンスを作成するとエラーとなる(int $invalidDay): void
102+
{
103+
// DateTimeImmutableでは無効な日を指定するとエラーになるため、
104+
// tryFromメソッドを直接テストすることはできないので、
105+
// このテストは省略します。実際のコードでは日の範囲チェックされています。
106+
107+
// 代わりに、アサーションで範囲外の日を検証するコードをテストします
108+
$this->expectException(AssertionError::class);
109+
110+
// PHPStanのエラーを回避するため
111+
// @phpstan-ignore-next-line
112+
LocalDate::of(2023, 1, $invalidDay);
113+
}
114+
115+
// ------------------------------------------
116+
// 存在しない日付によるエラーのテスト
117+
// ------------------------------------------
118+
119+
/**
120+
* @return array<string, array{int, int, int}>
121+
*/
122+
public static function 実在しない日付のデータを提供(): array
123+
{
124+
return [
125+
'2月30日(通常年)' => [2023, 2, 30],
126+
'2月29日(通常年)' => [2023, 2, 29],
127+
'4月31日' => [2023, 4, 31],
128+
'6月31日' => [2023, 6, 31],
129+
'9月31日' => [2023, 9, 31],
130+
'11月31日' => [2023, 11, 31],
131+
];
132+
}
133+
134+
#[Test]
135+
#[DataProvider('実在しない日付のデータを提供')]
136+
public function 実在しない日付でインスタンスを作成するとエラーとなる(int $year, int $month, int $day): void
137+
{
138+
// DateTimeImmutableでは実在しない日付を指定すると自動的に補正される(例:2月30日→3月2日)ため、
139+
// LocalDate::fromまたはLocalDate::tryFromでのテストは省略します。
140+
141+
// DateTimeImmutableの挙動をチェック(参考のため)
142+
$dateTime = new DateTimeImmutable(sprintf('%04d-%02d-%02d', $year, $month, $day));
143+
$this->assertNotEquals($day, (int)$dateTime->format('j'));
144+
145+
// アサーションで実在しない日付を検証するコードをテストします
146+
$this->expectException(AssertionError::class);
147+
148+
// PHPStanのエラーを回避するため
149+
// @phpstan-ignore-next-line
150+
LocalDate::of($year, $month, $day);
151+
}
152+
153+
// ------------------------------------------
154+
// うるう年関連の日付処理のテスト
155+
// ------------------------------------------
156+
157+
#[Test]
158+
public function うるう年以外の2月29日はエラーとなる(): void
159+
{
160+
$this->expectException(AssertionError::class);
161+
162+
// 2023年はうるう年ではない
163+
LocalDate::of(2023, 2, 29);
164+
}
165+
166+
#[Test]
167+
public function うるう年の2月29日は正常に作成できる(): void
168+
{
169+
// 2024年はうるう年
170+
$date = LocalDate::of(2024, 2, 29);
171+
172+
$this->assertSame(2024, $date->getYear());
173+
$this->assertSame(2, $date->getMonth());
174+
$this->assertSame(29, $date->getDay());
175+
}
176+
177+
// ------------------------------------------
178+
// addMonthsとうるう年の扱いのテスト
179+
// ------------------------------------------
180+
181+
#[Test]
182+
public function うるう年の2月29日から1年後は2月28日になる(): void
183+
{
184+
$leapDate = LocalDate::of(2024, 2, 29);
185+
$result = $leapDate->addYears(1);
186+
187+
$this->assertSame(2025, $result->getYear());
188+
$this->assertSame(2, $result->getMonth());
189+
$this->assertSame(28, $result->getDay());
190+
}
191+
192+
#[Test]
193+
public function うるう年の2月29日から4年後は2月29日になる(): void
194+
{
195+
$leapDate = LocalDate::of(2024, 2, 29);
196+
$result = $leapDate->addYears(4);
197+
198+
$this->assertSame(2028, $result->getYear());
199+
$this->assertSame(2, $result->getMonth());
200+
$this->assertSame(29, $result->getDay());
201+
}
202+
203+
#[Test]
204+
public function 月末から月を加算すると正しく調整される(): void
205+
{
206+
// 1月31日から1ヶ月後は2月28日(通常年)または2月29日(うるう年)
207+
$date1 = LocalDate::of(2023, 1, 31);
208+
$result1 = $date1->addMonths(1);
209+
$this->assertSame(2023, $result1->getYear());
210+
$this->assertSame(2, $result1->getMonth());
211+
$this->assertSame(28, $result1->getDay());
212+
213+
$date2 = LocalDate::of(2024, 1, 31);
214+
$result2 = $date2->addMonths(1);
215+
$this->assertSame(2024, $result2->getYear());
216+
$this->assertSame(2, $result2->getMonth());
217+
$this->assertSame(29, $result2->getDay());
218+
219+
// 3月31日から1ヶ月後は4月30日
220+
$date3 = LocalDate::of(2023, 3, 31);
221+
$result3 = $date3->addMonths(1);
222+
$this->assertSame(2023, $result3->getYear());
223+
$this->assertSame(4, $result3->getMonth());
224+
$this->assertSame(30, $result3->getDay());
225+
}
226+
227+
// ------------------------------------------
228+
// toEpochDayの境界値テスト
229+
// ------------------------------------------
230+
231+
#[Test]
232+
public function 極端な年のtoEpochDayが正しく動作する(): void
233+
{
234+
// 最小年
235+
$minDate = LocalDate::of(LocalDate::MIN_YEAR, 1, 1);
236+
$minEpochDay = $minDate->toEpochDay();
237+
238+
// 最大年
239+
$maxDate = LocalDate::of(LocalDate::MAX_YEAR, 12, 31);
240+
$maxEpochDay = $maxDate->toEpochDay();
241+
242+
// 最小年から最大年の日付を作成できることを確認
243+
$reconstructedMinDate = LocalDate::ofEpochDay($minEpochDay);
244+
$reconstructedMaxDate = LocalDate::ofEpochDay($maxEpochDay);
245+
246+
$this->assertSame(LocalDate::MIN_YEAR, $reconstructedMinDate->getYear());
247+
$this->assertSame(1, $reconstructedMinDate->getMonth());
248+
$this->assertSame(1, $reconstructedMinDate->getDay());
249+
250+
$this->assertSame(LocalDate::MAX_YEAR, $reconstructedMaxDate->getYear());
251+
$this->assertSame(12, $reconstructedMaxDate->getMonth());
252+
$this->assertSame(31, $reconstructedMaxDate->getDay());
253+
}
254+
}
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WizDevelop\PhpValueObject\Tests\Unit\DateTime;
6+
7+
use DateTimeZone;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\Attributes\Group;
10+
use PHPUnit\Framework\Attributes\Test;
11+
use PHPUnit\Framework\Attributes\TestDox;
12+
use WizDevelop\PhpValueObject\DateTime\LocalDate;
13+
use WizDevelop\PhpValueObject\DateTime\LocalDateTime;
14+
use WizDevelop\PhpValueObject\DateTime\LocalTime;
15+
use WizDevelop\PhpValueObject\Tests\TestCase;
16+
17+
/**
18+
* LocalDateTimeクラスの異常系テスト
19+
*/
20+
#[TestDox('LocalDateTimeクラスの異常系テスト')]
21+
#[Group('DateTime')]
22+
#[CoversClass(LocalDateTime::class)]
23+
final class LocalDateTimeErrorTest extends TestCase
24+
{
25+
// ------------------------------------------
26+
// 日付をまたぐ演算処理のテスト
27+
// ------------------------------------------
28+
29+
#[Test]
30+
public function 日付境界での時間加算が正しく処理される(): void
31+
{
32+
// 23:59:59.999999から1マイクロ秒加算すると翌日の00:00:00.000000になる
33+
$dateTime = LocalDateTime::of(
34+
LocalDate::of(2023, 5, 15),
35+
LocalTime::of(23, 59, 59, 999999)
36+
);
37+
38+
$result = $dateTime->addMicros(1);
39+
40+
$this->assertSame(2023, $result->getYear());
41+
$this->assertSame(5, $result->getMonth());
42+
$this->assertSame(16, $result->getDay());
43+
$this->assertSame(0, $result->getHour());
44+
$this->assertSame(0, $result->getMinute());
45+
$this->assertSame(0, $result->getSecond());
46+
$this->assertSame(0, $result->getMicro());
47+
}
48+
49+
#[Test]
50+
public function 大きな時間加算で日付が正しく計算される(): void
51+
{
52+
$dateTime = LocalDateTime::of(
53+
LocalDate::of(2023, 5, 15),
54+
LocalTime::of(12, 0, 0)
55+
);
56+
57+
// 48時間(2日分)加算
58+
$result = $dateTime->addHours(48);
59+
60+
$this->assertSame(2023, $result->getYear());
61+
$this->assertSame(5, $result->getMonth());
62+
$this->assertSame(17, $result->getDay());
63+
$this->assertSame(12, $result->getHour());
64+
$this->assertSame(0, $result->getMinute());
65+
$this->assertSame(0, $result->getSecond());
66+
}
67+
68+
#[Test]
69+
public function 月をまたぐ日付加算が正しく処理される(): void
70+
{
71+
$dateTime = LocalDateTime::of(
72+
LocalDate::of(2023, 5, 31),
73+
LocalTime::of(12, 0, 0)
74+
);
75+
76+
// 1日加算すると6月になる
77+
$result = $dateTime->addDays(1);
78+
79+
$this->assertSame(2023, $result->getYear());
80+
$this->assertSame(6, $result->getMonth());
81+
$this->assertSame(1, $result->getDay());
82+
$this->assertSame(12, $result->getHour());
83+
$this->assertSame(0, $result->getMinute());
84+
$this->assertSame(0, $result->getSecond());
85+
}
86+
87+
#[Test]
88+
public function 年をまたぐ日付加算が正しく処理される(): void
89+
{
90+
$dateTime = LocalDateTime::of(
91+
LocalDate::of(2023, 12, 31),
92+
LocalTime::of(23, 59, 59)
93+
);
94+
95+
// 1秒加算すると翌年になる
96+
$result = $dateTime->addSeconds(1);
97+
98+
$this->assertSame(2024, $result->getYear());
99+
$this->assertSame(1, $result->getMonth());
100+
$this->assertSame(1, $result->getDay());
101+
$this->assertSame(0, $result->getHour());
102+
$this->assertSame(0, $result->getMinute());
103+
$this->assertSame(0, $result->getSecond());
104+
}
105+
106+
// ------------------------------------------
107+
// 日時の演算の組み合わせテスト
108+
// ------------------------------------------
109+
110+
#[Test]
111+
public function 複数の演算を組み合わせた場合も正しく計算される(): void
112+
{
113+
$dateTime = LocalDateTime::of(
114+
LocalDate::of(2023, 5, 15),
115+
LocalTime::of(12, 30, 45)
116+
);
117+
118+
// 日付と時間の両方に対する演算
119+
$result = $dateTime
120+
->addMonths(1) // 6月15日
121+
->addDays(20) // 7月5日
122+
->addHours(6) // 18:30:45
123+
->addMinutes(-45); // 17:45:45
124+
125+
$this->assertSame(2023, $result->getYear());
126+
$this->assertSame(7, $result->getMonth());
127+
$this->assertSame(5, $result->getDay());
128+
$this->assertSame(17, $result->getHour());
129+
$this->assertSame(45, $result->getMinute());
130+
$this->assertSame(45, $result->getSecond());
131+
}
132+
133+
// ------------------------------------------
134+
// うるう年の日付処理のテスト
135+
// ------------------------------------------
136+
137+
#[Test]
138+
public function うるう年の2月29日の処理が正しく行われる(): void
139+
{
140+
// うるう年の2月29日
141+
$dateTime = LocalDateTime::of(
142+
LocalDate::of(2024, 2, 29),
143+
LocalTime::of(12, 0, 0)
144+
);
145+
146+
// 1年後は2月28日になる
147+
$result1 = $dateTime->addYears(1);
148+
$this->assertSame(2025, $result1->getYear());
149+
$this->assertSame(2, $result1->getMonth());
150+
$this->assertSame(28, $result1->getDay());
151+
152+
// 4年後は2月29日になる
153+
$result2 = $dateTime->addYears(4);
154+
$this->assertSame(2028, $result2->getYear());
155+
$this->assertSame(2, $result2->getMonth());
156+
$this->assertSame(29, $result2->getDay());
157+
}
158+
159+
// ------------------------------------------
160+
// 時間演算と負の値のテスト
161+
// ------------------------------------------
162+
163+
#[Test]
164+
public function 負の時間加算が正しく処理される(): void
165+
{
166+
$dateTime = LocalDateTime::of(
167+
LocalDate::of(2023, 5, 15),
168+
LocalTime::of(0, 0, 0)
169+
);
170+
171+
// -1時間(前日の23時になる)
172+
$result1 = $dateTime->addHours(-1);
173+
$this->assertSame(2023, $result1->getYear());
174+
$this->assertSame(5, $result1->getMonth());
175+
$this->assertSame(14, $result1->getDay());
176+
$this->assertSame(23, $result1->getHour());
177+
$this->assertSame(0, $result1->getMinute());
178+
$this->assertSame(0, $result1->getSecond());
179+
180+
// -1分(前日の23:59になる)
181+
$result2 = $dateTime->addMinutes(-1);
182+
$this->assertSame(2023, $result2->getYear());
183+
$this->assertSame(5, $result2->getMonth());
184+
$this->assertSame(14, $result2->getDay());
185+
$this->assertSame(23, $result2->getHour());
186+
$this->assertSame(59, $result2->getMinute());
187+
$this->assertSame(0, $result2->getSecond());
188+
}
189+
190+
#[Test]
191+
public function 月の境界での日付加減算が正しく処理される(): void
192+
{
193+
// 月初
194+
$dateTime1 = LocalDateTime::of(
195+
LocalDate::of(2023, 5, 1),
196+
LocalTime::of(12, 0, 0)
197+
);
198+
199+
// 1日減算すると前月末になる
200+
$result1 = $dateTime1->addDays(-1);
201+
$this->assertSame(2023, $result1->getYear());
202+
$this->assertSame(4, $result1->getMonth());
203+
$this->assertSame(30, $result1->getDay());
204+
205+
// 月末
206+
$dateTime2 = LocalDateTime::of(
207+
LocalDate::of(2023, 2, 28),
208+
LocalTime::of(12, 0, 0)
209+
);
210+
211+
// 1日加算すると翌月初になる
212+
$result2 = $dateTime2->addDays(1);
213+
$this->assertSame(2023, $result2->getYear());
214+
$this->assertSame(3, $result2->getMonth());
215+
$this->assertSame(1, $result2->getDay());
216+
}
217+
218+
// ------------------------------------------
219+
// 日時比較メソッドのエッジケーステスト
220+
// ------------------------------------------
221+
222+
#[Test]
223+
public function 日付は同じで時刻が異なる場合の比較が正しく処理される(): void
224+
{
225+
$dateTime1 = LocalDateTime::of(
226+
LocalDate::of(2023, 5, 15),
227+
LocalTime::of(10, 0, 0)
228+
);
229+
230+
$dateTime2 = LocalDateTime::of(
231+
LocalDate::of(2023, 5, 15),
232+
LocalTime::of(11, 0, 0)
233+
);
234+
235+
$this->assertTrue($dateTime1->isBefore($dateTime2));
236+
$this->assertFalse($dateTime2->isBefore($dateTime1));
237+
$this->assertFalse($dateTime1->isAfter($dateTime2));
238+
$this->assertTrue($dateTime2->isAfter($dateTime1));
239+
}
240+
241+
#[Test]
242+
public function 時刻は同じで日付が異なる場合の比較が正しく処理される(): void
243+
{
244+
$dateTime1 = LocalDateTime::of(
245+
LocalDate::of(2023, 5, 15),
246+
LocalTime::of(10, 0, 0)
247+
);
248+
249+
$dateTime2 = LocalDateTime::of(
250+
LocalDate::of(2023, 5, 16),
251+
LocalTime::of(10, 0, 0)
252+
);
253+
254+
$this->assertTrue($dateTime1->isBefore($dateTime2));
255+
$this->assertFalse($dateTime2->isBefore($dateTime1));
256+
$this->assertFalse($dateTime1->isAfter($dateTime2));
257+
$this->assertTrue($dateTime2->isAfter($dateTime1));
258+
}
259+
260+
// ------------------------------------------
261+
// isFutureとisPastのテスト
262+
// ------------------------------------------
263+
264+
#[Test]
265+
public function isFutureとisPastが現在時刻に基づいて正しく判定される(): void
266+
{
267+
// テスト実行時の現在日時
268+
$now = LocalDateTime::now(new DateTimeZone('UTC'));
269+
270+
// 確実に過去の日時
271+
$past = $now->subDays(1);
272+
$this->assertTrue($past->isPast(new DateTimeZone('UTC')));
273+
$this->assertFalse($past->isFuture(new DateTimeZone('UTC')));
274+
275+
// 確実に未来の日時
276+
$future = $now->addDays(1);
277+
$this->assertTrue($future->isFuture(new DateTimeZone('UTC')));
278+
$this->assertFalse($future->isPast(new DateTimeZone('UTC')));
279+
}
280+
281+
// ------------------------------------------
282+
// addWithOverflowの内部動作テスト
283+
// ------------------------------------------
284+
285+
#[Test]
286+
public function 複雑な時間加算の内部処理が正しく動作する(): void
287+
{
288+
// 時、分、秒、マイクロ秒が全て影響する加算
289+
$dateTime = LocalDateTime::of(
290+
LocalDate::of(2023, 5, 15),
291+
LocalTime::of(23, 59, 59, 999999)
292+
);
293+
294+
// 1時間、1分、1秒、1マイクロ秒を加算
295+
$result = $dateTime
296+
->addHours(1)
297+
->addMinutes(1)
298+
->addSeconds(1)
299+
->addMicros(1);
300+
301+
$this->assertSame(2023, $result->getYear());
302+
$this->assertSame(5, $result->getMonth());
303+
$this->assertSame(16, $result->getDay());
304+
$this->assertSame(1, $result->getHour());
305+
$this->assertSame(1, $result->getMinute());
306+
$this->assertSame(1, $result->getSecond());
307+
$this->assertSame(0, $result->getMicro());
308+
}
309+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WizDevelop\PhpValueObject\Tests\Unit\DateTime;
6+
7+
use AssertionError;
8+
use DateTimeImmutable;
9+
use PHPUnit\Framework\Attributes\CoversClass;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\Attributes\Group;
12+
use PHPUnit\Framework\Attributes\Test;
13+
use PHPUnit\Framework\Attributes\TestDox;
14+
use WizDevelop\PhpValueObject\DateTime\LocalTime;
15+
use WizDevelop\PhpValueObject\Tests\TestCase;
16+
17+
/**
18+
* LocalTimeクラスの異常系テスト
19+
*/
20+
#[TestDox('LocalTimeクラスの異常系テスト')]
21+
#[Group('DateTime')]
22+
#[CoversClass(LocalTime::class)]
23+
final class LocalTimeErrorTest extends TestCase
24+
{
25+
// ------------------------------------------
26+
// 範囲外の値によるエラーのテスト
27+
// ------------------------------------------
28+
29+
/**
30+
* @return array<string, array{int}>
31+
*/
32+
public static function 無効な時のデータを提供(): array
33+
{
34+
return [
35+
'負の時' => [-1],
36+
'24時' => [24],
37+
'極端に小さい時' => [-100],
38+
'極端に大きい時' => [100],
39+
];
40+
}
41+
42+
#[Test]
43+
#[DataProvider('無効な時のデータを提供')]
44+
public function 無効な時でインスタンスを作成するとエラーとなる(int $invalidHour): void
45+
{
46+
// DateTimeImmutableでは無効な時間を指定すると自動補正されるため、
47+
// tryFromメソッドを直接テストすることはできません。
48+
49+
// 代わりに、アサーションで範囲外の時間を検証するコードをテストします
50+
$this->expectException(AssertionError::class);
51+
52+
// PHPStanのエラーを回避するため
53+
// @phpstan-ignore-next-line
54+
LocalTime::of($invalidHour, 0);
55+
}
56+
57+
/**
58+
* @return array<string, array{int}>
59+
*/
60+
public static function 無効な分のデータを提供(): array
61+
{
62+
return [
63+
'負の分' => [-1],
64+
'60分' => [60],
65+
'極端に小さい分' => [-100],
66+
'極端に大きい分' => [100],
67+
];
68+
}
69+
70+
#[Test]
71+
#[DataProvider('無効な分のデータを提供')]
72+
public function 無効な分でインスタンスを作成するとエラーとなる(int $invalidMinute): void
73+
{
74+
$this->expectException(AssertionError::class);
75+
76+
// @phpstan-ignore-next-line
77+
LocalTime::of(0, $invalidMinute);
78+
}
79+
80+
/**
81+
* @return array<string, array{int}>
82+
*/
83+
public static function 無効な秒のデータを提供(): array
84+
{
85+
return [
86+
'負の秒' => [-1],
87+
'60秒' => [60],
88+
'極端に小さい秒' => [-100],
89+
'極端に大きい秒' => [100],
90+
];
91+
}
92+
93+
#[Test]
94+
#[DataProvider('無効な秒のデータを提供')]
95+
public function 無効な秒でインスタンスを作成するとエラーとなる(int $invalidSecond): void
96+
{
97+
$this->expectException(AssertionError::class);
98+
99+
// @phpstan-ignore-next-line
100+
LocalTime::of(0, 0, $invalidSecond);
101+
}
102+
103+
/**
104+
* @return array<string, array{int}>
105+
*/
106+
public static function 無効なマイクロ秒のデータを提供(): array
107+
{
108+
return [
109+
'負のマイクロ秒' => [-1],
110+
'1,000,000マイクロ秒' => [1000000],
111+
'極端に小さいマイクロ秒' => [-100000],
112+
'極端に大きいマイクロ秒' => [10000000],
113+
];
114+
}
115+
116+
#[Test]
117+
#[DataProvider('無効なマイクロ秒のデータを提供')]
118+
public function 無効なマイクロ秒でインスタンスを作成するとエラーとなる(int $invalidMicro): void
119+
{
120+
$this->expectException(AssertionError::class);
121+
122+
// @phpstan-ignore-next-line
123+
LocalTime::of(0, 0, 0, $invalidMicro);
124+
}
125+
126+
// ------------------------------------------
127+
// ofSecondOfDayの境界値テスト
128+
// ------------------------------------------
129+
130+
/**
131+
* @return array<string, array{int}>
132+
*/
133+
public static function 無効な秒オブデイのデータを提供(): array
134+
{
135+
return [
136+
'負の秒' => [-1],
137+
'1日の秒数以上' => [LocalTime::SECONDS_PER_DAY],
138+
'極端に小さい秒' => [-100000],
139+
'極端に大きい秒' => [1000000],
140+
];
141+
}
142+
143+
#[Test]
144+
#[DataProvider('無効な秒オブデイのデータを提供')]
145+
public function 無効な秒オブデイ値が適切に例がをスローすること(int $invalidSecondOfDay): void
146+
{
147+
// DateTimeImmutableでは無効な秒オブデイを指定すると自動補正されるため、
148+
// tryFromメソッドを直接テストすることはできません。
149+
150+
// 代わりに、アサーションで範囲外の秒オブデイを検証するコードをテストします
151+
$this->expectException(AssertionError::class);
152+
153+
// PHPStanのエラーを回避するため
154+
// @phpstan-ignore-next-line
155+
LocalTime::ofSecondOfDay($invalidSecondOfDay);
156+
}
157+
158+
// ------------------------------------------
159+
// 演算メソッドの端値テスト
160+
// ------------------------------------------
161+
162+
#[Test]
163+
public function 加算メソッドで時刻が循環することを確認(): void
164+
{
165+
// 23時に2時間加算すると1時になる
166+
$time1 = LocalTime::of(23, 0, 0);
167+
$result1 = $time1->addHours(2);
168+
$this->assertSame(1, $result1->getHour());
169+
170+
// 0時から1時間引くと23時になる
171+
$time2 = LocalTime::of(0, 0, 0);
172+
$result2 = $time2->addHours(-1);
173+
$this->assertSame(23, $result2->getHour());
174+
175+
// 23:59:59に1秒加算すると00:00:00になる
176+
$time3 = LocalTime::of(23, 59, 59);
177+
$result3 = $time3->addSeconds(1);
178+
$this->assertSame(0, $result3->getHour());
179+
$this->assertSame(0, $result3->getMinute());
180+
$this->assertSame(0, $result3->getSecond());
181+
}
182+
183+
#[Test]
184+
public function マイクロ秒の桁上がりが正しく処理される(): void
185+
{
186+
// 999,999マイクロ秒に1マイクロ秒加算すると次の秒に桁上がり
187+
$time = LocalTime::of(12, 30, 45, 999999);
188+
$result = $time->addMicros(1);
189+
190+
$this->assertSame(12, $result->getHour());
191+
$this->assertSame(30, $result->getMinute());
192+
$this->assertSame(46, $result->getSecond());
193+
$this->assertSame(0, $result->getMicro());
194+
195+
// 0マイクロ秒から1マイクロ秒引くと前の秒から桁借り
196+
$time2 = LocalTime::of(12, 30, 46, 0);
197+
$result2 = $time2->addMicros(-1);
198+
199+
$this->assertSame(12, $result2->getHour());
200+
$this->assertSame(30, $result2->getMinute());
201+
$this->assertSame(45, $result2->getSecond());
202+
$this->assertSame(999999, $result2->getMicro());
203+
}
204+
205+
#[Test]
206+
public function 大きな時間加算が正しく処理される(): void
207+
{
208+
// 1年分(1日×365)の時間を加算しても正しく循環する
209+
$time = LocalTime::of(12, 0, 0);
210+
$result = $time->addHours(24 * 365);
211+
212+
// 1年後も同じ時刻
213+
$this->assertSame(12, $result->getHour());
214+
$this->assertSame(0, $result->getMinute());
215+
$this->assertSame(0, $result->getSecond());
216+
}
217+
218+
// ------------------------------------------
219+
// 存在しない時刻によるエラーのテスト
220+
// ------------------------------------------
221+
222+
#[Test]
223+
public function ISO文字列への変換で秒とマイクロ秒が省略される(): void
224+
{
225+
// 時:分のみ
226+
$time1 = LocalTime::of(12, 30, 0, 0);
227+
$this->assertSame('12:30', $time1->toISOString());
228+
229+
// 時:分:秒(マイクロ秒は0)
230+
$time2 = LocalTime::of(12, 30, 45, 0);
231+
$this->assertSame('12:30:45', $time2->toISOString());
232+
233+
// 時:分:秒.マイクロ秒
234+
$time3 = LocalTime::of(12, 30, 45, 123456);
235+
$this->assertSame('12:30:45.123456', $time3->toISOString());
236+
237+
// マイクロ秒の末尾の0は削除される
238+
$time4 = LocalTime::of(12, 30, 45, 123000);
239+
$this->assertSame('12:30:45.123', $time4->toISOString());
240+
}
241+
242+
// ------------------------------------------
243+
// tryFromメソッドのエラーテスト
244+
// ------------------------------------------
245+
246+
#[Test]
247+
public function tryFromでバリデーションエラーが適切に処理される(): void
248+
{
249+
// tryFromメソッドでは、DateTimeImmutableから時間を抽出し、
250+
// その後にバリデーションを行うため、直接無効な値を渡すのは難しいです。
251+
// したがって、バリデーションが呼び出されることを確認するテストとなります。
252+
253+
// 実装上、DateTimeImmutableから取得するhour, minute, secondなどの値は
254+
// すでに有効な範囲に収まっていることが期待されるため、
255+
// 通常のケースではエラーは発生しません。
256+
257+
$validTime = new DateTimeImmutable('12:30:45');
258+
$result = LocalTime::tryFrom($validTime);
259+
260+
$this->assertTrue($result->isOk());
261+
$this->assertSame(12, $result->unwrap()->getHour());
262+
$this->assertSame(30, $result->unwrap()->getMinute());
263+
$this->assertSame(45, $result->unwrap()->getSecond());
264+
}
265+
}

0 commit comments

Comments
 (0)
Please sign in to comment.