Adding Period to startDate doesn't produce endDate - kotlin

I have two LocalDates declared as following:
val startDate = LocalDate.of(2019, 10, 31) // 2019-10-31
val endDate = LocalDate.of(2019, 9, 30) // 2019-09-30
Then I calculate the period between them using Period.between function:
val period = Period.between(startDate, endDate) // P-1M-1D
Here the period has the negative amount of months and days, which is expected given that endDate is earlier than startDate.
However when I add that period back to the startDate, the result I'm getting is not the endDate, but the date one day earlier:
val endDate1 = startDate.plus(period) // 2019-09-29
So the question is, why doesn't the invariant
startDate.plus(Period.between(startDate, endDate)) == endDate
hold for these two dates?
Is it Period.between who returns an incorrect period, or LocalDate.plus who adds it incorrectly?

If you look how plus is implemented for LocalDate
#Override
public LocalDate plus(TemporalAmount amountToAdd) {
if (amountToAdd instanceof Period) {
Period periodToAdd = (Period) amountToAdd;
return plusMonths(periodToAdd.toTotalMonths()).plusDays(periodToAdd.getDays());
}
...
}
you'll see plusMonths(...) and plusDays(...) there.
plusMonths handles cases when one month has 31 days, and the other has 30. So the following code will print 2019-09-30 instead of non-existent 2019-09-31
println(startDate.plusMonths(period.months.toLong()))
After that, subtracting one day results in 2019-09-29. This is the correct result, since 2019-09-29 and 2019-10-31 are 1 month 1 day apart
The Period.between calculation is weird and in this case boils down to
LocalDate end = LocalDate.from(endDateExclusive);
long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();
int days = end.day - this.day;
long years = totalMonths / 12;
int months = (int) (totalMonths % 12); // safe
return Period.of(Math.toIntExact(years), months, days);
where getProlepticMonth is total number of months from 00-00-00. In this case, it's 1 month and 1 day.
From my understanding, it's a bug in a Period.between and LocalDate#plus for negative periods interaction, since the following code has the same meaning
val startDate = LocalDate.of(2019, 10, 31)
val endDate = LocalDate.of(2019, 9, 30)
val period = Period.between(endDate, startDate)
println(endDate.plus(period))
but it prints the correct 2019-10-31.
The problem is that LocalDate#plusMonths normalises date to be always "correct". In the following code, you can see that after subtracting 1 month from 2019-10-31 the result is 2019-09-31 that is then normalised to 2019-10-30
public LocalDate plusMonths(long monthsToAdd) {
...
return resolvePreviousValid(newYear, newMonth, day);
}
private static LocalDate resolvePreviousValid(int year, int month, int day) {
switch (month) {
...
case 9:
case 11:
day = Math.min(day, 30);
break;
}
return new LocalDate(year, month, day);
}

I believe that you are simply out of luck. The invariant that you have invented sounds reasonable, but doesn’t hold in java.time.
It seems that the between method just subtracts the month numbers and the days of month and since the results have the same sign, is content with this result. I think I agree that probably a better decision could have been taken here, but as #Meno Hochschild has correctly stated, math involving the 29, 30 or 31 of months can hardly be clearcut, and I dare not suggest what the better rule would have been.
I bet they are not going to change it now. Not even if you file a bug report (which you can always try). Too much code is already relying on how it’s been working for more than five and a half years.
Adding P-1M-1D back into the start date works the way I would have expected. Subtracting 1 month from (really adding –1 month to) October 31 yeilds September 30, and subtracting 1 day yields September 29. Again, it’s not clear-cut, you could argue in favour of September 30 instead.

