@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class AphrontFormDateControlValue extends Phobject {
4
5 private $valueDate;
6 private $valueTime;
7 private $valueEnabled;
8
9 private $viewer;
10 private $zone;
11 private $optional;
12
13 public function getValueDate() {
14 return $this->valueDate;
15 }
16
17 public function getValueTime() {
18 return $this->valueTime;
19 }
20
21 public function isValid() {
22 if ($this->isDisabled()) {
23 return true;
24 }
25 return ($this->getEpoch() !== null);
26 }
27
28 public function isEmpty() {
29 if ($this->valueDate) {
30 return false;
31 }
32
33 if ($this->valueTime) {
34 return false;
35 }
36
37 return true;
38 }
39
40 public function isDisabled() {
41 return ($this->optional && !$this->valueEnabled);
42 }
43
44 public function setEnabled($enabled) {
45 $this->valueEnabled = $enabled;
46 return $this;
47 }
48
49 public function setOptional($optional) {
50 $this->optional = $optional;
51 return $this;
52 }
53
54 public function getOptional() {
55 return $this->optional;
56 }
57
58 public function getViewer() {
59 return $this->viewer;
60 }
61
62 public static function newFromRequest(AphrontRequest $request, $key) {
63 $value = new AphrontFormDateControlValue();
64 $value->viewer = $request->getViewer();
65
66 $date = $request->getStr($key.'_d', '');
67 $time = $request->getStr($key.'_t', '');
68
69 // If we have the individual parts, we read them preferentially. If we do
70 // not, try to read the key as a raw value. This makes it so that HTTP
71 // prefilling is overwritten by the control value if the user changes it.
72 if (!strlen($date) && !strlen($time)) {
73 $date = $request->getStr($key);
74 $time = null;
75 }
76
77 $value->valueDate = $date;
78 $value->valueTime = $time;
79
80 $formatted = $value->getFormattedDateFromDate(
81 $value->valueDate,
82 $value->valueTime);
83
84 if ($formatted) {
85 list($value->valueDate, $value->valueTime) = $formatted;
86 }
87
88 $value->valueEnabled = $request->getStr($key.'_e');
89 return $value;
90 }
91
92 public static function newFromEpoch(PhabricatorUser $viewer, $epoch) {
93 $value = new AphrontFormDateControlValue();
94 $value->viewer = $viewer;
95
96 if (!$epoch) {
97 return $value;
98 }
99
100 $readable = $value->formatTime($epoch, 'Y!m!d!g:i A');
101 $readable = explode('!', $readable, 4);
102
103 $year = $readable[0];
104 $month = $readable[1];
105 $day = $readable[2];
106 $time = $readable[3];
107
108 list($value->valueDate, $value->valueTime) =
109 $value->getFormattedDateFromParts(
110 $year,
111 $month,
112 $day,
113 $time);
114
115 return $value;
116 }
117
118 public static function newFromDictionary(
119 PhabricatorUser $viewer,
120 array $dictionary) {
121 $value = new AphrontFormDateControlValue();
122 $value->viewer = $viewer;
123
124 $value->valueDate = idx($dictionary, 'd');
125 $value->valueTime = idx($dictionary, 't');
126
127 $formatted = $value->getFormattedDateFromDate(
128 $value->valueDate,
129 $value->valueTime);
130
131 if ($formatted) {
132 list($value->valueDate, $value->valueTime) = $formatted;
133 }
134
135 $value->valueEnabled = idx($dictionary, 'e');
136
137 return $value;
138 }
139
140 public static function newFromWild(PhabricatorUser $viewer, $wild) {
141 if (is_array($wild)) {
142 return self::newFromDictionary($viewer, $wild);
143 } else if (is_numeric($wild)) {
144 return self::newFromEpoch($viewer, $wild);
145 } else {
146 throw new Exception(
147 pht(
148 'Unable to construct a date value from value of type "%s".',
149 gettype($wild)));
150 }
151 }
152
153 public function getDictionary() {
154 return array(
155 'd' => $this->valueDate,
156 't' => $this->valueTime,
157 'e' => $this->valueEnabled,
158 );
159 }
160
161 public function getValueAsFormat($format) {
162 return phabricator_format_local_time(
163 $this->getEpoch(),
164 $this->viewer,
165 $format);
166 }
167
168 private function formatTime($epoch, $format) {
169 $date = phorge_localize_time($epoch, $this->viewer);
170 if (!$date) {
171 return '';
172 }
173 // Call DateTime->format directly (bypassing PhutilTranslator)
174 // so that getFormattedDateFromParts below can decode the parts
175 // back into a DateTime
176 return $date->format($format);
177 }
178
179 public function getEpoch() {
180 if ($this->isDisabled()) {
181 return null;
182 }
183
184 $datetime = $this->newDateTime($this->valueDate, $this->valueTime);
185 if (!$datetime) {
186 return null;
187 }
188
189 return (int)$datetime->format('U');
190 }
191
192 private function getTimeFormat() {
193 $viewer = $this->getViewer();
194 $time_key = PhabricatorTimeFormatSetting::SETTINGKEY;
195 return $viewer->getUserSetting($time_key);
196 }
197
198 private function getDateFormat() {
199 $viewer = $this->getViewer();
200 $date_key = PhabricatorDateFormatSetting::SETTINGKEY;
201 return $viewer->getUserSetting($date_key);
202 }
203
204 private function getFormattedDateFromDate($date, $time) {
205 $datetime = $this->newDateTime($date, $time);
206 if (!$datetime) {
207 return null;
208 }
209
210 return array(
211 $datetime->format($this->getDateFormat()),
212 $datetime->format($this->getTimeFormat()),
213 );
214 }
215
216 /**
217 * Create a DateTime object including timezone
218 * @param string $date Date, like "2024-08-20" or "2024-07-1" or such
219 * @param string|null $time Time, like "12:00 AM" or such
220 * @return DateTime|null
221 */
222 private function newDateTime($date, $time) {
223 $date = $this->getStandardDateFormat($date);
224 $time = $this->getStandardTimeFormat($time);
225
226 try {
227 // We need to provide the timezone in the constructor, and also set it
228 // explicitly. If the date is an epoch timestamp, the timezone in the
229 // constructor is ignored. If the date is not an epoch timestamp, it is
230 // used to parse the date.
231 $zone = $this->getTimezone();
232 $datetime = new DateTime("{$date} {$time}", $zone);
233 $datetime->setTimezone($zone);
234 } catch (Exception $ex) {
235 return null;
236 }
237
238
239 return $datetime;
240 }
241
242 public function newPhutilDateTime() {
243 $datetime = $this->getDateTime();
244 if (!$datetime) {
245 return null;
246 }
247
248 $all_day = !strlen($this->valueTime);
249 $zone_identifier = $this->viewer->getTimezoneIdentifier();
250
251 $result = id(new PhutilCalendarAbsoluteDateTime())
252 ->setYear((int)$datetime->format('Y'))
253 ->setMonth((int)$datetime->format('m'))
254 ->setDay((int)$datetime->format('d'))
255 ->setHour((int)$datetime->format('G'))
256 ->setMinute((int)$datetime->format('i'))
257 ->setSecond((int)$datetime->format('s'))
258 ->setTimezone($zone_identifier);
259
260 if ($all_day) {
261 $result->setIsAllDay(true);
262 }
263
264 return $result;
265 }
266
267
268 private function getFormattedDateFromParts(
269 $year,
270 $month,
271 $day,
272 $time) {
273
274 $zone = $this->getTimezone();
275 $date_time = new DateTime("{$year}-{$month}-{$day} {$time}", $zone);
276
277 return array(
278 $date_time->format($this->getDateFormat()),
279 $date_time->format($this->getTimeFormat()),
280 );
281 }
282
283 private function getFormatSeparator() {
284 $format = $this->getDateFormat();
285 switch ($format) {
286 case 'n/j/Y':
287 return '/';
288 default:
289 return '-';
290 }
291 }
292
293 /**
294 * @return DateTime|null
295 */
296 public function getDateTime() {
297 return $this->newDateTime($this->valueDate, $this->valueTime);
298 }
299
300 /**
301 * @return DateTimeZone
302 */
303 private function getTimezone() {
304 if ($this->zone) {
305 return $this->zone;
306 }
307
308 $viewer_zone = $this->viewer->getTimezoneIdentifier();
309 $this->zone = new DateTimeZone($viewer_zone);
310 return $this->zone;
311 }
312
313 private function getStandardDateFormat($date) {
314 // no value entered into the field at all
315 if (!$date) {
316 return null;
317 }
318 $colloquial = array(
319 'newyear' => 'January 1',
320 'valentine' => 'February 14',
321 'pi' => 'March 14',
322 'christma' => 'December 25',
323 );
324
325 // Lowercase the input, then remove punctuation, a "day" suffix, and an
326 // "s" if one is present. This allows all of these to match. This allows
327 // variations like "New Year's Day" and "New Year" to both match.
328 $normalized = phutil_utf8_strtolower($date);
329 $normalized = preg_replace('/[^a-z]/', '', $normalized);
330 $normalized = preg_replace('/day\z/', '', $normalized);
331 $normalized = preg_replace('/s\z/', '', $normalized);
332
333 if ($normalized !== null && isset($colloquial[$normalized])) {
334 return $colloquial[$normalized];
335 }
336
337 // If this looks like an epoch timestamp, prefix it with "@" so that
338 // DateTime() reads it as one. Assume small numbers are a "Ymd" digit
339 // string instead of an epoch timestamp for a time in 1970.
340 if (ctype_digit($date) && ($date > 30000000)) {
341 $date = '@'.$date;
342 }
343
344 $separator = $this->getFormatSeparator();
345 $parts = preg_split('@[,./:-]@', $date);
346 return implode($separator, $parts);
347 }
348
349 private function getStandardTimeFormat($time) {
350 $colloquial = array(
351 'crack of dawn' => '5:00 AM',
352 'dawn' => '6:00 AM',
353 'early' => '7:00 AM',
354 'morning' => '8:00 AM',
355 'elevenses' => '11:00 AM',
356 'morning tea' => '11:00 AM',
357 'noon' => '12:00 PM',
358 'high noon' => '12:00 PM',
359 'lunch' => '12:00 PM',
360 'afternoon' => '2:00 PM',
361 'tea time' => '3:00 PM',
362 'evening' => '7:00 PM',
363 'late' => '11:00 PM',
364 'witching hour' => '12:00 AM',
365 'midnight' => '12:00 AM',
366 );
367
368 $normalized = phutil_utf8_strtolower($time);
369 if (isset($colloquial[$normalized])) {
370 $time = $colloquial[$normalized];
371 }
372 // Convert localized times to English
373 $am = pht('AM');
374 if ($am !== 'AM') {
375 $time = preg_replace('/'.preg_quote($am).'$/', 'AM', $time);
376 }
377 $pm = pht('PM');
378 if ($pm !== 'PM') {
379 $time = preg_replace('/'.preg_quote($pm).'$/', 'PM', $time);
380 }
381 return $time;
382 }
383
384}