Introduction

Dealing with dates and times in programming is often thought to be simple. It isn’t. Time zones are already difficult enough. But there are daylight savings time, leap years and leap seconds. Things can get very complicated.

Some programming languages make their users happy (or unhappy) by allowing them to schedule meetings on 2021-02-31 08:00 UTC. Other programming languages complain when they are told to create dates from invalid input.

The purpose of this little article is to see how some programming languages behave when they are asked to create some data structure representing the date 2021-02-31 or the time 2021-02-31 08:00 UTC. (I don’t want to deal with time zones.)

I am not an expert in most of the programming languages i will be talking about. If you find an error, or if I missed some interesting aspect, or if I oversimplified and created a wrong impression, please let me know.

This is not a rating of programming programming languages. It is not my intention to say that one language handles dates and times better than some other language.

When I started to work on this article, I believed that it should not take too long to have a quick look at the documentation of each language and write down a few examples. I believed that in each language there would be only one way for dealing with dates and times and that this way should be easy to find. Not true. The more I read, the more I wondered whether what I wrote down was actually an idiomatic way of dealing with dates and times in the respective languages.

The examples below are merely examples. They show one way for creating dates and times. But this may not even be an idiomatic way.

Elixir

Elixir has a notion of dates, times, and datetimes (i.e. timestamps). Dates can be created using Date.new/3, times can be created using Time.new/3 and timestamps can be created using DateTime.new/4.

defmodule ImpossibleDates do
  def examples do
    # Elixir Date.new/3 with valid input
    valid_date = Date.new(2021, 1, 15)

    IO.inspect(valid_date) # prints {:ok, ~D[2021-01-15]}

    # Elixir Date.new/3 with invalid input
    invalid_date = Date.new(2021, 2, 30)

    IO.inspect(invalid_date) # prints {:error, :invalid_date}
  end
end

Creating a timestamp requires a date and a time.

defmodule ImpossibleTimes do
  def examples do
    # Elixir DateTime.new/2 with valid input
    {:ok, date} = Date.new(2021, 1, 15)
    {:ok, time} = Time.new(8, 0, 0)
    valid_timestamp = DateTime.new(date, time)

    IO.inspect(valid_timestamp) # prints {:ok, ~U[2021-01-15 08:00:00Z]}
  end
end

When no time zone is specified in the DateTime.new/4 function, Elixir assumes UTC.

There is no way to write down the code that would lead to an invalid timestamp because we would need to pass in an invalid date. And, as we have seen, we cannot create invalid dates.

Erlang

Erlang is an interesting language with respect to date and time manipulation. Dates and times are not constructed using some method or function. They are simply data structures.

A date value is represented by a tuple consisting of year, month, and date. The documentation of the calendar module states that “the date tuple must denote a valid date”.

A time value is represented by a tuple consisting ot hour, minute, and second.

A timestamp is represented by a tuple consisting of a date and a time. There is no room for a time zone in this data structure. Such a timestamp represents a time in a timezone that is implicit to the application.

Creating valid and invalid dates is exceptionally simple.

-module(impossible_dates).

-export([examples/0]).

examples() ->
    %% Erlang valid date as tuple
    ValidDate = {2021, 1, 15},

    io:format("~p~n", [ValidDate]), % prints {2021,1,15}

    %% Erlang invalid date as tuple
    InvalidDate = {2021, 2, 30},

    io:format("~p~n", [InvalidDate]). % prints {2021,2,30}

Erlang provides the calendar:valid_date/1 and calendar:valid_date/3 functions to validate that a given date is actually valid.

Let’s see what happens if we attempt to do artihmetic with invalid dates. We will try to add one day to 2021-1-15 and to 2021-2-30.

-module(date_arithmetics).

-export([examples/0]).

examples() ->
    %% Erlang valid date as tuple
    ValidDate = {2021, 1, 15},
    ValidDatePlus1 =
        calendar:gregorian_days_to_date(
          calendar:date_to_gregorian_days(ValidDate) + 1
        ),

    io:format("~p~n", [ValidDatePlus1]), % prints {2021,1,16}

    %% Erlang invalid date as tuple
    InvalidDate = {2021, 2, 30},
    try
        InvalidDatePlus1 =
            calendar:gregorian_days_to_date(
              calendar:date_to_gregorian_days(InvalidDate) + 1
            ),
        io:format("~p~n", [InvalidDatePlus1])
    catch
        error:Error -> io:format("~p~n", [Error]) % prints if_clause
    end.

