Skip to content
This repository has been archived by the owner on Oct 1, 2023. It is now read-only.

Implement date/time #93

Open
fredemmott opened this issue Dec 4, 2019 · 1 comment
Open

Implement date/time #93

fredemmott opened this issue Dec 4, 2019 · 1 comment

Comments

@fredemmott
Copy link
Contributor

Dates & Times in the Hack Standard Library

This document is in-progress; the design of the library is subject to change at any time.

Goal

This document outlines the design of the Time namespace in the HSL snip. The goal of this namespace is to provide an ergonomic and predictable API for operating on dates/times that helps developers fall into the pit of success.

Absolute Time

Abstractions

An absolute time is a point on an absolute timeline, represented by a single timestamp in nanoseconds. Intervals between timestamps are a single value in nanoseconds. There are two types of absolute times in the HSL:

  • Monotonic time: the number of nanoseconds elapsed since some OS-defined starting point. This is useful for doing local computations, such as timing a function call.
  • Unix time: the number of nanoseconds elapsed since 00:00:00 1 January 1970 (UTC). This is useful for doing non-local computations, e.g. using data provided by another service.

In the HSL, an absolute time is represented by the Time\Timestamp opaque type alias, which has a Time\Clock type parameter to distinguish between monotonic and Unix time. An interval is represented as the Time\Interval opaque type alias. Note that intervals and timestamps may be negative.

type Nanoseconds = int; // Purely for documentation purposes.

newtype Clock = mixed;
newtype Mono as Clock = Clock;
newtype Unix as Clock = Clock;
newtype Timestamp<Tc as Clock> = (Tc, Nanoseconds);
newtype Interval as Nanoseconds = Nanoseconds;

const Mono CLOCK_MONO = 1;
const Unix CLOCK_UNIX = 2;

These timestamps help the developer fall into the pit of success because:

  • Using nanoseconds allows for high-precision timestamps without potential rounding errors (e.g. if we were to use seconds and floating-point numbers).
  • They enforce that any arithmetic on absolute times always uses the same unit.
  • They enforce that it is impossible (or at least, fairly difficult) to mix up timestamps and intervals during arithmetic.

API

/**
 * Returns a Timestamp representing the current
 * monotonic time, i.e. the number of nanoseconds elapsed
 * since some consistent internal point.
 */
function now(): Timestamp<Mono>;

/**
 * Returns a Timestamp representing the current
 * Unix time, i.e. the number of nanoseconds elapsed
 * since 00:00:00 1 January 1970 (UTC).
 */
function now_unix(): Timestamp<Unix>;

/**
 * Returns a Timestamp representing the given number of nanoseconds elapsed
 * since 00:00:00 1 January 1970 (UTC).
 */
function timestamp_ns(
  int $nanoseconds,
): Timestamp<Unix>;

/**
 * Returns a Timestamp representing the given number of seconds elapsed
 * since 00:00:00 1 January 1970 (UTC).
 */
function timestamp_s(
  int $seconds,
): Timestamp<Unix>;

/**
 * Returns an Interval representing the elapsed
 * time given in nanoseconds.
 */
function interval_ns(
  int $nanoseconds,
): Interval;

/**
 * Returns an Interval representing the elapsed
 * time given in seconds.
 */
function interval_s(
  int $seconds,
): Interval;

/**
 * Returns an Interval representing the time
 * elapsed since the given Timestamp.
 *
 * - Throw Exception if time is in the future?
 */
function since<Tc as Clock>(
  Timestamp<Tc> $timestamp,
): Interval;

/**
 * Returns an Interval representing the time
 * remaining until the given Timestamp.
 *
 * - Throw Exception if time is in the past?
 */
function until<Tc as Clock>(
  Timestamp<Tc> $timestamp,
): Interval;

/**
 * Returns an Interval representing the time
 * elapsed between the given Timestamps.
 */
function between<Tc as Clock>(
  Timestamp<Tc> $start_timestamp,
  Timestamp<Tc> $end_timestamp,
): Interval;

/**
 * Returns the result of adding the given
 * Interval to the given Timestamp.
 */
function add<Tc as Clock>(
  Timestamp<Tc> $timestamp,
  Interval $interval,
): Timestamp<Tc>;

/**
 * Returns the result of subtracting the given
 * Interval from the given Timestamp.
 */
