Inverse of DateTime.truncate?

Is there an inverse of DateTime.truncate? I have a DB column with :utc_datetime_usec type and I’m putting data coming from external system that has only precision up to a second.

I’m doing %{datetime | microsecond: {0, 6}} but maybe there’s a better way?

Should this be handled by Ecto internally? Seems like it’s failing on purpose.

DateTime.convert!(datetime, datetime.calendar)

Doesn’t seem to work for me on Elixir 1.10:

iex> DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.convert!(Calendar.ISO) |> Map.get(:microsecond)
{0, 0}
1 Like

I was wrong there. I wasn’t able to find anything.
Sounds like a set of extend/2 functions which take the same arguments as truncate/2 could be helpful.

1 Like

I can’t find anything (concise) either but what’s wrong with that one? You can always have a small wrapper in your app e.g. MyApp.Dates.put_usec/1 that does the same.

1 Like

from the DateTime docs:

Developers should avoid creating the DateTime struct directly and instead rely on the functions provided by this module as well as the ones in third-party calendar libraries.

1 Like

…Oh. In that case I agree, an addition to the stdlib should definitely be on the radar. Especially having in mind that just doing DateTime.add will not help in this case. So yep, extend/2 looks like a good option.

1 Like

Here’s the helper that I came up with. Only covers the use case I need, doctests included:

@doc """
Ensures that the given `DateTime` has the specified precision.

    iex> ensure_precision(~U[2022-01-01 10:00:00Z], :microsecond)
    ~U[2022-01-01 10:00:00.000000Z]

    iex> datetime = ~U[2022-01-01 10:00:00.123Z]
    iex> DateTime.compare(datetime, ensure_precision(datetime, :microsecond))
    :eq
"""
def ensure_precision(%DateTime{microsecond: {_, 6}} = datetime, :microsecond) do
  datetime
end

def ensure_precision(%DateTime{microsecond: {value, _precision}} = datetime, :microsecond) do
  %{datetime | microsecond: {value, 6}}
end
1 Like

DateTime.add(datetime, value, :microsecond)

Edit: ignore me, this does not change the precision of the DateTime but perhaps it should.

The issue here is also this:

iex> {:ok, dt, 0} = DateTime.from_iso8601("2022-01-01T10:00:00.123Z")
iex> dt.microsecond
{123000, 3}

But Ecto will still raise when trying to put the value in :utc_datetime_usec:

defp check_usec!(%{microsecond: {_, 6}} = datetime, _kind), do: datetime

defp check_usec!(datetime, kind) do
  raise ArgumentError,
        "#{inspect(kind)} expects microsecond precision, got: #{inspect(datetime)}"
end
1 Like

I understand but note that Ecto is not wrong here. You are saying you are expecting something with usec precision but you can’t guarantee that. Perhaps you should not be using _usec for those cases.

At the same time, we likely should have a better API to do this in DateTime.

1 Like

Sorry, got a bit confused on how the microsecond field works in DateTime - it always stores microseconds; the precision “represents the number of digits that must be used when representing the microseconds to external format”. So even for datetimes with millisecond precision, the microseconds are already stored, but with precision of 3.

Agreed on that Ecto is not wrong here, but I guess the precision could be automatically increased.

I think my use case is justified - I’m tracking the timestamp of the last update in the external system in a generic way and so various external systems use different precision for this. That’s why I went with _usec and need to “untruncate” it.

1 Like

I’ve bumped into this as well from time to time. I can see that there are cases where precision is required and therefore it’s an error if not supplied, but there’s also the case of just wanting to store the best precision available.