The error is not particularly enlightening, but making Erlang errors more readable is not the subject of the current article.

There is nothing that distinguishes some tuple from a tuple representing a date. {2021, 2, 30} is a perfectly fine tuple but it is simply not a representation of a date.

Valid timestamps depend on valid dates. There is no need to try to create a timestamp representing 2021-02-30 08:00. Of course, we can easily write down a tuple of tuples such as

{{2021, 2, 30}, {8, 0, 0}}

and insist that this is a representation of an invalid timestamp. Whether this is true is probably a philosophical question. (And I would argue that this not a representation of a timetamp at all.)

Erlang is a little bit unusual in this list of programming languages since dates and timestamps are created by creating the corresoponding representation without the help of any constructor functions.

Haskell

Everything I have learned about dates and times in Haskell is summarized in the Haskell Time Library Tutorial.

Haskell comes with the Data.Time module which provides the fromGregorian function. fromGregorian takes three arguments: the year, the month, and the day. Months are counted from 1. fromGregorian returns a value of type Day.

import Data.Time

-- Haskell fromGregorian with valid input
validDate :: Day
validDate = fromGregorian 2021 1 15

-- Haskell fromGregorian with invalid input
invalidDate :: Day
invalidDate = fromGregorian 2021 2 30

main :: IO ()
main = do
    putStrLn $ show validDate -- prints 2021-01-15
    putStrLn $ show invalidDate -- prints 2021-02-28

Really? Just return the last day of the month? This is surprising enough to dig a little bit deeper. I was expecting Haskell to fight against invalid data. And, indeed, there is a fromGregorianValid function that works similarly to fromGregorian but returns a Maybe Day value.

import Data.Time

-- Haskell fromGregorianValid with invalid input
maybeInvalidDate :: Maybe Day
maybeInvalidDate = fromGregorianValid 2021 2 30

main :: IO ()
main = do
    putStrLn $ show maybeInvalidDate -- prints Nothing

UTC timestamps in Haskell are represented by values of the UTCTime type. And UTCTime values are built from a Day value and an offset that stores how many seconds of the given day have passed.

Creating UTCTime values is easy when using fromGregorian since fromGregorian returns a Day value.

Things become somewhat more complicated when using fromGregorianValid to construct a Day value because fromGregorianValid returns a Maybe Day value.

import Data.Time

-- Haskell fromGregorian with invalid input
validUtcTime :: UTCTime
validUtcTime =
  UTCTime (fromGregorian 2021 2 30) (8 * 60 * 60)

-- Haskell fromGregorianValid with valid input
maybeValidUtcTime :: Maybe UTCTime
maybeValidUtcTime =
  case (fromGregorianValid 2021 1 15) of
    Nothing -> Nothing
    Just day -> Just $ UTCTime day (8 * 60 * 60)

-- Haskell fromGregorianValid with invalid input
maybeInvalidUtcTime :: Maybe UTCTime
maybeInvalidUtcTime =
  case (fromGregorianValid 2021 2 30) of
    Nothing -> Nothing
    Just day -> Just $ UTCTime day (8 * 60 * 60)

main :: IO ()
main = do
  putStrLn $ show validUtcTime -- prints 2021-02-28 08:00:00 UTC
  putStrLn $ show maybeValidUtcTime -- prints Just 2021-01-15 08:00:00 UTC
  putStrLn $ show maybeInvalidUtcTime -- prints Nothing

Java

The Java SE 8 Date and Time documentation seems to be a useful introduction to date and time handling in Java. (I know that Java 8 is not the latest version.)

Java ships with the LocalDate class whose purpose is to represent dates without time zones. Exactly what we want for the first test.

Dates can be created using the LocalDate.of method, passing in year, month, and day. Months are counted from 1, so 1 corresponds to January and 12 corresponds to December.

import java.time.LocalDate;

class ImpossibleDates {
    public static void main(String[] args) {

        // Java LocalDate.of with valid input
        LocalDate validDate = LocalDate.of(2021, 1, 15);

        System.out.println(validDate); // prints "2021-01-15"

        // Java Localdate.of with invalid input
        try {
            LocalDate invalidDate = LocalDate.of(2021, 2, 30);
            System.out.println(invalidDate);
        }
        catch(Exception e) {
            System.out.println(e); // prints java.time.DateTimeException: ...
        }
    }
}

