Adapt relative date logic from F# version

This commit is contained in:
Daniel J. Summers 2024-06-22 18:13:45 -04:00
parent b759c3494e
commit 3ebb03d470
4 changed files with 92 additions and 39 deletions

View File

@ -526,45 +526,45 @@ let routes = [
GET_HEAD [ route "/" Home.home ] GET_HEAD [ route "/" Home.home ]
subRoute "/components/" [ subRoute "/components/" [
GET_HEAD [ GET_HEAD [
route "journal-items" Components.journalItems route "journal-items" Components.journalItems // done
routef "request/%s/add-notes" Components.addNotes routef "request/%s/add-notes" Components.addNotes
routef "request/%s/item" Components.requestItem routef "request/%s/item" Components.requestItem
routef "request/%s/notes" Components.notes routef "request/%s/notes" Components.notes
routef "request/%s/snooze" Components.snooze routef "request/%s/snooze" Components.snooze
] ]
] ]
GET_HEAD [ route "/docs" Home.docs ] GET_HEAD [ route "/docs" Home.docs ] // done
GET_HEAD [ route "/journal" Journal.journal ] GET_HEAD [ route "/journal" Journal.journal ] // done
subRoute "/legal/" [ subRoute "/legal/" [
GET_HEAD [ GET_HEAD [
route "privacy-policy" Legal.privacyPolicy route "privacy-policy" Legal.privacyPolicy // done
route "terms-of-service" Legal.termsOfService route "terms-of-service" Legal.termsOfService // done
] ]
] ]
subRoute "/request" [ subRoute "/request" [
GET_HEAD [ GET_HEAD [
routef "/%s/edit" Request.edit routef "/%s/edit" Request.edit // done
routef "/%s/full" Request.getFull routef "/%s/full" Request.getFull // done
route "s/active" Request.active route "s/active" Request.active // done
route "s/answered" Request.answered route "s/answered" Request.answered // done
route "s/snoozed" Request.snoozed route "s/snoozed" Request.snoozed
] ]
PATCH [ PATCH [
route "" Request.update route "" Request.update // done
routef "/%s/cancel-snooze" Request.cancelSnooze routef "/%s/cancel-snooze" Request.cancelSnooze
routef "/%s/prayed" Request.prayed routef "/%s/prayed" Request.prayed // done
routef "/%s/show" Request.show routef "/%s/show" Request.show
routef "/%s/snooze" Request.snooze routef "/%s/snooze" Request.snooze
] ]
POST [ POST [
route "" Request.add route "" Request.add // done
routef "/%s/note" Request.addNote routef "/%s/note" Request.addNote
] ]
] ]
subRoute "/user/" [ subRoute "/user/" [
GET_HEAD [ GET_HEAD [
route "log-off" User.logOff route "log-off" User.logOff // done
route "log-on" User.logOn route "log-on" User.logOn // done
] ]
] ]
] ]

View File

@ -2,7 +2,6 @@
namespace MyPrayerJournal; namespace MyPrayerJournal;
use DateTimeImmutable;
use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find, Mapper\DocumentMapper}; use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find, Mapper\DocumentMapper};
use Exception; use Exception;
use JsonSerializable; use JsonSerializable;

View File