function subtract<Tc as Clock>(
  Timestamp<Tc> $timestamp,
  Interval $interval,
): Timestamp<Tc>;

/**
 * Returns whether `$timestamp1` is after `$timestamp2`.
 */
function is_after<Tc as Clock>(
  Timestamp<Tc> $timestamp1,
  Timestamp<Tc> $timestamp2,
): bool;

/**
 * Returns whether `$timestamp1` is before `$timestamp2`.
 */
function is_before<Tc as Clock>(
  Timestamp<Tc> $timestamp1,
  Timestamp<Tc> $timestamp2,
): bool;

/**
 * Returns whether the given timestamp is between the given lower and upper
 * upper bounds, exclusive.
 */
function is_between<Tc as Clock>(
  Timestamp<Tc> $timestamp,
  Timestamp<Tc> $lo_timestamp,
  Timestamp<Tc> $hi_timestamp,
): bool;

Implementation

The majority of functions handling the monotonic time will be implemented in Hack, with the exception of Time\now(), which will call into the clock_gettime_ns() API provided by HHVM to obtain the current time in nanoseconds.

Local Time

Abstractions

Local times are different from absolute times in that they also contain time zone information, expressly for the purpose of representing times in a human-readable format. Local times are represented by an immutable Time\DateTime object, which contains a timestamp and a time zone.

Time zones in Hack are represented by the Time\TimeZone opaque type alias. To obtain a time zone, (TODO: Represent as object or as an opaque type alias to string?) There are three ways of identifying time zones:

  1. Identifying. These take the form of a named region, e.g. America/New_York.
  2. Offset. These take the form of an offset from UTC, e.g. +0400 or -05:00.
  3. Aliased. PHP also supports time zone aliases like “EDT” and “PST”; but the Hack Standard Library will not support these aliases.

These abstractions help the developer fall into the pit of success because:

  • Making Time\DateTime immutable prevents classes of bugs where dates are inadvertently mutated away from the construction site.
  • Disallowing aliased time zones prevents the developer from accidentally using “PDT” when they are actually in “PST”, for example.

TODO: Similar to the re'...' string in Regex, could we have a dt'...' string to validate datetime formatting? Maybe this requires a generalization of the FormatString abstraction that we've hacked into Hack.

Construction API

TBD: This API is subject to change at any time! See design meeting notes for more information.

final class DateTime {

  private function __construct(
    public TimeZone $time_zone,
    public int $year,
    public int $month,
    public int $day,
    public int $hour = 0,
    public int $minute = 0,
    public int $seconds = 0,
    public int $milliseconds = 0,
    public int $microseconds = 0,
    public int $nanoseconds = 0,
  );

  public function getTimestamp(): Timestamp<Unix>;

  /**
   * Returns a new DateTime constructed with the
   * given Unix timestamp.
   */
  public static function fromTimestamp(
    TimeZone $time_zone,
    Timestamp<Unix> $timestamp,
  ): DateTime;

  public function fromLocalDateTime(
    TimeZone $timezone,
    LocalDateTime $datetime,
  ): (reason, DateTime);

  public function fromLocalDateTimeX(
    TimeZone $timezone,
    LocalDateTime $datetime,
  ): DateTime;

  /**
   * Returns a new DateTime constructed with the
   * given format string and date.
   *
   * - Can we restrict the types of formats the function accepts? Should we?
   */
  public static function fromFormattedString(
    TimeZone $time_zone,
    DateTimeFormat $format,
    string $formatted_date,
  ): DateTime;

  /**
   * Returns the DateTime as a string formatted per
   * the given format string.
   */
  public function format(
    DateTimeFormat $format,
  ): string;

}

Transformation API

There are two ways to transform a DateTime object: setting (setting individual calendar/clock values) and shifting (i.e. adding or subtracting a time interval). Note that “transformation” in this context means returning a new immutable DateTime based on the one we're “mutating.”

Setting consists of setting individual values (e.g. year, day, hour, minute, etc.). When setting, the clock time of the resultant DateTime will be preserved to best of the library's ability. In certain situations, a resultant DateTime may not exist, such as when mutating across DST boundaries. In this case, the user must choose the proper API: one that returns the nearest valid DateTime along with a reason for its invalidity, or one that throws. The API is as follows:

