Skip to content

Move DateTime constants for month and weekday into extension types. #60669

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

Open
mmcdon20 opened this issue May 3, 2025 · 18 comments
Open

Move DateTime constants for month and weekday into extension types. #60669

mmcdon20 opened this issue May 3, 2025 · 18 comments
Assignees
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. feature-dot-shorthands Implementation of the dot shorthands feature. library-core type-enhancement A request for a change that isn't a bug

Comments

@mmcdon20
Copy link

mmcdon20 commented May 3, 2025

Currently DateTime has the following definition:

class DateTime implements Comparable<DateTime> {
  // Weekday constants that are returned by [weekday] method:
  static const int monday = 1;
  static const int tuesday = 2;
  static const int wednesday = 3;
  static const int thursday = 4;
  static const int friday = 5;
  static const int saturday = 6;
  static const int sunday = 7;
  static const int daysPerWeek = 7;

  // Month constants that are returned by the [month] getter.
  static const int january = 1;
  static const int february = 2;
  static const int march = 3;
  static const int april = 4;
  static const int may = 5;
  static const int june = 6;
  static const int july = 7;
  static const int august = 8;
  static const int september = 9;
  static const int october = 10;
  static const int november = 11;
  static const int december = 12;
  static const int monthsPerYear = 12;

  ...
  external int get weekday;
  external int get month;
  ...
}

I propose that we deprecate the constants in DateTime and create extension types to represent the Month and Weekday.

class DateTime implements Comparable<DateTime> {
  ...
  external Weekday get weekday;
  external Month get month;
  ...
}

extension type const Weekday(int _) implements int {
  // Weekday constants that are returned by [weekday] method:
  static const Weekday monday = Weekday(1);
  static const Weekday tuesday = Weekday(2);
  static const Weekday wednesday = Weekday(3);
  static const Weekday thursday = Weekday(4);
  static const Weekday friday = Weekday(5);
  static const Weekday saturday = Weekday(6);
  static const Weekday sunday = Weekday(7);
  static const int daysPerWeek = 7;
}

extension type const Month(int _) implements int {
  // Month constants that are returned by the [month] getter.
  static const Month january = Month(1);
  static const Month february = Month(2);
  static const Month march = Month(3);
  static const Month april = Month(4);
  static const Month may = Month(5);
  static const Month june = Month(6);
  static const Month july = Month(7);
  static const Month august = Month(8);
  static const Month september = Month(9);
  static const Month october = Month(10);
  static const Month november = Month(11);
  static const Month december = Month(12);
  static const int monthsPerYear = 12;
}

The reason for this change is to better enable the upcoming dot-shorthands feature.

The proposed change would allow us to write for example:

void main() {
  if (DateTime.now() case DateTime(weekday: .saturday || .sunday)) {
    print('today is a weekend');
  } else {
    print('today is a weekday');
  }
}
@lrhn
Copy link
Member

lrhn commented May 3, 2025

Could work. Would probably still be available in DateTime for backwards compatibility.
Like the idea.

@lrhn lrhn added area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-core type-enhancement A request for a change that isn't a bug feature-dot-shorthands Implementation of the dot shorthands feature. labels May 3, 2025
@lrhn lrhn self-assigned this May 3, 2025
@RohitSaily
Copy link
Contributor

RohitSaily commented May 4, 2025

Can't we implement the various units as extension types without breaking backwards compatibility? It should work if we keep the identifiers the same and the different units still implement int. eg

class DateTime implements Comparable<DateTime> {
  ...
  static const Weekday monday = Weekday(1);
  static const Weekday tuesday = Weekday(2);
  static const Weekday wednesday = Weekday(3);
  static const Weekday thursday = Weekday(4);
  static const Weekday friday = Weekday(5);
  static const Weekday saturday = Weekday(6);
  static const Weekday sunday = Weekday(7);

