Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting date/time adjuster usage (.With()) #257

Closed
chrisimcevoy opened this issue Jan 26, 2025 · 0 comments · Fixed by #259
Closed

Porting date/time adjuster usage (.With()) #257

chrisimcevoy opened this issue Jan 26, 2025 · 0 comments · Fixed by #259

Comments

@chrisimcevoy
Copy link
Owner

Noda Time has "date adjusters" and "time adjusters". Some adjustments are discussed at the bottom of the recipes page.

These are essentially functions which take an argument of either LocalTime or LocalDate, adjust it in some way, and then return the adjusted instance.

They can be user-defined, or selected from predefined adjusters for dates or times.

They are intended to be used by passing the adjuster to a .With() method on instances of the following types:

  • Date adjusters
    • LocalDate
    • LocalDateTime
    • OffsetDate
    • OffsetDateTime
  • Time adjusters
    • LocalTime
    • LocalDateTime
    • OffsetTime
    • OffsetDateTime

In Python, the natural inclination would be to treat "date adjusters" as Callable[[LocalDate], LocalDate] and "time adjusters" as Callable[[LocalTime], LocalTime].

But an issue arises when we come to types which provides overloads for both types of adjuster, namely LocalDateTime and OffsetDateTime.

In C# this looks roughly like:

struct LocalDateTime 
{
    public LocalDateTime With(Func<LocalDate, LocalDate> adjuster) => date.With(adjuster) + time;

    public LocalDateTime With(Func<LocalTime, LocalTime> adjuster) => date + time.With(adjuster);
}

The problem in Python is that we cannot distinguish between Callable[[LocalDate], LocalDate] and Callable[[LocalTime], LocalTime] at runtime. You cannot do something like this:

class LocalDateTime:

    def with_(self, adjuster: Callable[[LocalDate], LocalDate] | Callable[[LocalTime], LocalTime]) -> LocalDateTime:
        if isinstance(adjuster, Callable[[LocalDate], LocalDate]):  # TypeError: Subscripted generics cannot be used with class and instance checks
            ...
        else:
            ...

There are probably some third party libraries which can do single dispatch based on type annotations, but I'm not interested in adding a dependency just for that.

So what options do we have here? I think there are two:

  1. Rather than Callable[[LocalDate], LocalDate] and Callable[[LocalTime], LocalTime], port "adjusters" as discrete types which implement __call__ (or some other abstract method). This would allow for isinstance checks at runtime, and would allow us to keep a consistent .with_(adjuster) interface for all types which may be "adjusted".
  2. Rather than adding .with_() methods and overloads, just have separate methods for different types of adjustment. For example, .with_date_adjuster() and .with_time_adjuster. Not the catchiest or most "fluent", but they are explicit. And this way you can keep the type annotations as Callable, which avoids the penalty of class instantiation from the first option, and potentially allows for plain old functions or even lambdas to be passed. It also avoids the eyesore of a .with_() method.

I find option 2 more appealing. The most important thing is to be consistent with naming across the various types, and to some day get around to documenting this and other differences between Noda Time and Pyoda Time!

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

Successfully merging a pull request may close this issue.

1 participant