Reputation: 2810
I originally asked this question here with a different use case but now I realize I could have more cases for this problem.
I have a filter that is a String and can only accept 3 date-formats - YYYY
, YYYY-MM
, YYYY-MM-DD
.
I want to check if the requested String is in the range of two string-valued dates.
Let's say, I have two dates strings which can be in any of the above 3 date-formats
Use case 1
Start date: 2010-05-15
& end date: 2020-05
.
All the below requested String
are In range results:
2010
, 2020
, 2010-05
, 2020-05
, 2020-05-22
, 2010-05-15
, 2010-05-22
, 2015-02-25
All the requested String
below are NOT in range
2009
, 2021
, 2010-04
, 2020-06
, 2010-05-14
, 2020-06-01
Use case 2
Start date: 2010
& end date: 2020-05-15
All the below values are In range results:
2010
, 2020
, 2010-05
, 2020-05
, 2010-05-22
, 2010-01-01
, 2020-05-15
, 2015-02-25
All the values below are NOT in range
2009
, 2021
, 2020-06
, 2020-05-16
, 2020-06-01
Use case 3
Start date: NULL
& end date: 2020-05
All the requested dates which are before 2020-05-31
are In range results:
All the values after 2020-05-31
are NOT in range.
Use case 4
Start date: 2010-05-15
& end date: NULL
All the requested dates which are after 2010-05-15
are In range results:
All the values before 2010-05-15
are NOT in range.
I am using Java time to check if a date is before or after the given dates, but in my case, I have a string as a request date, start date, and end date that could be in any of the 3 date-formats.
I am not sure if this is a valid solution but this is what I am thinking
Simply break all the dates (requested, start, & end) into numbers and compare requested year with years in start & end, if months in requested date is available and also available in the start and end then compare if the month is in range, and same for the dates.
Can someone please help to solve this problem?
Upvotes: 2
Views: 1110
Reputation: 86399
I am not sure if this is a valid solution but this is what I am thinking
Simply break all the dates (requested, start, & end) into numbers and compare requested year with years in start & end, if months in requested date is available and also available in the start and end then compare if the month is in range, and same for the dates.
Yes, your approach is valid. The classes from java.time don’t offer perfect support for it, but with the low-level TemporalAccessor
interface there is a way through:
class IsInRangeWithMissingFields {
@Test
void useCase1() {
assertTrue(isInRange("2010-05-15", "2020-05", "2010"));
assertTrue(isInRange("2010-05-15", "2020-05", "2020"));
assertTrue(isInRange("2010-05-15", "2020-05", "2010-05"));
assertTrue(isInRange("2010-05-15", "2020-05", "2020-05"));
assertTrue(isInRange("2010-05-15", "2020-05", "2020-05-22"));
assertTrue(isInRange("2010-05-15", "2020-05", "2010-05-15"));
assertTrue(isInRange("2010-05-15", "2020-05", "2010-05-22"));
assertTrue(isInRange("2010-05-15", "2020-05", "2015-02-25"));
assertFalse(isInRange("2010-05-15", "2020-05", "2009"));
assertFalse(isInRange("2010-05-15", "2020-05", "2021"));
assertFalse(isInRange("2010-05-15", "2020-05", "2010-04"));
assertFalse(isInRange("2010-05-15", "2020-05", "2020-06"));
assertFalse(isInRange("2010-05-15", "2020-05", "2010-05-14"));
assertFalse(isInRange("2010-05-15", "2020-05", "2020-06-01"));
}
@Test
void useCase2() {
assertTrue(isInRange("2010", "2020-05-15", "2010"));
assertTrue(isInRange("2010", "2020-05-15", "2020"));
assertTrue(isInRange("2010", "2020-05-15", "2010-05"));
assertTrue(isInRange("2010", "2020-05-15", "2020-05"));
assertTrue(isInRange("2010", "2020-05-15", "2010-05-22"));
assertTrue(isInRange("2010", "2020-05-15", "2010-01-01"));
assertTrue(isInRange("2010", "2020-05-15", "2020-05-15"));
assertTrue(isInRange("2010", "2020-05-15", "2015-02-25"));
assertFalse(isInRange("2010", "2020-05-15", "2009"));
assertFalse(isInRange("2010", "2020-05-15", "2021"));
assertFalse(isInRange("2010", "2020-05-15", "2020-06"));
assertFalse(isInRange("2010", "2020-05-15", "2020-05-16"));
assertFalse(isInRange("2010", "2020-05-15", "2020-06-01"));
}
@Test
void useCase3() {
assertTrue(isInRange(null, "2020-05", "2020-05-31"));
assertFalse(isInRange(null, "2020-05", "2020-06-01"));
}
@Test
void useCase4() {
assertTrue(isInRange("2010-05-15", null, "2010-05-15"));
assertFalse(isInRange("2010-05-15", null, "2010-05-14"));
}
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu[-MM[-dd]]");
public static boolean isInRange(String startStr, String endStr, String requestStr) {
TemporalAccessor request = formatter.parse(requestStr);
// is request before start?
if (startStr != null) {
TemporalAccessor start = formatter.parse(startStr);
if (request.get(ChronoField.YEAR)
< start.get(ChronoField.YEAR)) {
return false;
}
if (request.get(ChronoField.YEAR)
== start.get(ChronoField.YEAR)) {
if (request.isSupported(ChronoField.MONTH_OF_YEAR)
&& start.isSupported(ChronoField.MONTH_OF_YEAR)) {
if (request.get(ChronoField.MONTH_OF_YEAR)
< start.get(ChronoField.MONTH_OF_YEAR)) {
return false;
}
if (request.get(ChronoField.MONTH_OF_YEAR)
== start.get(ChronoField.MONTH_OF_YEAR)) {
if (request.isSupported(ChronoField.DAY_OF_MONTH)
&& start.isSupported(ChronoField.DAY_OF_MONTH)) {
if (request.get(ChronoField.DAY_OF_MONTH)
< start.get(ChronoField.DAY_OF_MONTH)) {
return false;
}
}
}
}
}
}
// is request after end?
if (endStr != null) {
TemporalAccessor end = formatter.parse(endStr);
if (request.get(ChronoField.YEAR) > end.get(ChronoField.YEAR)) {
return false;
}
if (request.get(ChronoField.YEAR) == end.get(ChronoField.YEAR)) {
if (request.isSupported(ChronoField.MONTH_OF_YEAR)
&& end.isSupported(ChronoField.MONTH_OF_YEAR)) {
if (request.get(ChronoField.MONTH_OF_YEAR)
> end.get(ChronoField.MONTH_OF_YEAR)) {
return false;
}
if (request.get(ChronoField.MONTH_OF_YEAR)
== end.get(ChronoField.MONTH_OF_YEAR)) {
if (request.isSupported(ChronoField.DAY_OF_MONTH)
&& end.isSupported(ChronoField.DAY_OF_MONTH)) {
if (request.get(ChronoField.DAY_OF_MONTH)
> end.get(ChronoField.DAY_OF_MONTH)) {
return false;
}
}
}
}
}
}
return true;
}
}
This unit test runs green-barred on my computer.
Upvotes: 1
Reputation: 86399
Convert everything to date ranges. Represent a date range as a half-open interval from start date inclusive to end date exclusive. And represent from and to dates as two LocalDate
objects.
For example, from your use case 1
Start date:
2010-05-15
& end date:2020-05
As a date range this becomes from 2010-05-15 inclusive to 2020-06-01 exclusive.
To test whether 2010 is in the range, convert 2010 to the range from 2010-01-01 to 2011-01-01 exclusive. Now 2010 is in the range if and only if the two ranges overlap. They do if and only of the first from date is before the second to date and the second from date is before the first to date. Use LocalDate.isBefore()
. We need “strictly before”, and this method gives us exactly that. Since 2010-05-15 is before 2011-01-01 and 2010-01-01 is before 2020-06-01, the ranges overlap, so we conclude that 2010 is in the range.
Another example from case 1, to test whether 2010-05-14 is in the same range, convert it to a range from 2010-05-14 to 2010-05-15 (exclusive). See if 2010-05-15 is before 2010-05-15 and 2010-05-14 is before 2020-06-01. Already the first half is false, so we conclude that the date is not in the range.
Implementation note: I might use this formatter for parsing all of your strings:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu[-MM[-dd]]");
You may use the parseBest
method of this formatter to obtain either a LocalDate
, a YearMonth
or a Year
from any of the strings you have mentioned. In each case you can convert from there.
Link: Determine Whether Two Date Ranges Overlap
Upvotes: 1
Reputation: 159260
Answer by Basil Bourque is nice and concise, so you should use it, however it only works because the date formats are simple ISO-8601 texts. If the formats are different, you'd need to parse them, before being able to compare them. Here is a solution that parses the strings, which of course makes the logic more complex.
I would start by creating a helper class:
/**
* Class for storing a {@code Temporal} of varying precision,
* {@code Year}, {@code YearMonth}, or {@code LocalDate}.
*/
class DynamicTemporal {
private enum Precision {
YEAR {
@Override
protected int compare(Temporal a, Temporal b) {
return widen(a).compareTo(widen(b));
}
private Year widen(Temporal value) {
return Year.from(value);
}
},
MONTH {
@Override
protected int compare(Temporal a, Temporal b) {
return widen(a).compareTo(widen(b));
}
private YearMonth widen(Temporal value) {
return YearMonth.from(value);
}
},
DAY {
@Override
protected int compare(Temporal a, Temporal b) {
return widen(a).compareTo(widen(b));
}
private LocalDate widen(Temporal value) {
return (LocalDate) value;
}
};
protected abstract int compare(Temporal a, Temporal b);
}
private final Temporal value;
private final Precision precision;
public static DynamicTemporal parse(String text) {
if (text == null || text.equals("NULL"))
return null;
if (text.length() >= 10)
return new DynamicTemporal(LocalDate.parse(text), Precision.DAY);
if (text.length() >= 7)
return new DynamicTemporal(YearMonth.parse(text), Precision.MONTH);
return new DynamicTemporal(Year.parse(text), Precision.YEAR);
}
private DynamicTemporal(Temporal value, Precision precision) {
this.value = value;
this.precision = precision;
}
public int compareTo(DynamicTemporal other) {
Precision effectivePrecision = (this.precision.compareTo(other.precision) <= 0 ? this.precision : other.precision);
return effectivePrecision.compare(this.value, other.value);
}
}
Now you can easily implement the isDateInRange()
method:
private static boolean isDateInRange(String start, String end, String date) {
DynamicTemporal dateTemporal = DynamicTemporal.parse(date);
DynamicTemporal startTemporal = DynamicTemporal.parse(start);
DynamicTemporal endTemporal = DynamicTemporal.parse(end);
if (startTemporal != null && dateTemporal.compareTo(startTemporal) < 0)
return false; // date is before start
if (endTemporal != null && dateTemporal.compareTo(endTemporal) > 0)
return false; // date is after end
return true;
}
Test (from question)
public static void main(String[] args) {
test("2010-05-15", "2020-05", true, "2010", "2020", "2010-05", "2020-05", "2020-05-22", "2010-05-15", "2010-05-22", "2015-02-25");
test("2010-05-15", "2020-05", false, "2009", "2021", "2010-04", "2020-06", "2010-05-14", "2020-06-01");
test("2010", "2020-05-15", true, "2010", "2020", "2010-05", "2020-05", "2010-05-22", "2010-01-01", "2020-05-15", "2015-02-25");
test("2010", "2020-05-15", false, "2009", "2021", "2020-06", "2020-05-16", "2020-06-01");
test("NULL", "2020-05", true, "2020-05-31", "2020-05", "2020", "2020-05-30", "2020-04", "2019");
test("NULL", "2020-05", false, "2020-06-01", "2020-06", "2021", "2020-06-02", "2020-07", "2022");
test("2010-05-15", "NULL", true, "2010-05-15", "2010-05", "2010", "2010-05-16", "2010-06", "2011");
test("2010-05-15", "NULL", false, "2010-05-14", "2010-04", "2009", "2010-05-13", "2010-03", "2008");
}
private static void test(String start, String end, boolean expected, String... dates) {
for (String date : dates) {
boolean actual = isDateInRange(start, end, date);
System.out.printf("isDateInRange(%-10s, %-10s, %-10s) = %-5s %s%n",
start, end, date, actual, (actual == expected ? "OK" : "FAILED!"));
}
}
Output
isDateInRange(2010-05-15, 2020-05 , 2010 ) = true OK
isDateInRange(2010-05-15, 2020-05 , 2020 ) = true OK
isDateInRange(2010-05-15, 2020-05 , 2010-05 ) = true OK
isDateInRange(2010-05-15, 2020-05 , 2020-05 ) = true OK
isDateInRange(2010-05-15, 2020-05 , 2020-05-22) = true OK
isDateInRange(2010-05-15, 2020-05 , 2010-05-15) = true OK
isDateInRange(2010-05-15, 2020-05 , 2010-05-22) = true OK
isDateInRange(2010-05-15, 2020-05 , 2015-02-25) = true OK
isDateInRange(2010-05-15, 2020-05 , 2009 ) = false OK
isDateInRange(2010-05-15, 2020-05 , 2021 ) = false OK
isDateInRange(2010-05-15, 2020-05 , 2010-04 ) = false OK
isDateInRange(2010-05-15, 2020-05 , 2020-06 ) = false OK
isDateInRange(2010-05-15, 2020-05 , 2010-05-14) = false OK
isDateInRange(2010-05-15, 2020-05 , 2020-06-01) = false OK
isDateInRange(2010 , 2020-05-15, 2010 ) = true OK
isDateInRange(2010 , 2020-05-15, 2020 ) = true OK
isDateInRange(2010 , 2020-05-15, 2010-05 ) = true OK
isDateInRange(2010 , 2020-05-15, 2020-05 ) = true OK
isDateInRange(2010 , 2020-05-15, 2010-05-22) = true OK
isDateInRange(2010 , 2020-05-15, 2010-01-01) = true OK
isDateInRange(2010 , 2020-05-15, 2020-05-15) = true OK
isDateInRange(2010 , 2020-05-15, 2015-02-25) = true OK
isDateInRange(2010 , 2020-05-15, 2009 ) = false OK
isDateInRange(2010 , 2020-05-15, 2021 ) = false OK
isDateInRange(2010 , 2020-05-15, 2020-06 ) = false OK
isDateInRange(2010 , 2020-05-15, 2020-05-16) = false OK
isDateInRange(2010 , 2020-05-15, 2020-06-01) = false OK
isDateInRange(NULL , 2020-05 , 2020-05-31) = true OK
isDateInRange(NULL , 2020-05 , 2020-05 ) = true OK
isDateInRange(NULL , 2020-05 , 2020 ) = true OK
isDateInRange(NULL , 2020-05 , 2020-05-30) = true OK
isDateInRange(NULL , 2020-05 , 2020-04 ) = true OK
isDateInRange(NULL , 2020-05 , 2019 ) = true OK
isDateInRange(NULL , 2020-05 , 2020-06-01) = false OK
isDateInRange(NULL , 2020-05 , 2020-06 ) = false OK
isDateInRange(NULL , 2020-05 , 2021 ) = false OK
isDateInRange(NULL , 2020-05 , 2020-06-02) = false OK
isDateInRange(NULL , 2020-05 , 2020-07 ) = false OK
isDateInRange(NULL , 2020-05 , 2022 ) = false OK
isDateInRange(2010-05-15, NULL , 2010-05-15) = true OK
isDateInRange(2010-05-15, NULL , 2010-05 ) = true OK
isDateInRange(2010-05-15, NULL , 2010 ) = true OK
isDateInRange(2010-05-15, NULL , 2010-05-16) = true OK
isDateInRange(2010-05-15, NULL , 2010-06 ) = true OK
isDateInRange(2010-05-15, NULL , 2011 ) = true OK
isDateInRange(2010-05-15, NULL , 2010-05-14) = false OK
isDateInRange(2010-05-15, NULL , 2010-04 ) = false OK
isDateInRange(2010-05-15, NULL , 2009 ) = false OK
isDateInRange(2010-05-15, NULL , 2010-05-13) = false OK
isDateInRange(2010-05-15, NULL , 2010-03 ) = false OK
isDateInRange(2010-05-15, NULL , 2008 ) = false OK
Upvotes: 2
Reputation: 45339
As your dates are in the yyyy-MM-dd
format, you should be able to perform string comparison, making sure that you only compare strings of the same length (to take care of your rules about comparing dates of different lengths):
private static boolean isDateInRange(String startDate, String endDate, String date) {
return (null == startDate || compareDateString(startDate, date) <= 0)
&& (null == endDate || compareDateString(date, endDate) <= 0);
}
private static int compareDateString(String date1, String date2) {
if (date1.length() == date2.length())
return date1.compareTo(date2);
int length = Math.min(date1.length(), date2.length());
return date1.substring(0, length).compareTo(date2.substring(0, length));
}
And you can check in the following way:
String startDate = "2010-05-15";
String endDate = "2020-05";
String date = "2020-06";
boolean inRange = isDateInRange(startDate, endDate, date);
Upvotes: 3
Reputation: 340350
LocalDate
objectsStop thinking in strings. The strings are merely for user-interface and for serializing values to storage. When your app is running, you should be performing your logic using objects, java.time objects, LocalDate
ultimately.
Convert all inputs to LocalDate
.
Check for the length of the input.
if( input.length() = 4 ) {…}
If the input in 4 characters long, parse as a Year
. Trap for exception thrown in case faulty input gets past your filter. From the year, get a LocalDate
by calling atDay
.
try{
Year year = Year.parse( "2020" ) ;
LocalDate ld = year.atDay( 1 ) ;
} catch ( DateTimeParseException e ) {
…
}
If input is seven characters, parse as a YearMonth
. Form that get a LocalDate
via the atDay
method.
try{
YearMonth YearMonth = YearMonth.parse( "2020-05" ) ;
LocalDate ld = yearMonth.atDay( 1 ) ;
} catch ( DateTimeParseException e ) {
…
}
If input is 10 characters, parse as a LocalDate
.
try{
LocalDate localDate = LocalDate.parse( "2020-05-23" ) ;
} catch ( DateTimeParseException e ) {
…
}
Once you have all your LocalDate
objects in place, compare. Use Half-Open approach consistently when defining your spans of time. The beginning is inclusive while the ending is exclusive.
Tip: “not before” is a shorter way of asking “is equal to or later”.
( ! target.isBefore( start ) ) && target.isBefore( stop )
Upvotes: 3
Reputation: 62
Date min, max; // assume these are set to something
Date d; // the date in question
return d.after(min) && d.before(max);
Upvotes: -1