Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

2. Klepsidra datetime-local timestamp manipulations

02-klepsidra-datetime-local_manipulations.livemd

2. Klepsidra datetime-local timestamp manipulations

Mix.install([
  {:timex, "~> 3.7"},
  {:date_time_parser, "~> 1.2"}
])

Summary

In the process of working on the timer in LiveView, one aspect of friction has come up: parsing HTML’s datetime-local datetime stamp.

Essentially, JavaScript elides the seconds value, unless it has been entered or is non-zero. When the seconds are zero, only the hour and minute components are stored and transferred over the wire. This provides a better user experience, particularly with manually created timers, since it isn’t desirable to ask the user an input for the seconds component. As it is an effectively meaningless precision in the context of timing activities, it’s best to avoid it.

This causes data interoperability problems. As a result of the above, datetime stamps passed by the front end will therefore always be in the YYYY-MM-DD hh:mm format, which is almost ISO 8601:2019 compatible (‘ISO 8601’, 2024). But not quite since it misses the seconds component (HTML Standard, n.d.). Attempting to ingest that non standards-compliant string, NaiveDateTime.from_iso8601/1 will fail with an error. This adds a friction point when ingesting data from the front end, needing the adding of a seconds time component.

> 2.3.5.5 Local dates and times > > A local date and time consists of a specific proleptic-Gregorian date, consisting of a year, a month, and a day, and a time, consisting of an hour, a minute, a second, and a fraction of a second, but expressed without a time zone. [GREGORIAN]

> A string is a valid local date and time string representing a date and time if it consists of the following components in the given order:

> A valid date string representing the date > A U+0054 LATIN CAPITAL LETTER T character (T) or a U+0020 SPACE character > A valid time string representing the time > A string is a valid normalized local date and time string representing a date and time if it consists of the following components in the given order:

> A valid date string representing the date > A U+0054 LATIN CAPITAL LETTER T character (T) > A valid time string representing the time, expressed as the shortest possible string for the given time (e.g. omitting the seconds component entirely if the given time is zero seconds past the minute)

The reverse is also a problem, although a smaller one, when stringified NativeDateTime structures are passed as values into HTML forms. Since there is a seconds component in these structures, converting it to a string and applying it to HTML elements as a value, now produces a less readable value in the input field. Anything that further complicates fast user comprehension must be removed. In these cases, the seconds component must be stripped out.

The ideal solution is to rely on Elixir’s core functionality, but in the interest of better maintainability, the simplest external libraries are a good solution.

Using the date_time_parser library

The date_time_parser library seems like a simple solution to the first problem, gracefully ingesting incomplete datetime strings passed from the HTML user interface, parsing them to the correct extended ISO 8601 specification.

Let us specify two example string formats to be expected, one with a T delimiter between the date and time components, and the other with just a space.

html_datetimestamp_t_delimited = "2024-03-15T15:01"
html_datetimestamp_space_delimited = "2024-03-15 15:01"
"2024-03-15 15:01"

Both of these will be passed into the parse_datetime/1 function. To see both results, they are returned in a tuple.

{
  DateTimeParser.parse_datetime!(html_datetimestamp_t_delimited),
  DateTimeParser.parse_datetime!(html_datetimestamp_space_delimited)
}
{~N[2024-03-15 15:01:00], ~N[2024-03-15 15:01:00]}

This experiment satisfies the desired input parsing, returning proper NativeDateTime structures, with zeroed seconds components. Our core requirement of handling only these structures internally has been met, and this conversion will take place at the application boundary layer.

Using the Timex library

As the “…richest, most comprehensive date/time library for Elixir…”, Timex was considered (Getting Started — Timex v3.7.11, 2023). While undoubtedly a heavy library with even more extencive dependencies than date_time_parser, it really does provide a rich choice of date and time calculation and manipulation functionality.

Let’s try to parse the inbound datetime-local stamp with Timex, as before. Timex’ parse/2 function takes the input string as the first argument, and a format_string as the second, which can be one of two types. Timex offers its own default directive format, which is simple to read and memorise, and the standard strftime format, which many people are used to.

Let’s see both in action, just for comparison.

[
  {
    Timex.parse!(html_datetimestamp_t_delimited, "%Y-%m-%dT%H:%M", :strftime),
    Timex.parse!(html_datetimestamp_space_delimited, "%Y-%m-%d %H:%M", :strftime)
  },
  {
    Timex.parse!(html_datetimestamp_t_delimited, "{YYYY}-{M}-{D}T{h24}:{m}"),
    Timex.parse!(html_datetimestamp_space_delimited, "{YYYY}-{M}-{D} {h24}:{m}")
  }
]
[
  {~N[2024-03-15 15:01:00], ~N[2024-03-15 15:01:00]},
  {~N[2024-03-15 15:01:00], ~N[2024-03-15 15:01:00]}
]

Unlike date_time_parser, Timex provides formatting functions as well. Can it be used at the boundary to easily convert NativeDateTime timestamps to a string, and elide the seconds component?

Towards this, there is a format/2 function, offering an identical conversion in the opposite (outbound) direction as well. Let’s see it in action.

datetime_stamp = ~N[2024-03-15 15:01:00]
~N[2024-03-15 15:01:00]
[
  {
    Timex.format!(datetime_stamp, "%Y-%m-%dT%H:%M", :strftime),
    Timex.format!(datetime_stamp, "%Y-%m-%d %H:%M", :strftime)
  },
  {
    Timex.format!(datetime_stamp, "{YYYY}-{M}-{D}T{h24}:{m}"),
    Timex.format!(datetime_stamp, "{YYYY}-{M}-{D} {h24}:{m}")
  }
]
[{"2024-03-15T15:01", "2024-03-15 15:01"}, {"2024-3-15T15:01", "2024-3-15 15:01"}]

13:38:48.058 [debug] Tzdata polling for update.

13:38:49.044 [debug] Tzdata polling shows the loaded tz database is up to date.

18:03:28.850 [debug] Tzdata polling for update.

18:03:30.631 [debug] Tzdata polling shows the loaded tz database is up to date.

02:13:35.209 [debug] Tzdata polling for update.

02:13:36.252 [debug] Tzdata polling shows the loaded tz database is up to date.

03:36:28.253 [debug] Tzdata polling for update.

03:36:30.007 [debug] Tzdata polling shows the loaded tz database is up to date.

15:30:43.429 [debug] Tzdata polling for update.

15:30:44.573 [debug] Tzdata polling shows the loaded tz database is up to date.

03:23:06.746 [debug] Tzdata polling for update.

03:23:07.783 [debug] Tzdata polling shows the loaded tz database is up to date.

01:28:37.052 [debug] Tzdata polling for update.

01:28:38.019 [debug] Tzdata polling shows the loaded tz database is up to date.

01:09:47.026 [debug] Tzdata polling for update.

01:09:48.294 [debug] Tzdata polling shows the loaded tz database is up to date.

19:51:03.305 [debug] Tzdata polling for update.

19:51:05.024 [debug] Tzdata polling shows the loaded tz database is up to date.

And it returns properly formatted T and space-delimited datetime strings, with the seconds component elided, ready to pass to HTML elements’ value slots.

This satisfies both directions of boundary conversions needed, and demonstrates the best path to follow.

Concluding thoughts

Given that it works almost as easily, while providing a wealth of functionality to be used in future development, such as filtering timers to reveal only those in a time period, it is more sensible to use the Timex library for boundary conversions.

References