Skip to content

Commit 2b7cbeb

Browse files
Amaia Anabitartesarjona
Amaia Anabitarte
authored andcommitted
MDL-83873 core_calendar: New human date format
1 parent 7a318d5 commit 2b7cbeb

File tree

6 files changed

+741
-0
lines changed

6 files changed

+741
-0
lines changed

calendar/classes/output/humandate.php

+324
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace core_calendar\output;
18+
19+
use DateInterval;
20+
use DateTimeImmutable;
21+
use core\output\pix_icon;
22+
use core\output\templatable;
23+
use core\output\renderable;
24+
use core\output\renderer_base;
25+
use \core\clock;
26+
use core\url;
27+
28+
/**
29+
* Class humandate.
30+
*
31+
* This class is used to render a timestamp as a human readable date.
32+
* The main difference between userdate and this class is that this class
33+
* will render the date as "Today", "Yesterday", "Tomorrow" if the date is
34+
* close to the current date. Also, it will add alert styling if the date
35+
* is near.
36+
*
37+
* @package core_calendar
38+
* @copyright 2024 Ferran Recio <ferran@moodle.com>
39+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40+
*/
41+
class humandate implements renderable, templatable {
42+
43+
/** @var int|null The number of seconds within which a date is considered near. 1 day by default. */
44+
protected ?int $near = DAYSECS;
45+
46+
/** @var bool Whether we should show time only or date and time. */
47+
protected bool $timeonly = false;
48+
49+
/** @var url|null Link for the date. */
50+
protected ?url $link = null;
51+
52+
/** @var string|null An optional date format to apply. */
53+
protected ?string $langtimeformat = null;
54+
55+
/** @var bool Whether to use human relative terminology. */
56+
protected bool $userelatives = true;
57+
58+
/** @var clock The clock interface to handle time. */
59+
protected clock $clock;
60+
61+
/**
62+
* Class constructor.
63+
*
64+
* Use the factory methods, such as create_from_timestamp or create_from_datetime, instead.
65+
*
66+
* @param DateTimeImmutable $datetime The datetime.
67+
*/
68+
protected function __construct(
69+
/** @var DateTimeImmutable $datetime The datetime. **/
70+
protected DateTimeImmutable $datetime,
71+
) {
72+
$this->clock = \core\di::get(clock::class);
73+
}
74+
75+
/**
76+
* Creates a new humandate instance from a timestamp.
77+
*
78+
* @param int $timestamp The timestamp.
79+
* @param int|null $near The number of seconds within which a date is considered near. 1 day by default.
80+
* @param bool $timeonly Whether we should show time only or date and time.
81+
* @param mixed $link Link for the date.
82+
* @param mixed $langtimeformat An optional date format to apply.
83+
* @param bool $userelatives Whether to use human relative terminology.
84+
* @return humandate The new instance.
85+
*/
86+
public static function create_from_timestamp(
87+
int $timestamp,
88+
?int $near = DAYSECS,
89+
bool $timeonly = false,
90+
?url $link = null,
91+
?string $langtimeformat = null,
92+
bool $userelatives = true,
93+
): self {
94+
95+
return self::create_from_datetime(
96+
(new DateTimeImmutable("@{$timestamp}")),
97+
$near,
98+
$timeonly,
99+
$link,
100+
$langtimeformat,
101+
$userelatives
102+
);
103+
}
104+
105+
/**
106+
* Creates a new humandate instance from a datetime.
107+
*
108+
* @param DateTimeImmutable $datetime The datetime.
109+
* @param int|null $near The number of seconds within which a date is considered near. 1 day by default.
110+
* @param bool $timeonly Whether we should show time only or date and time.
111+
* @param mixed $link Link for the date.
112+
* @param mixed $langtimeformat An optional date format to apply.
113+
* @param bool $userelatives Whether to use human relative terminology.
114+
* @return humandate The new instance.
115+
*/
116+
public static function create_from_datetime(
117+
DateTimeImmutable $datetime,
118+
?int $near = DAYSECS,
119+
bool $timeonly = false,
120+
?url $link = null,
121+
?string $langtimeformat = null,
122+
bool $userelatives = true,
123+
): self {
124+
125+
if (!($datetime instanceof DateTimeImmutable)) {
126+
// Always use an Immutable object to ensure that the value does not change externally before it is rendered.
127+
$datetime = DateTimeImmutable::createFromInterface($datetime);
128+
}
129+
130+
return (new self($datetime))
131+
->set_near_limit($near)
132+
->set_display_time_only($timeonly)
133+
->set_link($link)
134+
->set_lang_time_format($langtimeformat)
135+
->set_use_relatives($userelatives);
136+
}
137+
138+
/**
139+
* Sets the number of seconds within which a date is considered near.
140+
*
141+
* @param int|null $near The number of seconds within which a date is considered near.
142+
* @return humandate The instance.
143+
*/
144+
public function set_near_limit(?int $near): self {
145+
$this->near = $near;
146+
return $this;
147+
}
148+
149+
/**
150+
* Sets whether we should show time only or date and time.
151+
*
152+
* @param bool $timeonly Whether we should show time only or date and time.
153+
* @return humandate The instance.
154+
*/
155+
public function set_display_time_only(bool $timeonly): self {
156+
$this->timeonly = $timeonly;
157+
return $this;
158+
}
159+
160+
/**
161+
* Sets the link for the date. If null, no link will be added.
162+
*
163+
* @param url|null $link The link for the date.
164+
* @return humandate The instance.
165+
*/
166+
public function set_link(?url $link): self {
167+
$this->link = $link;
168+
return $this;
169+
}
170+
171+
/**
172+
* Sets an optional date format to apply.
173+
*
174+
* @param string|null $langtimeformat Lang date and time format to use to format the date.
175+
* @return humandate The instance.
176+
*/
177+
public function set_lang_time_format(?string $langtimeformat): self {
178+
$this->langtimeformat = $langtimeformat;
179+
return $this;
180+
}
181+
182+
/**
183+
* Sets whether to use human relative terminology.
184+
*
185+
* @param bool $userelatives Whether to use human relative terminology.
186+
* @return humandate The instance.
187+
*/
188+
public function set_use_relatives(bool $userelatives): self {
189+
$this->userelatives = $userelatives;
190+
return $this;
191+
}
192+
193+
#[\Override]
194+
public function export_for_template(renderer_base $output): array {
195+
$timestamp = $this->datetime->getTimestamp();
196+
$userdate = userdate($timestamp, get_string('strftimedayshort'));
197+
$due = $this->datetime->diff($this->clock->now());
198+
$relative = null;
199+
if ($this->userelatives) {
200+
$relative = $this->format_relative_date();
201+
}
202+
203+
if ($this->timeonly) {
204+
$date = null;
205+
} else {
206+
$date = $relative ?? $userdate;
207+
}
208+
$data = [
209+
'timestamp' => $this->datetime->getTimestamp(),
210+
'userdate' => $userdate,
211+
'date' => $date,
212+
'time' => $this->format_time(),
213+
'ispast' => $this->datetime < $this->clock->now(),
214+
'needtitle' => ($relative !== null || $this->timeonly),
215+
'link' => $this->link ? $this->link->out(false) : '',
216+
];
217+
if (($this->near !== null) && ($this->intervalInSeconds($due) < $this->near && $this->intervalInSeconds($due) > 0)) {
218+
$icon = new pix_icon(
219+
pix: 'i/warning',
220+
alt: get_string('warning'),
221+
component: 'moodle',
222+
attributes: ['class' => 'me-0 pb-1']
223+
);
224+
$data['isnear'] = true;
225+
$data['nearicon'] = $icon->export_for_template($output);
226+
}
227+
return $data;
228+
}
229+
230+
/**
231+
* Converts a DateInterval object to total seconds.
232+
*
233+
* @param \DateInterval $interval The interval to convert.
234+
* @return int The total number of seconds.
235+
*/
236+
private function intervalInSeconds(DateInterval $interval): int {
237+
return $interval->s
238+
+ $interval->i * 60
239+
+ $interval->h * 3600
240+
+ $interval->d * 86400
241+
+ $interval->m * 2592000 // Assuming 30 days in a month
242+
+ $interval->y * 31536000; // Assuming 365 days in a year
243+
}
244+
245+
/**
246+
* Formats the timestamp as a relative date string (e.g., "Today", "Yesterday", "Tomorrow").
247+
*
248+
* This method compares the given timestamp with the current date and returns a formatted
249+
* string representing the relative date. If the timestamp corresponds to today, yesterday,
250+
* or tomorrow, it returns the appropriate string. Otherwise, it returns null.
251+
*
252+
* @return string|null
253+
*/
254+
private function format_relative_date(): string|null {
255+
$usertimestamp = $this->get_user_date($this->datetime->getTimestamp());
256+
if ($usertimestamp == $this->get_user_date($this->clock->now()->getTimestamp())) {
257+
$format = get_string('strftimerelativetoday', 'langconfig');
258+
} else if ($usertimestamp == $this->get_user_date(strtotime('yesterday', $this->clock->now()->getTimestamp()))) {
259+
$format = get_string('strftimerelativeyesterday', 'langconfig');
260+
} else if ($usertimestamp == $this->get_user_date(strtotime('tomorrow', $this->clock->now()->getTimestamp()))) {
261+
$format = get_string('strftimerelativetomorrow', 'langconfig');
262+
} else {
263+
return null;
264+
}
265+
266+
return userdate($this->datetime->getTimestamp(), $format);
267+
}
268+
269+
/**
270+
* Formats the timestamp as a human readable time.
271+
*
272+
* @param int $timestamp The timestamp to format.
273+
* @param string $format The format to use.
274+
* @return string The formatted date.
275+
*/
276+
private function get_user_date(int $timestamp, string $format = '%Y-%m-%d'): string {
277+
$calendartype = \core_calendar\type_factory::get_calendar_instance();
278+
$timezone = \core_date::get_user_timezone_object();
279+
return $calendartype->timestamp_to_date_string(
280+
time: $timestamp,
281+
format: '%Y-%m-%d',
282+
timezone: $timezone->getName(),
283+
fixday: true,
284+
fixhour: true,
285+
);
286+
}
287+
288+
/**
289+
* Formats the timestamp as a human readable time.
290+
*
291+
* This method compares the given timestamp with the current date and returns a formatted
292+
* string representing the time.
293+
*
294+
* @return string
295+
*/
296+
private function format_time(): string {
297+
global $CFG;
298+
// Ensure calendar constants are loaded.
299+
require_once($CFG->dirroot . '/calendar/lib.php');
300+
301+
$timeformat = get_user_preferences('calendar_timeformat');
302+
if (empty($timeformat)) {
303+
$timeformat = get_config(null, 'calendar_site_timeformat');
304+
}
305+
306+
// Allow language customization of selected time format.
307+
if ($timeformat === CALENDAR_TF_12) {
308+
$timeformat = get_string('strftimetime12', 'langconfig');
309+
} else if ($timeformat === CALENDAR_TF_24) {
310+
$timeformat = get_string('strftimetime24', 'langconfig');
311+
}
312+
313+
if ($timeformat) {
314+
return userdate($this->datetime->getTimestamp(), $timeformat);
315+
}
316+
317+
// Let's use default format.
318+
if ($this->langtimeformat === null) {
319+
$langtimeformat = get_string('strftimetime');
320+
}
321+
322+
return userdate($this->datetime->getTimestamp(), $langtimeformat);
323+
}
324+
}

0 commit comments

Comments
 (0)