Skip to content

ADR: Date Formatting and Timezones

Status

Accepted (May 2024)

This has been merged into master and released as part of the timezone/due-date-to-end-date release. All future handling of dates and times MUST follow this ADR.

Context

The Gantt feature has led to more awareness of our currently handling of dates/times and a greater need for consistency.

Previously, formatting of dates and times was done in different ways in different places in the app. The app also used timezones inconsistently. Some places used the browser's timezone, others used the server timezone, others used the member setting.

Decision

API

Times in API responses: - MUST be ISO 8601 formatted - MUST be in UTC and formatted in Zulu time, e.g. 2022-11-13T08:51:02Z

Times in API parameters: - MUST be ISO 8601 formatted - MAY be in UTC and formatted in Zulu time, e.g. 2022-11-13T08:51:02Z (milliseconds are optional)

Dates in API parameters: - MUST be ISO 8601 formatted excluding the time, i.e. Y-m-d (e.g. 2022-01-03)

Dates in API responses: - MUST be ISO 8601 formatted excluding the time

Backend

Database storage: - Times in the database MUST be stored as timestamps in UTC

Function parameters & return types: - Function parameter & return types that are a date MUST be CarbonImmutable (not Carbon, int, or string)

Output formatting: - Whenever a time is output (e.g. in a template or email), it MUST be done so specifying a timezone (that is, the developer must explicitly specify the timezone, not that the timezone must be visible to the user). It MUST NOT default to the server timezone - Formatting of dates SHOULD be done on the frontend or in phtml templates, not in models/controllers/API responses

DateTimeFormatter methods:

There are four formatting methods in DateTimeFormatter to help that will output the date/time to the correct timezone and format. These methods SHOULD be used whenever a date is formatted:

// Member has timezone set to "Australia/Perth"
// Organisation has timezone set to "America/New_York"

$time = CarbonImmutable::parse("2024-01-01T00:00:00Z")->utc();

$dateTimeFormatter->memberDate($time);
// 2024-01-01

$dateTimeFormatter->memberDateTime($time);
// 2024-01-01 08:00

$dateTimeFormatter->organisationDate($time);
// 2023-12-31

$dateTimeFormatter->organisationDateTime($time);
// 2023-12-31 19:00

Note that $dateTimeFormatter is always available in phtml templates, you will almost never have to instantiate DateTimeFormatter manually.

TypedController parsing:

On the backend, TypedController handles parsing ISO 8601 dates and date-times automatically, e.g.:

A request with ISO 8601 params:

{
  "dateWithTime": "2022-11-13T08:51:02Z",
  "dateWithoutTime": "2022-11-15"
}

Controller:

class SomeController extends Controller
{
    use TypedController;

    public function invoke(CarbonImmutable $dateWithTime, DateOnly $dateWithoutTime): void
    {
        // Both $dateWithTime and $dateWithoutTime will be set by
        // TypedController based on their type without any manual parsing
        ...
    }
}

Frontend

Output formatting: - Whenever a time is output, it MUST be done so specifying a timezone (that is, the developer must explicitly specify the timezone, not that the timezone must be visible to the user). They MUST NOT default to the browser timezone

dateHelper methods:

There are four formatting methods in dateHelper to help that will output the date/time to the correct timezone and format. These methods SHOULD be used whenever a date is formatted:

import { formatDate } from "@js/utils/dateHelper";

// Member has timezone set to "Australia/Perth"
// Organisation has timezone set to "America/New_York"

// const date = dayjs("2024-01-01T00:00:00.000Z");

formatDate.memberDate(date);
// 2024-01-01

formatDate.memberDateTime(date);
// 2024-01-01 08:00

formatDate.organisationDate(date);
// 2023-12-31

formatDate.organisationDateTime(date);
// 2023-12-31 19:00

These helper methods all accept dayjs objects, date objects, timestamps (with or without milliseconds), ISO8601 strings, or strings in the user's preferred format. E.g. all of the following will work:

// Timestamp in seconds
formatDate.memberDateTime(1710858000);

// Timestamp in milliseconds
formatDate.memberDateTime(1710858000 * 1000);

// Date object
formatDate.memberDateTime(new Date(1710858000 * 1000));

// Dayjs object
formatDate.memberDateTime(dayjs(1710858000 * 1000));

// ISO8601 string
formatDate.memberDateTime("2024-03-19T14:20:00.000Z");

// User format string
formatDate.memberDateTime("19 Mar 2024");

Consequences

This allows developers to format dates and times in a consistent way, without having to reinvent the wheel every time we need to format a date or convert it into a specific timezone.

Impact

Medium

Driver

@Marc North

Contributors

[Team]

Accepted Date

May 2024

Resources

Last modified by: Unknown