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

Relative functions for DateTimeType #4276

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

clinique
Copy link
Contributor

@clinique clinique commented Jun 18, 2024

As from one scripting language to the other, dealing with Dates is not always so easy, I propose to add these small functions that I use very often while scripting.

Here are the methods added to DateTimeType :

  • isBefore / isAfter another DateTimeType
  • sameDay compared to another DateTimeType or a ZonedDateTime
  • isToday / isTomorrow / isYesterday
  • toToday, toTomorrow, toYesterday changes day/month/year accordingly, preserving time part of the DateTimeType
  • isBeforeDate / isAfterDate compared to another DateTimeType or a ZonedDateTime ignoring the time component
  • isBeforeTime / isAfterTime compared to another DateTimeType or a ZonedDateTime ignoring the date component

@clinique clinique requested a review from a team as a code owner June 18, 2024 13:00
@clinique clinique changed the title Date time type relative functions Relative functions for DateTimeType Jun 18, 2024
@rkoshak
Copy link

rkoshak commented Jun 18, 2024

Given that some of the languages do not expose and have you work with the raw Java DateTimeType Objects adding these will not make them available to all rules languages.

Some of the stuff like this though has been added to JS Scripting already so I think this PR could be more ambitious if youi wanted to be. There you will find:

  • time.toZDT(): will convert just about anything to a ZonedDateTime that can be converted to a ZonedDateTime including DateTime Items, Number Items (state is treated as the number of milliseconds from now), Number:Time Items (state is treated as the amount of time from now), ISO8601 Strings, Java DateTime Strings, ISO8601 duration Strings (e.g. PT5M30S, treated as the amount of time from now), a String representing a time of day in 12hr or 24hr format, etc.

  • myZDT.toToday(): returns the ZDT with today's date, adjusting for DST changeovers as necessary

  • isBeforeTime(), isBeforeDate(), isBeforeDateTime(), the isAfter equivalents, and isBetweenTimes() isBetweenDates(), and isBetweenDateTimes() which lets you compare ZDTs to see where they fall compared to other ZDTs.

  • isClose() which is a fuzzy comparison to see if a ZDT is within a certain amount of time of another.

So you can do stuff like this:

// Is it currently between 8AM and 4PM
time.toZDT().isBetweenTimes('8:00AM', `4:00PM`);

// Is now between the times in two Items?
time.toZDT().isBetweenTimes(items.StartTime, items.EndTime); // dates are ignored

// is now between the start time in the Item and three hours after the start time?
time.toZDT().isBetweenTimes(items.StartTime, time.toZDT(items.StartTime).plusHours(3));

// Is the Item's state today? (there are others)
time.toZDT(items.MyDateTime).isBetweenDateTimes('00:00', '23:59:59');

// Move the Item's date time to today
time.toZDT(items.MyDateTime).toToday();

// Is the Item's state tomorrow?
time.toZDT(items.MyDateTime).toLocalDate().equals(time.toZDT('P1DT').toLocalDate());

// Move an Item's time to today
time.toZDT(items.MyDateTime).toToday();

Note that time.toZDT() returns now.

The real magic (IMO) with the JS Scripting approach is that in many of these conversion and comparison functions you can use anything that can be converted by time.toZDT(). It really gets interesting when stuff that usually takes a DateTime can accept anything that can be converted by toZDT(). For example, creating a Timer to go off in five minutes:

actions.ScriptExecution.createTimer(Quantity('5 M'), () => { do something }); // note this doesn't currently work because createTimer doesn't support this sort of conversion

In addition to toToday(), my OHRT also implements a toTomorrow() and toYesterday() method.

In JS you can monkeypatch these into the ZonedDateTime class but I'm pretty sure that's not an option on the Java side of things. But a series of utility functions on the DateTimeType to not only compare but also convert "stuff" to ZonedDateTimes.

There is also a lot of convenience stuff built into jRuby which might prove some inspiration as well.

Once I implemented a lot of the above (originally as part of OHRT and then I was able to get most of them accepted into the openhab-js library) all the complexities of working with DateTimes kind of just disappeared. time.toZDT() normalizes everything and once normalized comparisons and manipulation becomes standardized and simple. And because time.toZDT() treats anything that can be interpreted as a time duration and how much time to add to now, and the main comparison functions can accept one of these durations instead of a ZDT itself, I pretty much never have to do a string of plusSeconds().plusMinutes().plusHours() type stuff nor withHour().withMinute().withSecond() type stuff. The resultant code becomes much clearer and easier to read.

It's just an idea and may be more than you're willing to take on, but I can say this has greatly simplified JS Scripting rules.

Copy link
Contributor

@jimtng jimtng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a handy addition, especially for rulesdsl. There is currently not a direct equivalent in jruby for what this adds, and when this is added, it will be automatically usable from within jruby without any modifications to the jruby helper library. However it's currently already quite easy to do this in jruby

# dt1 and dt2 are DateTimeType objects, e.g. A_DateTime_Item.state

# Checking for same day
dt1.to_date == dt2.to_date # to_date creates a Date object, stripping off the time data

# to check for today
dt1.to_date == Date.today

# to check for tomorrow
dt1.to_date == Date.today + 1 

# to check for yesterday
dt1.to_date == Date.today - 1 

After this PR, JRuby will be able to call:

dt1.today? #jruby automatically translates isToday to ruby method naming convention `today?`

dt1.yesterday?
dt1.tomorrow?
dt1.same_day(dt2) # The Ruby way would be dt1 === dt2