public function withNanosecond(int $nanosecond): (reason, this);
public function withMicrosecond(int $microsecond): (reason, this);
public function withMillisecond(int $millisecond): (reason, this);
public function withSecond(int $second): (reason, this);
public function withMinute(int $minute): (reason, this);
public function withHour(int $hour): (reason, this);
public function withDay(int $day): (reason, this);
public function withMonth(int $month): (reason, this);
public function withYear(int $year): (reason, this);
public function withDate(int $year, int $month, int $day): (reason, this);
public function withTime(int $hour, int $minute, int $second, ...): (reason, this);

public function withNanosecondX(int $nanosecond): this;
public function withMicrosecondX(int $microsecond): this;
public function withMillisecondX(int $millisecond): this;
public function withSecondX(int $second): this;
public function withMinuteX(int $minute): this;
public function withHourX(int $hour): this;
public function withDayX(int $day): this;
public function withMonthX(int $month): this;
public function withYearX(int $year): this;
public function withDateX(int $year, int $month, int $day): this;
public function withTimeX(int $hour, int $minute, int $second, ...): (reason, this);

public function withTimezone(Timezone $timezone): this; // Updates clock timer

Shifting consists of adding or subtracting nanoseconds to/from the object's Unix timestamp. When shifting, mutations may change the clock time of the resultant DateTime. This means that each shifting API is guaranteed to return a valid DateTime, so we don't need different versions of them. Sometimes, this behavior may be undesirable, e.g. if a user shifts forward by one day on a DateTime representing the day before DST, the hour of the resultant DateTime would be different. In this case, users can use the setter API to preserve clock time. The API is as follows:

public function plusNanoseconds(int $nanoseconds): this;
public function plusMicroseconds(int $microseconds): this;
public function plusMilliseconds(int $milliseconds): this;
public function plusSeconds(int $seconds): this;
public function plusMinutes(int $minutes): this;
public function plusHours(int $hours): this;
public function plusDays(int $days): this;
public function plusWeeks(int $weeks): this;
// Omitted, because they vary in length.
// Users may choose to shift by however many days or use a set API.
// public function plusMonths(int $months): this;
// public function plusYears(int $years): this;

public function minusNanoseconds(int $nanoseconds): this;
public function minusMicroseconds(int $microseconds): this;
public function minusMilliseconds(int $milliseconds): this;
public function minusSeconds(int $seconds): this;
public function minusMinutes(int $minutes): this;
public function minusHours(int $hours): this;
public function minusDays(int $days): this;
public function minusWeeks(int $weeks): this;
// Omitted, because they vary in length. See above.
// public function minusMonths(int $months): this;
// public function minusYears(int $years): this;

// Enables "Next Tuesday at 2:30pm"-style mutations.
// Example: ->nextDay(Time\TUESDAY)->setTime(14, 30);
// TODO: DST can probably mess this up, make sure to test for it.
public function nextDay(Day $day, int $nth = 1): this;
public function nextMonth(Month $month, int $nth = 1): this;
public function prevDay(Day $day, int $nth = 1): this;
public function prevMonth(Month $month, int $nth = 1): this;

Implementation

TBD - See Design Meeting Notes for up-to-date information. We'd like to implement as much of the library in Hack as possible. Ideally, most of the Time\DateTime class could be written in Hack, and dispatch to the runtime only when formatting or transformation is needed (since we will need to use the timelib library for that).

Timezone-Agnostic Local Time

Oftentimes, developers want to work with the concept of a date/time free from the notion of a time zone. In that case, there is a LocalDateTime class, which has a similar API to that of DateTime, but does not take a time zone. It may be converted to a DateTime by adding a time zone via the toDateTime method. It will support the same format API as DateTime, minus the time zone portion.

API

final class LocalDateTime {

  public function __construct(
    public TimeZone $time_zone,
    public int $year,
    public int $month = 1,
    public int $day = 1,
    public int $hour = 0,
    public int $minute = 0,
    public int $seconds = 0,
    public int $milliseconds = 0,
    public int $microseconds = 0,
    public int $nanoseconds = 0,
  );

  public function fromDateTime(
    DateTime $datetime,
  ): this;
}

Transformation API

The LocalDateTime transformation API will be identical to that of DateTime, in that it will have shifting and setting, the only difference being the lack of uncertainty regarding time zones. However, because the user may still specify an invalid date (e.g. February 29th on a non-leap year), the setting APIs will still need two versions.