Analyzing your expectation (in pseudo code)
startDate.plus(Period.between(startDate, endDate)) == endDate
we have to discuss several topics:
how to handle separate units like months or days?
how is the addition of a duration (or "period") defined?
how to determine the temporal distance (duration) between two dates?
how is the subtraction of a duration (or "period") defined?
Let's first look at the units. Days are no problem because they are the smallest possible calendar unit and every calendar date differs by any other date in full integers of days. So we always have in pseudo code equal if positive or negative:
startDate.plus(ChronoUnit.Days.between(startDate, endDate)) == endDate
Months however are tricky because the gregorian calendar defines calendar months with different lengths. So the situation can arise that the addition of any integer of months to a date can cause an invalid date:
[2019-08-31] + P1M = [2019-09-31]
The decision of java.time to reduce the end date to a valid one - here [2019-09-30] - is reasonable and corresponds to the expectations of most users because the final date still preserves the calculated month. However, this addition including an end-of-month-correction is NOT reversible, see the reverted operation called subtraction:
[2019-09-30] - P1M = [2019-08-30]
The result is also reasonable because a) the basic rule of month addition is to keep the day-of-month as much as possible and b) [2019-08-30] + P1M = [2019-09-30].
What is the addition of a duration (period) exactly?
In java.time, a Period is a composition of items consisting of years, months and days with any integer partial amounts. So the addition of a Period can be resolved to the addition of the partial amounts to the starting date. Since years are always convertible to 12-multiples of months, we can first combine years and months and then add the total in one step in order to avoid strange side effects in leap years. The days can be added in the last step. A reasonable design as done in java.time.
How to determine the right Period between two dates?
Let's first discuss the case when the duration is positive, meaning the starting date is before the ending date. Then we can always define the duration by first determining the difference in months and then in days. This order is important to achieve a month component because otherwise every duration between two dates would only consist of days. Using your example dates:
[2019-09-30] + P1M1D = [2019-10-31]
Technically, the starting date is first moved forward by the calculated difference in months between start and end. Then the day delta as difference between the moved start date and the end date is added to the moved start date. This way we can calculate the duration as P1M1D in the example. So far so reasonable.
How to subtract a duration?
Most interesting point in the previous addition example is, there is by accident NO end-of-month-correction. Nevertheless java.time fails to do the reverse subtraction.
It first subtracts the months and then the days:
[2019-10-31] - P1M1D = [2019-09-29]
If java.time had instead tried to reverse the steps in the addition before then the natural choice would have been to first subtract the days and then the months. With this changed order, we would get [2019-09-30]. The changed order in the subtraction would help as long as there was no end-of-month-correction in the corresponding addition step. This is especially true if the day-of-month of any starting or ending date is not bigger than 28 (the minimum possible month length). Unfortunately java.time has defined another design for the subtraction of Period which leads to less consistent results.
Is the addition of a duration reversible in the subtraction?
First we have to understand that the suggested changed order in the subtraction of a duration from a given calendar date does not guarantee the reversibility of the addition. Counter example which has an end-of-month-correction in the addition:
[2011-03-31] + P3M1D = [2011-06-30] + P1D = [2011-07-01] (ok)
[2011-07-01] - P3M1D = [2011-06-30] - P3M = [2011-03-30] :-(
Changing the order is not bad because it yields more consistent results. But
how to cure the remaining deficiencies? The only way left is to change the calculation of the duration, too. Instead of using P3M1D, we can see that the duration P2M31D will work in both directions:
[2011-03-31] + P2M31D = [2011-05-31] + P31D = [2011-07-01] (ok)
[2011-07-01] - P2M31D = [2011-05-31] - P2M = [2011-03-31] (ok)
So the idea is to change the normalization of the computed duration. This can be done by looking if the addition of the computed month delta is reversible in a subtraction step - i.e. avoids the need for an end-of-month-correction. java.time does unfortunately not offer such a solution. It is not a bug, but can be considered as a design limitation.
Alternatives?
I have enhanced my time library Time4J by reversible metrics which deploy the ideas given above. See following example:
PlainDate d1 = PlainDate.of(2011, 3, 31);
PlainDate d2 = PlainDate.of(2011, 7, 1);
TimeMetric<CalendarUnit, Duration<CalendarUnit>> metric =
Duration.inYearsMonthsDays().reversible();
Duration<CalendarUnit> duration =
metric.between(d1, d2); // P2M31D
Duration<CalendarUnit> invDur =
metric.between(d2, d1); // -P2M31D
assertThat(d1.plus(duration), is(d2)); // first invariance
assertThat(invDur, is(duration.inverse())); // second invariance
assertThat(d2.minus(duration), is(d1)); // third invariance

Related

SQL datediff translation

I am not a SQL user, it happens that I need to know what this type of SQL code means, can you help me please translating it to me?
CASE
WHEN DATEDIFF(to_date(B.DT_CAREPACK_END),
LAST_DAY(date_sub(add_months(current_date, 3), 20))) > 0
CASE statements let you introduce some conditional logic.
A full CASE sttatement would have a syntax like:
CASE
WHEN some logic is true THEN x
WHEN some other logic is true THEN y
ELSE THEN z
END as column_title
In your example, it doesn't look like you've provided the full statement as their is no END keyword.
Basically, this logic is checking when the difference between two dates (date-x and date-y) is positive or not. DATE_DIFF looks at the different between a start date (date-x) and an end date (date-y).
If date-x, the start date, is before date-y, the end date, then the result is positive. If the start date is after the end date, then the result is negative.
date-x is a date representation of the column DT_CAREPACK_END
date-y is taking the current_date, adding on 3 months (e.g. 4th September becomes 4th December), is is then subtracting 20 units (presumably days) and then setting that date to the last date of that month.
So, imagine DT_CAREPACK_END (presumably a date when something ends) is in the future and is 2022-10-02.
The inner logic here will take the current date (2022-09-04) and add 3 months to that date, making it 2022-12-04. Then, we are subtracting 20 days which is 2022-11-14. Then, we find the last day in that month, which would be 2022-11-30.
Finally, we look at the difference between 2022-10-02 (start date) and 2022-11-30 (end date). If that is a positive number, then the logic is satisfied. In this case, 2nd October is before 30th November, resulting in a positive logic and therefore the case logic is satisfied.
If the DT_CAREPACK_END is before the current_date logic, then it would be negative.
*N.B. I thought that date_add, date_sub and date_diff functions needed an interval unit to be explicitly stated (e.g. INTERVAL 20 DAY). I'm guessing the default here is days but that's an assumption on my part. I'm working in good-faith that the code snip is syntatically correct. *
Resources:-
Add Months:
https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions004.htm
Date Sub:
https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/date-and-time-functions.html#function_date-add
Last Day:
https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/date-and-time-functions.html#function_last-day

How does one get number of full days and half days from a start and end date

Background
I've been working on some reporting views that get a multi-day work shift and are supposed to do some calculations based on data, but I'm a bit stuck here.
A typical shift is either 3 calendar days usually 1 half-day and two full days, or a whole week consisting of 2 half-days (end and start) and 5 full days.
Specifications
I have the following specifications for what is a full day and half-day. These rules are based on regulation and can't be changed.
2 half-days != 1 full-day, the 2 halves is more "valuable"
Given a started_at iso datetime and end_at iso datetime
I want to get two numbers, full_days, and half_days
A half day is
A day at the start of the range starting at or after 12.00
A day at the end of the range which ends before 19.00
A full day is
A day within the range (traditional 24hours)
A day at the start of the range starting before 12.00
A day at the end of the range which ends at or after 19.00
I'm thinking either a row per full-day and half-day or an aggregated row with half_days and full_days as two separate columns would be ideal in the view to connect it with my other views.
Simplified model
I simplified the data model to leave out unnecessary columns.
create table if not exists [trip]
(
trip_id integer
constraint trip_pk
primary key,
started_at text default (datetime('now')),
end_at text default (datetime('now'))
);
And I'm a bit stuck with how I should design this query. A simple time delta doesn't work.
SQLFiddle with sample data and answers: http://sqlfiddle.com/#!5/de7551/2
You can solve this with a CTE which calculates the day span (number of days the shift spans). Since half days are always 1, 2 or 0 (only occur on end and start) we don't actually need to consider each day by itself.
You can use julianday to get the day as a number, however julian days start at noon so you'll need to subtract 0.5 to get the "actual" day for your calculation. Floor the ending day to avoid a to long span if the end time is later then the start time on each respective day, and round up the result to include partial days as a spanned day.
At this point we can calculate number of half days by checking the end and start. To get the number of full days we simply subtract the half days from the result.
with trip_spans as (
select
ceil(julianday(end_at)-0.5 - floor(julianday(started_at)-0.5)) day_span
, t.*
, (
iif(time(started_at) > time('12:00'), 1, 0)
+
iif(time(end_at) <= time('19:00'), 1, 0)
) half_days
from trip t
)
select
trip_spans.*
, day_span-half_days full_days
from trip_spans

in rails i need to get all records based on a date range, ignoring the year

i am trying to get all records within a date range in rails that fall after a given start date and before a given end date, ignoring the year. the start date will just be a month. the end of the date range is a month and date.
the following example is to get users with a hire date month equal to january or greater, but less than 2(february) / 28(day).
i was trying this but it doesnt work:
users.where('extract(month from hire_date) >= ?', 1).where('extract(month from hire_date) <= ? AND extract(day from hire_date) <= ?', 2, 28)
the reason this doesnt work is it will exclude users, for example who fall on 1/29, because their day in january is not less than 28.
is there a good way to make this work, ignoring the year on the date field?
I would use to_char to transform the dates into strings including just the month and the day. For example 2021-12-31 could be translated into "12-31". That string can then be compared to the range of date strings you are interested in.
users.where("to_char(hire_date, 'MM-DD') BETWEEN '01-01' AND '02-29'")
Note that this, in theory, this would also match invalid date strings like '01-40'. But I guess it is safe to assume that to_char with a valid date will never return such invalid date strings.
Let me start by saying that I'm not familiar with ruby-on-rails, sql, or activerecord but this question really piqued my interest because it contains a number of complexities that I've never had to deal with. Most notably:
How to accommodate leap years when the test range and target date may
have differing leap year statuses.
How to deal with a hire date of February 29th. This may seem to be
the same as the first item on my list, but it has its own unique
nuances.
How to deal with ranges that started in one year and then overlap into the next.
I did some searching myself and there seems to be very little information out there on the subject, particularly any that seems to properly deal with the various complexities listed above, so I decided to see if I could come up with a logical approach of my own. It seems like it may be a novel approach, but ultimately I decided that converting month and day to float values would allow an easy way to address the leap year issue as well as the issue with February 29th, and that testing the date ranges using an if/else statement along with opposing boolean operations would solve the ranges that overlap years.
You may have to extrapolate and reconfigure for your specific needs, but here's the general idea written out in pure ruby. It's rather verbose, but I did so just to try and make it more clear what I'm doing. I could be made much more compact:
hire_date = Time.new(2004, 1, 22)
provided_start_date = Time.new(2008, 12, 22)
day_value = (60*60*24) #60 seconds * 60 minutes * 24 hours = 1 day
num_days = 30
calculated_end_date = provided_start_date + num_days*day_value
start_range = provided_start_date.month + provided_start_date.day/100.0
end_range = calculated_end_date.month + calculated_end_date.day/100.0
day_float = hire_date.month + hire_date.day/100.0
if start_range < end_range
result = day_float >= start_range and day_float <= end_range
else
result = day_float >= start_range or day_float <= end_range
end
result
So i figured out something that worked for this.
given the following:
start_date = 01 Jan 2021
end_date = 02 Feb 2021
Users.where("(extract(month from hire_date) * 100 + extract(day from hire_date)) BETWEEN ? AND ?", ((start_date.month * 100) + start_date.day), ((end_date.month * 100) + end_date.day))
start date and end date can be any dates and this should work to get all users between, since SQL between is inclusive, it will also get users whos end and start dates fall on the start / end date, ignoring year.

WHERE statement to choose record previous day but choose Friday record when current day is Monday Microsoft SQL

I need a WHERE statement where the date of the record is the previous day. I have the below code which will do this
WHERE DOC_DATE = dateadd(day,datediff(day,1,GETDATE()),0)
However I need this statement to get Friday's record when the current day is Monday. I have the below code but it will not work for me. No errors come back on SQL although no records results come back either. I have the below code for this
WHERE DOC_DATE = DATEADD(day, CASE WHEN datepart(dw,(GETDATE())) IN (2) then -3 ELSE -1 END ,0)
Important to add that this needs to be in a WHERE clause. This is for a Docuware administrative view I am creating. I have no control on how to write the SELECT statement, I only have access to edit the WHERE clause:
Here's a slightly "magical" way to compute the value that doesn't depend on any particular server settings such as datefirst. It's probably not immediately obvious how it works:
WHERE DOC_DATE = dateadd(day,datediff(day,'20150316',getdate()),
CASE WHEN DATEPART(weekday,getdate()) = DATEPART(weekday,'20150330')
THEN '20150313'
ELSE '20150315' END)
In the first line, we're computing the number of days which have elapsed since some arbitrary date. I picked a day in March 2015 to use as my base date.
The second line asks what today's day of the week is and if it's the same as some arbitrary "Known good" Monday. Just taking one value and comparing it to 2 depends on what your DATEFIRST setting is so I prefer not to write that.
In the third line, we decide what to do if it's a monday - I give a date that is 3 days before my arbitrary date above. If it wasn't a monday, we pick the day before.
Adding it all together, when we add the days difference from the arbitrary date back to one of these two dates from lines 3 and 4, it has the effect of shifting the date backwards 1 or 3 days.
It's can be an odd structure to see if you're not familiar with it - but combining dateadd/datediff and exploiting relationships between an arbitrary date and other dates computed from it can be useful for performing all kinds of calculations. A similar structure can be used for computing e.g. the last day of the month 15 months ago using just dateadd/datediff, an arbitrary date and another date with the right offset from the first:
SELECT DATEADD(month,DATEDIFF(month,'20010101',GETDATE()),'19991031')
As I said in a comment though, usually doing this sort of thing is only a short step away from needing to properly model your organisation's business days, at which point you'd typically want to introduce a calendar table. At one row per day, 20 years worth of pre-calculated calendar (adjusted as necessary as the business changes) is still less than 10000 rows.
You can try this.
WHERE DOC_DATE = DATEADD(DAY, CASE WHEN datepart(dw, GETDATE()) = 2 THEN -3 ELSE -1 END, CAST(GETDATE() AS DATE))

SQL ORACLE Get week numbers from multiple datetime rows

I have 70.000 rows of data, including a date time column (YYYY-MM-DD HH24-MM-SS.).
I want to split this data into 3 separate columns; Hour, day and Week number.
The date time column name is 'REGISTRATIONDATE' from the table 'CONTRACTS'.
This is what I have so far for the day and hour columns:
SELECT substr(REGISTRATIONDATE, 0, 10) AS "Date",
substr(REGISTRATIONDATE, 11, 9) AS "Hour"
FROM CONTRACTS;
I have seen the options to get a week number for specific dates, this assignment concerns 70.000 dates so this is not an option.
You (the OP) still have to explain what week number to assign to the first few days in a year, until the first Monday of the year. Do you assign a week number for the prior calendar year? In a Comment I asked about January 1, 2017, as an example; that was a Sunday. The week from January 2 to January 8 of 2017 is "week 1" according to your definition; what week number do you assign to Sunday, January 1, 2017?
The straightforward calculation below assigns to it week number 0. Other than that, the computation is trivial.
Notes: To find the Monday of the week for any given date dt, we can use trunc(dt, 'iw'). iw stands for ISO Week, standard week which starts on Monday and ends on Sunday.
Then: To find the first Monday of the year, we can start with the date January 7 and ask for the Monday of the week in which January 7 falls. (I won't explain that one - it's easy logic and it has nothing to do with programming.)
To input a fixed date, the best way is with the date literal syntax: date '2017-01-07' for January 7. Please check the Oracle documentation for "date literals" if you are not familiar with it.
So: to find the week number for any date dt, compute
1 + ( trunc(dt, 'iw') - trunc(date '2017-01-07', 'iw') ) / 7
This formula finds the Monday of the ISO Week of dt and subtracts the first Monday of the year - using Oracle date arithmetic, where the difference between two dates is the number of days between them. So to find the number of weeks we divide by 7; and to have the first Monday be assigned the number 1, instead of 0, we need to add 1 to the result of dividing by 7.
The other issue you will have to address is to convert your strings into dates. The best solution would be to fix the data model itself (change the data type of the column so that it is DATE instead of VARCHAR2); then all the bits of data you need could be extracted more easily, you would make sure you don't have dates like '2017-02-29 12:30:00' in your data (currently, if you do, you will have a very hard time making any date calculations work), queries will be a lot faster, etc. Anyway, that's an entirely different issue so I'll leave it out of this discussion.
Assuming your REGISTRATIONDATE if formatted as 'MM/DD/YYYY'
the simples (and the faster ) query is based ond to to_char(to_date(REGISTRATIONDATE,'MM/DD/YYYY'),'WW')
(otherwise convert you column in a proper date and perform the conversio to week number)
SELECT substr(REGISTRATIONDATE, 0, 10) AS "Date",
substr(REGISTRATIONDATE, 11, 9) AS "Hour",
to_char(to_date(REGISTRATIONDATE,'MM/DD/YYYY'),'WW') as "Week"
FROM CONTRACTS;
This is messy, but it looks like it works:
to_char(
to_date(RegistrationDate,'YYYY-MM-DD HH24-MI-SS') +
to_number(to_char(trunc(to_date(RegistrationDate,'YYYY-MM-DD HH24-MI-SS'),'YEAR'),'D'))
- 2,
'WW')
On the outside you have the solution previous given by others but using the correct date format. In the middle there is an adjustment of a certain number of days to adjust for where the 1st Jan falls. The trunc part gets the first of Jan from the date, the 'D' gets the weekday of 1st Jan. Since 1 represents Sunday, we have to use -2 to get what we need.
EDIT: I may delete this answer later, but it looks to me that the one from #mathguy is the best. See also the comments on that answer for how to extend to a general solution.
But first you need to:
Decide what to do dates in Jan before the first Monday, and
Resolve the underlying problems in the date which prevent it being converted to dates.
On point 1, if assigning week 0 is not acceptable (you want week 52/53) it gets a bit more complicated, but we'll still be able to help.
As I see it, on point 2, either there is something systematically wrong (perhaps they are timestamps and include fractions of a second) or there are isolated cases of invalid data.
Either the length, or the format, or the specific values don't compute. The error message you got suggests that at least some of the data is "too long", and the code in my comment should help you locate that.