date_ap_style-8.x-1.x-dev/src/ApStyleDateFormatter.php
src/ApStyleDateFormatter.php
<?php
declare(strict_types=1);
namespace Drupal\date_ap_style;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Services for formatting date types using AP Style Date rules.
*/
class ApStyleDateFormatter {
use StringTranslationTrait;
/**
* Configuration object for AP Style settings.
*/
protected ImmutableConfig $config;
/**
* Constructor.
*/
public function __construct(
protected LanguageManagerInterface $languageManager,
protected ConfigFactoryInterface $configFactory,
) {
$this->config = $configFactory->get('date_ap_style.settings');
}
/**
* Get base default options from config when none are provided.
*
* @return array<string,bool>
* The default config settings.
*/
private function getConfigOptions(): array {
return [
'always_display_year' => $this->config->get('always_display_year'),
'display_day' => $this->config->get('display_day'),
'use_today' => $this->config->get('use_today'),
'cap_today' => $this->config->get('cap_today'),
'display_time' => $this->config->get('display_time'),
'hide_date' => $this->config->get('hide_date'),
'time_before_date' => $this->config->get('time_before_date'),
'use_all_day' => $this->config->get('use_all_day'),
'display_noon_and_midnight' => $this->config->get('display_noon_and_midnight'),
'capitalize_noon_and_midnight' => $this->config->get('capitalize_noon_and_midnight'),
'timezone' => $this->config->get('timezone'),
'month_only' => $this->config->get('month_only'),
'separator' => $this->config->get('separator'),
];
}
/**
* Separator options for date ranges.
*
* @return array<string,string>
* The available separator options.
*/
private function getSeparatorOptions(): array {
return [
'to' => ' to ',
'endash' => ' – ',
'hyphen' => '-',
];
}
/**
* Gets a default options array to set base option keys.
*
* @return array<string,bool>
* The default options when no config is set.
*/
private function getDefaultOptions(): array {
return [
'always_display_year' => FALSE,
'display_day' => FALSE,
'use_today' => FALSE,
'cap_today' => FALSE,
'display_time' => FALSE,
'hide_date' => FALSE,
'time_before_date' => FALSE,
'use_all_day' => FALSE,
'display_noon_and_midnight' => FALSE,
'capitalize_noon_and_midnight' => FALSE,
'timezone' => '',
'month_only' => FALSE,
'separator' => 'to',
];
}
/**
* Gets the merged option set.
*
* Merges user-specified options with default key values. If no user options
* are provided, retrieves the default configuration options.
*/
private function getOptions(array $options): array {
if (empty($options)) {
return $this->getConfigOptions();
}
else {
$defaults = $this->getDefaultOptions();
return array_merge($defaults, $options);
}
}
/**
* Format a timestamp to an AP style date format.
*
* @param int $timestamp
* The timestamp to convert.
* @param array<string,bool> $options
* An array of options that affect how the date string is formatted.
* @param \DateTimeZone|string|null $timezone
* \DateTimeZone object, time zone string or NULL. NULL uses the
* default system time zone. Defaults to NULL.
* @param string|null $langcode
* The language code.
* @param string|null $fieldtype
* Type of field. Example smartdate.
*
* @return string
* The formatted date string.
*/
public function formatTimestamp(int $timestamp, array $options = [], \DateTimeZone|string|null $timezone = NULL, ?string $langcode = NULL, ?string $fieldtype = NULL): string {
$options = $this->getOptions($options);
if (empty($langcode)) {
$langcode = $this->languageManager->getCurrentLanguage()->getId();
}
// If no timezone is specified, use the user's if available, or the site
// or system default.
if (empty($timezone)) {
$timezone = date_default_timezone_get();
}
// Create a DrupalDateTime object from the timestamp and timezone.
$datetime_settings = [
'langcode' => $langcode,
];
$date_string = '';
$format_date = '';
// Create a DrupalDateTime object from the timestamp and timezone.
$date = DrupalDateTime::createFromTimestamp($timestamp, $timezone, $datetime_settings);
$now = new DrupalDateTime('now', $timezone, $datetime_settings);
if ($options['month_only']) {
$format_date .= $this->formatMonth($date);
}
else {
if ($options['use_today'] && $date->format('Y-m-d') == $now->format('Y-m-d')) {
$date_string = $this->t('today');
if ($options['cap_today']) {
$date_string = ucfirst((string) $date_string);
}
}
// Determine if the date is within the current week and set final output.
elseif ($options['display_day'] && $date->format('W o') == $now->format('W o')) {
$format_date .= 'l';
}
else {
$format_date .= $this->formatMonth($date) . ' j';
$format_date .= $this->formatYear($date, $now, $options['always_display_year']);
}
}
$date_string .= $date->format($format_date);
if ($options['display_time'] && !$options['month_only']) {
switch ($date->format('H:i')) {
case '00:00':
if ($options['use_all_day']) {
$ap_time_string = $this->t('All Day');
}
else {
$ap_time_string = $options['display_noon_and_midnight'] ? $this->t('midnight') : $date->format('g a');
if ($options['display_noon_and_midnight'] && $options['capitalize_noon_and_midnight']) {
$ap_time_string = ucfirst((string) $ap_time_string);
}
}
break;
case '12:00':
$ap_time_string = $options['display_noon_and_midnight'] ? $this->t('noon') : $date->format('g a');
if ($options['display_noon_and_midnight'] && $options['capitalize_noon_and_midnight']) {
$ap_time_string = ucfirst((string) $ap_time_string);
}
break;
default:
if ($date->format('i') === '00') {
// Don't display the minutes if it's the top of the hour.
$ap_time_string = $date->format('g a');
}
else {
$ap_time_string = $date->format('g:i a');
}
break;
}
// Format the meridian if it's there.
$ap_time_string = str_replace(['am', 'pm'], ['a.m.', 'p.m.'], (string) $ap_time_string);
if ($options['hide_date']) {
$output = $ap_time_string;
}
elseif ($options['time_before_date']) {
$output = $ap_time_string . ', ' . $date_string;
}
else {
$output = $date_string . ', ' . $ap_time_string;
}
}
else {
$output = $date_string;
}
return $output;
}
/**
* Format a timestamp to an AP style date format.
*
* @param string[] $timestamps
* The start and end timestamps to convert.
* @param array<string,bool> $options
* An array of options that affect how the date string is formatted.
* @param \DateTimeZone|string|null $timezone
* \DateTimeZone object, time zone string or NULL. NULL uses the
* default system time zone. Defaults to NULL.
* @param string|null $langcode
* The language code.
* @param string|null $fieldtype
* Type of field. Example smartdate.
*
* @return string
* The formatted date string.
*/
public function formatRange(array $timestamps, array $options = [], \DateTimeZone|string|null $timezone = NULL, ?string $langcode = NULL, ?string $fieldtype = NULL): string {
if (empty($timestamps)) {
return '';
}
$options = $this->getOptions($options);
$separators = $this->getSeparatorOptions();
$separator = $separators[$options['separator']] ?? ' to ';
$normalized_timestamps = $this->getTimeStamps($timestamps, $timezone, $langcode);
/** @var \Drupal\Core\Datetime\DrupalDateTime $start_stamp */
$start_stamp = $normalized_timestamps['start_stamp'];
/** @var \Drupal\Core\Datetime\DrupalDateTime $end_stamp */
$end_stamp = $normalized_timestamps['end_stamp'];
/** @var \Drupal\Core\Datetime\DrupalDateTime $now */
$now = $normalized_timestamps['now'];
$format_start_date = $format_end_date = $start_date_string = $end_date_string = '';
// Check if the start date is today.
$is_start_today = ($options['use_today'] ?? FALSE) && $start_stamp->format('Y-m-d') == $now->format('Y-m-d');
// Check if the end date is today.
$is_end_today = ($options['use_today'] ?? FALSE) && $end_stamp->format('Y-m-d') == $now->format('Y-m-d');
if ($is_start_today && $is_end_today) {
$date_output = $this->t('today');
if ($options['cap_today'] ?? FALSE) {
$date_output = ucfirst((string) $date_output);
}
}
elseif ($options['month_only']) {
if ($start_stamp->format('Y-m') == $end_stamp->format('Y-m')) {
// Same month and year - combine into single output.
$format_start_date = $this->formatMonth($start_stamp);
// Append year to the start date instead of using end_date.
$format_start_date .= str_replace(',', '', $this->formatYear($end_stamp, $now, $options['always_display_year']));
// Clear end date to prevent connector from being added.
$format_end_date = '';
}
elseif ($start_stamp->format('Y') == $end_stamp->format('Y')) {
// Different months, same year.
$format_start_date = $this->formatMonth($start_stamp);
$format_end_date = $this->formatMonth($end_stamp);
$format_end_date .= str_replace(',', '', $this->formatYear($end_stamp, $now, $options['always_display_year']));
}
else {
// Different years.
$format_start_date = $this->formatMonth($start_stamp);
$format_start_date .= str_replace(',', '', $this->formatYear($start_stamp, $now, $options['always_display_year']));
$format_end_date = $this->formatMonth($end_stamp);
$format_end_date .= str_replace(',', '', $this->formatYear($end_stamp, $now, $options['always_display_year']));
}
}
elseif ($start_stamp->format('Y-m-d') == $end_stamp->format('Y-m-d')) {
// The Y-M-D is identical.
if ($options['display_day'] && $start_stamp->format('W o') == $now->format('W o')) {
$format_start_date .= 'l, ';
}
$format_start_date .= $this->formatMonth($start_stamp) . ' j';
// Display Y if not equal to current year or option set to always show
// year.
$format_start_date .= $this->formatYear($start_stamp, $now, $options['always_display_year']);
}
elseif ($start_stamp->format('Y-m') == $end_stamp->format('Y-m')) {
// The Y-M is identical, but different D.
$format_start_date = $this->formatMonth($start_stamp) . ' j';
$format_end_date = 'j';
// Display Y if end_time year not equal to current year.
$format_end_date .= $this->formatYear($end_stamp, $now, $options['always_display_year']);
}
elseif ($start_stamp->format('Y') == $end_stamp->format('Y')) {
// The Y is identical, but different M-D.
$format_start_date = $this->formatMonth($start_stamp) . ' j';
$format_end_date = $this->formatMonth($end_stamp) . ' j';
// Display Y if end_time year not equal to current year.
$format_end_date .= $this->formatYear($end_stamp, $now, $options['always_display_year']);
}
elseif ($start_stamp->format('m-d') == $end_stamp->format('m-d')) {
// The M-D is identical, but different Y.
$format_start_date = $this->formatMonth($start_stamp) . ' j, Y';
$format_end_date = 'Y';
}
else {
// All three are different or only D is identical.
$format_start_date = $this->formatMonth($start_stamp) . ' j, Y';
$format_end_date = $this->formatMonth($end_stamp) . ' j, Y';
}
$date_output = $date_output ?? $start_stamp->format($format_start_date);
if (!empty($format_end_date)) {
$date_output .= $separator . $end_stamp->format($format_end_date);
}
if ($options['display_time']) {
$time_output = $this->getTimeOutput($normalized_timestamps, $options, $fieldtype);
if ($options['hide_date']) {
$output = $time_output;
}
elseif (!empty($time_output) && $options['time_before_date']) {
$output = $time_output . ', ' . $date_output;
}
elseif (!empty($time_output)) {
$output = $date_output . ', ' . $time_output;
}
else {
$output = $date_output;
}
}
else {
$output = $date_output;
}
return $output;
}
/**
* Return month format code based on AP Style rules.
*
* @param \Drupal\Core\Datetime\DrupalDateTime $date
* Drupal date object.
*
* @return string
* Date format string.
*/
private function formatMonth(DrupalDateTime $date): string {
return match ($date->format('m')) {
// Short months get the full print out of their name.
'03', '04', '05', '06', '07' => 'F',
// September is abbreviated to 'Sep' by PHP, but we want 'Sept'.
'09' => 'M\t.',
// Other months get an abbreviated print-out followed by a period.
default => 'M.',
};
}
/**
* Check if year is needed in format and provide format value.
*
* @param \Drupal\Core\Datetime\DrupalDateTime $time_stamp
* The date to check.
* @param \Drupal\Core\Datetime\DrupalDateTime $now
* The current date.
* @param bool $always_display_year
* The option to display year when current year.
*
* @return string
* The format value.
*/
protected function formatYear(DrupalDateTime $time_stamp, DrupalDateTime $now, ?bool $always_display_year = FALSE): string {
$format_year = '';
if ($always_display_year || $time_stamp->format('Y') != $now->format('Y')) {
$format_year = ', Y';
}
return $format_year;
}
/**
* Create an array of Drupal DateTimes.
*
* @param string[] $timestamps
* The start and end timestamps to convert.
* @param \DateTimeZone|string|null $timezone
* \DateTimeZone object, time zone string or NULL. NULL uses the
* default system time zone. Defaults to NULL.
* @param string|null $langcode
* The language code.
*
* @return string[]
* Array with Drupal DateTimes.
* - start_stamp
* - end_stamp
* - now
*/
private function getTimeStamps(array $timestamps = [], \DateTimeZone|string|null $timezone = NULL, ?string $langcode = NULL): array {
if (empty($langcode)) {
$langcode = $this->languageManager->getCurrentLanguage()->getId();
}
// Create a DrupalDateTime object from the timestamp and timezone.
$datetime_settings = [
'langcode' => $langcode,
];
// If no timezone is specified, use the user's if available, or the site
// or system default.
if (empty($timezone)) {
$timezone = date_default_timezone_get();
}
// Create a DrupalDateTime object from the timestamp and timezone.
$start_stamp = DrupalDateTime::createFromTimestamp($timestamps['start'], $timezone, $datetime_settings);
$now = new DrupalDateTime('now', $timezone, $datetime_settings);
if (!empty($timestamps['end']) || $timestamps['end'] != 0) {
$end_stamp = DrupalDateTime::createFromTimestamp($timestamps['end'], $timezone, $datetime_settings);
}
else {
$end_stamp = $start_stamp;
}
return [
'start_stamp' => $start_stamp,
'end_stamp' => $end_stamp,
'now' => $now,
];
}
/**
* Format a timestamp to an AP style time format.
*
* @param string[] $timestamps
* The start and end timestamps to convert.
* @param array<string,bool> $options
* An array of options that affect how the date string is formatted.
* @param string|null $fieldtype
* Type of field. Example smartdate.
*
* @return string
* The formatted time string.
*/
private function getTimeOutput(array $timestamps = [], array $options = [], ?string $fieldtype = NULL): string {
/** @var \Drupal\Core\Datetime\DrupalDateTime $start_stamp */
$start_stamp = $timestamps['start_stamp'];
/** @var \Drupal\Core\Datetime\DrupalDateTime $end_stamp */
$end_stamp = $timestamps['end_stamp'];
$time_end = $time_start_string = $time_end_string = $time_output = '';
$options = $this->getOptions($options);
$separators = $this->getSeparatorOptions();
$separator = $separators[$options['separator']] ?? ' to ';
switch ($fieldtype) {
case 'smartdate':
if ($options['use_all_day']) {
if ($start_stamp->format('H:i') === '00:00' && $end_stamp->format('H:i') === '23:59') {
return (string) $this->t('All Day');
}
}
break;
default:
if ($options['use_all_day']) {
if ($start_stamp->format('H:i') == '00:00' || $start_stamp->format('gia') == $end_stamp->format('gia')) {
return (string) $this->t('All Day');
}
}
break;
}
// Don't display the minutes if it's the top of the hour.
$time_start = $start_stamp->format('i') == '00' ? 'g' : 'g:i';
// If same day, same start/end meridians and different start/end time,
// don't include meridian in start.
$time_start .= ($start_stamp->format('a') == $end_stamp->format('a') && $start_stamp->format('gia') != $end_stamp->format('gia') && $end_stamp->format('Y-m-d') === $start_stamp->format('Y-m-d') ? '' : ' a');
// Set preformatted start and end times based on.
// Replace 12:00 am with Midnight & 12:00 pm with Noon.
switch ($start_stamp->format('H:i')) {
case '00:00':
$time_start_string = $options['display_noon_and_midnight'] ? $this->t('midnight') : $start_stamp->format('g a');
if ($options['display_noon_and_midnight'] && $options['capitalize_noon_and_midnight']) {
$time_start_string = ucfirst((string) $time_start_string);
}
break;
case '12:00':
$time_start_string = $options['display_noon_and_midnight'] ? $this->t('noon') : $start_stamp->format('g a');
if ($options['display_noon_and_midnight'] && $options['capitalize_noon_and_midnight']) {
$time_start_string = ucfirst((string) $time_start_string);
}
break;
}
if ($start_stamp->format('Hi') != $end_stamp->format('Hi')) {
$time_end = $end_stamp->format('i') == '00' ? 'g a' : 'g:i a';
switch ($end_stamp->format('H:i')) {
case '00:00':
$time_end_string = $options['display_noon_and_midnight'] ? $this->t('midnight') : $end_stamp->format('g a');
if ($options['display_noon_and_midnight'] && $options['capitalize_noon_and_midnight']) {
$time_end_string = ucfirst((string) $time_end_string);
}
break;
case '12:00':
$time_end_string = $options['display_noon_and_midnight'] ? $this->t('noon') : $end_stamp->format('g a');
if ($options['display_noon_and_midnight'] && $options['capitalize_noon_and_midnight']) {
$time_end_string = ucfirst((string) $time_end_string);
}
break;
}
}
$time_output .= $time_start_string ?: $start_stamp->format($time_start);
if (!empty($time_end)) {
$time_output .= $separator;
$time_output .= $time_end_string ?: $end_stamp->format($time_end);
}
return str_replace(['am', 'pm'], ['a.m.', 'p.m.'], $time_output);
}
}
