Format Datetime
Overview
Format a preset-based date, time, or date range from custom fields. The default preset is event, but the config at the top of the snippet can be extended for other CPTs that use different field names.
The helper is intended for Bricks {echo:...} calls and regular WordPress templates. It reads ACF fields with get_field() when available, then falls back to get_post_meta().
Usage
mac_format_datetime(
?string $preset = null,
?string $view = null,
int|string|null $post_id = null
): string
The preset defines the fields and input formats. The view defines the return mode, output formats, timezone behavior, relative labels, and diff labels.
When $preset or $view is null or empty, the helper uses $mac_datetime_config['default_preset'] and $mac_datetime_config['default_view']. When $post_id is null, the helper uses the current post from get_the_ID(). This is the normal path inside WordPress templates and Bricks query loops. Unknown presets or views return an empty string.
The plain view is not a WordPress or Bricks special value. It is only the default because $mac_datetime_config['default_view'] points to it. If the default preset or view is missing and the call does not pass those arguments, the helper returns an empty string.
In the OOP variant, the same defaults live on \MacCore\Utils\FormatDatetime::$config, and the global mac_format_datetime() wrapper delegates to \MacCore\Utils\FormatDatetime::format().
Bricks examples:
{echo:mac_format_datetime()}
{echo:mac_format_datetime('event')}
{echo:mac_format_datetime('event','html')}
{echo:mac_format_datetime('event','plain')}
{echo:mac_format_datetime('event','attr')}
{echo:mac_format_datetime('event','plain_relative')}
{echo:mac_format_datetime('event','plain_diff')}
{echo:mac_format_datetime('event','plain_with_timezone')}
{echo:mac_format_datetime('event','plain_relative_with_timezone')}
{echo:mac_format_datetime('event','html_relative')}
{echo:mac_format_datetime('event','html_with_timezone')}
{echo:mac_format_datetime('event','html_relative_with_timezone')}
{echo:mac_format_datetime('event','html_diff')}
Defaults And Views
Use $mac_datetime_config to set the default preset and view:
$mac_datetime_config = [
'default_preset' => 'event',
'default_view' => 'plain',
];
In the OOP variant:
\MacCore\Utils\FormatDatetime::$config = [
'default_preset' => 'event',
'default_view' => 'plain',
];
Shared views live in $mac_datetime_views, so presets do not need to duplicate the same view definitions. View return values can be html, plain, or attr.
$mac_datetime_views['long'] = [
'return' => 'plain',
'output_datetime_format' => 'F j, Y \a\t g:i a',
'timezone_display' => 'value',
];
In the OOP variant, use \MacCore\Utils\FormatDatetime::$views.
Presets
Add new presets in $mac_datetime_presets for other CPTs. Blank fields are unused; they do not inherit from the event preset.
The built-in event preset supports combined datetime fields and separate date/time fields. It checks combined fields first, then falls back to separate date/time fields when the combined values are missing. Missing optional fields are skipped, so a date-only event can output just the date.
The input_*_format values describe what ACF or post meta returns. The output_*_format values describe what a view prints. Set output formats to null to use WordPress General Settings. WordPress has separate date and time settings, so output_datetime_format => null combines the resolved output date and time naturally; set it to a non-empty string only when you want an explicit combined override. timezone_display chooses label or value when an ACF choice field returns both. date_labels and diff_labels can be set at the preset root or in a shared view.
$mac_datetime_presets['webinar'] = [
'start_datetime' => 'webinar_start_datetime',
'end_datetime' => 'webinar_end_datetime',
'start_date' => '',
'end_date' => '',
'start_time' => '',
'end_time' => '',
'timezone' => 'webinar_timezone',
'timezone_display' => 'label',
'input_datetime_format' => 'Y-m-d H:i:s',
'input_date_format' => 'Y-m-d',
'input_time_format' => 'H:i',
'output_datetime_format' => null,
'output_date_format' => null,
'output_time_format' => null,
'date_labels' => [],
'diff_labels' => [],
];
In the OOP variant, use \MacCore\Utils\FormatDatetime::$presets.
Presets can still define their own output defaults. Shared views can override those defaults when needed.
$mac_datetime_presets['webinar']['output_date_format'] = 'M j';
$mac_datetime_presets['webinar']['output_time_format'] = 'g:i a';
$mac_datetime_views['short'] = [
'return' => 'plain',
'output_date_format' => 'M j',
'output_time_format' => 'g:i a',
];
Built-in views:
html: HTML output for styling each date/time part.plain: plain text output.attr: machine datetime output for a<time datetime="">attribute.plain_relative: plain text output with labels likeToday,Tomorrow, andYesterday.plain_diff: plain lifecycle text likeStarts in 3 days,Ends in 2 hours, orEnded 1 week ago.plain_with_timezone: plain text output with the configured timezone label or value.plain_relative_with_timezone: plain text output with relative labels and timezone.html_relative: HTML output with labels likeToday,Tomorrow, andYesterday.html_with_timezone: HTML output with the configured timezone label or value.html_relative_with_timezone: HTML output with relative labels and timezone.html_diff: HTML lifecycle text likeStarts in 3 days.
HTML date/range views use <time datetime=""> for start and end values. Diff text is not a specific datetime, so html_diff uses a styled <span> instead of a <time> element. Diff views ignore timezone output, even if a custom diff view sets show_timezone.
Recommended ACF return formats:
- Date Picker:
Y-m-d - Time Picker:
H:i - Date Time Picker:
Y-m-d H:i:s
Variants
- Procedural
- OOP
<?php
/**
* Format preset-based date and time fields for Bricks or templates.
*/
/* -------------------------------------------------------------------------
* Config
* ------------------------------------------------------------------------- */
/**
* Main defaults.
*
* These are used when mac_format_datetime() is called with null or blank
* preset/view arguments.
*
* @var array{default_preset:string,default_view:string}
*/
$mac_datetime_config = [
'default_preset' => 'event',
'default_view' => 'plain',
];
/**
* Shared output views.
*
* Return formats:
* - html: styled span markup.
* - plain: plain text.
* - attr: machine datetime for a <time datetime=""> attribute.
*
* Optional view settings:
* - show_timezone: append the configured timezone field or site timezone.
* - relative: use Today, Tomorrow, and Yesterday labels.
* - show_year_now: force the year even when the output date format omits it.
* - diff: return lifecycle text like "Starts in 3 days"; ignores timezone.
* - timezone_display: label or value for ACF choice-field arrays.
* - output_datetime_format, output_date_format, output_time_format:
* override the preset output formats for this view.
* - date_labels, diff_labels: override relative or lifecycle text.
*
* @var array<string,array<string,mixed>>
*/
$mac_datetime_views = [
'plain' => [
'return' => 'plain',
],
'plain_relative' => [
'return' => 'plain',
'relative' => true,
],
'plain_diff' => [
'return' => 'plain',
'diff' => true,
],
'plain_with_timezone' => [
'return' => 'plain',
'show_timezone' => true,
],
'plain_relative_with_timezone' => [
'return' => 'plain',
'relative' => true,
'show_timezone' => true,
],
'html' => [
'return' => 'html',
],
'html_relative' => [
'return' => 'html',
'relative' => true,
],
'html_with_timezone' => [
'return' => 'html',
'show_timezone' => true,
],
'html_relative_with_timezone' => [
'return' => 'html',
'relative' => true,
'show_timezone' => true,
],
'html_diff' => [
'return' => 'html',
'diff' => true,
],
'attr' => [
'return' => 'attr',
],
];
/**
* Field presets.
*
* Add more presets for other CPTs. Blank fields are unused.
* Combined datetime fields are checked before separate date/time fields.
*
* Input formats describe ACF or post meta return values.
* Output formats describe display values; null uses WordPress General Settings.
* timezone_display accepts label or value for ACF choice-field arrays.
*
* @var array<string,array<string,mixed>>
*/
$mac_datetime_presets = [
'event' => [
'start_datetime' => 'event_start_datetime',
'end_datetime' => 'event_end_datetime',
'start_date' => 'event_start_date',
'end_date' => 'event_end_date',
'start_time' => 'event_start_time',
'end_time' => 'event_end_time',
'timezone' => 'event_timezone',
'timezone_display' => 'label',
'input_datetime_format' => 'Y-m-d H:i:s',
'input_date_format' => 'Y-m-d',
'input_time_format' => 'H:i',
'output_datetime_format' => null,
'output_date_format' => null,
'output_time_format' => null,
'date_labels' => [],
'diff_labels' => [],
],
];
/** @var array<string,string> $mac_datetime_classes */
$mac_datetime_classes = [
'wrapper' => 'mac-datetime',
'start' => 'mac-datetime__start',
'start_date' => 'mac-datetime__start-date',
'start_time' => 'mac-datetime__start-time',
'end' => 'mac-datetime__end',
'end_date' => 'mac-datetime__end-date',
'end_time' => 'mac-datetime__end-time',
'separator' => 'mac-datetime__separator',
'timezone' => 'mac-datetime__timezone',
'diff' => 'mac-datetime__diff',
];
/** @var array<string,string> $mac_datetime_labels */
$mac_datetime_labels = [
'today' => 'Today',
'tomorrow' => 'Tomorrow',
'yesterday' => 'Yesterday',
];
/** @var array<string,string> $mac_datetime_diff_labels */
$mac_datetime_diff_labels = [
'starts_in' => 'Starts in',
'ends_in' => 'Ends in',
'started' => 'Started',
'ended' => 'Ended',
'ago' => 'ago',
'minute' => 'minute',
'minutes' => 'minutes',
'hour' => 'hour',
'hours' => 'hours',
'day' => 'day',
'days' => 'days',
'week' => 'week',
'weeks' => 'weeks',
'month' => 'month',
'months' => 'months',
'year' => 'year',
'years' => 'years',
];
/* -------------------------------------------------------------------------
* Helpers
* ------------------------------------------------------------------------- */
if ( ! function_exists( 'mac_datetime_bool' ) ) {
function mac_datetime_bool( mixed $value ): bool {
if ( is_bool( $value ) ) {
return $value;
}
if ( is_int( $value ) ) {
return $value !== 0;
}
if ( ! is_string( $value ) ) {
return false;
}
$value = strtolower( trim( (string) $value ) );
return in_array( $value, [ '1', 'true', 'yes', 'y', 'on' ], true );
}
}
if ( ! function_exists( 'mac_datetime_esc_html' ) ) {
function mac_datetime_esc_html( string $value ): string {
return function_exists( 'esc_html' )
? esc_html( $value )
: htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'mac_datetime_esc_attr' ) ) {
function mac_datetime_esc_attr( string $value ): string {
return function_exists( 'esc_attr' )
? esc_attr( $value )
: htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'mac_datetime_timezone' ) ) {
function mac_datetime_timezone(): DateTimeZone {
if ( function_exists( 'wp_timezone' ) ) {
return wp_timezone();
}
if ( function_exists( 'wp_timezone_string' ) ) {
$timezone_string = (string) wp_timezone_string();
if ( $timezone_string !== '' ) {
try {
return new DateTimeZone( $timezone_string );
} catch ( Exception $exception ) {
// Fall back to PHP's configured timezone.
}
}
}
return new DateTimeZone( date_default_timezone_get() );
}
}
if ( ! function_exists( 'mac_datetime_timezone_label' ) ) {
function mac_datetime_timezone_label(): string {
if ( function_exists( 'wp_timezone_string' ) ) {
$timezone_string = (string) wp_timezone_string();
if ( $timezone_string !== '' ) {
return $timezone_string;
}
}
return date_default_timezone_get();
}
}
if ( ! function_exists( 'mac_datetime_now' ) ) {
function mac_datetime_now(): int {
return function_exists( 'current_time' )
? (int) current_time( 'timestamp', true )
: time();
}
}
if ( ! function_exists( 'mac_datetime_format_ts' ) ) {
function mac_datetime_format_ts( string $format, int $timestamp ): string {
return function_exists( 'wp_date' )
? wp_date( $format, $timestamp )
: date( $format, $timestamp );
}
}
if ( ! function_exists( 'mac_datetime_wp_option_format' ) ) {
function mac_datetime_wp_option_format( string $option_name, string $fallback ): string {
if ( function_exists( 'get_option' ) ) {
$format = get_option( $option_name );
if ( is_string( $format ) && trim( $format ) !== '' ) {
return $format;
}
}
return $fallback;
}
}
if ( ! function_exists( 'mac_datetime_output_format' ) ) {
function mac_datetime_output_format( mixed $format, string $option_name, string $fallback ): string {
if ( is_string( $format ) && trim( $format ) !== '' ) {
return $format;
}
return $option_name !== ''
? mac_datetime_wp_option_format( $option_name, $fallback )
: $fallback;
}
}
if ( ! function_exists( 'mac_datetime_is_blank' ) ) {
function mac_datetime_is_blank( mixed $value ): bool {
return $value === null
|| $value === false
|| ( is_string( $value ) && trim( $value ) === '' );
}
}
if ( ! function_exists( 'mac_datetime_context_id' ) ) {
function mac_datetime_context_id( int|string|null $post_id ): int|string|null {
if ( $post_id === null || $post_id === '' ) {
$post_id = function_exists( 'get_the_ID' ) ? get_the_ID() : null;
}
if ( is_numeric( $post_id ) ) {
$post_id = (int) $post_id;
}
return $post_id ?: null;
}
}
if ( ! function_exists( 'mac_datetime_get_field_value' ) ) {
function mac_datetime_get_field_value( string $field_name, int|string $post_id ): mixed {
if ( $field_name === '' ) {
return null;
}
if ( function_exists( 'get_field' ) ) {
$value = get_field( $field_name, $post_id );
if ( ! mac_datetime_is_blank( $value ) ) {
return $value;
}
}
if ( is_int( $post_id ) && $post_id > 0 && function_exists( 'get_post_meta' ) ) {
$value = get_post_meta( $post_id, $field_name, true );
if ( ! mac_datetime_is_blank( $value ) ) {
return $value;
}
}
return null;
}
}
if ( ! function_exists( 'mac_datetime_display_value' ) ) {
function mac_datetime_display_value( mixed $value, string $preferred_key = 'label' ): string {
if ( mac_datetime_is_blank( $value ) ) {
return '';
}
$preferred_key = strtolower( trim( $preferred_key ) );
$preferred_key = in_array( $preferred_key, [ 'label', 'value' ], true ) ? $preferred_key : 'label';
$fallback_key = $preferred_key === 'label' ? 'value' : 'label';
if ( is_array( $value ) ) {
foreach ( [ $preferred_key, $fallback_key ] as $key ) {
if ( isset( $value[ $key ] ) && is_scalar( $value[ $key ] ) ) {
return trim( (string) $value[ $key ] );
}
}
$items = [];
foreach ( $value as $item ) {
$item = mac_datetime_display_value( $item, $preferred_key );
if ( $item !== '' ) {
$items[] = $item;
}
}
return implode( ', ', array_unique( $items ) );
}
if ( is_scalar( $value ) || $value instanceof Stringable ) {
return trim( (string) $value );
}
return '';
}
}
if ( ! function_exists( 'mac_datetime_format_list' ) ) {
/**
* @param array<string,mixed> $preset
* @param string[] $fallback
* @return string[]
*/
function mac_datetime_format_list( array $preset, string $key, array $fallback ): array {
$formats = [];
$format = trim( (string) ( $preset[ $key ] ?? '' ) );
if ( $format !== '' ) {
$formats[] = $format;
}
foreach ( $fallback as $fallback_format ) {
if ( ! in_array( $fallback_format, $formats, true ) ) {
$formats[] = $fallback_format;
}
}
return $formats;
}
}
if ( ! function_exists( 'mac_datetime_parse_by_format' ) ) {
function mac_datetime_parse_by_format( string $value, string $format ): int|false {
if ( $format === '' ) {
return false;
}
$parse_format = $format;
if ( $parse_format[0] !== '!' && $parse_format[0] !== '|' ) {
$parse_format = '!' . $parse_format;
}
$datetime = DateTimeImmutable::createFromFormat(
$parse_format,
$value,
mac_datetime_timezone()
);
$errors = DateTimeImmutable::getLastErrors();
if (
! $datetime instanceof DateTimeImmutable ||
(
is_array( $errors ) &&
( $errors['warning_count'] > 0 || $errors['error_count'] > 0 )
)
) {
return false;
}
return $datetime->getTimestamp();
}
}
if ( ! function_exists( 'mac_datetime_parse_value' ) ) {
/**
* @param string[] $formats
*/
function mac_datetime_parse_value( mixed $value, array $formats ): int|false|null {
if ( mac_datetime_is_blank( $value ) ) {
return null;
}
if ( is_int( $value ) ) {
return $value > 0 ? $value : false;
}
if ( is_float( $value ) ) {
return $value > 0 ? (int) $value : false;
}
if ( ! is_string( $value ) ) {
return false;
}
$value = trim( (string) $value );
foreach ( $formats as $format ) {
$timestamp = mac_datetime_parse_by_format( $value, $format );
if ( $timestamp !== false ) {
return $timestamp;
}
}
if ( preg_match( '/^\d{10,}$/', $value ) ) {
$timestamp = (int) $value;
return $timestamp > 0 ? $timestamp : false;
}
try {
$datetime = new DateTimeImmutable( $value, mac_datetime_timezone() );
} catch ( Exception $exception ) {
return false;
}
$timestamp = $datetime->getTimestamp();
return $timestamp > 0 ? $timestamp : false;
}
}
if ( ! function_exists( 'mac_datetime_parse_separate' ) ) {
/**
* @param string[] $date_formats
* @param string[] $time_formats
*/
function mac_datetime_parse_separate(
mixed $date_value,
mixed $time_value,
array $date_formats,
array $time_formats
): int|false {
if ( ! is_int( $date_value ) && ! is_string( $date_value ) ) {
return false;
}
if ( ! mac_datetime_is_blank( $time_value ) && ! is_int( $time_value ) && ! is_string( $time_value ) ) {
return false;
}
if ( mac_datetime_is_blank( $time_value ) ) {
$timestamp = mac_datetime_parse_value( $date_value, $date_formats );
return is_int( $timestamp ) ? $timestamp : false;
}
$datetime_value = trim( (string) $date_value ) . ' ' . trim( (string) $time_value );
foreach ( $date_formats as $date_format ) {
foreach ( $time_formats as $time_format ) {
$timestamp = mac_datetime_parse_by_format(
$datetime_value,
$date_format . ' ' . $time_format
);
if ( $timestamp !== false ) {
return $timestamp;
}
}
}
return false;
}
}
if ( ! function_exists( 'mac_datetime_resolve_point' ) ) {
/**
* @param array<string,mixed> $preset
* @return array{timestamp:int|null,has_time:bool,invalid:bool}
*/
function mac_datetime_resolve_point(
array $preset,
string $prefix,
int|string $post_id
): array {
$datetime_formats = mac_datetime_format_list(
$preset,
'input_datetime_format',
[ 'Y-m-d H:i:s', 'Y-m-d H:i', 'Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'd/m/Y g:i a', 'd/m/Y H:i', 'M j, Y g:i a', 'F j, Y g:i a' ]
);
$date_formats = mac_datetime_format_list(
$preset,
'input_date_format',
[ 'Y-m-d', 'Ymd', 'd/m/Y', 'm/d/Y', 'M j, Y', 'F j, Y' ]
);
$time_formats = mac_datetime_format_list(
$preset,
'input_time_format',
[ 'H:i', 'H:i:s', 'g:i a', 'h:i a', 'g:i A', 'h:i A' ]
);
$datetime_field = (string) ( $preset[ $prefix . '_datetime' ] ?? '' );
if ( $datetime_field !== '' ) {
$datetime_value = mac_datetime_get_field_value( $datetime_field, $post_id );
if ( ! mac_datetime_is_blank( $datetime_value ) ) {
$timestamp = mac_datetime_parse_value( $datetime_value, $datetime_formats );
return [
'timestamp' => is_int( $timestamp ) ? $timestamp : null,
'has_time' => is_int( $timestamp ),
'invalid' => $timestamp === false,
];
}
}
$date_field = (string) ( $preset[ $prefix . '_date' ] ?? '' );
if ( $date_field === '' ) {
return [
'timestamp' => null,
'has_time' => false,
'invalid' => false,
];
}
$date_value = mac_datetime_get_field_value( $date_field, $post_id );
if ( mac_datetime_is_blank( $date_value ) ) {
return [
'timestamp' => null,
'has_time' => false,
'invalid' => false,
];
}
$time_field = (string) ( $preset[ $prefix . '_time' ] ?? '' );
$time_value = $time_field !== ''
? mac_datetime_get_field_value( $time_field, $post_id )
: null;
$timestamp = mac_datetime_parse_separate( $date_value, $time_value, $date_formats, $time_formats );
return [
'timestamp' => $timestamp !== false ? $timestamp : null,
'has_time' => $timestamp !== false && ! mac_datetime_is_blank( $time_value ),
'invalid' => $timestamp === false,
];
}
}
if ( ! function_exists( 'mac_datetime_format_has_year' ) ) {
function mac_datetime_format_has_year( string $format ): bool {
return (bool) preg_match( '/(?<!\\\\)[Yyo]/', $format );
}
}
if ( ! function_exists( 'mac_datetime_date_label' ) ) {
/**
* @param array<string,string> $label_overrides
*/
function mac_datetime_date_label(
int $timestamp,
string $date_format,
bool $relative,
bool $force_year,
bool $range_cross_year,
array $label_overrides = []
): string {
global $mac_datetime_labels;
$labels = array_merge( $mac_datetime_labels, $label_overrides );
if ( $relative ) {
$now = mac_datetime_now();
$day = defined( 'DAY_IN_SECONDS' ) ? DAY_IN_SECONDS : 86400;
$today = mac_datetime_format_ts( 'Ymd', $now );
$date = mac_datetime_format_ts( 'Ymd', $timestamp );
if ( $date === $today ) {
return $labels['today'];
}
if ( $date === mac_datetime_format_ts( 'Ymd', $now + $day ) ) {
return $labels['tomorrow'];
}
if ( $date === mac_datetime_format_ts( 'Ymd', $now - $day ) ) {
return $labels['yesterday'];
}
}
$format = $date_format !== '' ? $date_format : 'Y-m-d';
if (
! mac_datetime_format_has_year( $format ) &&
(
$force_year ||
$range_cross_year ||
mac_datetime_format_ts( 'Y', $timestamp ) !== mac_datetime_format_ts( 'Y', mac_datetime_now() )
)
) {
$format .= ' Y';
}
return mac_datetime_format_ts( $format, $timestamp );
}
}
if ( ! function_exists( 'mac_datetime_duration' ) ) {
/**
* @param array<string,string> $label_overrides
*/
function mac_datetime_duration( int $seconds, array $label_overrides = [] ): string {
global $mac_datetime_diff_labels;
$labels = array_merge( $mac_datetime_diff_labels, $label_overrides );
$seconds = abs( $seconds );
$minute = defined( 'MINUTE_IN_SECONDS' ) ? MINUTE_IN_SECONDS : 60;
$hour = defined( 'HOUR_IN_SECONDS' ) ? HOUR_IN_SECONDS : 3600;
$day = defined( 'DAY_IN_SECONDS' ) ? DAY_IN_SECONDS : 86400;
$week = defined( 'WEEK_IN_SECONDS' ) ? WEEK_IN_SECONDS : 604800;
$month = defined( 'MONTH_IN_SECONDS' ) ? MONTH_IN_SECONDS : 2592000;
$year = defined( 'YEAR_IN_SECONDS' ) ? YEAR_IN_SECONDS : 31536000;
if ( $seconds < $hour ) {
$value = max( 1, (int) floor( $seconds / $minute ) );
$unit = $value === 1 ? $labels['minute'] : $labels['minutes'];
} elseif ( $seconds < $day ) {
$value = max( 1, (int) floor( $seconds / $hour ) );
$unit = $value === 1 ? $labels['hour'] : $labels['hours'];
} elseif ( $seconds < $week ) {
$value = max( 1, (int) floor( $seconds / $day ) );
$unit = $value === 1 ? $labels['day'] : $labels['days'];
} elseif ( $seconds < $month ) {
$value = max( 1, (int) floor( $seconds / $week ) );
$unit = $value === 1 ? $labels['week'] : $labels['weeks'];
} elseif ( $seconds < $year ) {
$value = max( 1, (int) floor( $seconds / $month ) );
$unit = $value === 1 ? $labels['month'] : $labels['months'];
} else {
$value = max( 1, (int) floor( $seconds / $year ) );
$unit = $value === 1 ? $labels['year'] : $labels['years'];
}
return $value . ' ' . $unit;
}
}
if ( ! function_exists( 'mac_datetime_human_diff' ) ) {
/**
* @param array<string,string> $label_overrides
*/
function mac_datetime_human_diff( ?int $start_timestamp, ?int $end_timestamp = null, array $label_overrides = [] ): string {
global $mac_datetime_diff_labels;
$labels = array_merge( $mac_datetime_diff_labels, $label_overrides );
if ( ! $start_timestamp ) {
return '';
}
$now = mac_datetime_now();
if ( $now < $start_timestamp ) {
return $labels['starts_in'] . ' ' . mac_datetime_duration( $start_timestamp - $now, $label_overrides );
}
if ( $end_timestamp && $now <= $end_timestamp ) {
return $labels['ends_in'] . ' ' . mac_datetime_duration( $end_timestamp - $now, $label_overrides );
}
if ( $end_timestamp ) {
return $labels['ended'] . ' ' . mac_datetime_duration( $now - $end_timestamp, $label_overrides ) . ' ' . $labels['ago'];
}
return $labels['started'] . ' ' . mac_datetime_duration( $now - $start_timestamp, $label_overrides ) . ' ' . $labels['ago'];
}
}
if ( ! function_exists( 'mac_datetime_attr_from_point' ) ) {
/**
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $point
*/
function mac_datetime_attr_from_point( array $point ): string {
if ( ! $point['timestamp'] ) {
return '';
}
$format = $point['has_time'] ? 'Y-m-d\TH:i:s' : 'Y-m-d';
return mac_datetime_esc_attr( mac_datetime_format_ts( $format, $point['timestamp'] ) );
}
}
if ( ! function_exists( 'mac_datetime_time_html' ) ) {
/**
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $point
*/
function mac_datetime_time_html(
array $point,
string $time_class,
string $date_class,
string $time_part_class,
string $date_label,
string $time_label
): string {
$datetime = mac_datetime_attr_from_point( $point );
if ( $datetime === '' || ( $date_label === '' && $time_label === '' ) ) {
return '';
}
$out = '<time class="' . mac_datetime_esc_attr( $time_class ) . '" datetime="' . $datetime . '">';
if ( $date_label !== '' ) {
$out .= '<span class="' . mac_datetime_esc_attr( $date_class ) . '">' . mac_datetime_esc_html( $date_label ) . '</span>';
}
if ( $time_label !== '' ) {
$out .= $date_label !== '' ? ' ' : '';
$out .= '<span class="' . mac_datetime_esc_attr( $time_part_class ) . '">' . mac_datetime_esc_html( $time_label ) . '</span>';
}
$out .= '</time>';
return $out;
}
}
/* -------------------------------------------------------------------------
* Formatter
* ------------------------------------------------------------------------- */
if ( ! function_exists( 'mac_format_datetime' ) ) {
function mac_format_datetime(
?string $preset = null,
?string $view = null,
int|string|null $post_id = null
): string {
global $mac_datetime_config;
global $mac_datetime_presets;
global $mac_datetime_views;
global $mac_datetime_classes;
$config = is_array( $mac_datetime_config ) ? $mac_datetime_config : [];
$presets = is_array( $mac_datetime_presets ) ? $mac_datetime_presets : [];
$views = is_array( $mac_datetime_views ) ? $mac_datetime_views : [];
$preset = strtolower( trim( (string) $preset ) );
$preset = $preset !== ''
? $preset
: strtolower( trim( (string) ( $config['default_preset'] ?? '' ) ) );
if ( ! isset( $presets[ $preset ] ) || ! is_array( $presets[ $preset ] ) ) {
return '';
}
$preset_config = array_merge(
[
'start_datetime' => '',
'end_datetime' => '',
'start_date' => '',
'end_date' => '',
'start_time' => '',
'end_time' => '',
'timezone' => '',
'timezone_display' => 'label',
'input_datetime_format' => '',
'input_date_format' => '',
'input_time_format' => '',
'output_datetime_format' => null,
'output_date_format' => null,
'output_time_format' => null,
'date_labels' => [],
'diff_labels' => [],
],
$presets[ $preset ]
);
$view = strtolower( trim( (string) $view ) );
$view = $view !== ''
? $view
: strtolower( trim( (string) ( $config['default_view'] ?? '' ) ) );
if ( ! isset( $views[ $view ] ) || ! is_array( $views[ $view ] ) ) {
return '';
}
$view_config = array_merge(
[
'return' => 'html',
'show_timezone' => false,
'relative' => false,
'show_year_now' => false,
'diff' => false,
'timezone_display' => $preset_config['timezone_display'],
'output_datetime_format' => $preset_config['output_datetime_format'],
'output_date_format' => $preset_config['output_date_format'],
'output_time_format' => $preset_config['output_time_format'],
'date_labels' => [],
'diff_labels' => [],
],
$views[ $view ]
);
$post_id = mac_datetime_context_id( $post_id );
if ( $post_id === null ) {
return '';
}
$show_timezone = mac_datetime_bool( $view_config['show_timezone'] );
$relative = mac_datetime_bool( $view_config['relative'] );
$show_year_now = mac_datetime_bool( $view_config['show_year_now'] );
$diff = mac_datetime_bool( $view_config['diff'] );
$return = is_string( $view_config['return'] ) ? strtolower( trim( $view_config['return'] ) ) : 'html';
$return = in_array( $return, [ 'html', 'plain', 'attr' ], true ) ? $return : 'html';
$date_format = mac_datetime_output_format( $view_config['output_date_format'], 'date_format', 'M j, Y' );
$time_format = mac_datetime_output_format( $view_config['output_time_format'], 'time_format', 'g:i a' );
$date_format = $date_format !== '' ? $date_format : 'Y-m-d';
$time_format = $time_format !== '' ? $time_format : 'H:i';
$datetime_format = mac_datetime_output_format( $view_config['output_datetime_format'], '', '' );
$date_labels = array_merge(
is_array( $preset_config['date_labels'] ) ? $preset_config['date_labels'] : [],
is_array( $view_config['date_labels'] ) ? $view_config['date_labels'] : []
);
$diff_labels = array_merge(
is_array( $preset_config['diff_labels'] ) ? $preset_config['diff_labels'] : [],
is_array( $view_config['diff_labels'] ) ? $view_config['diff_labels'] : []
);
$timezone_display = is_string( $view_config['timezone_display'] )
? strtolower( trim( $view_config['timezone_display'] ) )
: 'label';
$timezone_display = in_array( $timezone_display, [ 'label', 'value' ], true ) ? $timezone_display : 'label';
$start = mac_datetime_resolve_point( $preset_config, 'start', $post_id );
$end = mac_datetime_resolve_point( $preset_config, 'end', $post_id );
if ( $start['invalid'] || $end['invalid'] || ! $start['timestamp'] ) {
return '';
}
if ( $diff ) {
$diff_text = mac_datetime_human_diff( $start['timestamp'], $end['timestamp'], $diff_labels );
if ( $return === 'html' && $diff_text !== '' ) {
return '<span class="' . mac_datetime_esc_attr( $mac_datetime_classes['wrapper'] ) . '">'
. '<span class="' . mac_datetime_esc_attr( $mac_datetime_classes['diff'] ) . '">' . mac_datetime_esc_html( $diff_text ) . '</span>'
. '</span>';
}
return $diff_text;
}
$start_date_key = mac_datetime_format_ts( 'Y-m-d', $start['timestamp'] );
$end_date_key = $end['timestamp'] ? mac_datetime_format_ts( 'Y-m-d', $end['timestamp'] ) : '';
$same_day_range = $end['timestamp'] && $start_date_key === $end_date_key;
$cross_year_range = $end['timestamp']
&& mac_datetime_format_ts( 'Y', $start['timestamp'] ) !== mac_datetime_format_ts( 'Y', $end['timestamp'] );
if ( $return === 'attr' ) {
return mac_datetime_attr_from_point( $start );
}
$start_date = mac_datetime_date_label(
$start['timestamp'],
$date_format,
$relative,
$show_year_now,
$cross_year_range,
$date_labels
);
$end_date = $end['timestamp']
? mac_datetime_date_label( $end['timestamp'], $date_format, false, $show_year_now, $cross_year_range, $date_labels )
: '';
$start_time = $start['has_time'] ? mac_datetime_format_ts( $time_format, $start['timestamp'] ) : '';
$end_time = $end['timestamp'] && $end['has_time'] ? mac_datetime_format_ts( $time_format, $end['timestamp'] ) : '';
if ( ! $relative && $datetime_format !== '' ) {
if ( $start_time !== '' ) {
$start_date = mac_datetime_format_ts( $datetime_format, $start['timestamp'] );
$start_time = '';
}
if ( $end_time !== '' && ! $same_day_range ) {
$end_date = mac_datetime_format_ts( $datetime_format, $end['timestamp'] );
$end_time = '';
}
}
$timezone = '';
if ( $show_timezone ) {
$timezone_field = (string) $preset_config['timezone'];
$timezone_value = $timezone_field !== ''
? mac_datetime_get_field_value( $timezone_field, $post_id )
: null;
$timezone = mac_datetime_display_value( $timezone_value, $timezone_display );
if ( $timezone === '' ) {
$timezone = mac_datetime_timezone_label();
}
}
if ( $return === 'plain' ) {
$parts = [ $start_date ];
if ( $start_time !== '' ) {
$parts[] = $start_time;
}
if ( $end['timestamp'] ) {
if ( $same_day_range ) {
if ( $end_time !== '' ) {
$parts[] = '-';
$parts[] = $end_time;
}
} else {
$parts[] = '-';
$parts[] = $end_date;
if ( $end_time !== '' ) {
$parts[] = $end_time;
}
}
}
if ( $timezone !== '' ) {
$parts[] = $timezone;
}
return implode( ' ', $parts );
}
$out = '<span class="' . mac_datetime_esc_attr( $mac_datetime_classes['wrapper'] ) . '">';
$out .= mac_datetime_time_html(
$start,
$mac_datetime_classes['start'],
$mac_datetime_classes['start_date'],
$mac_datetime_classes['start_time'],
$start_date,
$start_time
);
if ( $end['timestamp'] ) {
if ( $same_day_range ) {
if ( $end_time !== '' ) {
$out .= '<span class="' . mac_datetime_esc_attr( $mac_datetime_classes['separator'] ) . '"> - </span>';
$out .= mac_datetime_time_html(
$end,
$mac_datetime_classes['end'],
$mac_datetime_classes['end_date'],
$mac_datetime_classes['end_time'],
'',
$end_time
);
}
} else {
$out .= '<span class="' . mac_datetime_esc_attr( $mac_datetime_classes['separator'] ) . '"> - </span>';
$out .= mac_datetime_time_html(
$end,
$mac_datetime_classes['end'],
$mac_datetime_classes['end_date'],
$mac_datetime_classes['end_time'],
$end_date,
$end_time
);
}
}
if ( $timezone !== '' ) {
$out .= ' <span class="' . mac_datetime_esc_attr( $mac_datetime_classes['timezone'] ) . '">' . mac_datetime_esc_html( $timezone ) . '</span>';
}
$out .= '</span>';
return $out;
}
}
<?php
/**
* Format preset-based date and time fields for Bricks or templates.
*
* @package mac-core
*/
declare(strict_types=1);
namespace MacCore\Utils {
use DateTimeImmutable;
use DateTimeZone;
use Exception;
use Stringable;
/**
* Format preset-based date and time fields.
*/
final class FormatDatetime
{
/**
* Main defaults.
*
* These are used when format() is called with null or blank preset/view
* arguments. If either default is blank and no argument is passed, output is
* empty.
*
* @var array{default_preset:string,default_view:string}
*/
public static array $config = [
'default_preset' => 'event',
'default_view' => 'plain',
];
/**
* Shared output views.
*
* Return formats:
* - html: styled span markup with <time datetime=""> elements.
* - plain: plain text.
* - attr: machine datetime for a <time datetime=""> attribute.
*
* Optional view settings:
* - show_timezone: append the configured timezone field or site timezone.
* - relative: use Today, Tomorrow, and Yesterday labels.
* - show_year_now: force the year even when the output date format omits it.
* - diff: return lifecycle text like "Starts in 3 days"; ignores timezone.
* - timezone_display: label or value for ACF choice-field arrays.
* - output_datetime_format, output_date_format, output_time_format:
* override the preset output formats for this view.
* - date_labels, diff_labels: override relative or lifecycle text.
*
* @var array<string,array<string,mixed>>
*/
public static array $views = [
'plain' => [
'return' => 'plain',
],
'plain_relative' => [
'return' => 'plain',
'relative' => true,
],
'plain_diff' => [
'return' => 'plain',
'diff' => true,
],
'plain_with_timezone' => [
'return' => 'plain',
'show_timezone' => true,
],
'plain_relative_with_timezone' => [
'return' => 'plain',
'relative' => true,
'show_timezone' => true,
],
'html' => [
'return' => 'html',
],
'html_relative' => [
'return' => 'html',
'relative' => true,
],
'html_with_timezone' => [
'return' => 'html',
'show_timezone' => true,
],
'html_relative_with_timezone' => [
'return' => 'html',
'relative' => true,
'show_timezone' => true,
],
'html_diff' => [
'return' => 'html',
'diff' => true,
],
'attr' => [
'return' => 'attr',
],
];
/**
* Field presets.
*
* Add more presets for other CPTs. Blank fields are unused.
* Combined datetime fields are checked before separate date/time fields.
*
* Input formats describe ACF or post meta return values.
* Output formats describe display values; null uses WordPress General Settings.
* timezone_display accepts label or value for ACF choice-field arrays.
*
* @var array<string,array<string,mixed>>
*/
public static array $presets = [
'event' => [
'start_datetime' => 'event_start_datetime',
'end_datetime' => 'event_end_datetime',
'start_date' => 'event_start_date',
'end_date' => 'event_end_date',
'start_time' => 'event_start_time',
'end_time' => 'event_end_time',
'timezone' => 'event_timezone',
'timezone_display' => 'label',
'input_datetime_format' => 'Y-m-d H:i:s',
'input_date_format' => 'Y-m-d',
'input_time_format' => 'H:i',
'output_datetime_format' => null,
'output_date_format' => null,
'output_time_format' => null,
'date_labels' => [],
'diff_labels' => [],
],
];
/** @var array<string,string> */
public static array $classes = [
'wrapper' => 'mac-datetime',
'start' => 'mac-datetime__start',
'start_date' => 'mac-datetime__start-date',
'start_time' => 'mac-datetime__start-time',
'end' => 'mac-datetime__end',
'end_date' => 'mac-datetime__end-date',
'end_time' => 'mac-datetime__end-time',
'separator' => 'mac-datetime__separator',
'timezone' => 'mac-datetime__timezone',
'diff' => 'mac-datetime__diff',
];
/** @var array<string,string> */
public static array $dateLabels = [
'today' => 'Today',
'tomorrow' => 'Tomorrow',
'yesterday' => 'Yesterday',
];
/** @var array<string,string> */
public static array $diffLabels = [
'starts_in' => 'Starts in',
'ends_in' => 'Ends in',
'started' => 'Started',
'ended' => 'Ended',
'ago' => 'ago',
'minute' => 'minute',
'minutes' => 'minutes',
'hour' => 'hour',
'hours' => 'hours',
'day' => 'day',
'days' => 'days',
'week' => 'week',
'weeks' => 'weeks',
'month' => 'month',
'months' => 'months',
'year' => 'year',
'years' => 'years',
];
/**
* Format a preset date/time value.
*/
public static function format(
?string $preset = null,
?string $view = null,
int|string|null $postId = null
): string {
$preset = \strtolower(\trim((string) $preset));
$preset = $preset !== ''
? $preset
: \strtolower(\trim((string) (self::$config['default_preset'] ?? '')));
if (! isset(self::$presets[$preset]) || ! \is_array(self::$presets[$preset])) {
return '';
}
$presetConfig = \array_merge(
[
'start_datetime' => '',
'end_datetime' => '',
'start_date' => '',
'end_date' => '',
'start_time' => '',
'end_time' => '',
'timezone' => '',
'timezone_display' => 'label',
'input_datetime_format' => '',
'input_date_format' => '',
'input_time_format' => '',
'output_datetime_format' => null,
'output_date_format' => null,
'output_time_format' => null,
'date_labels' => [],
'diff_labels' => [],
],
self::$presets[$preset]
);
$view = \strtolower(\trim((string) $view));
$view = $view !== ''
? $view
: \strtolower(\trim((string) (self::$config['default_view'] ?? '')));
if (! isset(self::$views[$view]) || ! \is_array(self::$views[$view])) {
return '';
}
$viewConfig = \array_merge(
[
'return' => 'html',
'show_timezone' => false,
'relative' => false,
'show_year_now' => false,
'diff' => false,
'timezone_display' => $presetConfig['timezone_display'],
'output_datetime_format' => $presetConfig['output_datetime_format'],
'output_date_format' => $presetConfig['output_date_format'],
'output_time_format' => $presetConfig['output_time_format'],
'date_labels' => [],
'diff_labels' => [],
],
self::$views[$view]
);
$postId = self::contextId($postId);
if ($postId === null) {
return '';
}
$showTimezone = self::bool($viewConfig['show_timezone']);
$relative = self::bool($viewConfig['relative']);
$showYearNow = self::bool($viewConfig['show_year_now']);
$diff = self::bool($viewConfig['diff']);
$return = \is_string($viewConfig['return']) ? \strtolower(\trim($viewConfig['return'])) : 'html';
$return = \in_array($return, ['html', 'plain', 'attr'], true) ? $return : 'html';
$dateFormat = self::outputFormat($viewConfig['output_date_format'], 'date_format', 'M j, Y');
$timeFormat = self::outputFormat($viewConfig['output_time_format'], 'time_format', 'g:i a');
$dateFormat = $dateFormat !== '' ? $dateFormat : 'Y-m-d';
$timeFormat = $timeFormat !== '' ? $timeFormat : 'H:i';
$datetimeFormat = self::outputFormat($viewConfig['output_datetime_format'], '', '');
$dateLabels = \array_merge(
\is_array($presetConfig['date_labels']) ? $presetConfig['date_labels'] : [],
\is_array($viewConfig['date_labels']) ? $viewConfig['date_labels'] : []
);
$diffLabels = \array_merge(
\is_array($presetConfig['diff_labels']) ? $presetConfig['diff_labels'] : [],
\is_array($viewConfig['diff_labels']) ? $viewConfig['diff_labels'] : []
);
$timezoneDisplay = \is_string($viewConfig['timezone_display'])
? \strtolower(\trim($viewConfig['timezone_display']))
: 'label';
$timezoneDisplay = \in_array($timezoneDisplay, ['label', 'value'], true) ? $timezoneDisplay : 'label';
$start = self::resolvePoint($presetConfig, 'start', $postId);
$end = self::resolvePoint($presetConfig, 'end', $postId);
if ($start['invalid'] || $end['invalid'] || ! $start['timestamp']) {
return '';
}
if ($diff) {
$diffText = self::humanDiff($start['timestamp'], $end['timestamp'], $diffLabels);
if ($return === 'html' && $diffText !== '') {
return '<span class="' . self::escAttr(self::className('wrapper')) . '">'
. '<span class="' . self::escAttr(self::className('diff')) . '">' . self::escHtml($diffText) . '</span>'
. '</span>';
}
return $diffText;
}
$startDateKey = self::formatTimestamp('Y-m-d', $start['timestamp']);
$endDateKey = $end['timestamp'] ? self::formatTimestamp('Y-m-d', $end['timestamp']) : '';
$sameDayRange = $end['timestamp'] && $startDateKey === $endDateKey;
$crossYearRange = $end['timestamp']
&& self::formatTimestamp('Y', $start['timestamp']) !== self::formatTimestamp('Y', $end['timestamp']);
if ($return === 'attr') {
return self::attrFromPoint($start);
}
$startDate = self::dateLabel(
$start['timestamp'],
$dateFormat,
$relative,
$showYearNow,
$crossYearRange,
$dateLabels
);
$endDate = $end['timestamp']
? self::dateLabel($end['timestamp'], $dateFormat, false, $showYearNow, $crossYearRange, $dateLabels)
: '';
$startTime = $start['has_time'] ? self::formatTimestamp($timeFormat, $start['timestamp']) : '';
$endTime = $end['timestamp'] && $end['has_time'] ? self::formatTimestamp($timeFormat, $end['timestamp']) : '';
if (! $relative && $datetimeFormat !== '') {
if ($startTime !== '') {
$startDate = self::formatTimestamp($datetimeFormat, $start['timestamp']);
$startTime = '';
}
if ($endTime !== '' && ! $sameDayRange) {
$endDate = self::formatTimestamp($datetimeFormat, $end['timestamp']);
$endTime = '';
}
}
$timezone = '';
if ($showTimezone) {
$timezoneField = (string) $presetConfig['timezone'];
$timezoneValue = $timezoneField !== ''
? self::getFieldValue($timezoneField, $postId)
: null;
$timezone = self::displayValue($timezoneValue, $timezoneDisplay);
if ($timezone === '') {
$timezone = self::timezoneLabel();
}
}
if ($return === 'plain') {
return self::plainOutput($startDate, $startTime, $end, $endDate, $endTime, $sameDayRange, $timezone);
}
return self::htmlOutput($start, $startDate, $startTime, $end, $endDate, $endTime, $sameDayRange, $timezone);
}
private static function bool(mixed $value): bool
{
if (\is_bool($value)) {
return $value;
}
if (\is_int($value)) {
return $value !== 0;
}
if (! \is_string($value)) {
return false;
}
$value = \strtolower(\trim((string) $value));
return \in_array($value, ['1', 'true', 'yes', 'y', 'on'], true);
}
private static function escHtml(string $value): string
{
return \function_exists('esc_html')
? \esc_html($value)
: \htmlspecialchars($value, \ENT_QUOTES, 'UTF-8');
}
private static function escAttr(string $value): string
{
return \function_exists('esc_attr')
? \esc_attr($value)
: \htmlspecialchars($value, \ENT_QUOTES, 'UTF-8');
}
private static function timezone(): DateTimeZone
{
if (\function_exists('wp_timezone')) {
return \wp_timezone();
}
if (\function_exists('wp_timezone_string')) {
$timezoneString = (string) \wp_timezone_string();
if ($timezoneString !== '') {
try {
return new DateTimeZone($timezoneString);
} catch (Exception $exception) {
// Fall back to PHP's configured timezone.
}
}
}
return new DateTimeZone(\date_default_timezone_get());
}
private static function timezoneLabel(): string
{
if (\function_exists('wp_timezone_string')) {
$timezoneString = (string) \wp_timezone_string();
if ($timezoneString !== '') {
return $timezoneString;
}
}
return \date_default_timezone_get();
}
private static function now(): int
{
return \function_exists('current_time')
? (int) \current_time('timestamp', true)
: \time();
}
private static function formatTimestamp(string $format, int $timestamp): string
{
return \function_exists('wp_date')
? \wp_date($format, $timestamp)
: \date($format, $timestamp);
}
private static function wpOptionFormat(string $optionName, string $fallback): string
{
if (\function_exists('get_option')) {
$format = \get_option($optionName);
if (\is_string($format) && \trim($format) !== '') {
return $format;
}
}
return $fallback;
}
private static function outputFormat(mixed $format, string $optionName, string $fallback): string
{
if (\is_string($format) && \trim($format) !== '') {
return $format;
}
return $optionName !== ''
? self::wpOptionFormat($optionName, $fallback)
: $fallback;
}
private static function isBlank(mixed $value): bool
{
return $value === null
|| $value === false
|| (\is_string($value) && \trim($value) === '');
}
private static function contextId(int|string|null $postId): int|string|null
{
if ($postId === null || $postId === '') {
$postId = \function_exists('get_the_ID') ? \get_the_ID() : null;
}
if (\is_numeric($postId)) {
$postId = (int) $postId;
}
return $postId ?: null;
}
private static function getFieldValue(string $fieldName, int|string $postId): mixed
{
if ($fieldName === '') {
return null;
}
if (\function_exists('get_field')) {
$value = \get_field($fieldName, $postId);
if (! self::isBlank($value)) {
return $value;
}
}
if (\is_int($postId) && $postId > 0 && \function_exists('get_post_meta')) {
$value = \get_post_meta($postId, $fieldName, true);
if (! self::isBlank($value)) {
return $value;
}
}
return null;
}
private static function displayValue(mixed $value, string $preferredKey = 'label'): string
{
if (self::isBlank($value)) {
return '';
}
$preferredKey = \strtolower(\trim($preferredKey));
$preferredKey = \in_array($preferredKey, ['label', 'value'], true) ? $preferredKey : 'label';
$fallbackKey = $preferredKey === 'label' ? 'value' : 'label';
if (\is_array($value)) {
foreach ([$preferredKey, $fallbackKey] as $key) {
if (isset($value[$key]) && \is_scalar($value[$key])) {
return \trim((string) $value[$key]);
}
}
$items = [];
foreach ($value as $item) {
$item = self::displayValue($item, $preferredKey);
if ($item !== '') {
$items[] = $item;
}
}
return \implode(', ', \array_unique($items));
}
if (\is_scalar($value) || $value instanceof Stringable) {
return \trim((string) $value);
}
return '';
}
/**
* @param array<string,mixed> $preset
* @param string[] $fallback
* @return string[]
*/
private static function formatList(array $preset, string $key, array $fallback): array
{
$formats = [];
$format = \trim((string) ($preset[$key] ?? ''));
if ($format !== '') {
$formats[] = $format;
}
foreach ($fallback as $fallbackFormat) {
if (! \in_array($fallbackFormat, $formats, true)) {
$formats[] = $fallbackFormat;
}
}
return $formats;
}
private static function parseByFormat(string $value, string $format): int|false
{
if ($format === '') {
return false;
}
$parseFormat = $format;
if ($parseFormat[0] !== '!' && $parseFormat[0] !== '|') {
$parseFormat = '!' . $parseFormat;
}
$datetime = DateTimeImmutable::createFromFormat(
$parseFormat,
$value,
self::timezone()
);
$errors = DateTimeImmutable::getLastErrors();
if (
! $datetime instanceof DateTimeImmutable ||
(
\is_array($errors) &&
($errors['warning_count'] > 0 || $errors['error_count'] > 0)
)
) {
return false;
}
return $datetime->getTimestamp();
}
/**
* @param string[] $formats
*/
private static function parseValue(mixed $value, array $formats): int|false|null
{
if (self::isBlank($value)) {
return null;
}
if (\is_int($value)) {
return $value > 0 ? $value : false;
}
if (\is_float($value)) {
return $value > 0 ? (int) $value : false;
}
if (! \is_string($value)) {
return false;
}
$value = \trim((string) $value);
foreach ($formats as $format) {
$timestamp = self::parseByFormat($value, $format);
if ($timestamp !== false) {
return $timestamp;
}
}
if (\preg_match('/^\d{10,}$/', $value)) {
$timestamp = (int) $value;
return $timestamp > 0 ? $timestamp : false;
}
try {
$datetime = new DateTimeImmutable($value, self::timezone());
} catch (Exception $exception) {
return false;
}
$timestamp = $datetime->getTimestamp();
return $timestamp > 0 ? $timestamp : false;
}
/**
* @param string[] $dateFormats
* @param string[] $timeFormats
*/
private static function parseSeparate(
mixed $dateValue,
mixed $timeValue,
array $dateFormats,
array $timeFormats
): int|false {
if (! \is_int($dateValue) && ! \is_string($dateValue)) {
return false;
}
if (! self::isBlank($timeValue) && ! \is_int($timeValue) && ! \is_string($timeValue)) {
return false;
}
if (self::isBlank($timeValue)) {
$timestamp = self::parseValue($dateValue, $dateFormats);
return \is_int($timestamp) ? $timestamp : false;
}
$datetimeValue = \trim((string) $dateValue) . ' ' . \trim((string) $timeValue);
foreach ($dateFormats as $dateFormat) {
foreach ($timeFormats as $timeFormat) {
$timestamp = self::parseByFormat(
$datetimeValue,
$dateFormat . ' ' . $timeFormat
);
if ($timestamp !== false) {
return $timestamp;
}
}
}
return false;
}
/**
* @param array<string,mixed> $preset
* @return array{timestamp:int|null,has_time:bool,invalid:bool}
*/
private static function resolvePoint(
array $preset,
string $prefix,
int|string $postId
): array {
$datetimeFormats = self::formatList(
$preset,
'input_datetime_format',
['Y-m-d H:i:s', 'Y-m-d H:i', 'Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'd/m/Y g:i a', 'd/m/Y H:i', 'M j, Y g:i a', 'F j, Y g:i a']
);
$dateFormats = self::formatList(
$preset,
'input_date_format',
['Y-m-d', 'Ymd', 'd/m/Y', 'm/d/Y', 'M j, Y', 'F j, Y']
);
$timeFormats = self::formatList(
$preset,
'input_time_format',
['H:i', 'H:i:s', 'g:i a', 'h:i a', 'g:i A', 'h:i A']
);
$datetimeField = (string) ($preset[$prefix . '_datetime'] ?? '');
if ($datetimeField !== '') {
$datetimeValue = self::getFieldValue($datetimeField, $postId);
if (! self::isBlank($datetimeValue)) {
$timestamp = self::parseValue($datetimeValue, $datetimeFormats);
return [
'timestamp' => \is_int($timestamp) ? $timestamp : null,
'has_time' => \is_int($timestamp),
'invalid' => $timestamp === false,
];
}
}
$dateField = (string) ($preset[$prefix . '_date'] ?? '');
if ($dateField === '') {
return [
'timestamp' => null,
'has_time' => false,
'invalid' => false,
];
}
$dateValue = self::getFieldValue($dateField, $postId);
if (self::isBlank($dateValue)) {
return [
'timestamp' => null,
'has_time' => false,
'invalid' => false,
];
}
$timeField = (string) ($preset[$prefix . '_time'] ?? '');
$timeValue = $timeField !== ''
? self::getFieldValue($timeField, $postId)
: null;
$timestamp = self::parseSeparate($dateValue, $timeValue, $dateFormats, $timeFormats);
return [
'timestamp' => $timestamp !== false ? $timestamp : null,
'has_time' => $timestamp !== false && ! self::isBlank($timeValue),
'invalid' => $timestamp === false,
];
}
private static function formatHasYear(string $format): bool
{
return (bool) \preg_match('/(?<!\\\\)[Yyo]/', $format);
}
/**
* @param array<string,string> $labelOverrides
*/
private static function dateLabel(
int $timestamp,
string $dateFormat,
bool $relative,
bool $forceYear,
bool $rangeCrossYear,
array $labelOverrides = []
): string {
$labels = \array_merge(self::$dateLabels, $labelOverrides);
if ($relative) {
$now = self::now();
$day = \defined('DAY_IN_SECONDS') ? \DAY_IN_SECONDS : 86400;
$today = self::formatTimestamp('Ymd', $now);
$date = self::formatTimestamp('Ymd', $timestamp);
if ($date === $today) {
return (string) $labels['today'];
}
if ($date === self::formatTimestamp('Ymd', $now + $day)) {
return (string) $labels['tomorrow'];
}
if ($date === self::formatTimestamp('Ymd', $now - $day)) {
return (string) $labels['yesterday'];
}
}
$format = $dateFormat !== '' ? $dateFormat : 'Y-m-d';
if (
! self::formatHasYear($format) &&
(
$forceYear ||
$rangeCrossYear ||
self::formatTimestamp('Y', $timestamp) !== self::formatTimestamp('Y', self::now())
)
) {
$format .= ' Y';
}
return self::formatTimestamp($format, $timestamp);
}
/**
* @param array<string,string> $labelOverrides
*/
private static function duration(int $seconds, array $labelOverrides = []): string
{
$labels = \array_merge(self::$diffLabels, $labelOverrides);
$seconds = \abs($seconds);
$minute = \defined('MINUTE_IN_SECONDS') ? \MINUTE_IN_SECONDS : 60;
$hour = \defined('HOUR_IN_SECONDS') ? \HOUR_IN_SECONDS : 3600;
$day = \defined('DAY_IN_SECONDS') ? \DAY_IN_SECONDS : 86400;
$week = \defined('WEEK_IN_SECONDS') ? \WEEK_IN_SECONDS : 604800;
$month = \defined('MONTH_IN_SECONDS') ? \MONTH_IN_SECONDS : 2592000;
$year = \defined('YEAR_IN_SECONDS') ? \YEAR_IN_SECONDS : 31536000;
if ($seconds < $hour) {
$value = \max(1, (int) \floor($seconds / $minute));
$unit = $value === 1 ? $labels['minute'] : $labels['minutes'];
} elseif ($seconds < $day) {
$value = \max(1, (int) \floor($seconds / $hour));
$unit = $value === 1 ? $labels['hour'] : $labels['hours'];
} elseif ($seconds < $week) {
$value = \max(1, (int) \floor($seconds / $day));
$unit = $value === 1 ? $labels['day'] : $labels['days'];
} elseif ($seconds < $month) {
$value = \max(1, (int) \floor($seconds / $week));
$unit = $value === 1 ? $labels['week'] : $labels['weeks'];
} elseif ($seconds < $year) {
$value = \max(1, (int) \floor($seconds / $month));
$unit = $value === 1 ? $labels['month'] : $labels['months'];
} else {
$value = \max(1, (int) \floor($seconds / $year));
$unit = $value === 1 ? $labels['year'] : $labels['years'];
}
return $value . ' ' . $unit;
}
/**
* @param array<string,string> $labelOverrides
*/
private static function humanDiff(?int $startTimestamp, ?int $endTimestamp = null, array $labelOverrides = []): string
{
$labels = \array_merge(self::$diffLabels, $labelOverrides);
if (! $startTimestamp) {
return '';
}
$now = self::now();
if ($now < $startTimestamp) {
return $labels['starts_in'] . ' ' . self::duration($startTimestamp - $now, $labelOverrides);
}
if ($endTimestamp && $now <= $endTimestamp) {
return $labels['ends_in'] . ' ' . self::duration($endTimestamp - $now, $labelOverrides);
}
if ($endTimestamp) {
return $labels['ended'] . ' ' . self::duration($now - $endTimestamp, $labelOverrides) . ' ' . $labels['ago'];
}
return $labels['started'] . ' ' . self::duration($now - $startTimestamp, $labelOverrides) . ' ' . $labels['ago'];
}
/**
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $point
*/
private static function attrFromPoint(array $point): string
{
if (! $point['timestamp']) {
return '';
}
$format = $point['has_time'] ? 'Y-m-d\TH:i:s' : 'Y-m-d';
return self::escAttr(self::formatTimestamp($format, $point['timestamp']));
}
/**
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $point
*/
private static function timeHtml(
array $point,
string $timeClass,
string $dateClass,
string $timePartClass,
string $dateLabel,
string $timeLabel
): string {
$datetime = self::attrFromPoint($point);
if ($datetime === '' || ($dateLabel === '' && $timeLabel === '')) {
return '';
}
$out = '<time class="' . self::escAttr($timeClass) . '" datetime="' . $datetime . '">';
if ($dateLabel !== '') {
$out .= '<span class="' . self::escAttr($dateClass) . '">' . self::escHtml($dateLabel) . '</span>';
}
if ($timeLabel !== '') {
$out .= $dateLabel !== '' ? ' ' : '';
$out .= '<span class="' . self::escAttr($timePartClass) . '">' . self::escHtml($timeLabel) . '</span>';
}
$out .= '</time>';
return $out;
}
/**
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $end
*/
private static function plainOutput(
string $startDate,
string $startTime,
array $end,
string $endDate,
string $endTime,
bool $sameDayRange,
string $timezone
): string {
$parts = [$startDate];
if ($startTime !== '') {
$parts[] = $startTime;
}
if ($end['timestamp']) {
if ($sameDayRange) {
if ($endTime !== '') {
$parts[] = '-';
$parts[] = $endTime;
}
} else {
$parts[] = '-';
$parts[] = $endDate;
if ($endTime !== '') {
$parts[] = $endTime;
}
}
}
if ($timezone !== '') {
$parts[] = $timezone;
}
return \implode(' ', $parts);
}
/**
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $start
* @param array{timestamp:int|null,has_time:bool,invalid:bool} $end
*/
private static function htmlOutput(
array $start,
string $startDate,
string $startTime,
array $end,
string $endDate,
string $endTime,
bool $sameDayRange,
string $timezone
): string {
$out = '<span class="' . self::escAttr(self::className('wrapper')) . '">';
$out .= self::timeHtml(
$start,
self::className('start'),
self::className('start_date'),
self::className('start_time'),
$startDate,
$startTime
);
if ($end['timestamp']) {
if ($sameDayRange) {
if ($endTime !== '') {
$out .= '<span class="' . self::escAttr(self::className('separator')) . '"> - </span>';
$out .= self::timeHtml(
$end,
self::className('end'),
self::className('end_date'),
self::className('end_time'),
'',
$endTime
);
}
} else {
$out .= '<span class="' . self::escAttr(self::className('separator')) . '"> - </span>';
$out .= self::timeHtml(
$end,
self::className('end'),
self::className('end_date'),
self::className('end_time'),
$endDate,
$endTime
);
}
}
if ($timezone !== '') {
$out .= ' <span class="' . self::escAttr(self::className('timezone')) . '">' . self::escHtml($timezone) . '</span>';
}
$out .= '</span>';
return $out;
}
private static function className(string $key): string
{
return isset(self::$classes[$key]) && \is_string(self::$classes[$key])
? self::$classes[$key]
: '';
}
}
}
namespace {
if (! function_exists('mac_format_datetime')) {
/**
* Format a preset date/time value for Bricks or templates.
*/
function mac_format_datetime(
?string $preset = null,
?string $view = null,
int|string|null $post_id = null
): string {
return \MacCore\Utils\FormatDatetime::format($preset, $view, $post_id);
}
}
}