package org.fluxtream.core;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;
import org.fluxtream.core.utils.TimeUtils;
import org.fluxtream.core.utils.TimespanSegment;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
/**
* <p>
* <code>TimezoneMap</code> does something...
* </p>
*
* @author Anne Wright (arwright@cmu.edu)
*/
public class TimezoneMap {
public TreeSet<TimespanSegment<DateTimeZone>> spans = new TreeSet<TimespanSegment<DateTimeZone>>();
public TimezoneMap() {
}
public boolean add(final long start, final long end, org.joda.time.DateTimeZone tz) {
TimespanSegment<DateTimeZone> newSpan = new TimespanSegment<DateTimeZone>(start,end,tz);
return(spans.add(newSpan));
}
public static TimezoneMap fromConsensusTimezoneMap(final TreeMap<String, TimeZone> consensusTimezoneMap) {
TimezoneMap timezoneMap = new TimezoneMap();
Long holdStartMillis=null;
DateTimeZone holdDateTimezone=null;
Long lastEndMillis = null;
for (String date : consensusTimezoneMap.keySet()) {
final TimeZone timeZone = consensusTimezoneMap.get(date);
final DateTimeZone dateTimeZone = DateTimeZone.forTimeZone(timeZone);
final DateTime thisStartDateTime = TimeUtils.dateFormatter.withZone(dateTimeZone).parseDateTime(date);
final long thisStartMillis = thisStartDateTime.getMillis();
// Check if we need to flush a prior segment
if(holdStartMillis!=null && holdDateTimezone!=dateTimeZone) {
// Yes, this differs, end the previous segment with holdStartMillis as its
// start time and thisStartMillis-1 as its end time
timezoneMap.add(holdStartMillis, thisStartMillis-1, holdDateTimezone);
// Update holdStartMillis and holdDateTimezone for next time to be the start of this segment
holdStartMillis = thisStartMillis;
holdDateTimezone = dateTimeZone;
}
else if(holdStartMillis==null) {
// This is the first time through, hold onto holdStartMillis and holdDateTimezone
// for next time to be the start of the first segment
holdStartMillis = thisStartMillis;
holdDateTimezone = dateTimeZone;
} else {
// Continue with the earlier timezone, no need to change anything
}
// Update lastEndMillis for closing up on the last segment
lastEndMillis = thisStartMillis + DateTimeConstants.MILLIS_PER_DAY;
}
// Flush the last segment (unless it's empty)
if(holdDateTimezone!=null) {
timezoneMap.add(holdStartMillis, lastEndMillis, holdDateTimezone);
}
return timezoneMap;
}
public DateTimeZone getMainTimezone() {
Map<DateTimeZone, Long> timespentInTimezoneMap = new HashMap<DateTimeZone, Long>();
for (TimespanSegment<DateTimeZone> span : spans) {
long timeSpent = span.getEnd() - span.getStart();
if (timespentInTimezoneMap.containsKey(span.getValue())) {
final Long timeAlreadySpent = timespentInTimezoneMap.get(span.getValue());
timespentInTimezoneMap.put(span.getValue(), timeAlreadySpent + timeSpent);
}
else {
timespentInTimezoneMap.put(span.getValue(), timeSpent);
}
}
long maxTimespent = Long.MIN_VALUE;
DateTimeZone mainTimezone = null;
for (DateTimeZone dateTimeZone : timespentInTimezoneMap.keySet()) {
if (timespentInTimezoneMap.get(dateTimeZone) > maxTimespent) {
maxTimespent = timespentInTimezoneMap.get(dateTimeZone);
mainTimezone = dateTimeZone;
}
}
return mainTimezone;
}
public TimespanSegment<DateTimeZone> queryPoint(long ts) {
// Create a segment for retrieving the segment in spans which
// starts at a time <= ts. The returned segment will be one in
// the map or null if ts < the start time of the first segment.
// In that case, return the first item in the span instead.
TimespanSegment<DateTimeZone> querySeg = new TimespanSegment(ts, ts);
TimespanSegment<DateTimeZone> retSeg = spans.lower(querySeg);
if(retSeg==null) {
// This time is earlier than the earliest segment, return the first
return spans.first();
}
return retSeg;
}
public DateTime getStartOfDate(LocalDate date)
{
// Get the milisecond time for the start of that date in UTC
long utcStartMillis = date.toDateTimeAtStartOfDay(DateTimeZone.UTC).getMillis();
// Lookup the timezone for that time - 12 and +12 hours since timezones range from
// UTC-12h to UTC+12h so the real start time will be within that range
long minStartMillis = utcStartMillis - DateTimeConstants.MILLIS_PER_DAY/2;
long maxStartMillis = utcStartMillis + DateTimeConstants.MILLIS_PER_DAY/2;
TimespanSegment<DateTimeZone> minTimespan = this.queryPoint(minStartMillis);
TimespanSegment<DateTimeZone> maxTimespan = this.queryPoint(maxStartMillis);
DateTimeZone realTz = null;
DateTime realDateStart = null;
// Check if they agree
if(minTimespan==maxTimespan) {
// Ok, they agree so we're good, just use the consensus timezone
realTz = minTimespan.getValue();
realDateStart = date.toDateTimeAtStartOfDay(realTz);
}
else {
// The start and end timespans are different, compute the start time in each and see which if
// either intersect
DateTime minTzStartDT = date.toDateTimeAtStartOfDay(minTimespan.getValue());
DateTime maxTzStartDT = date.toDateTimeAtStartOfDay(maxTimespan.getValue());
// Does the earlier one fall within the timespan for the minTimezone
long minTzStartMillis = minTzStartDT.getMillis();
long maxTzStartMillis = maxTzStartDT.getMillis();
if(minTimespan.isTimeInSpan(minTzStartMillis)) {
// First one works, keep it
realTz=minTimespan.getValue();
realDateStart = minTzStartDT;
}
else if(maxTimespan.isTimeInSpan(maxStartMillis)) {
// Last one works, keep it
realTz=maxTimespan.getValue();
realDateStart = maxTzStartDT;
}
else {
// Something weird is going on here, complain and return GMT
System.out.println("Cant figure out start of date "+date.toString()+", "+minTimespan + " does not contain " + minTzStartDT + " and "+ maxTimespan + " does not contain " + maxTzStartDT);
return(date.toDateTimeAtStartOfDay(DateTimeZone.UTC));
}
}
//System.out.println("Start of date "+date.toString()+", in "+realTz + ": " + realDateStart);
return(realDateStart);
}
public static void main(final String[] args) {
// Create test table
TimezoneMap tzMap = new TimezoneMap();
// Create sample tzMap based on Anne's BodyMedia map:
// [ { "endDate" : "20110822T220048-0400", "startDate" : "20101221T000000-0500", "value" : "US/Eastern"},
// { "endDate" : "20110921T192919-0700", "startDate" : "20110822T190048-0700", "value" : "US/Pacific"},
// { "startDate" : "20110921T222919-0400","value" : "US/Eastern"
// } ]
// Then extended to have a segment in Central time starting Wed, 22 May 2013 13:45:56 GMT and ending
// Sat, 01 Jun 2013 00:00:00 GMT
tzMap.add(1292907600000L,1314064848000L, DateTimeZone.forID("America/New_York"));
tzMap.add(1314064848000L, 1316658559000L, DateTimeZone.forID("America/Los_Angeles"));
tzMap.add(1316658559000L, 1369230356963L, DateTimeZone.forID("America/New_York"));
tzMap.add(1369230356963L, 1370044800000L, DateTimeZone.forID("US/Central"));
// This should be Eastern
LocalDate d1 = new LocalDate(2011, 6, 15);
tzMap.getStartOfDate(d1);
// This should be Eastern
LocalDate d2 = new LocalDate(2011, 8, 22);
tzMap.getStartOfDate(d2);
// This should be Pacific
LocalDate d3 = new LocalDate(2011, 8, 23);
tzMap.getStartOfDate(d3);
// This should be Eastern
LocalDate d3b = new LocalDate(2013, 5, 22);
tzMap.getStartOfDate(d3b);
// This should be Central
LocalDate d3c = new LocalDate(2013, 5, 23);
tzMap.getStartOfDate(d3c);
// Get from before the start of the map. This should default to using the first item in the map and return
// in America/New_York
LocalDate d4 = new LocalDate(2010,12,21);
tzMap.getStartOfDate(d4);
LocalDate d5 = new LocalDate(2010,1,1);
tzMap.getStartOfDate(d5);
// Get from past the end of the map. This should be Central
LocalDate d6 = new LocalDate(2013, 6, 2);
tzMap.getStartOfDate(d6);
LocalDate d7 = new LocalDate(2013, 8, 2);
tzMap.getStartOfDate(d7);
// Try out fromConsensusTimezoneMap
TreeMap<String, TimeZone> ctm1 = new TreeMap<String, TimeZone>();
ctm1.put("2011-06-05",TimeZone.getTimeZone("America/New_York"));
TimezoneMap tzm1 = fromConsensusTimezoneMap(ctm1);
int tzm1S = tzm1.spans.size();// should be 1 covering 6/15/11
ctm1.put("2012-06-05",TimeZone.getTimeZone("America/New_York"));
TimezoneMap tzm2 = fromConsensusTimezoneMap(ctm1);
int tzm2S = tzm2.spans.size();// should be 1 covering 6/15/11 - 6/15/12
ctm1.put("2011-12-05",TimeZone.getTimeZone("US/Central"));
TimezoneMap tzm3 = fromConsensusTimezoneMap(ctm1);
int tzm3S = tzm3.spans.size();// should be 3 covering 6/15/11 - 2011-12-04, 2011-12-05 - 6/14/12, and 6/15/12
}
}