Skip to main content

Format Datetime

PropertyValue
descriptionFormat preset-based datetime fields for Bricks and WordPress templates.
tagslib, php, wp, oop
rating

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 like Today, Tomorrow, and Yesterday.
  • plain_diff: plain lifecycle text like Starts in 3 days, Ends in 2 hours, or Ended 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 like Today, Tomorrow, and Yesterday.
  • 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 like Starts 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

<?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;
}
}