Instances of the LocalDateTime class represent timestamps in local time. They do not have any notion of a time zone (so they are not too useful when trying to create an object that represents 2021-02-31 08:00 UTC. Fortunately, there is the ZonedDateTime class that adds time time zones to LocalDateTime.

import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;

class ImpossibleTimes {
    public static void main(String[] args) {

        // Java LocaldateTime.of with valid input
        LocalDateTime validLocalTime = LocalDateTime.of(2021, 1, 15, 8, 0, 0);
        ZonedDateTime validZonedTime =
            ZonedDateTime.of(validLocalTime, ZoneId.of("UTC"));
        System.out.println(validZonedTime); // prints 2021-01-15T08:00Z[UTC]

        // Java LocaldateTime.of with invalid input
        try {
            LocalDateTime invalidLocalTime = LocalDateTime.of(2021, 2, 30, 8, 0, 0);
            ZonedDateTime invalidZonedTime =
                ZonedDateTime.of(invalidLocalTime, ZoneId.of("UTC"));
            System.out.println(invalidZonedTime);
        } catch(Exception e) {
            System.out.println(e); // prints java.time.DateTimeException: ...
        }
    }
}

In some sense, Java does exactly what is expected from the big enterprise language. It simply refuses to create invalid dates and expects the user to handle the resulting errors.

JavaScript

JavaScript does not distinguish between dates and times. The JavaScript Date object represents a single moment in time. Internally, it stores milliseconds since 1 January 1970 UTC.

Date objects can be created by passing year, month, and day into the Date constructor. Months are counted from 0, so 0 represents January and 11 represents December.

// new Date with valid input
const validDate = new Date(2021, 0, 15);

console.log(validDate.toDateString()); // prints "Fri Jan 15 2021"

// new Date with invalid input
const invalidDate = new Date(2021, 1, 30);

console.log(invalidDate.toDateString()); // prints "Tue Mar 02 2021"

Since JavaScript does not distinguish between dates and times, it would be very surprising to see a different behavior when passing in hour, minutes, and seconds in addition to year, month, and day.

Note that the examples below use the Date.UTC() method to compute the number of milliseconds since January 1, 1970, 00:00:00 UTC. This number is then then be passed into the Date constructor.

// new Date with valid input
const validDate = new Date(Date.UTC(2021, 0, 15, 8, 0, 0));

console.log(validDate.toISOString()); // prints 2021-01-15T08:00:00.000Z

// new Date with invalid input
const invalidDate = new Date(Date.UTC(2021, 1, 30, 8, 0, 0));

console.log(invalidDate.toISOString()); // prints 2021-03-02T08:00:00.000Z

Apparently, JavaScript wants to spare its users from negative experiences in the browser.

Ruby

Ruby provides a Date, a DateTime, and a Time class.

Date objects store simple dates without seconds or time zones.

Time objects store timestamps.

And DateTime objects are deprecated (compare class DateTime), so we will look at Date and Time.

The constructor of the Date class takes three arguments: year, month, and date. Months are indexed from 1.

require 'date'

# Date.new with valid input
date = Date.new(2021, 1, 15)

puts date # prints 2021-01-15

# Date.new with invalid input
begin
  Date.new(2021, 2, 30)
rescue => e
  puts e # prints invalid date
end

And now, UTC timestamps. The Time.utc class method takes the six expected arguments in the expected order and returns a timestamp in the UTC time zone.

require 'date'

# Time.utc with valid input
timestamp = Time.utc(2021, 1, 15, 8, 0, 0)

puts timestamp # prints "2021-01-15 08:00:00 UTC"

# Time.utc with invalid input
timestamp = Time.utc(2021, 2, 30, 8, 0, 0)

puts timestamp # prints "2021-03-02 08:00:00 UTC"

Date.new raises an exception where Time.utc happily creates some timestamp in the following month.

Summary

The following table is a collection of the results.

Programming language and task Outcome when creating invalid date or time
Elixir Date returns error
Elixir UTC Time impossible to create
Erlang Date unusable syntactically correct representation
Erlang UTC Time unusable syntactically correct representation
Haskell Date provides two implementations
Haskell UTC Time depends on construction date values
Java Date raises exception
Java UTC Time raises exception
JavaScript Date rolls over to some valid date
JavaScript UTC Time rolls over to some valid timestamp
Ruby Date raises exception
Ruby UTC Time rolls over to some valid timestamp

Conclusion

All the programming languages mentioned above agree that it is a good idea to be able to have some data structure representing dates and times and that there should be some functions or methods that can do artihmetic operations on dates and times. Nevertheless, there is a lot of diversity in the different implementations and APIs.

All of the programming languages listed above differ in one aspect or another. There is no obvious common API. There are no universal truths.