Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Proposal: Add `Date.from_utc_today(duration)` or `Date.shift(:utc_today, duration)`

105 views
Skip to first unread message

Wojtek Mach

unread,
Jan 7, 2025, 11:43:49 AMJan 7
to elixir-lang-core
Hello,

I'd like to propose adding the following functions:

- `Date.from_utc_today(duration)`
- `NaiveDateTime.from_utc_today(duration)`
- `DateTime.from_utc_today(duration)`

For example:

    # Say, now is ~U[2025-01-07 16:22:40.003901Z]

    iex> Date.from_utc_now(month: 1, day: 1)
    ~D[2025-02-08]

    iex> NaiveDateTime.from_utc_now(hour: -1)
    ~N[2025-01-07 15:22:40.003901]

    iex> DateTime.from_utc_now(Duration.new!(hour: 1))
    ~U[2025-01-07 17:22:40.003901Z]

I believe they are especially useful when writing tests and they might give opportunity for some optimizations.

Another idea is to instead allow passing `:utc_today` / `:utc_now` to the existing shift/2 functions:

    iex> Date.shift(:utc_today, month: 1, day: 1)
    ~D[2025-02-08]

    iex> NaiveDateTime.shift(:utc_now, hour: -1)
    ~N[2025-01-07 15:22:40.003901]

    iex> DateTime.from_utc_now(:utc_now, hour: 1)
    ~U[2025-01-07 17:22:40.003901Z]

Btw and this is a related but separate conversion, I think a `Date.range(date, duration)` would be a nice addition. And so, I believe a `Date.range(:utc_today, month: 1)` would be a natural extension of this. I'm not sure if supporting `Date.add(:utc_today, 1)` and similar is worth it, perhaps just for consistency.

José Valim

unread,
Jan 8, 2025, 6:06:39 AMJan 8
to elixir-l...@googlegroups.com
I'd love to see something along those lines but I can't pick a favorite.

1. Supporting :utc_now in "shift" could be a welcome addition, as we could also support it in "add" and "diff" functions. However, I'd say it is more verbose than from_utc_today.

2. from_utc_now/from_utc_today is clearer but less applicable. If we go this route, we may find ourselves adding other functions, such as `add_to_utc_now` and `diff_to_utc_now`.

So I would love to hear everyone's thoughts.

"Date.range/2" with a duration is a no-brainer though and we could add it today.



--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/elixir-lang-core/2851ea28-d20e-4e8d-b957-38a582f7fa39n%40googlegroups.com.

Jon Rowe

unread,
Jan 8, 2025, 7:57:47 AMJan 8
to Elixir Lang Core
I'd love either of these proposals to be a reality, I often find myself building my own helpers in tests to do this sort of thing.

Initially I thought the function varient was better but if you look at it as "then we might have to add all these other functions" I found myself leaning towards allowing `:utc_now` as a placeholder in the existing api, it might be slightly more verbose but it leans towards a more compact core api overall... so that gets my +1.

Cheers
Jon

José Valim

unread,
Jan 8, 2025, 8:02:45 AMJan 8
to elixir-l...@googlegroups.com
Another scenario where :utc_now could be used is DateTime.after?(date, :utc_now)


Billy Lanchantin

unread,
Jan 8, 2025, 10:37:01 AMJan 8
to elixir-lang-core

I like the functionality and neither option seems like the obvious choice. I agree with José here:

> 2. If we go [the dedicated function] route, we may find ourselves adding other functions, such as `add_to_utc_now` and `diff_to_utc_now`.

So I somewhat favor `Date.shift(:utc_today, month: 1)`. It's naturally extensible to other bases (like `:now`, `:utc_now`) without polluting the temporal modules with helpers.

The downside is that the types will be more awkward. `DateTime.shift/2` and friends will accept either a struct or one of a set of special atoms. So we're stuck increasing the either size of the types or the number functions.

---

What does the `Date.range(date, duration)` return?

Zachary Daniel

unread,
Jan 8, 2025, 10:44:16 AMJan 8
to elixir-l...@googlegroups.com, elixir-lang-core
I’m wondering if an opaque struct may be better, representing a computed-on-demand datetime. Like `%DateTime.Lazy{}`. Then it could grow over time without needing to expand a set of magic atoms.

On Jan 8, 2025, at 10:37 AM, 'Billy Lanchantin' via elixir-lang-core <elixir-l...@googlegroups.com> wrote:



Zachary Daniel

unread,
Jan 8, 2025, 10:46:32 AMJan 8
to elixir-l...@googlegroups.com, elixir-lang-core
DateTime.shift(DateTime.lazy(:utc_now), duration)

Doesn’t have the same feel of convenience but it seems to me to be a more resilient option.

On Jan 8, 2025, at 10:44 AM, Zachary Daniel <zachary....@gmail.com> wrote:



José Valim

unread,
Jan 8, 2025, 11:10:50 AMJan 8
to elixir-l...@googlegroups.com
The problem is that the struct fields are public and we can't make them lazy. So if the user does `DateTime.lazy(:utc_now).year`, it won't work, and if the only use of said lazy types is to pass it to these functions, we might as well make it an atom specific to these functions. :)