Dependencies

Because opaque type aliases are only transparent in the file in which they're defined, it may be impossible to follow the HSL convention of splitting the library up into files. We may want to consider implementing a feature that allows opaque type aliases to be transparent within an entire namespace, especially if the Time\DateTime class ends up being implemented in the runtime.

Use Cases

snip examples

Comparing Against Fields of the User's Local Time

The developer simply wants the current hour in the user's time zone. We could construct a DateTime object, and get the hour as a getter.

$hour = Time\DateTime::now(Time\fb\user_timezone($user_id))->getHour();
if ($window['start'] < $window['stop']) {
  return $hour >= $window['start'] && $hour < $window['stop'];
} else {
  return $hour >= $window['start'] || $hour < $window['stop'];
}

Literally... Using Timezones

The developer wants to format a timestamp according to a particular timezone, and has to temporarily update global state to do so. Here, we can just construct a DateTime and call its format method.

return Time\DateTime::fromTimestamp($timezone, $timestamp)
  ->format(...);

Getting the Period Between DateTimes

Here, the developer wants to know the number of hours until midnight the following day. We can construct two DateTimes representing the two times, and get the period between them.

$tz = new DateTimeZone('America/Los_Angeles');
$now = Time\DateTime::now($tz);
$next_midnight = $now
  ->plusDays(1)
  ->withTimeX(0, 0, 0);
$hours = $now->periodUntil($next_midnight)->getHours();

References

Possible Inspirations

Known PHP DateTime bugs

  • calls to \DateTime's setTimestamp or getTimestamp after a time zone has been set can result in the object's timestamp to be different than expected
  • changing a \DateTime's time zone can result in the object's timestamp changing; changing a time zone should NEVER change the timestamp, only the date or time fields

Design Meeting Notes

09 Feb 2018

