File: DateTime.modify.phpt

package info (click to toggle)
php-nette-utils 4.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,460 kB
  • sloc: php: 4,146; xml: 9; makefile: 4
file content (188 lines) | stat: -rw-r--r-- 8,170 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?php

/**
 * Test: Nette\Utils\DateTime modify() method.
 */

declare(strict_types=1);

use Nette\Utils\DateTime;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';

date_default_timezone_set('Europe/Prague');


test('Basic operations (no DST involved)', function () {
	$base = new DateTime('2024-07-15 10:00:00'); // Summer time (CEST)

	$dt = clone $base;
	$dt->modify('+30 minutes');
	Assert::same('2024-07-15 10:30:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+30 minutes');

	$dt = clone $base;
	$dt->modify('+2 hours');
	Assert::same('2024-07-15 12:00:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+2 hours');

	$dt = clone $base;
	$dt->modify('-5 days');
	Assert::same('2024-07-10 10:00:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '-5 days');

	$dt = new DateTime('2024-01-15 10:00:00'); // Winter time (CET)
	$dt->modify('+1 month');
	Assert::same('2024-02-15 10:00:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), '+1 month in winter');
});


test('Spring DST transition (2025-03-30 02:00 -> 03:00)', function () {
	$startSpring = new DateTime('2025-03-30 01:45:00'); // Before the jump (CET +01:00)

	// Modification ending BEFORE the jump
	$dt = clone $startSpring;
	$dt->modify('+10 minutes');
	Assert::same('2025-03-30 01:55:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), '+10 min (ends before jump)');

	// Modification crossing the jump (duration logic thanks to Nette fix)
	$dt = clone $startSpring;
	$dt->modify('+30 minutes'); // 01:45 CET + 30 min duration = 01:15 UTC = 03:15 CEST
	Assert::same('2025-03-30 03:15:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+30 min (crosses jump)');

	$dt = clone $startSpring;
	$dt->modify('+90 minutes'); // 01:45 CET + 90 min duration = 02:15 UTC = 04:15 CEST (Key test!)
	Assert::same('2025-03-30 04:15:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+90 min (crosses jump)');

	// Adding a day across the jump (day has only 23 hours)
	$dt = clone $startSpring;
	$dt->modify('+1 day');
	Assert::same('2025-03-31 01:45:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+1 day');

	// Combination of day + hours across the jump
	$dt = clone $startSpring;
	$dt->modify('+1 day +1 hour');
	Assert::same('2025-03-31 02:45:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+1 day + 1 hour');

	$dt = clone $startSpring;
	$dt->modify('+2 hours'); // 01:45 CET + 2h duration = 02:45 UTC = 04:45 CEST
	Assert::same('2025-03-30 04:45:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+2 hours (crosses jump)');
});


test('Autumn DST transition (2024-10-27 03:00 -> 02:00)', function () {
	$startAutumn = new DateTime('2024-10-27 01:45:00'); // Before the fallback (CEST +02:00)

	// Modification ending BEFORE the fallback (still CEST)
	$dt = clone $startAutumn;
	$dt->modify('+30 minutes');
	Assert::same('2024-10-27 02:15:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), '+30 min (ends before fallback)');

	// Modification crossing the fallback (lands in the second 2:xx hour - CET)
	$dt = clone $startAutumn;
	$dt->modify('+90 minutes'); // 01:45 CEST + 90 min duration = 01:15 UTC = 02:15 CET
	Assert::same('2024-10-27 02:15:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), '+90 min (crosses fallback, lands in CET)');

	$dt = clone $startAutumn;
	$dt->modify('+1 hour + 30 minutes'); // Same as +90 minutes
	Assert::same('2024-10-27 02:15:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), '+1 hour + 30 minutes (crosses fallback)');

	// Adding a day across the fallback (day has 25 hours)
	$dt = clone $startAutumn;
	$dt->modify('+1 day');
	Assert::same('2024-10-28 01:45:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), '+1 day');

	// Combination of day + hours across the fallback
	$dt = clone $startAutumn;
	$dt->modify('+1 day +2 hours');
	Assert::same('2024-10-28 03:45:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), '+1 day + 2 hours');
});


test('Complex and varied format strings', function () {
	$dt = new DateTime('2024-04-10 12:00:00'); // CEST
	// Expected: -2m -> 2024-02-10 12:00 CET | +7d -> 2024-02-17 12:00 CET | +23h 59m 59s -> 2024-02-18 11:59:59 CET
	$dt->modify('- 2 months +7 days +23 hours +59 minutes +59 seconds');
	Assert::same('2024-02-18 11:59:59 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Complex mixed modification 1');

	$dt = new DateTime('2024-01-10 15:00:00'); // CET
	$dt->modify('  2days '); // Spaces and format variation
	Assert::same('2024-01-12 15:00:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Format "  2days "');

	// Textual relative modifier
	$dt = new DateTime('2024-04-10 12:00:00'); // CEST
	$dt->modify('first day of next month noon');
	Assert::same('2024-05-01 12:00:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Textual relative modifier');

	// Complex mixed modification 2 (year/month/day/hour/min)
	$dt = new DateTime('2023-11-20 08:30:00'); // CET
	// +1y -> 2024-11-20 08:30 CET | -3m -> 2024-08-20 08:30 CEST | +10d -> 2024-08-30 08:30 CEST | +5h -> 2024-08-30 13:30 CEST | -15min -> 2024-08-30 13:15 CEST
	$dt->modify('+1 year -3 months + 10 days + 5 hours - 15 minutes');
	Assert::same('2024-08-30 13:15:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Complex mixed modification 2');

	// Extra spaces and singular unit
	$dt = new DateTime('2024-05-15 10:00:00'); // CEST
	$dt->modify('+ 2 days   - 1hour'); // Extra spaces, 'hour' singular
	Assert::same('2024-05-17 09:00:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Extra spaces and singular unit');

	// Seconds and milliseconds
	$dt = new DateTime('2024-06-01 12:00:00.000000'); // CEST, explicit microseconds
	$dt->modify('+3 sec - 500 milliseconds');
	Assert::same('2024-06-01 12:00:02.500000', $dt->format('Y-m-d H:i:s.u'), 'Seconds and milliseconds');
	Assert::same('CEST (+02:00)', $dt->format('T (P)'), 'Timezone check for ms test');

	// Textual day + numeric hour
	$dt = new DateTime('2024-06-15 09:00:00'); // CEST (Saturday)
	// 'next sunday' -> 2024-06-16 00:00:00, '+ 4 hours' -> 2024-06-16 04:00:00
	$dt->modify('next sunday + 4 hours');
	Assert::same('2024-06-16 04:00:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Textual day + numeric hour');

	// Textual time + numeric minute
	$dt = new DateTime('2024-06-15 09:00:00'); // CEST
	// 'noon' -> 12:00:00, '- 30 minutes' -> 11:30:00
	$dt->modify('noon - 30 minutes');
	Assert::same('2024-06-15 11:30:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Textual time + numeric minute');

	// Zero value modifiers
	$dt = new DateTime('2024-05-05 05:05:05'); // CEST
	$dt->modify('+0 days - 0 hours + 0 seconds');
	Assert::same('2024-05-05 05:05:05 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Zero value modifiers');

	// Microsecond addition
	$dt = new DateTime('2024-07-01 10:20:30.123456'); // CEST
	$dt->modify('+ 100 usecs');
	Assert::same('2024-07-01 10:20:30.123556', $dt->format('Y-m-d H:i:s.u'), 'Microsecond addition');
	Assert::same('CEST (+02:00)', $dt->format('T (P)'), 'Timezone check for usec test');

	// Chained textual modifiers
	$dt = new DateTime('2024-03-10 10:00:00'); // CET
	// 'first day of may' -> 2024-05-01 00:00 | 'noon' -> 2024-05-01 12:00
	$dt->modify('first day of may 2024 noon');
	Assert::same('2024-05-01 12:00:00 CEST (+02:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Chained textual modifiers');

	// ago
	$dt = new DateTime('2024-03-10 10:00:00'); // CET
	$dt->modify('12 minutes ago');
	Assert::same('2024-03-10 09:48:00 CET (+01:00)', $dt->format('Y-m-d H:i:s T (P)'), 'Ago modifier');
});


test('Invalid modifier format exceptions', function () {
	if (PHP_VERSION_ID < 80300) {
		Assert::error(
			fn() => (new DateTime)->modify('+'),
			E_WARNING,
			'DateTime::modify(): Failed to parse time string (+) at position 0 (+): Unexpected character',
		);
	} else {
		Assert::exception(
			fn() => (new DateTime)->modify('+'),
			DateMalformedStringException::class,
			'DateTime::modify(): Failed to parse time string (+) at position 0 (+): Unexpected character',
		);
	}

	Assert::exception(
		fn() => (new DateTime)->modify('2024-02-31 10:00:00'), // Invalid day for February
		Throwable::class,
		"The parsed date was invalid '2024-02-31 10:00:00'",
	);
});