However, in jruby we can add these to ZonedDateTime directly, thus making it universally usable in many more places, including DateTimeType, because in jruby a DateTimeType delegates its methods to its ZonedDateTime value.

@ccutrer wdyt?

assertTrue(new DateTimeType().isToday());

DateTimeType now = new DateTimeType();
DateTimeType tomorrow = new DateTimeType(now.getZonedDateTime().plusHours(24));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DateTimeType tomorrow = new DateTimeType(now.getZonedDateTime().plusHours(24));
DateTimeType tomorrow = new DateTimeType(now.getZonedDateTime().plusDays(1));

I know this is an edge case, but the length of the day isn't always precisely 24h.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid using the same method than what's used in the object itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed


DateTimeType now = new DateTimeType();
DateTimeType tomorrow = new DateTimeType(now.getZonedDateTime().plusHours(24));
DateTimeType yesterday = new DateTimeType(now.getZonedDateTime().minusHours(24));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DateTimeType yesterday = new DateTimeType(now.getZonedDateTime().minusHours(24));
DateTimeType yesterday = new DateTimeType(now.getZonedDateTime().minusdays(1));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed


public boolean sameDay(ZonedDateTime other) {
return zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.isEqual(other.withZoneSameLocal(zonedDateTime.getZone()).truncatedTo(ChronoUnit.DAYS));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this use witZoneSameInstant ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

@Test
public void relativeTest() {
DateTimeType dt1 = new DateTimeType("2019-06-12T17:30:00Z");
DateTimeType dt2 = new DateTimeType("2019-06-12T00:00:00+0000");
Copy link
Contributor

@jimtng jimtng Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure that this checks for the same instant:

dt1 = "2019-06-13T01:10:00+02"
dt2 = "2019-06-12T23:00:00Z"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

Comment on lines +271 to +272
return zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.isEqual(other.withZoneSameInstant(zonedDateTime.getZone()).truncatedTo(ChronoUnit.DAYS));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.isEqual(other.withZoneSameInstant(zonedDateTime.getZone()).truncatedTo(ChronoUnit.DAYS));
return zonedDateTime.truncatedTo(ChronoUnit.DAYS).isEqual(other.truncatedTo(ChronoUnit.DAYS));

isEqual already compares the instant so it's not necessary to equalise the timezone prior to comparison.

jshell> var d1 = java.time.ZonedDateTime.parse("2024-01-01T00:00:00Z")
d1 ==> 2024-01-01T00:00Z

jshell> var d2 = java.time.ZonedDateTime.parse("2024-01-01T10:00:00+10:00")
d2 ==> 2024-01-01T10:00+10:00

jshell> d1.isEqual(d2)
$17 ==> true

jshell> d1.getZone()
$18 ==> Z

jshell> d2.getZone()
$19 ==> +10:00

Copy link
Contributor Author

@clinique clinique Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the truncation to ChronoUnit.DAYS, the comparison is false if they are not moved in the same time zone.

public boolean isAfter(DateTimeType other) {
return zonedDateTime.isAfter(other.zonedDateTime);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also need the versions of isBefore and isAfter that accept a ZonedDateTime

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These already exists, reason why I thought it wasn't needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically it's for convenience, so you can do DateTimeItem.state.isBefore(zdt) instead of DateTimeItem.state.getZonedDateTime().isBefore(zdt), and also for uniformity since you are adding the ability to do
DateTimeItem.state.isBefore(anotherDateTimeItem), but, I'll leave the decision whether to have it to the maintainers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

public boolean isBeforeDate(ZonedDateTime other) {
return zonedDateTime.truncatedTo(ChronoUnit.DAYS).isBefore(other.truncatedTo(ChronoUnit.DAYS));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am against the introduction of isBeforeDate, isBeforeTime, etc. because their meaning is ambiguous depending on the time zones of the two objects involved.

Take two zdt objects containing the same date but within two different time zones. They would fail the given comparison.

jshell> var d1 = java.time.ZonedDateTime.parse("2024-01-01T00:00:00-11:00")
d1 ==> 2024-01-01T00:00-11:00

jshell> var d2 = java.time.ZonedDateTime.parse("2024-01-01T10:00:00+11:00")
d2 ==> 2024-01-01T10:00+11:00

jshell> d2.withZoneSameInstant(d1.getZone())
$22 ==> 2023-12-31T12:00-11:00

jshell> d1.truncatedTo(java.time.temporal.ChronoUnit.DAYS).isBefore(d2.truncatedTo(java.time.temporal.ChronoUnit.DAYS))
$23 ==> false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do you recommend I should align Timezones before comparison, rename the methods or drop them ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to drop them, but that's just an opinion. Again, I defer to the maintainers to decide.

return new DateTimeType(zonedDateTime.withYear(now.getYear()).withMonth(now.getMonthValue())
.withDayOfMonth(now.getDayOfMonth()));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also has the same ambiguity issue related to differences in time zones between what's in zonedDateTime vs the system time zone.

jshell> var d1 = java.time.ZonedDateTime.parse("2024-01-01T00:00:00+11:00")
d1 ==> 2024-01-01T00:00+11:00

jshell> var now = java.time.ZonedDateTime.parse("2025-05-01T00:00:00-11:00")
now ==> 2025-05-01T00:00-11:00

jshell> d1.withYear(now.getYear()).withMonth(now.getMonthValue())
   ...>                 .withDayOfMonth(now.getDayOfMonth()).withZoneSameInstant(now.getZone())
$38 ==> 2025-04-30T02:00-11:00

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it relative to zonedDateTime zone's.

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 this pull request may close these issues.

3 participants