Object/Data Relationships

  • PHP DateTime object
    • contains HHVM DateTime
      • contains timelib_time
        • contains timelib_tzinfo
      • contains HHVM TimeZone object
        • contains [timelib_tzinfo](https://github.com/derickr/timelib/blob/master/timelib.h#L146)

Summary

  • Internal representation of DateTime is UTC Timestamp + Timezone
  • Timezone is serialized as the name: “America/NY”
  • DateTime can be serialized as UTC timestamp (requires user to pass in a TimeZone to re-hydrate) or CalendarTime + Timezone
  • We must separate mono time + unix time
  • How to handle leap seconds? If I create an event for five years from now, will a leap second distort the time of the event?
    • In mono time, they don't exist
    • For Clock Time, it depends on the representation.
  • Keep using timelib for compatibility with the rest of FB

TODO:

  • How to be predictable when mutating across time zones or across irregular points like “spring forward” and “fall back”?
    • Return null?
    • Throw exception? If Exceptions, what Exceptions should DateTime throw?
  • Missing APIs like “next Tuesday at 2:00pm”, possibly generators for “every following Tuesday at 2:00pm”. What if such a DateTime doesn't exist or exists twice?
  • How to design an efficient “mutation” API given that the object is immutable?
    • Maybe a single batch function?
    • Maybe a builder?
  • If we want to be consistent with the absolute portion of the library, we should target nanosecond precision, but can timelib support/format that? Do we care about formatting ability at that level of precision?
  • Do we want to require that the user pass in a TimeZone every time? Or should it default to the system time zone? If the former, we should have a convenience function like Time\current_time_zone(). I'm currently leaning toward explicit.

Ben's Sketch

  • In this version Unix, Local, Mono and Zoned times are all represented via types not classes. This is nice for consistency — we don't want Mono and Unix to be class based. But it does make the APIs less ergonomic. Long term my hope is that Hack might implement C#-like value types to allow for class-like semantics. We should design this API to be amenable to being codemoded to a class representation
  • The perf of accessing day/month/year might not be ideal in this implementation since that would have to be recalculated on every access.
type Nanoseconds = int; // Purely for documentation purposes.

newtype Clock = mixed;

newtype CalendarBasedClock = Clock;

// POSIX monotonic time
newtype Mono as Clock = Clock;

// POSIX realtime Clock time
newtype Unix as CalendarBasedClock = CalendarBasedClock;

// Represents the concept of X milliseconds since the unix epoch
// in some undefined timezone
newtype Local as CalendarBasedClock = CalendarBasedClock;

// I removed Tc from the tuple,  not sure why it's needed
newtype Timestamp<Tc as Clock> = Nanoseconds; 

type MonoTime = Timestamp<Mono>;
type UnixTime = Timestamp<Unix>;
type LocalTime = Timestamp<Local>;

// Represents the instant at the given UnixTime in the given timezone
// As an invariant, the local time of that unix time given current
// timezone rules must equal the local time.
//
// Users will serialize only one of UnixTime and LocalTime
// depending on if they want to represent a fixed instant (eg the time of a solar
// eclipse) or a fixed local time (eg POTUS is inaugrated at noon on January 20th
// of a given year, eastern time regardless of changes in timezone rules)
newtype ZonedTime = (UnixTime, LocalTime, Timezone);

newtype Duration as Nanoseconds = Nanoseconds;

const Mono CLOCK_MONO = 1; // TODO: should use the same constants as clock_gettime
const Unix CLOCK_UNIX = 2;
const Local CLOCK_LOCAL = 2;

function unix_now(): UnixTime;
function mono_now(): MonoTime;

function localtime(
   int $year,
   int $month = 0,
   int $day = 0,
   int $hour = 0,
   int $minute = 0,
   int $second = 0,
   int $nanos = 0): Local;

// Copied from ZonedDateTime.of in java 8
//
// In most cases, there is only one valid offset for a local date-time.
//
// In the case of an overlap, when clocks are set back, there are two valid offsets.
// This method uses the earlier offset typically corresponding to "summer".
//
// In the case of a gap, when clocks jump forward, there is no valid offset.
// Instead, the local date-time is adjusted to be later by the length of the gap.
// For a typical one hour daylight savings change, the local date-time will be
// moved one hour later into the offset typically corresponding to "summer".
function localToZoned(LocalTime $t, Timezone $z): ZonedTime;

// Returns 0, 1 or 2 possible times for this LocalTime in this timezone for the
// gap, normal and overlap cases respectively. The pair is sorted with nulls last

// Alternative representation:
// https://github.com/google/cctz/blob/master/include/cctz/time_zone.h#L123
// This is a google timezone library which represents these lookups using:
// (type, pre, trans, post)
// Where type = UNIQUE / SKIPPED REPEATED
// pre/post = a version of the time with the same minute/second before/after the
// transition
// trans = the transition point 
function localToZonedStrict(LocalTime $t, Timezone $z): (?ZonedTime, ?ZonedTime);

// The instant $t represented in timezone $z
function unixToZoned(UnixTime $t, Timezone $z): ZonedTime;

// Convert the same instant in $t to a timezone $z
function zonedToZoned(ZonedTime $t, Timezone $z): ZonedTime;
function zonedToUnix(ZonedTime $t, Timezone $z): UnixTime;
function zonedToLocal(ZonedTime $t, Timezone $z): LocalTime;

// "add one day" "this date but on monday"
function addUnits<Tc as CalendarBasedClock>(Tc $tm, UnitType $unit, int $value): Tc;
function subUnits<Tc as CalendarBasedClock>(Tc $tm, UnitType $unit, int $value): Tc;
function setField<Tc as CalendarBasedClock>(Tc $tm, UnitType $unit, int $value): Tc;
function getField<Tc as CalendarBasedClock>(Tc $tm, UnitType $unit): int;

function adjustZonedByLocal(
  ZonedTime $t,
  (function (LocalTime): LocalTime) $adj): ZonedTime;
  
function adjustZonedByUnix(
  ZonedTime $t,
  (function (UnixTime): UnixTime) $adj): ZonedTime;
  


Before and after

  // Get the current hour in user's timezone
  $hour = (int) PHP\date('H', timetozone($time, $user)));
  
  $hour = getField(UnitType::HOUR, zonedTimeForUser($time, $user))
 

16 Feb 2018

Last Week's Questions

  • How to be predictable when mutating across time zones or across irregular points like “spring forward” and “fall back”?
    • Return null?
    • Throw exception? If Exceptions, what Exceptions should DateTime throw?
  • Missing APIs like “next Tuesday at 2:00pm”, possibly generators for “every following Tuesday at 2:00pm”. What if such a DateTime doesn't exist or exists twice?
  • How to design an efficient “mutation” API given that the object is immutable?
    • Maybe a single batch function?
    • Maybe a builder?
  • If we want to be consistent with the absolute portion of the library, we should target nanosecond precision, but can timelib support/format that? Do we care about formatting ability at that level of precision?
  • Do we want to require that the user pass in a TimeZone every time? Or should it default to the system time zone? If the former, we should have a convenience function like Time\current_time_zone(). I'm currently leaning toward explicit.