  static const Month january = Month(1);
  static const Month february = Month(2);
  static const Month march = Month(3);
  static const Month april = Month(4);
  static const Month may = Month(5);
  static const Month june = Month(6);
  static const Month july = Month(7);
  static const Month august = Month(8);
  static const Month september = Month(9);
  static const Month october = Month(10);
  static const Month november = Month(11);
  static const Month december = Month(12);
  ...
}

Any computations which processed the units as int would still work, and new ones could use the specialized types for better safety, all while maintaining backwards compatability

@mmcdon20
Copy link
Author

mmcdon20 commented May 4, 2025

@RohitSaily

The constants still need to be defined inside the Weekday or Month type for the dot-shorthand to work.

But you could update the types of the existing constants also.

@RohitSaily
Copy link
Contributor

@mmcdon20 that makes sense that both would have to be done 👍

@lrhn @mmcdon20 I wouldn't mind attempting this change if neither of you are working on it. Let me know, I wouldn't want to do any redundant work

@lrhn
Copy link
Member

lrhn commented May 5, 2025

The problem here is that changing the parameter type to Month will break existing code which passes in an int.

We can make Month implement int, but a call of DateTime(1970, 1, 1) would become invalid, because int is not a Month.

If we don't change the parameter type, and keep it as int, then dot shorthands won't work unless the Month constants are also constants on int.
They don't belong on int, and there is no implicit coercion from int to Month, and no static extension that can put the constants on int.

So, sadly it's not possible with the current feature-set, not without breaking far too much code.

@RohitSaily
Copy link
Contributor

You are correct, I only considered usages of the constants directly. It would be very breaking to make the changes because int values could have been used from other places.

So it would only make sense to do as @mmcdon20 originally proposed, and add the new types while keeping the original class as it is. Then new constructors, methods, etc would have to be added to DateTime or current ones can be updated to optionally accept the new types as inputs.

@mmcdon20
Copy link
Author

mmcdon20 commented May 6, 2025

We can make Month implement int, but a call of DateTime(1970, 1, 1) would become invalid, because int is not a Month.

So, sadly it's not possible with the current feature-set, not without breaking far too much code.