Zach Daniel

unread,
Jan 8, 2025, 11:39:44 AMJan 8
to elixir-l...@googlegroups.com
I didn't mean that we would make a struct that people could pretend was a datetime and do things like call `.year` on it. I meant that if we are using a "magic value" we could make the "magic value" be something that can be expanded over time.

For example, if I want to make a function that takes "a thing that can be passed into `DateTime.shift`", it is much easier if I can say `DateTime.t() | DateTime.Lazy.t()` etc. Don't we have
similar treatment for `Regex`? Like the internals aren't meant to be pattern matched on/used, but it can be accepted by internal functions.

If we're sure there will only ever be one such atom then I guess it makes sense, but if I wanted go guard against "something that DateTime accepts" it feels like `when is_struct(datetime, DateTime) or is_struct(datetime, DateTime.Lazy)` would future proof that guard, where as `when is_struct(datetime, DateTime) or datetime == :utc_now` wouldn't.

Allen Madsen

unread,
Jan 8, 2025, 12:22:46 PMJan 8
to elixir-l...@googlegroups.com
It seems like the goal is just to have a shorter way to express utc_now and utc_today, because otherwise this code is the same:

Date.shift(Date.utc_today(), month: 1)
Date.shift(:utc_today, month: 1)

Two ideas come to mind besides supporting the atom, that would just give you a Date/DateTime to use here that could be shorter.

* Change what the sigils support ~D[utc_today]/~U[utc_now]
* Make utc_today/0 and utc_now/2 Kernel functions.



Zach Daniel

unread,
Jan 8, 2025, 12:27:53 PMJan 8
to elixir-l...@googlegroups.com
This overlooks part of the original proposal which included potential optimizations. I don't know what they would be, but I think the idea is that certain
Datetime functions might be optimizable if they don't have to start from "any given datetime" but can know that they are starting from "now".

Wojtek Mach

unread,
Jan 8, 2025, 1:55:46 PMJan 8
to elixir-l...@googlegroups.com
Btw, one of somewhat recent nice ergonomic improvement was replacing:

DateTime.utc_now() |> DateTime.truncate(:second)

with:

DateTime.utc_now(:second)

I think it might be relevant for the proposal. Is either of these appealing?

# option 1
DateTime.from_utc_now(:seconds, hour: 1)
# option 2
DateTime.from_utc_now([hour: 1], :seconds)

On the other hand, do we instead add :utc_now, :utc_now_seconds, and possibly others? I’m not sure.

If supporting precision is important, to me DateTime.from_utc_now(duration, precision \\ :microsecond) feels like the best option after all.



José Valim

unread,
Jan 8, 2025, 2:01:30 PMJan 8
to elixir-l...@googlegroups.com
I don't think I would worry about precision shortcuts for those APIs:

1. You can truncate after anyway. If the precision is not relevant to the operation, then after should have the same result. If it is important, then you want to either leave it or be explicit
2. Some operations don't return a Date/Time and therefore may not care about precision either


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Christopher Keele

unread,
Jan 8, 2025, 4:06:12 PMJan 8
to elixir-lang-core
> if I wanted go guard against "something that DateTime accepts" it feels like `when is_struct(datetime, DateTime) or is_struct(datetime, DateTime.Lazy)` would future proof that guard, where as `when is_struct(datetime, DateTime) or datetime == :utc_now` wouldn't.

Regardless of atom/struct/other implementation of a sentinel value, I think this is a compelling point: if we're going to expand the range of datatypes allowed as arguments to Date+Time functions, we should probably offer a stable guard API for callers to leverage.
Aka a Date.is_date/1 guard and {NativeDateTime, DateTime}.is_date_time/1 guards.

I do prefer the idea of a sentinel value to procure a utc_now datetime, to minimize the size of the Date+Time APIs. If we pursue this, I think we must be very intentional in the documentation about when the sentinel gets resolved into a proper Date+Time, though.
Ex. without clear instruction, I'd expect a struct called DateTime.Lazy to not get resolved until absolutely required by a Date+Time call that actually has to produce a value out of it, ex deferring shifting until serialized/stringified or a subcomponent is extracted.
My understanding that is not what's being proposed, though—rather that any Date+Time callsite that recieves the sentinel will immediately procure a proper utc_now Date+Time.
This ambiguity may be more a problem with the proposed term "Lazy" than anything else, but I think the docs would need to make resolution time crystal clear no matter the terminology chosen.

Theo Fiedler

unread,
Jan 9, 2025, 2:13:31 AMJan 9
to elixir-lang-core
Personally leaning towards option 1) just to avoid the urge of adding more and more convenience to the date/time modules. Most of what is proposed can already be achieved with "just" one additional function call. Once introduced would we want to support this in all applicable functions, e.g. treating :utc_today as Date.utc_today/1 everywhere? If this works in shift/2 and add/2, why shouldn't it work in diff/2 or beginning_of_month/1.

This is the only thing that makes me hesitant of adding this, since imho, it'd create the expectation that :utc_today` can be used interchangably with Date.utc_today/0 everywhere in the api.

---

All for supporting durations in Date.range/3 of course!
Reply all
Reply to author
Forward
0 new messages