Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / view / form / control / AphrontFormDateControlValue.php
blob873ad44fe4b3412e1755a7dd2401db036d1e37fd
1 <?php
3 final class AphrontFormDateControlValue extends Phobject {
5 private $valueDate;
6 private $valueTime;
7 private $valueEnabled;
9 private $viewer;
10 private $zone;
11 private $optional;
13 public function getValueDate() {
14 return $this->valueDate;
17 public function getValueTime() {
18 return $this->valueTime;
21 public function isValid() {
22 if ($this->isDisabled()) {
23 return true;
25 return ($this->getEpoch() !== null);
28 public function isEmpty() {
29 if ($this->valueDate) {
30 return false;
33 if ($this->valueTime) {
34 return false;
37 return true;
40 public function isDisabled() {
41 return ($this->optional && !$this->valueEnabled);
44 public function setEnabled($enabled) {
45 $this->valueEnabled = $enabled;
46 return $this;
49 public function setOptional($optional) {
50 $this->optional = $optional;
51 return $this;
54 public function getOptional() {
55 return $this->optional;
58 public function getViewer() {
59 return $this->viewer;
62 public static function newFromRequest(AphrontRequest $request, $key) {
63 $value = new AphrontFormDateControlValue();
64 $value->viewer = $request->getViewer();
66 $date = $request->getStr($key.'_d');
67 $time = $request->getStr($key.'_t');
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;
77 $value->valueDate = $date;
78 $value->valueTime = $time;
80 $formatted = $value->getFormattedDateFromDate(
81 $value->valueDate,
82 $value->valueTime);
84 if ($formatted) {
85 list($value->valueDate, $value->valueTime) = $formatted;
88 $value->valueEnabled = $request->getStr($key.'_e');
89 return $value;
92 public static function newFromEpoch(PhabricatorUser $viewer, $epoch) {
93 $value = new AphrontFormDateControlValue();
94 $value->viewer = $viewer;
96 if (!$epoch) {
97 return $value;
100 $readable = $value->formatTime($epoch, 'Y!m!d!g:i A');
101 $readable = explode('!', $readable, 4);
103 $year = $readable[0];
104 $month = $readable[1];
105 $day = $readable[2];
106 $time = $readable[3];
108 list($value->valueDate, $value->valueTime) =
109 $value->getFormattedDateFromParts(
110 $year,
111 $month,
112 $day,
113 $time);
115 return $value;
118 public static function newFromDictionary(
119 PhabricatorUser $viewer,
120 array $dictionary) {
121 $value = new AphrontFormDateControlValue();
122 $value->viewer = $viewer;
124 $value->valueDate = idx($dictionary, 'd');
125 $value->valueTime = idx($dictionary, 't');
127 $formatted = $value->getFormattedDateFromDate(
128 $value->valueDate,
129 $value->valueTime);
131 if ($formatted) {
132 list($value->valueDate, $value->valueTime) = $formatted;
135 $value->valueEnabled = idx($dictionary, 'e');
137 return $value;
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)));
153 public function getDictionary() {
154 return array(
155 'd' => $this->valueDate,
156 't' => $this->valueTime,
157 'e' => $this->valueEnabled,
161 public function getValueAsFormat($format) {
162 return phabricator_format_local_time(
163 $this->getEpoch(),
164 $this->viewer,
165 $format);
168 private function formatTime($epoch, $format) {
169 return phabricator_format_local_time(
170 $epoch,
171 $this->viewer,
172 $format);
175 public function getEpoch() {
176 if ($this->isDisabled()) {
177 return null;
180 $datetime = $this->newDateTime($this->valueDate, $this->valueTime);
181 if (!$datetime) {
182 return null;
185 return (int)$datetime->format('U');
188 private function getTimeFormat() {
189 $viewer = $this->getViewer();
190 $time_key = PhabricatorTimeFormatSetting::SETTINGKEY;
191 return $viewer->getUserSetting($time_key);
194 private function getDateFormat() {
195 $viewer = $this->getViewer();
196 $date_key = PhabricatorDateFormatSetting::SETTINGKEY;
197 return $viewer->getUserSetting($date_key);
200 private function getFormattedDateFromDate($date, $time) {
201 $datetime = $this->newDateTime($date, $time);
202 if (!$datetime) {
203 return null;
206 return array(
207 $datetime->format($this->getDateFormat()),
208 $datetime->format($this->getTimeFormat()),
211 return array($date, $time);
214 private function newDateTime($date, $time) {
215 $date = $this->getStandardDateFormat($date);
216 $time = $this->getStandardTimeFormat($time);
218 try {
219 // We need to provide the timezone in the constructor, and also set it
220 // explicitly. If the date is an epoch timestamp, the timezone in the
221 // constructor is ignored. If the date is not an epoch timestamp, it is
222 // used to parse the date.
223 $zone = $this->getTimezone();
224 $datetime = new DateTime("{$date} {$time}", $zone);
225 $datetime->setTimezone($zone);
226 } catch (Exception $ex) {
227 return null;
231 return $datetime;
234 public function newPhutilDateTime() {
235 $datetime = $this->getDateTime();
236 if (!$datetime) {
237 return null;
240 $all_day = !strlen($this->valueTime);
241 $zone_identifier = $this->viewer->getTimezoneIdentifier();
243 $result = id(new PhutilCalendarAbsoluteDateTime())
244 ->setYear((int)$datetime->format('Y'))
245 ->setMonth((int)$datetime->format('m'))
246 ->setDay((int)$datetime->format('d'))
247 ->setHour((int)$datetime->format('G'))
248 ->setMinute((int)$datetime->format('i'))
249 ->setSecond((int)$datetime->format('s'))
250 ->setTimezone($zone_identifier);
252 if ($all_day) {
253 $result->setIsAllDay(true);
256 return $result;
260 private function getFormattedDateFromParts(
261 $year,
262 $month,
263 $day,
264 $time) {
266 $zone = $this->getTimezone();
267 $date_time = id(new DateTime("{$year}-{$month}-{$day} {$time}", $zone));
269 return array(
270 $date_time->format($this->getDateFormat()),
271 $date_time->format($this->getTimeFormat()),
275 private function getFormatSeparator() {
276 $format = $this->getDateFormat();
277 switch ($format) {
278 case 'n/j/Y':
279 return '/';
280 default:
281 return '-';
285 public function getDateTime() {
286 return $this->newDateTime($this->valueDate, $this->valueTime);
289 private function getTimezone() {
290 if ($this->zone) {
291 return $this->zone;
294 $viewer_zone = $this->viewer->getTimezoneIdentifier();
295 $this->zone = new DateTimeZone($viewer_zone);
296 return $this->zone;
299 private function getStandardDateFormat($date) {
300 $colloquial = array(
301 'newyear' => 'January 1',
302 'valentine' => 'February 14',
303 'pi' => 'March 14',
304 'christma' => 'December 25',
307 // Lowercase the input, then remove punctuation, a "day" suffix, and an
308 // "s" if one is present. This allows all of these to match. This allows
309 // variations like "New Year's Day" and "New Year" to both match.
310 $normalized = phutil_utf8_strtolower($date);
311 $normalized = preg_replace('/[^a-z]/', '', $normalized);
312 $normalized = preg_replace('/day\z/', '', $normalized);
313 $normalized = preg_replace('/s\z/', '', $normalized);
315 if (isset($colloquial[$normalized])) {
316 return $colloquial[$normalized];
319 // If this looks like an epoch timestamp, prefix it with "@" so that
320 // DateTime() reads it as one. Assume small numbers are a "Ymd" digit
321 // string instead of an epoch timestamp for a time in 1970.
322 if (ctype_digit($date) && ($date > 30000000)) {
323 $date = '@'.$date;
326 $separator = $this->getFormatSeparator();
327 $parts = preg_split('@[,./:-]@', $date);
328 return implode($separator, $parts);
331 private function getStandardTimeFormat($time) {
332 $colloquial = array(
333 'crack of dawn' => '5:00 AM',
334 'dawn' => '6:00 AM',
335 'early' => '7:00 AM',
336 'morning' => '8:00 AM',
337 'elevenses' => '11:00 AM',
338 'morning tea' => '11:00 AM',
339 'noon' => '12:00 PM',
340 'high noon' => '12:00 PM',
341 'lunch' => '12:00 PM',
342 'afternoon' => '2:00 PM',
343 'tea time' => '3:00 PM',
344 'evening' => '7:00 PM',
345 'late' => '11:00 PM',
346 'witching hour' => '12:00 AM',
347 'midnight' => '12:00 AM',
350 $normalized = phutil_utf8_strtolower($time);
351 if (isset($colloquial[$normalized])) {
352 $time = $colloquial[$normalized];
355 return $time;