Skip to content

Commit b64dd41

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

File tree

8 files changed

+1119
-0
lines changed

8 files changed

+1119
-0
lines changed

calendar/classes/output/humandate.php

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

0 commit comments

Comments
 (0)