Guava's Ranges.asSet outputting infinite list - java

I am trying to get a date range using Guava's new Range functionality, via
Range<Date> dateRange = Ranges.range(start, BoundType.CLOSED, end, BoundType.CLOSED);
My goal is to get the hours in this date range. So I have created a DiscreteDomain like such:
private static final DiscreteDomain<Date> HOURS = new DiscreteDomain<Date>() {
public Date next(Date value) {
return addHours(value, 1);
}
private Date addHours(Date value, int i) {
Calendar cal = Calendar.getInstance();
cal.setTime(value);
cal.add(Calendar.HOUR_OF_DAY, i);
return cal.getTime();
}
public Date previous(Date value) {
return addHours(value, -1);
}
public long distance(Date start, Date end) {
Calendar cal1 = Calendar.getInstance();
cal1.setTime(start);
Calendar cal2 = Calendar.getInstance();
cal2.setTime(end);
return cal2.getTimeInMillis() - cal1.getTimeInMillis();
}
public Date minValue() {
return new Date(Long.MIN_VALUE);
}
public Date maxValue() {
return new Date(Long.MAX_VALUE);
}
};
If I merely sysout the output, I get the closed set
[Thu Feb 24 00:00:00 EST 2011..Thu Feb 24 00:02:00 EST 2011]
I really want to see each hour in the range, however, so I try a for loop:
for (Date hour : hours) {
System.out.println(hour);
}
When running this block, I seem to get an infinite set, beginning at the left side of the range, but not stopping at the right side, making me kill the IDE. What am I doing wrong?

I think this might be due to the behavior of the Iterator returned by the ContiguousSet (returned by Range.asSet()):
#Override public UnmodifiableIterator<C> iterator() {
return new AbstractLinkedIterator<C>(first()) {
final C last = last();
#Override
protected C computeNext(C previous) {
return equalsOrThrow(previous, last) ? null : domain.next(previous);
}
};
}
private static boolean equalsOrThrow(Comparable<?> left,
#Nullable Comparable<?> right) {
return right != null && compareOrThrow(left, right) == 0;
}
private static int compareOrThrow(Comparable left, Comparable right) {
return left.compareTo(right);
}
It only stops when the next computed value is equal to the right bound of the range.
In your case, have you tried calling it using Thu Feb 24 02:00:00 instead of Thu Feb 24 00:02:00 for the right bound of your range?
I think this behavior is problematic, and it might be worth asking if equalsOrThrow() could be changed to check for left <= right instead of left == right
Also, your distance() method is incorrect. It should return the distance in hours, not in milliseconds, according to the method contract.
EDIT
All this being said, I believe the real problem is that, according to the DiscreteDomain's javadoc:
A discrete domain always represents
the entire set of values of its type;
it cannot represent partial domains
such as "prime integers" or "strings
of length 5."
In your case, you are attempting to create a discrete domain over hourly dates, which is a partial domain of all dates. This is, I think, the root cause of the problem. When you have a partial domain, the equalsOrThrow method becomes unreliable, and it can "miss" the right bound of your range.

I just tried this and it worked fine for me. #eneveu already pointed out the issue with your distance method as well. I'm also guessing that there's some minor difference at the millisecond level between start and end which means that you'll never actually get a Date equal to end by adding hours to start.
However, that's all just symptoms of using the classes in a way they aren't designed to work. The Javadoc for DiscreteDomain states:
A discrete domain always represents the entire set of values of its type; it cannot represent partial domains such as "prime integers" or "strings of length 5."
A DiscreteDomain of "hours" does not represent the domain of all possible Date objects and as such breaks its contract.

Related

How to check if time period is overlapping another time period irrespective of AM/PM

The following link: How to check a timeperiod is overlapping another time period in java is helpful to check if a given timestamp overlaps with another timestamp.
However, this only works if the two timestamp are either in AM or PM. What I want to check is, if a given timestamp is overlapping another timestamp irrespective if it's in AM or PM.
For example:
If I have an Array of timestamps (in 24 hour format):
1:00 - 3:00
13:45 - 14:45
3:15 - 4:00.
The first two are technically overlapping since 13:45 - 14:45 falls between 1:00-3:00 (if converted into 12 hour format)
How do I check for the same?
I want to essentially check if there is any overlap between the timestamps (+/- 30 mins)
Assuming that each interval is within either AM or PM
I am assuming that each interval is either completely within AM (00:00 through 12) or PM (12:00 through 00). I didn’t understand what you meant by “(+/- 30 mins)”, so I have ignored that.
As an aside I consider this an artificial challenge. In the real world 2 AM and 2 PM are not the same, they just happen to have identical representations on a 12 hour clock. Just as a flag pole and a person from Poland are not the same even though they both have the representation “Pole”.
As Sweeper suggested in a comment I am converting each interval to AM (if it was in PM) before comapring.
LocalTime begin1 = LocalTime.of(1, 0);
LocalTime end1 = LocalTime.of(3, 0);
LocalTime begin2 = LocalTime.of(13, 45);
LocalTime end2 = LocalTime.of(14, 45);
// Convert interval 1 to AM
if (begin1.get(ChronoField.AMPM_OF_DAY) == 1) { // PM
begin1 = begin1.minusHours(12);
end1 = end1.minusHours(12);
}
// validate
if (end1.isBefore(begin1)) {
throw new IllegalStateException("end1 " + end1 + " must not be before begin1 " + begin1);
}
if (end1.isAfter(LocalTime.NOON)) {
throw new IllegalStateException("Interval 1 must be completely within either AM or PM");
}
// Convert interval 2 to AM
if (begin2.get(ChronoField.AMPM_OF_DAY) == 1) {
begin2 = begin2.minusHours(12);
end2 = end2.minusHours(12);
}
// validate
if (end2.isBefore(begin2)) {
throw new IllegalStateException("end2 " + end2 + " must not be before begin2 " + begin2);
}
if (end2.isAfter(LocalTime.NOON)) {
throw new IllegalStateException("Interval 2 must be completely within either AM or PM");
}
if (end2.isAfter(begin1) && end1.isAfter(begin2)) {
System.out.println("They overlap");
} else {
System.out.println("They do not overlap");
}
Output from this code is:
They overlap
Corner case: I am accepting an end time of 12:00 (noon) for an AM interval and of 00:00 for a PM interval. LocalTime.minusHours() has cyclic underflow, so subtracting 12 hours from 00:00 gives 12:00.
The code may be simpler and easier to find your way through if you define a TimePeriod class with fields for begin and end, a method for checking overlap and an auxiliary method for converting into AM.
With no restrictions
Edit: Assuming that each interval can be any length from 0 (inclusive) to 24 hours (exclusive) and may cross 00:00, this is somewhat more complicated, but I couldn’t let the challenge rest.
Some observations:
If one interval is 12 hours or longer and the other has non-zero length, the two necessarily overlap.
If both intervals are shorter than 12 hours, then if they do not overlap, we can go (count) cyclically forward from begin1 through end1 and begin2 to end2 in the order given here and either not cross 12 o’clock or cross 12 once and end up before begin1. If this cycle doesn’t work, then the intervals must overlap somehow.
In code:
public static boolean overlaps(LocalTime begin1, LocalTime end1, LocalTime begin2, LocalTime end2) {
if (begin1.equals(end1)) { // zero length, cannot overlap anything
return false;
}
if (begin2.equals(end2)) {
return false;
}
// If any interval is 12 hours or longer,
// the other one is necessarily included, that is, overlaps
if (is12HoursOrLonger(begin1, end1)) {
return true;
}
if (is12HoursOrLonger(begin2, end2)) {
return true;
}
// Convert all times to AM
begin1 = toAm(begin1);
end1 = toAm(end1);
begin2 = toAm(begin2);
end2 = toAm(end2);
// For the two intervals *not* to overlap we must be able to go forward
// from begin1 through end1 and begin2 to end2 in this order either
// not crossing 12 or crossing 12 once and ending before or on begin1
boolean crossed12OClock = false;
if (end1.isBefore(begin1)) { // to go forward to end1 we are crossing 12 o’clock
crossed12OClock = true;
}
if (begin2.isBefore(end1)) {
if (crossed12OClock) {
// crossing 12 for the second time;
// intervals cannot be in non-overlapping order
return true;
}
crossed12OClock = true;
}
if (end2.isBefore(begin2)) {
if (crossed12OClock) {
return true;
}
crossed12OClock = true;
}
if (crossed12OClock) {
return end2.isAfter(begin1);
} else {
return false;
}
}
This method uses the following two auxiliary methods:
private static boolean is12HoursOrLonger(LocalTime begin, LocalTime end) {
Duration length = Duration.between(begin, end);
if (length.isNegative()) {
length = length.plusDays(1);
}
return ! length.minusHours(12).isNegative();
}
private static LocalTime toAm(LocalTime time) {
return time.with(ChronoField.AMPM_OF_DAY, 0);
}
Let’s try it out using the times from before:
if (overlaps(begin1, end1, begin2, end2)) {
System.out.println("They overlap");
} else {
System.out.println("They do not overlap");
}
They overlap
Since the code and the arguments are complicated, make sure to cover the methods thoroughly with unit tests.

How to determine if the specific time is between given range?

Problem: I have a list containg hours, for example:
08:15:00
08:45:00
09:00:00
12:00:00
...
application is allowing user to make an appointment for a specific hour let'say: 8:15:00, each meeting takes half an hour.
Question: How to determine if there is a slot needed for appointment like this? I know that Calendar class have methods before() nad after(), but it doesn'solve my problem. I mean if there is appointment at 12:00 and another one at 12:00, how to prevent before making another one at 12:15?
edit:
I've tried using methods I mentioned before, like:
Calendar cal1 = Calendar.getInstance(); // for example 12:00:00
Calendar cal2 = Calendar.getInstance(); // for exmaple 12:30:00
Calendar userTime = Calendar.getInstance(); // time to test: 12:15:00
if(user.after(cal1)&& user.before(cal2)){
... // do sth
}
Check if the date to check is between the two provided:
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy hh:mm");
Date before = sdf.parse("07/05/2012 08:00");
Date after = sdf.parse("07/05/2012 08:30");
Date toCheck = sdf.parse("07/05/2012 08:15");
//is toCheck between the two?
boolean isAvailable = (before.getTime() < toCheck.getTime()) && after.getTime() > toCheck.getTime();
To book for a determinate hour, I would do a class with two dates and a method to check this:
public class Appointment{
private Date start;
private Date end;
public boolean isBetween(Date toCheck){....}
}
Then you can simply do an Schedule class extending ArrayList, adding a method isDateAvailable(Date toCheck), iterating the list of Appointments and checking that there is no one conflicting.
I'd have some kind of appointment class with either a start timestamp and a duration or a start time and an end time. Then when adding new appointments to the schedule, check that the appointment with the start time before the new appointment doesn't run over the start time of the proposed new appointment.
Well how you would do it specifically depends on how you are storing your data, format, etc., but generally what you would do is simply check if there is an appointment for any time between the requested time to the requested time + requested length.
// Example (using int time(1 = 1 minute), assuming that appointments can only be at 15min intervals)
boolean isHalfHourTimeSlotAvaliable(int time) {
for (int i = 0; i < appointments.size(); i++) {
if (appointments.get(i).time == time || appointments.get(i).time == time + 15) {
return false;
}
}
return true;
}

Java. Check time range. How?

For example, I have input parameter this format: "04:00-06:00" or "23:00-24:00". Type of parameter - String.
And in my method I must check, that time range in input parameter NOT before current time. How I can do it?
More details:
input time range: "12:00-15:00"
current time: 16:00.
In this case, method must return false.
Another example:
input time range: "10:30-12:10"
current time: 09:51.
method must return true.
Can you please give me some idea or algorithm? How I can implement this method?
First off, you should probably just learn to use Joda time.
That said, since the times are all zero padded, you can just compare strings lexically.
public static boolean inRange(String time, String range) {
return time.compareTo(range.substring(0, 5)) >= 0
&& time.compareTo(range.substring(6)) <= 0;
}
It's good practice to fail fast on malformed inputs.
private static final Pattern VALID_TIME = Pattern.compile("[012][0-9]:[0-5][0-9]");
private static final Pattern VALID_RANGE = Pattern.compile("[012][0-9]:[0-5][0-9]-[012][0-9]:[0-5][0-9]");
and then put an assert at the top of inRange:
assert VALID_TIME.matcher(time).matches() : time
assert VALID_RANGE.matcher(range).matches() : range
EDIT:
If you really need to represent the current time as a Date, then you should compare it this way:
public final class Range {
/** Inclusive as minutes since midnight */
public final int start, end;
public Range(int start, int end) {
assert end >= start;
}
/** #param time in minutes since midnight */
public boolean contains(int time) {
return start <= time && time <= end;
}
public static Range valueOf(String s) {
assert VALID_RANGE.matcher(s).matches() : s;
return new Range(minutesInDay(s.substring(0, 5)),
minutesInDay(s.substring(6));
}
private static int minutesInDay(String time) {
return Integer.valueOf(time.substring(0, 2)) * 60
+ Integer.valueOf(time.substring(3));
}
}
Use Range.valueOf to convert from a String, convert your Date to a number of minutes since midnight in whatever timezone you like using whatever calendar implementation you like, and then use Range.contains.
Date currentDate = new Date();
Date maxDate;
Date minDate;
//Parse range to two substrings
//parse two substrings to [HH, MM]
//for HH && MM parseInt()
//
minDate= new Date.SetHour(HH); minDate.SetMinute(MM);
//repeat for max date
if(currentDate.Before(maxDate) && currentDate.After(minDate))
{
return true;
}
else
return false;

Joda Time LocalTime of 24:00 end-of-day

We're creating a scheduling application and we need to represent someone's available schedule during the day, regardless of what time zone they are in. Taking a cue from Joda Time's Interval, which represents an interval in absolute time between two instances (start inclusive, end exclusive), we created a LocalInterval. The LocalInterval is made up of two LocalTimes (start inclusive, end exclusive), and we even made a handy class for persisting this in Hibernate.
For example, if someone is available from 1:00pm to 5:00pm, we would create:
new LocalInterval(new LocalTime(13, 0), new LocalTime(17, 0));
So far so good---until someone wants to be available from 11:00pm until midnight on some day. Since the end of an interval is exclusive, this should be easily represented as such:
new LocalInterval(new LocalTime(23, 0), new LocalTime(24, 0));
Ack! No go. This throws an exception, because LocalTime cannot hold any hour greater than 23.
This seems like a design flaw to me---Joda didn't consider that someone may want a LocalTime that represents a non-inclusive endpoint.
This is really frustrating, as it blows a hole in what was otherwise a very elegant model that we created.
What are my options---other than forking Joda and taking out the check for hour 24? (No, I don't like the option of using a dummy value---say 23:59:59---to represent 24:00.)
Update: To those who keep saying that there is no such thing as 24:00, here's a quote from ISO 8601-2004 4.2.3 Notes 2,3: "The end of one calendar day [24:00] coincides with [00:00] at the start of the next calendar day ..." and "Representations where [hh] has the value [24] are only preferred to represent the end of a time interval ...."
Well after 23:59:59 comes 00:00:00 on the next day. So maybe use a LocalTime of 0, 0 on the next calendar day?
Although since your start and end times are inclusive, 23:59:59 is really what you want anyways. That includes the 59th second of the 59th minute of the 23rd hour, and ends the range exactly on 00:00:00.
There is no such thing as 24:00 (when using LocalTime).
The solution we finally went with was to use 00:00 as a stand-in for 24:00, with logic throughout the class and the rest of the application to interpret this local value. This is a true kludge, but it's the least intrusive and most elegant thing I could come up with.
First, the LocalTimeInterval class keeps an internal flag of whether the interval endpoint is end-of-day midnight (24:00). This flag will only be true if the end time is 00:00 (equal to LocalTime.MIDNIGHT).
/**
* #return Whether the end of the day is {#link LocalTime#MIDNIGHT} and this should be considered midnight of the
* following day.
*/
public boolean isEndOfDay()
{
return isEndOfDay;
}
By default the constructor considers 00:00 to be beginning-of-day, but there is an alternate constructor for manually creating an interval that goes all day:
public LocalTimeInterval(final LocalTime start, final LocalTime end, final boolean considerMidnightEndOfDay)
{
...
this.isEndOfDay = considerMidnightEndOfDay && LocalTime.MIDNIGHT.equals(end);
}
There is a reason why this constructor doesn't just have a start time and an "is end-of-day" flag: when used with a UI with a drop-down list of times, we don't know if the user will choose 00:00 (which is rendered as 24:00), but we know that as the drop-down list is for the end of the range, in our use case it means 24:00. (Although LocalTimeInterval allows empty intervals, we don't allow them in our application.)
Overlap checking requires special logic to take care of 24:00:
public boolean overlaps(final LocalTimeInterval localInterval)
{
if (localInterval.isEndOfDay())
{
if (isEndOfDay())
{
return true;
}
return getEnd().isAfter(localInterval.getStart());
}
if (isEndOfDay())
{
return localInterval.getEnd().isAfter(getStart());
}
return localInterval.getEnd().isAfter(getStart()) && localInterval.getStart().isBefore(getEnd());
}
Similarly, converting to an absolute Interval requires adding another day to the result if isEndOfDay() returns true. It is important that application code never constructs an Interval manually from a LocalTimeInterval's start and end values, as the end time may indicate end-of-day:
public Interval toInterval(final ReadableInstant baseInstant)
{
final DateTime start = getStart().toDateTime(baseInstant);
DateTime end = getEnd().toDateTime(baseInstant);
if (isEndOfDay())
{
end = end.plusDays(1);
}
return new Interval(start, end);
}
When persisting LocalTimeInterval in the database, we were able to make the kludge totally transparent, as Hibernate and SQL have no 24:00 restriction (and indeed have no concept of LocalTime anyway). If isEndOfDay() returns true, our PersistentLocalTimeIntervalAsTime implementation stores and retrieves a true time value of 24:00:
...
final Time startTime = (Time) Hibernate.TIME.nullSafeGet(resultSet, names[0]);
final Time endTime = (Time) Hibernate.TIME.nullSafeGet(resultSet, names[1]);
...
final LocalTime start = new LocalTime(startTime, DateTimeZone.UTC);
if (endTime.equals(TIME_2400))
{
return new LocalTimeInterval(start, LocalTime.MIDNIGHT, true);
}
return new LocalTimeInterval(start, new LocalTime(endTime, DateTimeZone.UTC));
and
final Time startTime = asTime(localTimeInterval.getStart());
final Time endTime = localTimeInterval.isEndOfDay() ? TIME_2400 : asTime(localTimeInterval.getEnd());
Hibernate.TIME.nullSafeSet(statement, startTime, index);
Hibernate.TIME.nullSafeSet(statement, endTime, index + 1);
It's sad that we had to write a workaround in the first place; this is the best I could do.
It's not a design flaw. LocalDate doesn't handle (24,0) because there's no such thing as 24:00.
Also, what happens when you want to represent an interval between, say 9pm and 3am?
What's wrong with this:
new LocalInterval(new LocalTime(23, 0), new LocalTime(0, 0));
You just have to handle the possibility that the end time might be "before" the start time, and add a day when necessary, and just hope that noone wants to represent an interval longer than 24 hours.
Alternatively, represent the interval as a combination of a LocalDate and a Duration or Period. That removes the "longer than 24 hours" problem.
Your problem can be framed as defining an interval on a domain that wraps around. Your min is 00:00, and your max is 24:00 (not inclusive).
Suppose your interval is defined as (lower, upper). If you require that lower < upper, you can represent (21:00, 24:00), but you are still unable to represent (21:00, 02:00), an interval that wraps across the min/max boundary.
I don't know whether your scheduling application would involve wrap-around intervals, but if you are going to go to (21:00, 24:00) without involving days, I don't see what will stop you from requiring (21:00, 02:00) without involving days (thus leading to a wrap-around dimension).
If your design is amenable to a wrap-around implementation, the interval operators are quite trivial.
For example (in pseudo-code):
is x in (lower, upper)? :=
if (lower <= upper) return (lower <= x && x <= upper)
else return (lower <= x || x <= upper)
In this case, I have found that writing a wrapper around Joda-Time implementing the operators is simple enough, and reduces impedance between thought/math and API. Even if it is just for the inclusion of 24:00 as 00:00.
I do agree that the exclusion of 24:00 annoyed me at the start, and it'll be nice if someone offered a solution. Luckily for me, given that my use of time intervals is dominated by wrap-around semantics, I always end up with a wrapper, which incidentally solves the 24:00 exclusion.
The time 24:00 is a difficult one. While we humans can understand what is meant, coding up an API to represent that without negatively impacting everything else appears to me to be nigh on impossible.
The value 24 being invalid is deeply encoded in Joda-Time - trying to remove it would have negative implications in a lot of places. I wouldn't recommend trying to do that.
For your problem, the local interval should consist of either (LocalTime, LocalTime, Days) or (LocalTime, Period). The latter is slightly more flexible. This is needed to correctly support an interval from 23:00 to 03:00.
I find JodaStephen's proposal of (LocalTime, LocalTime, Days) acceptable.
Considering on 13 March 2011 and your availability on Sunday from 00:00-12:00 you would have (00:00, 12:00, 0) which were in fact 11 hours long because of DST.
An availability from say 15:00-24:00 you could then code as (15:00, 00:00, 1) which would expanded to 2011-03-13T15:00 - 2011-03-14T00:00 whereat the end would be desired 2011-03-13T24:00. That means you would use a LocalTime of 00:00 on the next calendar day like already aroth proposed.
Of course it would be nice to use a 24:00 LocalTime directly and ISO 8601 conform but this seems not possible without changing a lot inside JodaTime so this approach seems the lesser evil.
And last but not least you could even extend the barrier of a single day with something like (16:00, 05:00, 1)...
this is our implementation of TimeInterval, using null as end Date for end-of-day. It supports the overlaps() and contains() methods and is also based on joda-time. It supports intervals spanning multiple days.
/**
* Description: Immutable time interval<br>
* The start instant is inclusive but the end instant is exclusive.
* The end is always greater than or equal to the start.
* The interval is also restricted to just one chronology and time zone.
* Start can be null (infinite).
* End can be null and will stay null to let the interval last until end-of-day.
* It supports intervals spanning multiple days.
*/
public class TimeInterval {
public static final ReadableInstant INSTANT = null; // null means today
// public static final ReadableInstant INSTANT = new Instant(0); // this means 1st jan 1970
private final DateTime start;
private final DateTime end;
public TimeInterval() {
this((LocalTime) null, null);
}
/**
* #param from - null or a time (null = left unbounded == LocalTime.MIDNIGHT)
* #param to - null or a time (null = right unbounded)
* #throws IllegalArgumentException if invalid (to is before from)
*/
public TimeInterval(LocalTime from, LocalTime to) throws IllegalArgumentException {
this(from == null ? null : from.toDateTime(INSTANT),
to == null ? null : to.toDateTime(INSTANT));
}
/**
* create interval spanning multiple days possibly.
*
* #param start - start distinct time
* #param end - end distinct time
* #throws IllegalArgumentException - if start > end. start must be <= end
*/
public TimeInterval(DateTime start, DateTime end) throws IllegalArgumentException {
this.start = start;
this.end = end;
if (start != null && end != null && start.isAfter(end))
throw new IllegalArgumentException("start must be less or equal to end");
}
public DateTime getStart() {
return start;
}
public DateTime getEnd() {
return end;
}
public boolean isEndUndefined() {
return end == null;
}
public boolean isStartUndefined() {
return start == null;
}
public boolean isUndefined() {
return isEndUndefined() && isStartUndefined();
}
public boolean overlaps(TimeInterval other) {
return (start == null || (other.end == null || start.isBefore(other.end))) &&
(end == null || (other.start == null || other.start.isBefore(end)));
}
public boolean contains(TimeInterval other) {
return ((start != null && other.start != null && !start.isAfter(other.start)) || (start == null)) &&
((end != null && other.end != null && !other.end.isAfter(end)) || (end == null));
}
public boolean contains(LocalTime other) {
return contains(other == null ? null : other.toDateTime(INSTANT));
}
public boolean containsEnd(DateTime other) {
if (other == null) {
return end == null;
} else {
return (start == null || !other.isBefore(start)) &&
(end == null || !other.isAfter(end));
}
}
public boolean contains(DateTime other) {
if (other == null) {
return start == null;
} else {
return (start == null || !other.isBefore(start)) &&
(end == null || other.isBefore(end));
}
}
#Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("TimeInterval");
sb.append("{start=").append(start);
sb.append(", end=").append(end);
sb.append('}');
return sb.toString();
}
}
For the sake of completeness this test fails:
#Test()
public void testJoda() throws DGConstraintViolatedException {
DateTimeFormatter simpleTimeFormatter = DateTimeFormat.forPattern("HHmm");
LocalTime t1 = LocalTime.parse("0000", simpleTimeFormatter);
LocalTime t2 = LocalTime.MIDNIGHT;
Assert.assertTrue(t1.isBefore(t2));
}
This means the MIDNIGHT constant is not very usefull for the problem, as someone suggested.
This question is old, but many of these answers focus on Joda Time, and only partly address the true underlying problem:
The model in the OP's code doesn't match the reality it's modeling.
Unfortunately, since you do appear to care about the boundary condition between days, your "otherwise elegant model" isn't a good match for the problem you are modeling. You've used a pair of time values to represent intervals. Attempting to simplify the model down to a pair of times is simplifying below the complexity of the real world problem. Day boundaries actually do exist in reality and a pair of times looses that type of information. As always, over simplification results in subsequent complexity to restore or compensate for the missing information. Real complexity can only be pushed around from one part of the code to another.
The complexity of reality can only be eliminated with the magic of "unsupported use cases".
Your model would only make sense in a problem space where one didn't care how many days might exist between the start and end times. That problem space doesn't match most real world problems. Therefore, it's not surprising that Joda Time doesn't support it well. The use of 25 values for the hours place (0-24) is a code smell and usually points to a weakness in the design. There are only 24 hours in the day so 25 values should not be needed!
Note that since you aren't capturing the date on either end of LocalInterval, your class also does not capture sufficient information to account for daylight savings time. [00:30:00 TO 04:00:00) is usually 3.5 hours long but could also be 2.5, or 4.5 hours long.
You should either use a start date/time and duration, or a start date/time and an end date/time (inclusive start, exclusive end is a good default choice). Using a duration becomes tricky if you intend to display the end time because of things like daylight savings time, leap years and leap seconds. On the other hand using an end date becomes just as tricky if you expect to display the duration. Storing both of course is dangerous because it violates the DRY principle. If I were writing such a class I would store an end date/time and encapsulate the logic for obtaining the duration via a method on the object. That way clients of the class class do not all come up with their own code to calculate the duration.
I'd code up a example, but there's an even better option. Use the standard Interval Class from Joda time, which already accepts a start instant and either duration or end instant. It will also and happily calculate the duration or the end time for you. Sadly JSR-310 doesn't have an interval or similar class. (though one can use ThreeTenExtra to make up for that)
The relatively bright folks at Joda Time and Sun/Oracle (JSR-310) both thought very carefully about these problems. You might be smarter than them. It's possible. However, even if you are a brighter bulb, your 1 hour is probably not going to accomplish what they spent years on. Unless you are somewhere out in an esoteric edge case, it's usually waste of time and money to spend effort second guessing them. (of course at the time of the OP JSR-310 wasn't complete...)
Hopefully the above will help folks who find this question while designing or fixing similar issues.

Grouping objects by date: am I an idiot?

I have a list of objects called Activity:
class Activity {
public Date activityDate;
public double amount;
}
I want to iterate through List, group them by date and return a new list . Here's what I currently do:
private List<Activity> groupToList(List<Activity> activityList) {
SimpleDateFormatter sdf = new SimpleDateFormatter("YYYY-MM-DD");
Map<String,Activity> groupMap = new HashMap<String,Activity>();
for (Activity a in activityList) {
String key = sdf.format(a.getActivityDate());
Activity group = groupMap.get(key);
if (group == null) {
group = new Activity();
groupMap.add(key, group);
}
group.setAmount(group.getAmount() + a.getAmount());
}
return new ArrayList<Activity>(groupMap.values());
}
Is it a WTF to use the DateFormatter in this way?
I'm using the DateFormatter because each activityDate could have time information.
I would just use the date object itself as the key. If it it bothers you because the date object is mutable, then use its toString() value. No reason to go making formats.
If the issue is that you want to normalize the date by removing the time component, it would be much better to do that withing the Activity object and remove the time component. If the issue is still further that there are potential time zone issues, I would use JodaTime, but there is no object in the JDK currently that represents a pure date without time, so going with a string isn't outrageous, but it should be hidden behind a method in the Activity object and the fact that it is a date formatted string without a time component should be an implementation detail.
java.util.Date is a quite poor abstraction for your need; it is IMO fair to stick to strings if nothing better is around, HOWEVER Joda-time provides a good datatype for you: DateMidnight or alternatively LocalDate if Activity is strictly timezome-independant.
other than that, the code looks good to me, you might be able to shorten it a bit using an implementation of Multimap, to avoid messy null-checking code. to be honest, it doesn't get much shorter than your solution:
public List<Activity> groupedByDate(List<Activity> input) {
//group by day
final Multimap<DateMidnight, Activity> activityByDay
= Multimaps.index(input, new Function<Activity, DateMidnight>() {
#Override
public DateMidnight apply(Activity from) {
return new DateMidnight(from.activityDate);
}
});
//for each day, sum up amount
List<Activity> ret = Lists.newArrayList();
for (DateMidnight day : activityByDay.keySet()) {
Activity ins = new Activity();
ins.activityDate = day.toDate();
for (Activity activity : activityByDay.get(day)) {
ins.amount+=activity.amount;
}
}
return ret;
}
Why not simply create a HashMap<Date, Activity>() instead of the roundabout way with Strings?
Sorry, I didn't answer the question. The answer is: yes, unless I am an idiot ;)
You could do this using the Date as the key if you used a TreeMap and provided a Comparator that only compared the year, month and day and not the time.
As already mentioned the best solution is to represent your date with day precission. If this is not possible joda is nice library.
If you can ignore daylight saving time then grouping by date can be accomplished much easier. A unix time day is 86 400 s long. The timestamp does ignore leap seconds. (Your timer stops for one second or the leap second is distributed in some way.) All date values were day is equal are the same day:
int msPerDay = 86400 * 1000;
long day = new Date().getTime() / msPerDay
One minor point is to adjust the timezone. For my timezone CET (UTC/GMT +1 hour) the GMT day starts one our later:
new GregorianCalendar(2009, 10, 1, 1, 0).getTime().getTime() / msPerDay) ==
new GregorianCalendar(2009, 10, 2, 0, 59).getTime().getTime() / msPerDay) ==
new Date().getTime() / msPerDay
If the daylight saving time is significant the best way is to use joda. The rules are just to complicated and locale specific to implement.

Categories

Resources