I have an enum TimeFrame that has values like: Yesterday, LastWeek, NextMonth etc.
I'm trying to write a method that takes in a TimeFrame and returns the start and end dates of that period of time.
I looked into the new Java 8 Period class which can take a start time and end time but it doesn't seem there's any clean way to retrieve those values afterwards.
How could I return the start and end date at once cleanly without using a List (seems like the wrong data structure) or some ugly datetime arithmetic?
No, a Period doesn't record a start/end - it just represents the amount of time between them, in a calendrical sense (rather than the "precise amount of time" represented by a Duration).
It sounds like basically you should create your own class that has a start date and an end date. You can still use Period to implement that, but you'd have getStartDate() and getEndDate() returning LocalDate values. You shouldn't need to do any heavy date/time arithmetic yourself - just work out the start date and add the appropriate period, or the end date and subtract the appropriate period, depending on the time frame.
Time marches on
Be careful about passing around enum values for “yesterday”, “tomorrow”, and so on. That raises a couple of issues, time zone and midnight.
Dates and day-of-week only have meaning in the context of a time zone. For any given moment, the date varies around the globe. For example, a few minutes after midnight in Paris is still “yesterday” in Montréal. So when ever you intend “yesterday” and such, always specify the desired/expected time zone as well.
Each non-atomic command and line of code executes separately from the previous. Each execution takes a moment of time, however brief. At any of those moments midnight in the specified time zone may be rolling over into a new day. At that stroke of midnight, your relative-time flag such as “yesterday” suddenly takes on a whole new meaning. That meaning is likely different that was intended by the earlier code given that conditions (the date) were different when that code began.
So it makes more sense to me to be passing around Instant objects, or perhaps OffsetDateTime or ZonedDateTime objects. These date-time values are frozen, representing a specific moment on the timeline. Your earlier original code can verify the meaning of that value, check that the date is indeed a Friday or some such. After such verification, the value can be passed on to other code. Now you need not worry about strange occasional bugs occurring at midnight.
java.time
You don't really need to build a class or enum to express your intention of relative time such as “yesterday”. The java.time classes built into Java 8 and later have plain-reading methods for adding and subtracting days, weeks, months. These are basically one-liners, so just call these plus… and minus… methods directly.
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime yesterday = now.minusDays( 1 );
ZonedDateTime weekLater = now.plusWeeks( 1 );
That code is implicitly applying the JVM’s current default time zone. Better to specify.
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime now = ZonedDateTime.now( zoneId );
You may want the date-only without time-of-day and without time zone. Use LocalDate.
LocalDate weekPrior = now.toLocalDate().minusWeeks( 1 );
You may want to get first moment of the day. Not always the time of 00:00:00.0.
ZonedDateTime yesterdayStart = now.minusDays( 1 ).toLocalDate().atStartOfDay( zoneId );
If you want to represent the span of time defined by a pair of date-time values, look at the Interval class found in the ThreeTen-Extra project that extends the java.time framework. This class tracks a pair of Instant objects which are moments on the timeline in UTC. You can extract an Instant from your ZonedDateTime by calling toInstant.
Interval intervalYesterday = Interval.of( yesterdayStart.toInstant() , yesterdayStart.plusDays( 1 ).toInstant() );
To get from an Instant back to a zoned date-time, apply a ZoneId.
ZonedDateTime zdt = ZonedDateTime( instant , zoneId );
For a date-only interval, you'll need to build your own class that stores a pair of LocalDate objects. I would call it something like DateRange.
Tip: Search "Half-Open" to learn about the practice of tracking spans of time where beginning in inclusive while the ending is exclusive.
Related
Is there a standard for encoding a date as a timestamp? My thoughts:
This should be 12:00pm UTC in local time, eg 9:00am at T-3, therefore anyone consuming the timestamp, regardless of their -12/+12 offset, will recognize the same date, regardless of whether they parse at the UTC timezone
It could be 12:00pm at UTC
It could be the start of the day (12:00am) at UTC
It could be start of the day (12:00am UTC) in local time eg 9:00pm at T-3
Is there an official spec or standard to adhere to?
It would be easy to point to this document and say 'this is the standard' as opposed to being unaware and having to change our logic down the line.
There isn't a standard for this, because a date and a timestamp are logically two very different concepts.
A date covers the entire range of time on that day, not a specific point in time.
It may be a different date for a person in another time zone at any given point in time, but dates themselves do not have any association with time zones. Visualize a date as just a square on a calendar, not a point on a timeline.
Many APIs will use midnight (00:00) as the default time when a date-only value is assigned to a date+time value. However:
Whether it is UTC based or local-time based is very dependent on that particular API. There is no standard for this, nor is one answer necessarily better than the other.
Assigning a local-time midnight can be problematic for time zones with transitions near midnight. For example, in Santiago, Chile on 2019-09-08, the day started at 01:00 due to the start of DST. There was no 00:00 on that day.
Also, you tagged your question with momentjs. Since a Moment object is basically a timestamp (not a date), then Moment.js will generally assign the start of the day if provided a date-only value. The time zone involved is key to deciding which moment that actually is, which illustrates my prior points.
For example:
// Parsing as UTC
moment.utc('2019-09-08').format() //=> "2019-09-08T00:00:00Z"
// Parsing as Local Time (my local time zone is US Pacific Time)
moment('2019-09-08').format() //=> "2019-09-08T00:00:00-07:00"
// Parsing in a specific time zone (on a day without midnight)
moment.tz('2019-09-08', 'America/Santiago').format() //=> "2019-09-08T01:00:00-03:00"
Also keep in mind that sometimes APIs can be misnamed. The JavaScript Date object is not a date-only value, but actually a timestamp, just like a moment.
I have a date object and a time object that I want to combine into a datetime object, not as a string but as a datetime object that I can store to later test against other datetime objects. All questions like this appear to deal with string formats of datetime.
I tried simply adding the two objects together which works for two elements of a sheet but does not work for script variables.
var lastDayPumpOn = new Date();
lastDayPumpOn = lastPumpDate + lastPumpTime;
Browser.msgBox('Last on time = '+lastDayPumpOn);
This simple addition produces something that appears to be a concatenation of the date with zero time and a time with zero date. It's not clear to me if it is actually a datetime object or some string.
There is no "time" or "datetime" object in JavaScript, just Date.
Why you get a string, not a new date
If you want to add the milliseconds stored inside your Date objects together, you have to take care that both arguments are indeed Date objects or numbers (representing milliseconds), and get the milliseconds with either .getTime() or prefix a + to the variable.
// this assumes lastPumpTime is some Date that has had its date component "zeroed out"
var lastDayPumpOn = new Date(lastPumpDate.getTime() + lastPumpTime.getTime()); // construct new Date from milliseconds argument
// or
var lastDayPumpOnAlt = new Date(+lastPumpDate + +lastPumpTime); // implicitly using Date.prototype.valueOf
// lastDayPumpOn.getTime() === lastDayPumpOnAlt.getTime() is true
Browser.msgBox('Last on time = ' + lastDayPumpOn); // implicitly calls lastDayPumpOn.toString()
If either variable around a + is a string, then the result will be a string. Any objects will have their .toString function called implicitly.
Zeroing out date part
To create your "time" object (Date without the date part), there are various methods, one of which is to just set the date part to Jan 1, 1970
lastPumpTime.setFullYear(1970);
lastPumpTime.setMonth(0); // Jan === 0, zero indexed
lastPumpTime.setDate(1);
Now lastPumpTime.getTime() is some milliseconds less than 24 hours worth of milliseconds.
I finally got this to work so I thought I should list the working code in case others might find it useful. Perhaps my requirement is very unusual since I could not find a similar example. All other examples to combine date and time dealt with string formatted dates and times.
I learned a lot about date manipulation by this exercise. dwmorrin was a great help. The last surprise was the need to correct the time zone. I found that the time fixing left the result in GMT zone, well actually GMT offset by daylight savings time shift. My use of getTimezoneOffset may not be the most general method to account for this but it works for my time zone.
Thank you again dwmorrin.
// The variables lastPumpDate and lastPumpTime are a date and time read from two columns of
// dates and times that had been at some earlier time deconstructed from a column of full
// date-time objects using the spreadsheet functions int(date) and mod(date,1).
// This code segment of a script is to reconstruct the full date-time object to be stored in another
// sheet for further processing.
Var lastPumpTimeFixed = lastPumpTime; // Time object must have date component cleared
lastPumpTimeFixed.setFullYear(1970); // This done by setting it to base date
lastPumpTimeFixed.setMonth(0);
lastPumpTimeFixed.setDate(1);
var valTimezoneOffset = lastPumpTime.getTimezoneOffset()*60*1000;
var lastDayPumpOn = new Date(+lastPumpDate.getTime() +lastPumpTimeFixed.getTime() - valTimezoneOffset);
A bit further explanation for this solution: The date-only object lastPumpDate was read from an existing Google sheet by a .getValues() function which also added a timezone offset that didn’t exist in the sheet cell. This set the time of the script variable to the equivalent UTC if/when it would be combined with a time object. This timezone offset was based upon the timezone of the running script and included any adjustment for daylight savings time based on the date read. The time-only object lastPumpTime was likewise read from the pre-existing sheet using the .getValues() function which also added a timezone offset that may not have existed in the sheet cell, I didn’t dig that far to be sure. This timezone offset was also correct for the time zone of the running script to set the time to the equivalent UTC but without any adjustment for daylight savings time since its date was unknown. Therefore, when adding the date-only object to the time-only object in the “new Date” operation, the result had two timezone offsets, one adjusted for daylight savings time and one not. That is the reason that one offset had to be subtracted and it must be the one associated with the time-only object, which did not include a daylight savings adjustment. Then when a later part of the script put the resultant full date-time object into a new sheet, the remaining offset was effectively un-applied leaving the time of the full date-time cell the same as that of the original time-only cell. I cannot say for sure what would happen if the final sheet is in a different timezone from the original sheet, but I imagine that a different adjustment might be applied leaving the time of the full date-time cell with same UTC as the original but adjusted for the different timezone.
I think the name org.joda.time.LocalDate is kind of confusing. The documentation says:
LocalDate is an immutable datetime class representing a date without a time zone.
In comparison org.joda.time.DateTime says:
A DateTime calculates its fields with respect to a time zone.
I am always confusing those two so I hope somebody can tell me why those names are supposed to make sense.
Here is my intuition: A local date or time object would represent a point in time but with regards to a location. Hence it should contain the time zone information since the time zone also gives you some sort of location information. In any case you know a little more about somebodies location than without that time zone.
A date-time, at least as it sounds like, should only represent a date and a time. In this sense it is just the long value since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970.
So why is it actually the other way around? This naming bugs me every time I have something to do with time stamps. Can somebody explain?
Update:
Interesting link posted by a user: https://sourceforge.net/p/threeten/mailman/message/29962542/
Terminology
Your intuition is the opposite of the terminology used by both Joda-Time and java.time.
Both frameworks have a class named LocalDate to represent a date-only value without time-of-day and without time zone. The ”local“ means ”could be any locality, not any particular locality“.
The ”Local…“ classes are just a rough idea of a time. They are not on the timeline. They have no real meaning until you apply a time zone or offset-from-UTC to get actual moments on the timeline. When you see ”Local“ think: ”this value does not really make sense until we apply a time zone“.
For example, we say that Christmas is on 2016-12-25 this year. But the date is not the same around the world at any given moment. A new day dawns earlier in Paris than in Montréal for example. So to get the moment when Christmas starts in Paris, you must apply the Europe/Paris time zone and get the first moment of that day. That first moment will be represented by DateTime in Joda-Time and by ZonedDateTime (or OffsetDateTime) in java.time.
For example, in java.time:
LocalDate xmas2016 = LocalDate.of( 2016 , 12 , 25 );
ZonedDateTime xmas2016FirstMomentMontreal = xmas2016.atStartOfDay( ZoneId.of( "America/Montreal" ) );
java.time
In the java.time framework, a moment on the timeline in UTC is represented by the Instant class.
Instant now = Instant.now();
Apply a time zone, ZoneId, to get a ZonedDateTime.
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );
If all you have is an offset-from-UTC rather than a full time zone, than use OffsetDateTime rather than ZonedDateTime.
You can think of this way:
Instant + ZoneId -> ZonedDateTime
Instant + ZoneOffset ->OffsetDateTime
This information has been covered many hundreds of times already on StackOverflow. Please search to learn more and see many pieces of example code.
val date = "01-10-1967"
val pattern = "dd-MM-yyyy"
val formatter = DateTimeFormat.forPattern(pattern)
formatter.parseMillis(date) // this line fails
The last line fails with:
Cannot parse "01-10-1967": Illegal instant due to time zone offset transition (America/Argentina/Buenos_Aires)
Any idea why?
(JodaTime version is 2.3)
The 1st of October 1967 was in Argentina a day where they changed from standard time to summer time, i.e. added 1 hour, on 00:00.
Since you are not providing a concrete time, I would assume that it defaults to exactly 00:00 which simply did not exist on that day.
Cf. the official faq:
What does 'Illegal instant due to time zone offset transition' mean?
Joda-Time only allows the key classes to store valid date-times. For
example, 31st February is not a valid date so it can't be stored
(except in Partial). The same principle of valid date-times applies to
daylight savings time (DST). In many places DST is used, where the
local clock moves forward by an hour in spring and back by an hour in
autumn/fall. This means that in spring, there is a "gap" where a local
time does not exist. The error "Illegal instant due to time zone
offset transition" refers to this gap. It means that your application
tried to create a date-time inside the gap - a time that did not
exist. Since Joda-Time objects must be valid, this is not allowed.
Possible solutions might be (taken from the faq):
Use LocalDateTime, as all local date-times are valid.
When converting a LocalDate to a DateTime, then use toDateTimeAsStartOfDay() as this handles and manages any gaps.
When parsing, use parseLocalDateTime() if the string being parsed has no time-zone.
Since you aren't interested in time information anyway, I think you might even want to replace formatter.parseMillis(date) with formatter.parseLocalDate(date). If for some reason you still need milliseconds, this Stack Overflow question might help.
How do I get date function to return date according to current system date?
Right now, with the code snippet below, it always returns UK time, not the current system date.
<calculate>
<script>$ = concat( num2date(date(), DateFmt()), " ", num2Time(Time(), TimeFmt()) )</script>
Any help is appreciated!
It's probably not UK time exactly, but rather GMT (or UTC, to use a more precise term). The UK happens to be aligned to GMT in the winter, but in the summer it advances one hour to BST for daylight saving time.
Now, I've never used LiveCycle myself, but nonetheless, I've read through the somewhat minimal docs for LiveCycle FormCalc Date and Time Functions, and the spec, and it appears to me that a few critical mistakes were made.
The date and time functions return UTC-based values, but only the time-related functions have been made aware of the local time zone. That is, there are separate Num2Time and Num2GMTime functions, but there is only one Num2Date function.
The Num2Date function works in terms of whole integer days, and thus they are simply days since 1900-01-01. Therefore, the number being passed in to the function must already be representative of the desired time zone. However, the Date function only gets the current GMT date. There does not appear to be a function to get the current local date.
It's different on the time side, because of the millisecond precision involved. However, there's yet another flaw here. Despite the docs saying that the Time function returning "the number of milliseconds since the epoch", its actually returning only the number of milliseconds since midnight GMT. There is no day-over-day accumulation of milliseconds from the date part. The docs here are even lying in their sample code which says:
Returns the current system time as the number of milliseconds since the epoch.
Time() => 71533235 at precisely 3:52:15 P.M. on September 15th, 2003 to a user in the Eastern Standard Time (EST) zone.
If that was indeed the case (and ensuring to use their 1900-01-01 epoch), the value would actually include an additional 3272572800000 milliseconds representing the days between 1900-01-01 and 2003-09-15, bringing the total timestamp to 3272644333235. Additionally, there's a typo there, because the timestamp they give is 3:52:13, not 3:52:15. Clearly nobody paid close attention to these docs!
The real problem is that one cannot be certain that the number of milliseconds since midnight of the current day in the local time zone is the same on every day. If instead of getting the current time, you were working with past stored time values, you might be an hour off (+ or -) if the current offset is different due to daylight saving time. Example: Eastern time might be UTC-5 or UTC-4, but only the offset currently in effect will be used by the Num2Time function, even if the date you're working with is using the other offset.
So, in summary, the Date function is insufficient, leading to your observations, and the date/time functionality in general is poorly designed. Given the constraints of this API, I can't even recommend a workaround. There would have to be a LocalDate function of some kind to be used instead of the Date function, but it doesn't exist.
The only advice I can offer is that it appears (from my research, not experience) that LiveCycle can use either FormCalc or JavaScript. So - use JavaScript instead.