@ -6,6 +6,7 @@ use BitBadger\PDODocument\DocumentException;
use BitBadger\PDODocument\DocumentList; use BitBadger\PDODocument\DocumentList;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Exception;
/** /**
* User interface building blocks * User interface building blocks
@ -103,33 +104,69 @@ class UI
foreach ($attrs as $key => $value) echo " $key=\"" . htmlspecialchars($value) . "\""; ?>><?=$text?></a><?php foreach ($attrs as $key => $value) echo " $key=\"" . htmlspecialchars($value) . "\""; ?>><?=$text?></a><?php
} }
// Thanks, William Entriken! https://stackoverflow.com/a/42446994/276707
/** @var array Periods for the relative date/time display */
private static array $periods = [
[ 60, 1, '%s seconds ago', 'a second ago'],
[ 60*100, 60, '%s minutes ago', 'one minute ago'],
[ 3600*70, 3600, '%s hours ago', 'an hour ago'],
[ 3600*24*10, 3600*24, '%s days ago', 'yesterday'],
[ 3600*24*30, 3600*24*7, '%s weeks ago', 'a week ago'],
[3600*24*30*30, 3600*24*30, '%s months ago', 'last month'],
[ INF, 3600*24*265, '%s years ago', 'last year']
];
/** /**
* @throws \Exception * @throws Exception
*/ */
public static function relativeDate(string $date): void public static function relativeDate(string $date): void
{ {
$parsed = new DateTimeImmutable($date); $parsed = new DateTimeImmutable($date);
$diff = time() - $parsed->getTimestamp(); $inZone = $parsed->setTimezone(new DateTimeZone($_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC'));
foreach (self::$periods as $period) { echo '<span title="' . date_format($inZone, 'l, F j, Y \a\t g:ia T') . '">'
if ($diff > $period[0]) continue; . self::formatDistance('now', $parsed) . '</span>';
$diff = floor($diff / $period[1]); }
$value = $diff > 1 ? sprintf($period[2], $diff) : $period[3];
$inZone = $parsed->setTimezone(new DateTimeZone($_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC')); // Many thanks to date-fns (https://date-fns.org) for this logic
echo '<span title="' . date_format($inZone, 'l, F j, Y \a\t g:ia T') . '">' . $value . '</span>'; /**
break; * Format the distance between two dates
} *
* @param string|DateTimeImmutable $from The starting date/time
* @param string|DateTimeImmutable $to The ending date/time
* @return string The distance between two dates
* @throws Exception If date/time objects cannot be created
*/
public static function formatDistance(string|DateTimeImmutable $from, string|DateTimeImmutable $to): string
{
$aDay = 1_440.0;
$almost2Days = 2_520.0;
$aMonth = 43_200.0;
$twoMonths = 86_400.0;
$dtFrom = is_string($from) ? new DateTimeImmutable($from) : $from;
$dtTo = is_string($to) ? new DateTimeImmutable($to) : $to;
$minutes = abs($dtFrom->getTimestamp() - $dtTo->getTimestamp()) / 60;
$months = round($minutes / $aMonth);
$years = round($months / 12);
$typeAndNumber = match (true) {
$minutes < 1.0 => [FormatDistanceToken::LessThanXMinutes, 1],
$minutes < 45.0 => [FormatDistanceToken::XMinutes, round($minutes)],
$minutes < 90.0 => [FormatDistanceToken::AboutXHours, 1],
$minutes < $aDay => [FormatDistanceToken::AboutXHours, round($minutes / 60)],
$minutes < $almost2Days => [FormatDistanceToken::XDays, 1],
$minutes < $aMonth => [FormatDistanceToken::XDays, round($minutes / $aDay)],
$minutes < $twoMonths => [FormatDistanceToken::AboutXMonths, round($minutes / $aMonth)],
$months < 12 => [FormatDistanceToken::XMonths, round($minutes / $aMonth)],
$months % 12 < 3 => [FormatDistanceToken::AboutXYears, $years],
$months % 12 < 9 => [FormatDistanceToken::OverXYears, $years],
default => [FormatDistanceToken::AlmostXYears, $years]
};
$format = match ($typeAndNumber[0]) {
FormatDistanceToken::LessThanXMinutes => ['less than a minute', 'less than %d minutes'],
FormatDistanceToken::XMinutes => ['a minute', '%d minutes'],
FormatDistanceToken::AboutXHours => ['about an hour', 'about %d hours'],
FormatDistanceToken::XHours => ['an hour', '%d hours'],
FormatDistanceToken::XDays => ['a day', '%d days'],
FormatDistanceToken::AboutXWeeks => ['about a week', 'about %d weeks'],
FormatDistanceToken::XWeeks => ['a week', '%d weeks'],
FormatDistanceToken::AboutXMonths => ['about a month', 'about %d months'],
FormatDistanceToken::XMonths => ['a month', '%d months'],
FormatDistanceToken::AboutXYears => ['about a year', 'about %d years'],
FormatDistanceToken::XYears => ['a year', '%d years'],
FormatDistanceToken::OverXYears => ['over a year', 'over %d years'],
FormatDistanceToken::AlmostXYears => ['almost a year', 'almost %d years']
};
$value = $typeAndNumber[1] == 1 ? $format[0] : sprintf($format[1], $typeAndNumber[1]);
return $dtFrom > $dtTo ? "$value ago" : "in $value";
} }
public static function requestList(DocumentList $reqs): void public static function requestList(DocumentList $reqs): void
@ -171,3 +208,20 @@ class UI
</div><?php </div><?php
} }
} }
enum FormatDistanceToken
{
case LessThanXMinutes;
case XMinutes;
case AboutXHours;
case XHours;
case XDays;
case AboutXWeeks;
case XWeeks;
case AboutXMonths;
case XMonths;
case AboutXYears;
case XYears;
case OverXYears;
case AlmostXYears;
}

View File

@ -30,8 +30,8 @@ Layout::pageHead('Full Request');?>
<div class=card-body> <div class=card-body>
<h6 class="card-subtitle text-muted mb-2"><?php <h6 class="card-subtitle text-muted mb-2"><?php
if (!is_null($answered)) { ?> if (!is_null($answered)) { ?>
Answered <?=$answered->format('F j, Y')?> (<?php UI::relativeDate($req->history[0]->asOf); ?>) Answered <?=$answered->format('F j, Y')?>
&bull;<?php (<?=UI::formatDistance('now', $req->history[0]->asOf);?>) &bull;<?php
} ?> } ?>
Prayed <?=number_format($prayed)?> times &bull; Open <?=number_format($daysOpen)?> days Prayed <?=number_format($prayed)?> times &bull; Open <?=number_format($daysOpen)?> days
</h6> </h6>