Thats unfortunate. I think it can be made to work with any of the following, but some of these might work better than others:

  1. implicit conversions (Implicit coercion through implicit constructors. language#3704, Implicit Constructor proposal language#108)
  2. union types (Sum/union types and type matching language#83)
  3. method overloading (Support method/function overloads language#1122)
  4. static extensions (Static extension methods language#723)

@eernstg
Copy link
Member

eernstg commented May 6, 2025

This is basically the motivating example for the parameter default scopes: We want to provide certain distinguished values by name (available as dot shorthands) in a situation where it does not make sense to add said names to the static namespace of the type (concretely: int should not declare a constant variable named monday).

An important property of this approach is that the weekday and month values are still of type int, which means that current code passing ints will continue to work, without implicit coercion of any kind. Also, the named values (.monday and such) are of type int. We could easily give them a type which is an extension type which is a subtype of int, but I don't think it's going to provide much of a benefit. Anyway, we can just do that if it is helpful.

Let's assume that we have static extensions, e.g., this proposal, such that we can provide a set of declarations in their own static namespace and also inject them into another static namespace (here: DateTime). Let's assume that we also have parameter default scopes (which is a small enhancement of the dot-shorthand feature which is being implemented right now).

class DateTime implements Comparable<DateTime> {
  // Remove `monday` .. `friday` and `january` .. `december`: Moved to static extensions.
  ...
  DateTime(
    int year, [
    int month = 1 in Month,
    int day = 1,
    int hour = 0,
    int minute = 0,
    int second = 0,
    int millisecond = 0,
    int microsecond = 0,
  ]);
  ...
  external int get weekday in Weekday; // Enable dot shorthand constant patterns.
  external int get month in Month; // Ditto.
  ...
}

static extension Weekday on DateTime {
  static const int monday = 1;
  static const int tuesday = 2;
  static const int wednesday = 3;
  static const int thursday = 4;
  static const int friday = 5;
  static const int saturday = 6;
  static const int sunday = 7;
  static const int daysPerWeek = 7;
}

static extension Month on DateTime {
  static const int january = 1;
  static const int february = 2;
  static const int march = 3;
  static const int april = 4;
  static const int may = 5;
  static const int june = 6;
  static const int july = 7;
  static const int august = 8;
  static const int september = 9;
  static const int october = 10;
  static const int november = 11;
  static const int december = 12;
  static const int monthsPerYear = 12;
}

We can now use DateTime.monday and DateTime.january as before, but also Weekday.monday and Month.january; we can pass the month in constructor invocations like Datetime(2025, .january, 1); and we can match dates based on the in clause on the getters like this:

void main() {
  if (DateTime.now() case DateTime(weekday: .saturday || .sunday)) {
    print('today is a weekend');
  } else {
    print('today is a weekday');
  }
}

An in clause on a getter is specifically aimed at dot shorthands as constant patterns, and that's a new idea that came up recently. All the other things are just using the parameter default scopes as proposed in 2024, plus static extensions.

@mmcdon20
Copy link
Author

mmcdon20 commented May 6, 2025

@eernstg With this approach,

What would happen if you wrote:

void main() {
  final weekday = DateTime.now().weekday;
  if (weekday == .saturday || weekday == .sunday) {
    print('today is a weekend');
  } else {
    print('today is a weekday');
  }
}

Would it still work?

@FMorschel
Copy link
Contributor

Yes, it would @mmcdon20, the last example in the comment above is basically the same thing you wrote, but uses pattern matching instead of expressions to test on weekends.

@mmcdon20
Copy link
Author

mmcdon20 commented May 6, 2025

I'm not sure it would work because the default scope is now on the weekday getter, the local variable weekday would presumably resolve to type int via type inference and then when using dot-shorthands on it would get the scope for int.

@FMorschel
Copy link
Contributor

I understand your question now, I guess I'm not that familiar with the parameter default scope feature to answer. If that got carried over to the variable, or maybe you could explicitly write that, I'm not sure.

Thanks for explaining. I'll see the discussion for the feature to try and answer.

@eernstg
Copy link
Member

eernstg commented May 6, 2025

Would it still work?

void main() {
  final weekday = DateTime.now().weekday;
  if (weekday == .saturday || weekday == .sunday) {
    print('today is a weekend');
  } else {
    print('today is a weekday');
  }
}

No, weekday is now just a local variable of type int, and there is no information whatsoever that could justify searching for the meaning of .saturday or .sunday in any particular static namespace other than that of int itself (and I'm assuming that it would be completely unacceptable to add those names to int).

So that's basically an impossible task (or, at least, it would call for some very twisted tricks ;-).

It would work, though, if DateTime.now().weekday had another type (e.g., an extension type), but in that case we'd want to have implicit constructors, in order to be able to use plain int values where a WeekDay is expected, in addition to the dot shorthands.

@mmcdon20
Copy link
Author

mmcdon20 commented May 6, 2025

No, weekday is now just a local variable of type int, and there is no information whatsoever that could justify searching for the meaning of .saturday or .sunday in any particular static namespace other than that of int itself (and I'm assuming that it would be completely unacceptable to add those names to int).

So that's basically an impossible task (or, at least, it would call for some very twisted tricks ;-).

Alright, that's what I thought would happen.

It would work, though, if DateTime.now().weekday had another type (e.g., an extension type), but in that case we'd want to have implicit constructors, in order to be able to use plain int values where a WeekDay is expected, in addition to the dot shorthands.

I think I would prefer the implicit constructor feature over default scopes due to cases like this.

@eernstg
Copy link
Member

eernstg commented May 7, 2025

It's an interesting pair of strategies. In both cases we enable dot shorthands by adding declarations of the desired distinguished values v1 .. vk to some static namespace N.

  1. With parameter default scopes, the type of vj is int and N is DateTime. We're using static extensions to provide these values in two namespaces (the static extension Weekday, and also DateTime), such that we can enable monday .. friday with one parameter of type int and january .. december with another parameter of type int as well.
  2. With extension types, the type of vj is Weekday or Month, and N is that type. This means that we can rely on the context type in order to enable monday .. friday with one parameter and january .. december with another one.

The first strategy is less disruptive because it allows DateTime.monday to continue to work, whereas it must be updated to Weekday.monday (or possibly .monday, if we have the right context type) with the second strategy. We could also simply duplicate the declarations of monday .. friday in Weekday and in DateTime (but duplication like this wouldn't be very maintenance friendly in general). Finally, we could consider further generalization of namespace management mechanisms, e.g., static extensions that are able to inject its declarations into more than one target.

The second strategy is more "sticky" in the sense that a type like extension type Weekday .. can be propagated by type inference, which could make it possible to use dot shorthands from Weekday in locations of code that are any number of steps of propagation away from a direct reference to DateTime itself. The type Weekday might also be used in its own right, with no connection to DateTime at all. Propagation (of anything) based on types is quite powerful.

By the way, I'd like to have something along the lines of parameter default scopes (that is, some mechanism that allows us to customize which static namespace to search for a dot shorthand) and implicit constructors. ;-)

@RohitSaily
Copy link
Contributor

Out of the various discussed alternatives, I think the ones that would be the easiest to integrate in existing code bases and easy to pick up as a new feature would be (1) static extensions with (2) overloading.

Then issues such as this one are solved simply by declaring what's needed without needing to reason about what namespaces are set or even having to introduce new ones.

@mmcdon20
Copy link
Author

mmcdon20 commented May 9, 2025

Based on @eernstg's comment (dart-lang/language#3834 (comment)):

I noticed that there is a proposal to use an extension type, but there haven't been any proposals to use a class to model Weekday and Month.

I'm not sure what the benefit of using a class in this situation over the extension type would be. I do see a potential benefit in modeling Month and Weekday as enum types so that they can benefit from exhaustiveness checking. This would likely require implicit conversions in both directions in order to preserve backwards compatability.

Something like this:

enum Weekday {
  monday,
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday;

  static const int daysPerWeek = 7;
}

enum Month {
  january,
  february,
  march,
  april,
  may,
  june,
  july,
  august,
  september,
  october,
  november,
  december;

  static const int monthsPerYear = 12;
}

static extension on Weekday {
  implicit factory Weekday.fromInt(int i) {
    return Weekday.values[(i - 1) % Weekday.daysPerWeek];
  }
}

static extension on Month {
  implicit factory Month.fromInt(int i) {
    return Month.values[(i - 1) % Month.monthsPerYear];
  }
}

static extension on int {
  implicit factory int.fromWeekday(Weekday weekday) {
    return weekday.index + 1;
  }
  implicit factory int.fromMonth(Month month) {
    return month.index + 1;
  }
}

Edit: thinking about it some more the above would very likely be breaking.

void main() {
  final today = DateTime.now().weekday; // enum implementation above
  print(today + 1); // I don't think this works even with implicit conversion
}

So because a regular class, or an enum can't implement or extend int, I don't think there is any way to make those work in a non-breaking way.

@mmcdon20
Copy link
Author

The second strategy is more "sticky" in the sense that a type like extension type Weekday .. can be propagated by type inference, which could make it possible to use dot shorthands from Weekday in locations of code that are any number of steps of propagation away from a direct reference to DateTime itself. The type Weekday might also be used in its own right, with no connection to DateTime at all. Propagation (of anything) based on types is quite powerful.

I think the "sticky" behavior is likely what most users will want when using/designing an api with dot-shorthands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. feature-dot-shorthands Implementation of the dot shorthands feature. library-core type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

5 participants