Summary

  • How to be predictable when constructing/mutating DateTimes that don't exist.
    • Suggestion: two APIs.
      • One returns a single time, comes up with a reasonable time (“approximate”)
        • There's a danger if you use it in iteration, e.g. a week at a time. Messes up iteration across timezone shifts. Gets worse over years. Call it out in documentation.
        • Recurrence class, or a generator API.
      • Other: Multiple times, all possible interpretations (“strict”, “detailed”
    • Validator API: Input date/time timezone, returns a boolean?
    • OR: DateTime constructor private, constructor APIs return a tuple.
  • How to design a “next Tuesday at 2:00pm” API?
    • Setting values is different from “shifting” value, so probably should be different APIs
    • How about a lazy builder esque API, when has the same constructor functions as DateTime.
    • Helpful to have different ways of getting to a time. Add, subtract, etc. vs. Go to.
      • Difference is that the user defines the delta in one, the endpoint in the other.
      • add/subtract vs. withHour, withDay, etc.
      • Different builders for shifters and setters.
    • Should avoid ambiguous combinations of calls
      • Last one wins?
  • How to design an efficient mutation API?
    • Mutation APIs on the DateTime object returns a builder that needs to be materialized into the next DateTime
  • Do we want to require that the user pass in a TimeZone every time?
    • Yes
    • But only allow it in CLI mode? FB-only restrictions?

Questions for next time?

  • Is there a next time?
    • Lets work on diffs first
    • Will pre-emptively book the room 2 weeks in advance, we can free it if we want.
  • What's the interface between Hack and HHVM?
  • Determining the full range of timelib.
  • Let's make sure that the HHVM extension is only available to the HSL.
  • But HHVM: Is there any long term hope of value classes? It influences how we'd think about the API.
    • e.g. if constructing a bunch of objects is a perf issue, value classes would mitigate that.

01 Mar 2018

  • Formatting
    • Probably some format specifiers we shouldn't allow
    • How do we localize? Let's talk to someone from I18N to figure out if it's possible to format dates in different locales.
    • Should we support constructing from a formatted string?
      • Maybe only LocalDateTime, so you don't deal with timezones.
      • How to deal with malformed strings? Monday, (and it's not a Monday).
    • Probably worth another meeting.
  • Internal Representation
    • LocalDateTime: “Local timestamp”, essentially a serialized clock time
    • DateTime: UTC timestamp + timezone (innards)
  • Timezones
    • Leaning toward representing as immutable objects
    • Two types of timezones: Identified and Offset
      • Shouldn't really have user visible behavior, but allows other APIs to only accept one or the other.
  • Let's continue via diffs, at least the interfaces. And exploring what the userland+native boundary will be.
    • This will be weird with HSL since we'll need the objects to be in an HHVM extension, which will be separate from the HSL repository.
    • If we can only use native functions, we'll be okay.
  • Need an introspection API as well. Folks don't just create and mutate, they want to know differences between times. Joda Time represents this as a Period object.

06 April 2018

  • Formatting
    • snip
    • Can a general localization API be built?
      • See how C++ and Java does it.
      • Takes a few generic arguments in a skeleton
      • Existing PHP API that's supposed to do this, but it doesn't take skeletons, it gives limited options for common patterns
      • snip
    • For standard library, format is a method
      • Will use the same formatting at strftime
      • Localized formatting will be a separate method that takes a limited set of patterns that ICU understands
      • Open Question: Why should they be methods? It seems hacky to snip, could do plain functions instead.
    • The legacy formatting will be a separate function
      • Doesn't even use timelib, we literally iterate on every character
    • Create from format
      • Should support, but require either Timezone passed-in or as trailing data in the string
@lexidor
Copy link
Contributor

lexidor commented Jun 29, 2020

I am looking forward to having a more typesafe version of \DateTime.
The properties of an immutable object should not be modifiable.
The properties in this API are public.
Does this rely on <<__Const>> to protect against accidental mutation?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants