Grouping objects by date: am I an idiot? - java

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.

Related

Android Joda Time, Having trouble sorting strings from ISOPeriodFormat

I've been looking everywhere for a solution but can't manage to find one that works.
I have a "Scoreboard" that needs to show the highest "times" (period between two instants) the app has calculated with Joda Time.
All the strings are stocked in an ArrayList and displayed through an ArrayAdapter and a ListView.
The problem : Collections.sort doesn't seem to work properly even with ISO format.
i'm saving the time using the format :
PeriodFormatter formatter = ISOPeriodFormat.standard();
Which gives out this : "PT1M15.664S"
(1 min 15seconds)
That i convert to a string and store into the ArrayList.
How can i sort these strings so it goes from the longest to the shortest amount of time in my Scoreboard ?
I've tried natural sorting and Alphanum Comparator with no luck. Every time it passes a cap (minutes, hours, days) the values get like this :
"PT2.455S"
"PT1.324S"
"PT1M15.333S"
Instead of what i would like :
"PT1M15.333S"
"PT2.455S"
"PT1.324S"
Using Collection.sort(myArrayList) doesn't work either.
Any idea what i should do ?
My code :
// set is a set<String> retrieving it's values from a stringset scores saved
in the sharedpreferences of the app
set = sharedPreferences.getStringSet("scores", null);
//scores is the ArrayList
scores.clear();
if (set != null){
scores.addAll(set);
}else{
scores.add("No Time Yet!");
set = new LinkedHashSet<String>();
set.addAll(scores);
sharedPreferences.edit().putStringSet("scores",set).apply();
}
//removing the String No Time Yet because it no longer serves a purpose here
if ((set != null)&& (set.size()>1)){
scores.remove("No Time Yet!");
}
arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,scores);
listView.setAdapter(arrayAdapter);
Collections.sort(scores);
Thank you for you time.
Short answer: Use the class Duration, not Period.
Explanation:
Your general approach using the class Period is wrong. This type represents a tuple of various amount-unit-pairs. Some of them are not convertible or comparable. For example, it is impossible to determine if P30D is greater or equal to or smaller than P1M (think of February, April or August). So it is pretty clear why you cannot sort by periods resp. why this class does not implement the interface Comparable. And this objection is valid for the objects of type Period as well as for its canonical ISO-representation (as String).
But since you want
the highest "times" (period between two instants)
you can use Duration to determine the absolute amount of elapsed seconds and milliseconds between two given instants. This type is comparable and only has two minor constraints which are probably not important for you:
precision limited to milliseconds
ignores leap seconds
I recommend to compare duration objects, not strings because you want a chronological order, not a lexicographical order. So you could use the String-representation of Duration (like PT72.345S) for storage but parse it for comparison:
Instant i1 = new Instant(0);
Instant i2 = new Instant(72_345);
Duration d1 = new Duration(i1, i2);
Instant i3 = new Instant(60_000);
Instant i4 = new Instant(200_710);
Duration d2 = new Duration(i3, i4);
List<String> scoreTimes = new ArrayList<>();
scoreTimes.add(d1.toString());
scoreTimes.add(d2.toString());
// order from longest times to shortest times
Collections.sort(
scoreTimes,
new Comparator<String>() {
#Override
public int compare(String s1, String s2) {
return Duration.parse(s2).compareTo(Duration.parse(s1));
}
}
);
System.out.println(scoreTimes); // [PT140.710S, PT72.345S]

Most efficient way of checking if Date object and Calendar object are in the same month

I am working on a project that will run many thousands of comparisons between dates to see if they are in the same month, and I am wondering what the most efficient way of doing it would be.
This isn't exactly what my code looks like, but here's the gist:
List<Date> dates = getABunchOfDates();
Calendar month = Calendar.getInstance();
for(int i = 0; i < numMonths; i++)
{
for(Date date : dates)
{
if(sameMonth(month, date)
.. doSomething
}
month.add(Calendar.MONTH, -1);
}
Creating a new Calendar object for every date seems like a pretty hefty overhead when this comparison will happen thousands of times, soI kind of want to cheat a bit and use the deprecated method Date.getMonth() and Date.getYear()
public static boolean sameMonth(Calendar month, Date date)
{
return month.get(Calendar.YEAR) == date.getYear() && month.get(Calendar.MONTH) == date.getMonth();
}
I'm pretty close to just using this method, since it seems to be the fastest, but is there a faster way? And is this a foolish way, since the Date methods are deprecated? Note: This project will always run with Java 7
I can't comment on whether to use the deprecated methods, but if you choose not to there's no need to instantiate a new Calendar for every Date you check. Just use one other Calendar and call setTime(date) before the check (or one Calendar for every thread if you parallelize it).
As a side note, I do have to agree with ChristopheD's comment that this is something worthy of a database.
I think you can define a static DateFormat to extract the month and year from Date and use both objects as date only.
public static DateFormat formatter= new SimpleDateForm("MMyyyy");
public static boolean sameMonth(Date date1, Date date2)
{
return formatter.format(date1).equals(formatter.format(date2));
}

Sorting out the dates in chronological order

Friends I am having a String that contains date-record
String date=10-Oct-2012 #12-Oct-2012 #$12-Oct-2012 #12-Oct-2012 #$12-Sept-2012 13:50#12-Oct-2012 13:50#$12-Feb-2012 13:50#12-Oct-2012 13:50#$
List<Date> myList=new ArrayList<Date>() ;
I need to compare the dates 10-Oct-2012,12-OCt-2012,12-Sept-2012,12-Feb-2012 ie every odd date such that I can arrange them in a chronological order.I am confused on this implementation, please provide me with guidance/hint to solve the problem.
In this case the solution after chronological order would be 12-Feb-2012 #12-Oct-2012 #12-Sept-2012 #12-Oct-2012 #$10-Oct-2012 #12-Oct-2012 #$12-Oct-2012 #12-Oct-2012
Friends,to solve the problem I have created a Hashmap where I am planning to save the first date as key and the entire String as value.
String[] tokens=date.split("\\$");
demo[0]=demo[0].replaceAll("-", ".");
if(tokens.length>0)
{
for(int iTmp=tokens.length-1;iTmp>=0;iTmp--)
{
String []demo = tokens[iTmp].split("\\#");
demo[0] = demo[0].replace("Jan", "1")
.replace("Feb", "2").replace("March","3").replace("April","4").replace("May","5").replace("Jun","6").replace("July","7").replace("Aug","8")
.replace("Sept","9").replace("Oct","10").replace("Nov","11").replace("Dec","12");
demo[0]=demo[0]+" 00:05:00";
Date date1 = null;
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yy");
try {
date1 = (Date)formatter.parse(demo[0]);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
myList.add(date1);
System.out.println("ADDED DATE IS"+date1);
//System.out.println("KEY VALUE PAIRS "+key+" "+tokens[iTmp]);
}
}
System.out.println("READING LISTs");
for(int iTmp=0;iTmp<myList.size();iTmp++)
{
System.out.println(myList.get(iTmp));
}
Collections.sort(myList);
System.out.println("After Sorting");
for(int iTmp=0;iTmp<myList.size();iTmp++)
{
System.out.println(myList.get(iTmp));
System.out.println();
}
It sounds pretty simple to me:
Parse each value into a more suitable type (Calendar, Date, Joda Time's LocalDate)
Sort in natural order
(Using Joda Time is the preferred option here IMO, as neither Calendar nor Date really represent "just a date"; you'd have to put all values into the same time zone etc.)
I would definitely not recommend trying to compare them as strings. As usual, convert your data into the most appropriate type for the information it's trying to represent as early as possible - and convert it into serializing representations (e.g. for storage, propagation to a web service etc) as late as possible.
Split the string and then
You can use gregorian calender (built in)
or you can use the yoda-time library
i can't say more about the sorting though
If you are developing for Android (there is an Android tag on your question), then you should know about the Time structures and methods in the Android SDK.
If possible, try to use a string representation of your date/time stamp that itself can be sorted naturally, like the RFC 3339 format (which Android supports with built-in methods). This will let you work more easily with string timestamps, and also give you a simple way to convert to a canonical or integer-type format if desired.

Guava's Ranges.asSet outputting infinite list

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.

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.

Categories

Resources